393 lines
16 KiB
Bash
Executable File
393 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# common.sh — 公共函数库,供各子脚本 source 引入
|
||
# 不可直接执行
|
||
# =============================================================================
|
||
|
||
# 防止重复加载
|
||
[[ -n "${_COMMON_LOADED:-}" ]] && return 0
|
||
_COMMON_LOADED=1
|
||
|
||
# ── 根目录(workspace46/)──────────────────────────────────────────────────────
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
|
||
# ── 运行时目录(测试服务器环境) ───────────────────────────────────────────────
|
||
LOG_DIR="$ROOT_DIR/.deploy-test/logs" # 后端服务日志
|
||
PID_DIR="$ROOT_DIR/.deploy-test/pids" # PID 文件(含日志收集进程)
|
||
BUILD_DIR="$ROOT_DIR/.deploy-test/bin" # 编译产物
|
||
DATA_DIR="$ROOT_DIR/.deploy-test/docker-data" # Docker 数据卷
|
||
DOCKER_LOG_DIR="$ROOT_DIR/.deploy-test/docker-logs" # Docker 容器日志
|
||
SCRIPT_LOG_DIR="$ROOT_DIR/.deploy-test/script-logs" # 脚本执行日志
|
||
ENV_FILE="$ROOT_DIR/.env.deploy-test"
|
||
|
||
# ── 颜色 ───────────────────────────────────────────────────────────────────────
|
||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||
|
||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||
step() { echo -e "\n${BOLD}${BLUE}▶ $*${NC}"; }
|
||
header() {
|
||
echo ""
|
||
echo -e "${BOLD}${BLUE}══════════════════════════════════════════${NC}"
|
||
echo -e "${BOLD}${BLUE} $*${NC}"
|
||
echo -e "${BOLD}${BLUE}══════════════════════════════════════════${NC}"
|
||
}
|
||
|
||
# ── 初始化运行时目录 ────────────────────────────────────────────────────────────
|
||
init_dirs() {
|
||
mkdir -p "$LOG_DIR" "$PID_DIR" "$BUILD_DIR" "$DATA_DIR" \
|
||
"$DOCKER_LOG_DIR" "$SCRIPT_LOG_DIR"
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 脚本执行日志
|
||
# 调用位置:每个脚本 init_dirs 之后
|
||
# 效果:所有输出(stdout+stderr)同时写入 .local-dev/script-logs/<name>-<ts>.log
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
init_script_log() {
|
||
local script_name
|
||
script_name="$(basename "${BASH_SOURCE[1]:-$0}" .sh)"
|
||
local ts; ts="$(date +%Y%m%d-%H%M%S)"
|
||
export _CURRENT_SCRIPT_LOG="$SCRIPT_LOG_DIR/${script_name}-${ts}.log"
|
||
mkdir -p "$SCRIPT_LOG_DIR"
|
||
|
||
# 写入文件头(纯文本,不含颜色码)
|
||
{
|
||
echo "========================================"
|
||
echo "Script : $script_name"
|
||
echo "Started: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
echo "========================================"
|
||
} > "$_CURRENT_SCRIPT_LOG"
|
||
|
||
# exec:将所有后续输出同时流向终端和日志文件
|
||
# 用 sed 去除 ANSI 颜色码,保证日志文件可读
|
||
exec > >(tee >(sed $'s/\033\\[[0-9;]*m//g' >> "$_CURRENT_SCRIPT_LOG")) 2>&1
|
||
|
||
info "脚本日志 → $_CURRENT_SCRIPT_LOG"
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Docker 容器日志收集
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
# 启动后台日志收集进程:docker logs -f → 本地文件(按日期滚动)
|
||
start_docker_logger() {
|
||
local cname="$1"
|
||
local svc="${cname#dev-}" # dev-redis → redis
|
||
local log_dir="$DOCKER_LOG_DIR/$svc"
|
||
local logfile="$log_dir/${svc}-$(date +%Y%m%d).log"
|
||
local pid_file="$PID_DIR/docker-log-${cname}.pid"
|
||
|
||
mkdir -p "$log_dir"
|
||
|
||
# 停止已有的收集进程
|
||
if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
|
||
kill "$(cat "$pid_file")" 2>/dev/null || true
|
||
sleep 0.3
|
||
fi
|
||
|
||
# 写分隔符,区分每次启动会话
|
||
{
|
||
echo ""
|
||
echo "──── 容器启动 $(date '+%Y-%m-%d %H:%M:%S') ────"
|
||
} >> "$logfile"
|
||
|
||
# 后台跟踪容器日志(docker logs 本身已含历史,--tail 0 只取新增)
|
||
# 首次启动时先 dump 当前快照,再 follow 新增
|
||
docker logs "$cname" >> "$logfile" 2>&1 || true
|
||
docker logs -f --tail 0 "$cname" >> "$logfile" 2>&1 &
|
||
echo $! > "$pid_file"
|
||
|
||
info " 容器日志 → $logfile"
|
||
}
|
||
|
||
# 停止容器日志收集进程
|
||
stop_docker_logger() {
|
||
local cname="$1"
|
||
local pid_file="$PID_DIR/docker-log-${cname}.pid"
|
||
if [[ -f "$pid_file" ]]; then
|
||
local pid; pid=$(cat "$pid_file")
|
||
kill "$pid" 2>/dev/null || true
|
||
rm -f "$pid_file"
|
||
fi
|
||
}
|
||
|
||
# ── 加载 .env.local ─────────────────────────────────────────────────────────────
|
||
load_env() {
|
||
if [[ ! -f "$ENV_FILE" ]]; then
|
||
error ".env.local 不存在,请先执行: ./deploy-test/01-init-env.sh"
|
||
exit 1
|
||
fi
|
||
set -a
|
||
# shellcheck source=/dev/null
|
||
source "$ENV_FILE"
|
||
set +a
|
||
}
|
||
|
||
# ── 检查必要工具 ────────────────────────────────────────────────────────────────
|
||
require_tools() {
|
||
local missing=()
|
||
for tool in "$@"; do
|
||
command -v "$tool" &>/dev/null || missing+=("$tool")
|
||
done
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
error "缺少必要工具: ${missing[*]}"
|
||
for t in "${missing[@]}"; do
|
||
case "$t" in
|
||
go) echo " → 安装 Go: https://go.dev/dl/" ;;
|
||
docker) echo " → 安装 Docker Desktop: https://www.docker.com/products/docker-desktop/" ;;
|
||
esac
|
||
done
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ── Docker daemon 检查 ──────────────────────────────────────────────────────────
|
||
require_docker_running() {
|
||
require_tools docker
|
||
if ! docker info &>/dev/null; then
|
||
error "Docker daemon 未运行,请启动 Docker Desktop"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ── 启动单个后端服务(nohup 后台) ───────────────────────────────────────────────
|
||
start_svc() {
|
||
local name="$1" bin="$2" args="${3:-}" workdir="${4:-$ROOT_DIR}"
|
||
local pidfile="$PID_DIR/$name.pid" logfile="$LOG_DIR/$name.log"
|
||
|
||
if [[ -f "$pidfile" ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then
|
||
warn "$name 已在运行 (PID=$(cat "$pidfile")),跳过"
|
||
return 0
|
||
fi
|
||
|
||
[[ ! -f "$bin" ]] && { error "$name 二进制不存在 ($bin),请先执行 04-build.sh"; return 1; }
|
||
|
||
info "启动 $name ..."
|
||
(
|
||
cd "$workdir"
|
||
# shellcheck disable=SC2086
|
||
nohup "$bin" $args > "$logfile" 2>&1 &
|
||
echo $! > "$pidfile"
|
||
)
|
||
sleep 1
|
||
if kill -0 "$(cat "$pidfile")" 2>/dev/null; then
|
||
success " $name 已启动 (PID=$(cat "$pidfile")) → $logfile"
|
||
else
|
||
error " $name 启动失败,查看日志:"
|
||
tail -20 "$logfile" 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ── 停止单个后端服务 ────────────────────────────────────────────────────────────
|
||
stop_svc() {
|
||
local name="$1"
|
||
local pidfile="$PID_DIR/$name.pid"
|
||
if [[ -f "$pidfile" ]]; then
|
||
local pid; pid=$(cat "$pidfile")
|
||
if kill -0 "$pid" 2>/dev/null; then
|
||
kill "$pid" && success "$name 已停止 (PID=$pid)"
|
||
else
|
||
warn "$name 进程 $pid 不存在(可能已退出)"
|
||
fi
|
||
rm -f "$pidfile"
|
||
else
|
||
warn "$name 没有 PID 记录(未运行)"
|
||
fi
|
||
}
|
||
|
||
# ── Docker 容器状态打印 ─────────────────────────────────────────────────────────
|
||
print_container_status() {
|
||
local label="$1" cname="$2" port="$3"
|
||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
|
||
printf " ${GREEN}●${NC} %-12s container=%-14s :%-5s\n" "$label" "$cname" "$port"
|
||
elif docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
|
||
printf " ${YELLOW}○${NC} %-12s stopped=%-14s :%-5s\n" "$label" "$cname" "$port"
|
||
else
|
||
printf " ${RED}✗${NC} %-12s (未创建) :%-5s\n" "$label" "$port"
|
||
fi
|
||
}
|
||
|
||
# ── 后端服务状态打印 ────────────────────────────────────────────────────────────
|
||
print_svc_status() {
|
||
local name="$1" desc="$2"
|
||
local pidfile="$PID_DIR/$name.pid"
|
||
if [[ -f "$pidfile" ]] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then
|
||
printf " ${GREEN}●${NC} %-18s PID=%-7s %s\n" "$name" "$(cat "$pidfile")" "$desc"
|
||
else
|
||
printf " ${RED}○${NC} %-18s %-11s %s\n" "$name" "未运行" "$desc"
|
||
fi
|
||
}
|
||
|
||
# ── 所有后端服务名列表 ──────────────────────────────────────────────────────────
|
||
ALL_SVCS=(openim-server chat-rpc admin-rpc chat-api admin-api meetingmsg livecloud livestream build-server)
|
||
|
||
# ── PC 反代入口环境归一化 ───────────────────────────────────────────────────────
|
||
normalize_pc_proxy_env() {
|
||
PC_PROXY_DOMAIN="${PC_PROXY_DOMAIN:-pc-jack.imharry.work}"
|
||
local default_origin="https://${PC_PROXY_DOMAIN}"
|
||
local origin="${PC_BACKEND_ORIGIN:-}"
|
||
local legacy_cms_origin="https://cms-jack.imharry.work"
|
||
local legacy_cms_origin_http="http://cms-jack.imharry.work"
|
||
origin="${origin%/}"
|
||
|
||
if [[ -z "$origin" ]]; then
|
||
PC_BACKEND_ORIGIN="$default_origin"
|
||
elif [[ "$origin" == "$legacy_cms_origin" ]] || [[ "$origin" == "$legacy_cms_origin_http" ]]; then
|
||
warn " 检测到旧 PC_BACKEND_ORIGIN=${origin},已切换为 ${default_origin}"
|
||
PC_BACKEND_ORIGIN="$default_origin"
|
||
elif [[ -n "${DEPLOY_TEST_IP:-}" ]] && [[ "$origin" =~ ^https?://${DEPLOY_TEST_IP}(:[0-9]+)?$ ]]; then
|
||
warn " 检测到旧 PC_BACKEND_ORIGIN=${origin},已切换为 ${default_origin}"
|
||
PC_BACKEND_ORIGIN="$default_origin"
|
||
elif [[ "$origin" == "http://${PC_PROXY_DOMAIN}" ]]; then
|
||
warn " 检测到 HTTP PC_BACKEND_ORIGIN=${origin},已切换为 ${default_origin}"
|
||
PC_BACKEND_ORIGIN="$default_origin"
|
||
else
|
||
PC_BACKEND_ORIGIN="$origin"
|
||
fi
|
||
|
||
export PC_PROXY_DOMAIN PC_BACKEND_ORIGIN
|
||
}
|
||
|
||
# ── PC 前端 Vite 环境(不写 pc 目录,由 07-start-frontend 在子 shell 内 export)────────
|
||
# 依赖 .env.deploy-test / .env.deploy-local 中的 PC_BACKEND_ORIGIN(及可选 PC_VITE_*)
|
||
pc_export_vite_backend_env() {
|
||
normalize_pc_proxy_env
|
||
local o="${PC_BACKEND_ORIGIN:-}"
|
||
o="${o%/}"
|
||
[[ -z "$o" ]] && return 0
|
||
|
||
local host
|
||
host=$(printf '%s' "$o" | sed -E 's#^https?://([^/]+).*#\1#')
|
||
[[ -z "$host" ]] && return 0
|
||
|
||
export VITE_BASE_DOMAIN="${PC_VITE_BASE_DOMAIN:-$host}"
|
||
export VITE_API_URL="${PC_VITE_API_URL:-$o/api/im}"
|
||
export VITE_CHAT_URL="${PC_VITE_CHAT_URL:-$o/api/chat}"
|
||
export VITE_USER_URL="${PC_VITE_USER_URL:-$o/api/user}"
|
||
if [[ "$o" == http://* ]]; then
|
||
export VITE_WS_URL="${PC_VITE_WS_URL:-ws://${host}/msg_gateway}"
|
||
else
|
||
export VITE_WS_URL="${PC_VITE_WS_URL:-wss://${host}/msg_gateway}"
|
||
fi
|
||
}
|
||
|
||
pc_print_vite_backend_env() {
|
||
if [[ -z "${PC_BACKEND_ORIGIN:-}" ]]; then
|
||
warn " pc 未设置 PC_BACKEND_ORIGIN,将使用 pc/.env 或代码默认地址;WebSocket 可能连错环境"
|
||
return 0
|
||
fi
|
||
|
||
info " pc 后端 PC_BACKEND_ORIGIN=${PC_BACKEND_ORIGIN}(注入 VITE_*,不写 pc 目录)"
|
||
info " VITE_API_URL=${VITE_API_URL:-<empty>}"
|
||
info " VITE_WS_URL=${VITE_WS_URL:-<empty>}"
|
||
info " VITE_CHAT_URL=${VITE_CHAT_URL:-<empty>}"
|
||
info " VITE_USER_URL=${VITE_USER_URL:-<empty>}"
|
||
info " VITE_ADMIN_URL=${VITE_ADMIN_URL:-${PC_VITE_ADMIN_URL:-<empty>}}"
|
||
|
||
if [[ "$PC_BACKEND_ORIGIN" == http://* ]] && [[ "$PC_BACKEND_ORIGIN" != http://127.0.0.1* ]] && [[ "$PC_BACKEND_ORIGIN" != http://localhost* ]]; then
|
||
warn " PC_BACKEND_ORIGIN 当前是 HTTP: ${PC_BACKEND_ORIGIN};推荐使用 https://${VITE_BASE_DOMAIN}"
|
||
fi
|
||
}
|
||
|
||
pc_check_nginx_gateway() {
|
||
local o="${PC_BACKEND_ORIGIN:-}"
|
||
o="${o%/}"
|
||
[[ -z "$o" ]] && return 0
|
||
|
||
if ! command -v curl &>/dev/null; then
|
||
warn " 未安装 curl,跳过 Nginx 网关检查;请手动访问 ${o}/nginx-health"
|
||
return 0
|
||
fi
|
||
|
||
local curl_tls=()
|
||
[[ "$o" == https://* ]] && curl_tls=(-k)
|
||
|
||
if curl "${curl_tls[@]}" -fsS --max-time 3 "${o}/nginx-health" >/dev/null 2>&1; then
|
||
success " Nginx 网关可达: ${o}/nginx-health"
|
||
else
|
||
warn " Nginx 网关不可达: ${o}/nginx-health"
|
||
warn " PC WebSocket 默认连 ${VITE_WS_URL:-wss://<host>/msg_gateway};请确认已执行 sudo ./deploy-test/00-init-tools.sh nginx,本机 Nginx 放行 TCP 80,外层 HTTPS 入口放行 TCP 443"
|
||
fi
|
||
|
||
pc_probe_msg_gateway "$o"
|
||
}
|
||
|
||
pc_probe_msg_gateway() {
|
||
local o="${1:-${PC_BACKEND_ORIGIN:-}}"
|
||
o="${o%/}"
|
||
[[ -z "$o" ]] && return 0
|
||
|
||
local gateway_pid
|
||
gateway_pid=$(lsof -ti :10001 2>/dev/null | head -1 || true)
|
||
if [[ -n "$gateway_pid" ]]; then
|
||
success " MsgGateway 本机端口监听正常: 127.0.0.1:10001 (PID=${gateway_pid})"
|
||
else
|
||
warn " MsgGateway 本机端口未监听: 127.0.0.1:10001(请确认 openim-server 已启动)"
|
||
fi
|
||
|
||
info " MsgGateway Nginx 入口: ${o}/msg_gateway(不做无 token HTTP 探测,避免在 openim-server.log 中产生 token is empty 告警)"
|
||
}
|
||
|
||
pc_check_wasm_assets() {
|
||
local origin="${1:-}"
|
||
origin="${origin%/}"
|
||
[[ -z "$origin" ]] && return 0
|
||
|
||
if ! command -v curl &>/dev/null; then
|
||
warn " 未安装 curl,跳过 PC WASM 资源检查"
|
||
return 0
|
||
fi
|
||
|
||
local curl_tls=()
|
||
[[ "$origin" == https://* ]] && curl_tls=(-k)
|
||
|
||
local asset url ct
|
||
for asset in \
|
||
openIM.wasm \
|
||
sql-wasm.wasm \
|
||
wasm_exec.js \
|
||
node_modules/@openim/wasm-client-sdk/lib/worker.js \
|
||
node_modules/@openim/wasm-client-sdk/lib/worker-legacy.js; do
|
||
url="${origin}/${asset}"
|
||
ct=$(curl "${curl_tls[@]}" -fsSI --max-time 5 "$url" 2>/dev/null | awk 'BEGIN{IGNORECASE=1} /^content-type:/ {sub(/\r$/, ""); print $0; exit}' || true)
|
||
if [[ -n "$ct" ]] || curl "${curl_tls[@]}" -fsS --max-time 5 -r 0-0 "$url" >/dev/null 2>&1; then
|
||
success " PC SDK 资源可达: ${url}${ct:+ (${ct#*: })}"
|
||
else
|
||
warn " PC SDK 资源不可达: ${url}(SDK login 可能卡住且不会发起 /msg_gateway WebSocket)"
|
||
fi
|
||
done
|
||
|
||
info " 浏览器侧 SDK 检查: typeof Go / typeof window.initSDK / typeof window.login / typeof window.commonEventFunc"
|
||
}
|
||
|
||
pc_ensure_wasm_assets() {
|
||
local public_dir="$ROOT_DIR/pc/public"
|
||
local assets_dir="$ROOT_DIR/pc/node_modules/@openim/wasm-client-sdk/assets"
|
||
local asset src dst size
|
||
|
||
for asset in openIM.wasm sql-wasm.wasm wasm_exec.js; do
|
||
src="$assets_dir/$asset"
|
||
dst="$public_dir/$asset"
|
||
|
||
if [[ -s "$dst" ]]; then
|
||
continue
|
||
fi
|
||
|
||
if [[ ! -s "$src" ]]; then
|
||
warn " PC SDK 资源缺失且无法自动补齐: $dst(未找到 $src)"
|
||
continue
|
||
fi
|
||
|
||
mkdir -p "$public_dir"
|
||
cp "$src" "$dst"
|
||
chmod 0644 "$dst" 2>/dev/null || true
|
||
size=$(wc -c < "$dst" 2>/dev/null | tr -d ' ' || true)
|
||
success " 已补齐 PC SDK 资源: $dst${size:+ (${size} bytes)}"
|
||
done
|
||
}
|