#!/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/-.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 前端 Vite 环境(不写 pc 目录,由 07-start-frontend 在子 shell 内 export)──────── # 依赖 .env.deploy-test / .env.deploy-local 中的 PC_BACKEND_ORIGIN(及可选 PC_VITE_*) pc_export_vite_backend_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:-}" info " VITE_WS_URL=${VITE_WS_URL:-}" info " VITE_CHAT_URL=${VITE_CHAT_URL:-}" info " VITE_USER_URL=${VITE_USER_URL:-}" info " VITE_ADMIN_URL=${VITE_ADMIN_URL:-${PC_VITE_ADMIN_URL:-}}" if [[ "$PC_BACKEND_ORIGIN" == http://* ]] && [[ "$PC_BACKEND_ORIGIN" != http://127.0.0.1* ]] && [[ "$PC_BACKEND_ORIGIN" != http://localhost* ]]; then warn " PC_BACKEND_ORIGIN 仍是 HTTP 公网入口;OpenIM WASM DB worker 可能继续 initDB 超时。建议改为 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:-ws:///msg_gateway};请确认已执行 sudo ./deploy-test/00-init-tools.sh nginx,并放行 TCP 80/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 }