#!/usr/bin/env bash # EchoChat 一键停止脚本 # 用法: # ./scripts/stop.sh # 停止应用层(Go 后端 + 前台 + 管理端 + 媒体),保留 Docker 容器 # ./scripts/stop.sh --all # 停止全部(含 Docker 中间件) # ./scripts/stop.sh full # 停止 docker compose 全栈(含 media-server 容器,含 public profile 的 coturn) # ./scripts/stop.sh backend # 仅停止指定服务(backend|frontend|admin|media|docker) # 不使用 set -e:即使某一项停止失败,其他项也要继续尝试 set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" RUN_DIR="$ROOT_DIR/.run" DEPLOY_DIR="$ROOT_DIR/deploy" COLOR_GREEN='\033[0;32m' COLOR_YELLOW='\033[0;33m' COLOR_RED='\033[0;31m' COLOR_CYAN='\033[0;36m' COLOR_RESET='\033[0m' log_info() { printf "${COLOR_CYAN}[INFO]${COLOR_RESET} %s\n" "$*"; } log_ok() { printf "${COLOR_GREEN}[OK]${COLOR_RESET} %s\n" "$*"; } log_warn() { printf "${COLOR_YELLOW}[WARN]${COLOR_RESET} %s\n" "$*"; } # 根据端口查找监听进程 PID pids_by_port() { lsof -iTCP:"$1" -sTCP:LISTEN -n -P -t 2>/dev/null || true } # 终止 PID(先 TERM,2s 后仍存在则 KILL),同时杀掉其子进程组 kill_pid() { local pid="$1" [[ -z "$pid" ]] && return 0 kill -0 "$pid" 2>/dev/null || return 0 # 尝试杀死进程组(负 PID 表示进程组) kill -TERM -"$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true for _ in 1 2 3 4; do kill -0 "$pid" 2>/dev/null || return 0 sleep 0.5 done kill -KILL -"$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true } # 停止由 start.sh 启动的命名服务(以 .pid 文件 + 端口兜底) stop_named() { local name="$1" local port="$2" local pid_file="$RUN_DIR/$name.pid" local killed=0 if [[ -f "$pid_file" ]]; then local pid pid="$(cat "$pid_file")" if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then log_info "停止 $name (PID=$pid, port=$port)..." kill_pid "$pid" killed=1 fi rm -f "$pid_file" fi # 端口兜底:清理任何仍占用目标端口的进程 local port_pids port_pids="$(pids_by_port "$port")" if [[ -n "$port_pids" ]]; then for p in $port_pids; do log_info "清理 $name 残留进程 (PID=$p, port=$port)..." kill_pid "$p" killed=1 done fi if [[ $killed -eq 1 ]]; then log_ok "$name 已停止" else log_warn "$name 未在运行" fi } stop_backend() { stop_named "backend" 8085; } stop_frontend() { stop_named "frontend" 5173; } stop_admin() { stop_named "admin" 3100; } stop_media() { stop_named "media" 3300; } stop_docker() { log_info "停止 Docker 中间件..." cd "$ROOT_DIR/deploy" docker compose -f docker-compose.dev.yml stop postgres redis minio || true log_ok "Docker 中间件已停止(数据卷保留)" } # Phase 2e-2 Task 14:停止 docker compose 全栈(含 media-server、coturn) # 使用 --profile public 确保即使 coturn 在 public profile 下运行也能被停掉 stop_full() { log_info "停止 docker compose 全栈(含 media-server / coturn)..." cd "$DEPLOY_DIR" docker compose -f docker-compose.dev.yml --profile public stop || true log_ok "全栈服务已停止(数据卷保留)" } main() { local target="${1:-app}" case "$target" in app|"") stop_backend stop_media stop_frontend stop_admin log_ok "应用层服务已停止(Docker 中间件保持运行)" ;; --all|all) stop_backend stop_media stop_frontend stop_admin stop_docker log_ok "所有服务已停止" ;; backend) stop_backend ;; frontend) stop_frontend ;; admin) stop_admin ;; media) stop_media ;; docker) stop_docker ;; full) stop_full ;; *) echo "用法: $0 [app|--all|backend|frontend|admin|media|docker|full]" exit 1 ;; esac } main "$@"