Files
opendance-web/deploy/ctl.sh
T
zijunchiang c092b833fb fix(deploy): 支持无 systemd 的环境,并修复降级路径的两个真实缺陷
用户实测:服务器没有 systemctl,install.sh 直接报错退出。

原错误信息本身也是错的(建议 SKIP_NGINX=1,但那只跳过 nginx,仍走 systemctl)。

进程管理改为四层降级:
  systemd → OpenRC → SysVinit → nohup 后台进程(deploy/ctl.sh)
- 新增 deploy/ctl.sh:start/stop/restart/status/logs,PID 文件 + 进程名双重判定
- 新增 deploy/openrc/opendance-api、deploy/initd/opendance-api
- 无 init 系统时写 crontab @reboot 实现开机自启
- uninstall.sh 同步支持四种模式清理(原先只处理 systemd,会留下运行中的进程)
- 汇总输出按实际初始化系统给出对应运维命令

测试中发现并修复降级路径的两个真实缺陷:

1. nohup 无法调用 shell 函数
    实测退出码 127 且什么都不执行
   (nohup 是外部命令,只能执行可执行文件)。
   这会让无 systemd 的降级路径完全失效——服务根本起不来。
   改为按 runuser / setpriv / su 显式拼接成外部命令后再 nohup。
   另加启动后 1 秒存活检测,失败立即打印日志,不再白等 30 秒。

2. find_pids 在无进程时返回一个空格
    在空输入下仍会执行一次 echo,输出空行,
   最终得到 " ";调用方 [ -n ... ] 会判为「有进程」,
   导致 start 直接跳过、status 谎报运行中。改为纯 bash 拼接,空时返回空串。

实测(真实 API 进程,端口 3100):
  start → 健康检查 200,PID 正确
  重复 start → 识别已在运行,进程数保持 1
  status → 运行中
  restart → 新 PID,健康检查 200
  stop → 无残留进程,端口释放
  无 process 时 find_pids 返回空串,is_running=false
2026-09-15 00:34:52 +08:00

179 lines
5.8 KiB
Bash
Executable File
Raw 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.
#!/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' "$*"; }
[ "$(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