2026-07-10 12:54:45 +00:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
# shellcheck disable=SC2034
|
|
|
|
|
|
# 共用:路徑、載入 env、log helper。由其他腳本 source。
|
|
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
|
|
ROOT_DIR="$(cd "$DEPLOY_DIR/.." && pwd)"
|
|
|
|
|
|
BACKEND_DIR="$ROOT_DIR/apps/backend"
|
|
|
|
|
|
WEB_DIR="$ROOT_DIR/apps/web"
|
|
|
|
|
|
RUN_DIR="$DEPLOY_DIR/run"
|
|
|
|
|
|
COMPOSE_FILE="$DEPLOY_DIR/docker-compose.infra.yml"
|
|
|
|
|
|
PID_GATEWAY="$RUN_DIR/gateway.pid"
|
|
|
|
|
|
PID_WEB="$RUN_DIR/web.pid"
|
2026-07-13 01:15:30 +00:00
|
|
|
|
PID_WORKER="$RUN_DIR/worker.pid"
|
2026-07-10 12:54:45 +00:00
|
|
|
|
LOG_GATEWAY="$RUN_DIR/gateway.log"
|
|
|
|
|
|
LOG_WEB="$RUN_DIR/web.log"
|
2026-07-13 01:15:30 +00:00
|
|
|
|
LOG_WORKER="$RUN_DIR/worker.log"
|
2026-07-10 12:54:45 +00:00
|
|
|
|
|
|
|
|
|
|
mkdir -p "$RUN_DIR"
|
|
|
|
|
|
|
|
|
|
|
|
log() { printf '[deploy] %s\n' "$*"; }
|
|
|
|
|
|
err() { printf '[deploy] ERROR: %s\n' "$*" >&2; }
|
|
|
|
|
|
die() { err "$*"; exit 1; }
|
|
|
|
|
|
|
|
|
|
|
|
# 優先 .env.dev,其次 .env
|
|
|
|
|
|
resolve_env_file() {
|
|
|
|
|
|
if [[ -n "${ENV_FILE:-}" && -f "$ENV_FILE" ]]; then
|
|
|
|
|
|
echo "$ENV_FILE"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [[ -f "$DEPLOY_DIR/.env.dev" ]]; then
|
|
|
|
|
|
echo "$DEPLOY_DIR/.env.dev"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [[ -f "$DEPLOY_DIR/.env" ]]; then
|
|
|
|
|
|
echo "$DEPLOY_DIR/.env"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
load_env() {
|
|
|
|
|
|
local f
|
|
|
|
|
|
if ! f="$(resolve_env_file)"; then
|
|
|
|
|
|
die "找不到 env。請先:cp deploy/.env.example deploy/.env.dev 並填密鑰"
|
|
|
|
|
|
fi
|
|
|
|
|
|
log "env: $f"
|
|
|
|
|
|
set -a
|
|
|
|
|
|
# shellcheck disable=SC1090
|
|
|
|
|
|
source "$f"
|
|
|
|
|
|
set +a
|
|
|
|
|
|
|
|
|
|
|
|
# 別名正規化(給 gateway ApplyEnv / migrate)
|
|
|
|
|
|
export MONGO_URI="${MONGO_URI:-${HAIXUN_MONGO_URI:-}}"
|
|
|
|
|
|
if [[ -z "${MONGO_URL:-}" ]]; then
|
|
|
|
|
|
if [[ -n "${MONGO_URI:-}" ]]; then
|
|
|
|
|
|
export MONGO_URL="$(
|
|
|
|
|
|
MONGO_URI="$MONGO_URI" MONGO_DATABASE="${MONGO_DATABASE:-haixun}" python3 -c "
|
|
|
|
|
|
import os, re
|
|
|
|
|
|
u = os.environ.get('MONGO_URI', '')
|
|
|
|
|
|
db = os.environ.get('MONGO_DATABASE', 'haixun')
|
|
|
|
|
|
if re.search(r'/\?', u):
|
|
|
|
|
|
print(re.sub(r'/\?', f'/{db}?', u, count=1))
|
|
|
|
|
|
elif re.search(r'/' + re.escape(db) + r'(\?|$)', u):
|
|
|
|
|
|
print(u)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(u.rstrip('/') + f'/{db}')
|
|
|
|
|
|
"
|
|
|
|
|
|
)"
|
|
|
|
|
|
else
|
|
|
|
|
|
local user="${MONGO_ROOT_USER:-haixun}"
|
|
|
|
|
|
local pass="${MONGO_ROOT_PASSWORD:-}"
|
|
|
|
|
|
local db="${MONGO_DATABASE:-haixun}"
|
|
|
|
|
|
export MONGO_URI="mongodb://${user}:${pass}@127.0.0.1:27017/?authSource=admin"
|
|
|
|
|
|
export MONGO_URL="mongodb://${user}:${pass}@127.0.0.1:27017/${db}?authSource=admin"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
export REDIS_PASSWORD="${REDIS_PASSWORD:-${HAIXUN_REDIS_PASSWORD:-}}"
|
|
|
|
|
|
export HAIXUN_REDIS_PASSWORD="${HAIXUN_REDIS_PASSWORD:-$REDIS_PASSWORD}"
|
|
|
|
|
|
|
|
|
|
|
|
export AUTH_ACCESS_SECRET="${AUTH_ACCESS_SECRET:-${HAIXUN_JWT_ACCESS_SECRET:-}}"
|
|
|
|
|
|
export AUTH_REFRESH_SECRET="${AUTH_REFRESH_SECRET:-${HAIXUN_JWT_REFRESH_SECRET:-}}"
|
|
|
|
|
|
|
|
|
|
|
|
export HAIXUN_STORAGE_S3_ACCESS_KEY="${HAIXUN_STORAGE_S3_ACCESS_KEY:-${MINIO_ROOT_USER:-haixun}}"
|
|
|
|
|
|
export HAIXUN_STORAGE_S3_SECRET_KEY="${HAIXUN_STORAGE_S3_SECRET_KEY:-${MINIO_ROOT_PASSWORD:-}}"
|
|
|
|
|
|
export MINIO_ROOT_USER="${MINIO_ROOT_USER:-haixun}"
|
|
|
|
|
|
export MINIO_ROOT_PASSWORD="${MINIO_ROOT_PASSWORD:-$HAIXUN_STORAGE_S3_SECRET_KEY}"
|
|
|
|
|
|
|
|
|
|
|
|
export OBJECT_STORAGE_PUBLIC_BASE_URL="${OBJECT_STORAGE_PUBLIC_BASE_URL:-${HAIXUN_STORAGE_S3_PUBLIC_BASE_URL:-}}"
|
|
|
|
|
|
export HAIXUN_STORAGE_S3_PUBLIC_BASE_URL="${HAIXUN_STORAGE_S3_PUBLIC_BASE_URL:-$OBJECT_STORAGE_PUBLIC_BASE_URL}"
|
|
|
|
|
|
|
|
|
|
|
|
export PUBLIC_WEB_BASE="${PUBLIC_WEB_BASE:-http://127.0.0.1:5173}"
|
|
|
|
|
|
export GATEWAY_PORT="${GATEWAY_PORT:-8888}"
|
|
|
|
|
|
export WEB_PORT="${WEB_PORT:-5173}"
|
|
|
|
|
|
|
|
|
|
|
|
ENV_FILE_RESOLVED="$f"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
compose() {
|
|
|
|
|
|
local f
|
|
|
|
|
|
f="$(resolve_env_file)" || die "no env"
|
|
|
|
|
|
docker compose --env-file "$f" -f "$COMPOSE_FILE" "$@"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pid_alive() {
|
|
|
|
|
|
local pid="${1:-}"
|
|
|
|
|
|
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 01:15:30 +00:00
|
|
|
|
# 累積無法殺掉的 pid(給最後印 sudo 指令)
|
|
|
|
|
|
STUCK_PIDS=""
|
|
|
|
|
|
|
|
|
|
|
|
# 溫和 → 強制殺掉一個 pid(必要時試 sudo -n)
|
|
|
|
|
|
kill_pid() {
|
|
|
|
|
|
local pid="${1:-}"
|
|
|
|
|
|
local label="${2:-pid}"
|
|
|
|
|
|
[[ -z "$pid" || ! "$pid" =~ ^[0-9]+$ ]] && return 0
|
|
|
|
|
|
[[ "$pid" == "$$" || "$pid" == "$PPID" ]] && return 0
|
|
|
|
|
|
|
|
|
|
|
|
log "kill $label pid=$pid"
|
|
|
|
|
|
# 一般使用者
|
|
|
|
|
|
if kill -TERM "$pid" 2>/dev/null; then
|
|
|
|
|
|
sleep 0.35
|
|
|
|
|
|
kill -KILL "$pid" 2>/dev/null || true
|
|
|
|
|
|
sleep 0.15
|
|
|
|
|
|
if ! kill -0 "$pid" 2>/dev/null; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
# passwordless sudo
|
|
|
|
|
|
if command -v sudo >/dev/null 2>&1; then
|
|
|
|
|
|
if sudo -n kill -TERM "$pid" 2>/dev/null; then
|
|
|
|
|
|
sleep 0.35
|
|
|
|
|
|
sudo -n kill -KILL "$pid" 2>/dev/null || true
|
|
|
|
|
|
sleep 0.15
|
|
|
|
|
|
if ! sudo -n kill -0 "$pid" 2>/dev/null; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
err "無法結束 $label pid=$pid(權限不足,多半是 root 起的)"
|
|
|
|
|
|
STUCK_PIDS="${STUCK_PIDS} ${pid}"
|
|
|
|
|
|
return 0 # 不讓 set -e 中斷;由呼叫端看 STUCK_PIDS / 埠
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 找出 listen 在 port 上的 PID(ss / fuser / lsof / /proc inode)
|
|
|
|
|
|
pids_on_port() {
|
|
|
|
|
|
local port="$1"
|
|
|
|
|
|
local out="" p
|
|
|
|
|
|
p="$(ss -tlnp 2>/dev/null | sed -n "s/.*:${port} .*pid=\([0-9]*\).*/\1/p" | sort -u | tr '\n' ' ' || true)"
|
|
|
|
|
|
out="$out $p"
|
|
|
|
|
|
if command -v fuser >/dev/null 2>&1; then
|
|
|
|
|
|
p="$( { fuser "${port}/tcp" 2>/dev/null || true; } | grep -oE '[0-9]+' | tr '\n' ' ' || true)"
|
|
|
|
|
|
out="$out $p"
|
|
|
|
|
|
fi
|
|
|
|
|
|
if command -v lsof >/dev/null 2>&1; then
|
|
|
|
|
|
p="$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)"
|
|
|
|
|
|
out="$out $p"
|
|
|
|
|
|
fi
|
|
|
|
|
|
p="$(
|
|
|
|
|
|
PORT="$port" python3 - <<'PY' 2>/dev/null || true
|
|
|
|
|
|
import os
|
|
|
|
|
|
port = int(os.environ["PORT"])
|
|
|
|
|
|
inodes = set()
|
|
|
|
|
|
for path in ("/proc/net/tcp", "/proc/net/tcp6"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
lines = open(path).read().splitlines()[1:]
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
for line in lines:
|
|
|
|
|
|
parts = line.split()
|
|
|
|
|
|
if len(parts) < 10 or parts[3] != "0A":
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
p = int(parts[1].rsplit(":", 1)[1], 16)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if p == port:
|
|
|
|
|
|
inodes.add(parts[9])
|
|
|
|
|
|
found = []
|
|
|
|
|
|
if inodes:
|
|
|
|
|
|
for pid in os.listdir("/proc"):
|
|
|
|
|
|
if not pid.isdigit():
|
|
|
|
|
|
continue
|
|
|
|
|
|
fd_dir = f"/proc/{pid}/fd"
|
|
|
|
|
|
try:
|
|
|
|
|
|
names = os.listdir(fd_dir)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
for name in names:
|
|
|
|
|
|
try:
|
|
|
|
|
|
target = os.readlink(f"{fd_dir}/{name}")
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if target.startswith("socket:[") and target[8:-1] in inodes:
|
|
|
|
|
|
found.append(pid)
|
|
|
|
|
|
break
|
|
|
|
|
|
print(" ".join(found))
|
|
|
|
|
|
PY
|
|
|
|
|
|
)"
|
|
|
|
|
|
out="$out $p"
|
|
|
|
|
|
echo "$out" | tr -s '[:space:]' '\n' | grep -E '^[0-9]+$' | sort -u | tr '\n' ' ' || true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 12:54:45 +00:00
|
|
|
|
kill_port() {
|
|
|
|
|
|
local port="$1"
|
|
|
|
|
|
local p
|
2026-07-13 01:15:30 +00:00
|
|
|
|
for p in $(pids_on_port "$port"); do
|
|
|
|
|
|
kill_pid "$p" "port:${port}" || true
|
|
|
|
|
|
done
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 依 cmdline 關鍵字找 PID(不用 pgrep -f,避免誤殺本腳本)
|
|
|
|
|
|
pids_by_cmdline_substr() {
|
|
|
|
|
|
local a="${1:-}" b="${2:-}"
|
|
|
|
|
|
[[ -z "$a" ]] && return 0
|
|
|
|
|
|
SUB_A="$a" SUB_B="$b" SELF="$$" python3 - <<'PY' 2>/dev/null || true
|
|
|
|
|
|
import os
|
|
|
|
|
|
a = os.environ.get("SUB_A", "")
|
|
|
|
|
|
b = os.environ.get("SUB_B", "")
|
|
|
|
|
|
self_pid = os.environ.get("SELF", "")
|
|
|
|
|
|
out = []
|
|
|
|
|
|
for pid in os.listdir("/proc"):
|
|
|
|
|
|
if not pid.isdigit() or pid == self_pid:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
raw = open(f"/proc/{pid}/cmdline", "rb").read()
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
cmd = raw.replace(b"\x00", b" ").decode("utf-8", "replace")
|
|
|
|
|
|
if a not in cmd:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if b and b not in cmd:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if any(x in cmd for x in ("restart.sh", "stop.sh", "start.sh", "deploy/scripts")):
|
|
|
|
|
|
continue
|
|
|
|
|
|
out.append(pid)
|
|
|
|
|
|
print(" ".join(out))
|
|
|
|
|
|
PY
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
port_in_use() {
|
|
|
|
|
|
local port="$1"
|
|
|
|
|
|
if ss -tln 2>/dev/null | grep -qE "[:.]${port}[[:space:]]"; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wait_port_free() {
|
|
|
|
|
|
local port="$1"
|
|
|
|
|
|
local secs="${2:-8}"
|
|
|
|
|
|
local i
|
|
|
|
|
|
for i in $(seq 1 "$secs"); do
|
|
|
|
|
|
if ! port_in_use "$port"; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
sleep 0.5
|
|
|
|
|
|
done
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print_stuck_hint() {
|
|
|
|
|
|
local uniq
|
|
|
|
|
|
# grep 無 match 時 exit 1 — 必須 || true,否則 set -o pipefail 會讓 stop 整段失敗
|
|
|
|
|
|
uniq="$(echo "${STUCK_PIDS:-}" | tr -s ' ' '\n' | grep -E '^[0-9]+$' | sort -u | tr '\n' ' ' || true)"
|
|
|
|
|
|
uniq="$(echo "$uniq" | xargs 2>/dev/null || true)"
|
|
|
|
|
|
if [[ -n "$uniq" ]]; then
|
|
|
|
|
|
err "下列 process 殺不掉(多半是 root 啟動的):${uniq}"
|
|
|
|
|
|
err "請執行: sudo kill -9 ${uniq}"
|
|
|
|
|
|
err "然後再: make -C deploy restart"
|
2026-07-10 12:54:45 +00:00
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
require_cmd() {
|
|
|
|
|
|
command -v "$1" >/dev/null 2>&1 || die "需要指令:$1"
|
|
|
|
|
|
}
|