#!/bin/bash # ============================================================ # OpenDance Studio Web API —— 进程控制(不依赖 systemd / OpenRC / SysV) # # 适用于没有 systemd 的环境(容器、LXC、精简系统等)。 # 以 nohup 后台运行 Node 进程并记录 PID,日志写入 LOG_FILE。 # # 用法(需 root): # bash deploy/ctl.sh start|stop|restart|status|logs # # 配置来源:$APP_DIR/deploy/.env # 可用环境变量覆盖:APP_DIR / SERVICE_USER / LOG_FILE # ============================================================ set -uo pipefail APP_DIR="${APP_DIR:-/opt/opendance}" SERVICE_USER="${SERVICE_USER:-opendance}" LOG_FILE="${LOG_FILE:-/var/log/opendance-api.log}" RUN_DIR="${RUN_DIR:-/run}" PID_FILE="${PID_FILE:-$RUN_DIR/opendance-api.pid}" MAIN_JS="$APP_DIR/web-server/dist/main.js" die() { printf '\033[1;31m[ctl]\033[0m %s\n' "$*" >&2; exit 1; } log() { printf '\033[1;34m[ctl]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[ctl]\033[0m %s\n' "$*"; } [ "$(id -u)" -eq 0 ] || die "需要 root 权限" # 以服务用户身份执行;不使用 sudo(本脚本已要求 root) as_service_user() { if command -v runuser >/dev/null 2>&1; then runuser -u "$SERVICE_USER" -- "$@" elif command -v setpriv >/dev/null 2>&1; then setpriv --reuid="$SERVICE_USER" --regid="$SERVICE_USER" --init-groups -- "$@" else su -s /bin/bash "$SERVICE_USER" -c "$(printf '%q ' "$@")" fi } # 后台启动服务进程,回显启动器 PID。 # 注意:这里**不能**写成 `nohup as_service_user ...`。 # nohup 是外部命令,只能执行可执行文件,无法调用 shell 函数 # (实测直接退出码 127 且什么都不运行),会让无 systemd 的降级路径完全失效。 # 因此三种降权工具各自显式拼接成外部命令。 spawn_service_process() { local node_bin="$1" script="$2" if command -v runuser >/dev/null 2>&1; then nohup runuser -u "$SERVICE_USER" -- "$node_bin" "$script" >>"$LOG_FILE" 2>&1 & elif command -v setpriv >/dev/null 2>&1; then nohup setpriv --reuid="$SERVICE_USER" --regid="$SERVICE_USER" --init-groups -- \ "$node_bin" "$script" >>"$LOG_FILE" 2>&1 & else nohup su -s /bin/bash "$SERVICE_USER" -c "$(printf '%q ' "$node_bin" "$script")" \ >>"$LOG_FILE" 2>&1 & fi echo $! } node_bin() { command -v node 2>/dev/null || die "未找到 node" } # 按 pid 文件与命令行双重判定,避免 PID 记录失准。 # 返回以空格分隔的 PID 列表;**没有进程时返回空串**。 # 注意:不能再用 `echo "$pids" | xargs -n1` 去重 —— xargs 在空输入下仍会 # 执行一次 echo,输出一个空行,最终得到一个空格。调用方 `[ -n "$(...)" ]` # 会把 " " 判为“有进程”,导致 start 直接跳过、服务根本没起来。 find_pids() { local pids="" if [ -f "$PID_FILE" ]; then local pid pid="$(cat "$PID_FILE" 2>/dev/null || true)" if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then pids="$pid" fi fi # 兜底:命令行里带 main.js 绝对路径的进程(启动时即用绝对路径,故匹配精确) local extra extra="$(pgrep -f -- "$MAIN_JS" 2>/dev/null || true)" for p in $extra; do case " $pids " in *" $p "*) ;; *) pids="${pids:+$pids }$p" ;; esac done printf '%s' "$pids" } is_running() { [ -n "$(find_pids)" ]; } start() { [ -f "$MAIN_JS" ] || die "找不到 $MAIN_JS,请先执行 deploy/install.sh" if is_running; then log "已在运行(PID:$(find_pids)),跳过启动" return 0 fi # 载入部署环境变量 if [ -f "$APP_DIR/deploy/.env" ]; then set -a; . "$APP_DIR/deploy/.env"; set +a else die "缺少 $APP_DIR/deploy/.env" fi mkdir -p "$(dirname "$LOG_FILE")" "$RUN_DIR" touch "$LOG_FILE" chown "$SERVICE_USER:$SERVICE_USER" "$LOG_FILE" 2>/dev/null || true local NODE; NODE="$(node_bin)" log "启动:$MAIN_JS(用户 $SERVICE_USER)" cd "$APP_DIR/web-server" || die "无法进入 $APP_DIR/web-server" # 用绝对路径启动,便于 stop 时精确匹配命令行 local launcher_pid launcher_pid="$(spawn_service_process "$NODE" "$MAIN_JS")" echo "$launcher_pid" > "$PID_FILE" # 先确认启动器没有立刻失败(如降权工具不可用、node 路径错误) sleep 1 if ! is_running; then warn "进程未能启动,日志如下:" tail -20 "$LOG_FILE" >&2 || true rm -f "$PID_FILE" return 1 fi # 等待端口就绪 local port="${PORT:-3000}" for _ in $(seq 1 30); do if command -v curl >/dev/null 2>&1; then curl -fsS -m 2 "http://127.0.0.1:${port}/health" >/dev/null 2>&1 && { log "已就绪(PID:$(find_pids))"; return 0; } else "$NODE" -e 'fetch(process.argv[1]).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))' \ "http://127.0.0.1:${port}/health" >/dev/null 2>&1 && { log "已就绪(PID:$(find_pids))"; return 0; } fi sleep 1 done log "警告:30 秒内未通过健康检查,请查看 $LOG_FILE" tail -20 "$LOG_FILE" >&2 || true return 1 } stop() { local pids; pids="$(find_pids)" if [ -z "$pids" ]; then log "未在运行" rm -f "$PID_FILE" return 0 fi log "停止:$pids" for p in $pids; do kill -TERM "$p" 2>/dev/null || true; done for _ in $(seq 1 15); do is_running || break; sleep 1; done pids="$(find_pids)" if [ -n "$pids" ]; then log "优雅退出超时,强制结束:$pids" for p in $pids; do kill -KILL "$p" 2>/dev/null || true; done fi rm -f "$PID_FILE" log "已停止" } status() { if is_running; then log "运行中(PID:$(find_pids))" return 0 fi log "未运行" return 3 } case "${1:-}" in start) start ;; stop) stop ;; restart) stop; start ;; status) status ;; logs) tail -n "${2:-80}" -f "$LOG_FILE" ;; *) die "用法:bash deploy/ctl.sh start|stop|restart|status|logs" ;; esac