thread-master/deploy/scripts/_common.sh

281 lines
8.0 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"
PID_WORKER="$RUN_DIR/worker.pid"
LOG_GATEWAY="$RUN_DIR/gateway.log"
LOG_WEB="$RUN_DIR/web.log"
LOG_WORKER="$RUN_DIR/worker.log"
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
}
# 累積無法殺掉的 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 上的 PIDss / 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
}
kill_port() {
local port="$1"
local p
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"
fi
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "需要指令:$1"
}