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
This commit is contained in:
2026-09-15 00:34:52 +08:00
parent 1ffe3059af
commit c092b833fb
7 changed files with 421 additions and 30 deletions
+102 -19
View File
@@ -92,8 +92,6 @@ API_PORT="${PORT:-3000}"
[ -n "${DB_PASS:-}" ] || die "deploy/.env 中 DB_PASS 未设置"
[ -n "${DB_NAME:-}" ] || die "deploy/.env 中 DB_NAME 未设置"
command -v systemctl >/dev/null 2>&1 || die "未找到 systemctl。本部署依赖 systemd 托管 Node 进程;
若目标系统没有 systemd(如容器/WSL1),请改用 SKIP_NGINX=1 手动以进程方式运行,或换一台服务器。"
command -v node >/dev/null 2>&1 || die "未找到 node。请安装 Node.js 22 或更高版本。"
NODE_BIN="$(command -v node)"
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
@@ -233,19 +231,81 @@ as_service_user env \
"DB_PASS=${DB_PASS}" "DB_NAME=${DB_NAME}" "DB_LOGGING=${DB_LOGGING:-false}" \
"$NODE_BIN" scripts/migrate.mjs run
# ---------- 4. systemd 服务 ----------
log "安装 systemd 服务"
SERVICE_FILE=/etc/systemd/system/opendance-api.service
sed -e "s|__SERVICE_USER__|$SERVICE_USER|g" \
-e "s|__APP_DIR__|$APP_DIR|g" \
-e "s|__NODE_BIN__|$NODE_BIN|g" \
-e "s|__STORAGE_PARENT__|$(dirname "$STORAGE_DIR")|g" \
"$APP_DIR/deploy/systemd/opendance-api.service" > "$SERVICE_FILE"
chmod 644 "$SERVICE_FILE"
# ---------- 4. 安装为系统服务(按可用的初始化系统降级) ----------
# systemd → OpenRC → SysVinit → 纯后台进程(nohup + PID 文件)
# “运行在 Linux 上”不应等于“必须有 systemd”:容器 / LXC / 精简系统都很常见。
INIT_SYSTEM=""
if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
INIT_SYSTEM=systemd
elif command -v rc-service >/dev/null 2>&1 && command -v rc-update >/dev/null 2>&1; then
INIT_SYSTEM=openrc
elif [ -d /etc/init.d ] && { command -v update-rc.d >/dev/null 2>&1 || command -v chkconfig >/dev/null 2>&1; }; then
INIT_SYSTEM=sysvinit
else
INIT_SYSTEM=none
fi
log "初始化系统:$INIT_SYSTEM"
systemctl daemon-reload
systemctl enable opendance-api >/dev/null
systemctl restart opendance-api
case "$INIT_SYSTEM" in
systemd)
log "安装 systemd 服务"
SERVICE_FILE=/etc/systemd/system/opendance-api.service
sed -e "s|__SERVICE_USER__|$SERVICE_USER|g" \
-e "s|__APP_DIR__|$APP_DIR|g" \
-e "s|__NODE_BIN__|$NODE_BIN|g" \
-e "s|__STORAGE_PARENT__|$(dirname "$STORAGE_DIR")|g" \
"$APP_DIR/deploy/systemd/opendance-api.service" > "$SERVICE_FILE"
chmod 644 "$SERVICE_FILE"
systemctl daemon-reload
systemctl enable opendance-api >/dev/null
systemctl restart opendance-api
;;
openrc)
log "安装 OpenRC 服务"
sed -e "s|__SERVICE_USER__|$SERVICE_USER|g" \
-e "s|__APP_DIR__|$APP_DIR|g" \
-e "s|__NODE_BIN__|$NODE_BIN|g" \
"$APP_DIR/deploy/openrc/opendance-api" > /etc/init.d/opendance-api
chmod 755 /etc/init.d/opendance-api
rc-update add opendance-api default >/dev/null 2>&1 || warn "rc-update add 失败,请手动加入默认运行级"
rc-service opendance-api restart 2>/dev/null || rc-service opendance-api start
;;
sysvinit)
log "安装 SysVinit 服务"
sed -e "s|__SERVICE_USER__|$SERVICE_USER|g" \
-e "s|__APP_DIR__|$APP_DIR|g" \
"$APP_DIR/deploy/initd/opendance-api" > /etc/init.d/opendance-api
chmod 755 /etc/init.d/opendance-api
if command -v update-rc.d >/dev/null 2>&1; then
update-rc.d opendance-api defaults >/dev/null 2>&1 || warn "update-rc.d 失败,请手动建立启动链接"
elif command -v chkconfig >/dev/null 2>&1; then
chkconfig --add opendance-api >/dev/null 2>&1 || true
fi
/etc/init.d/opendance-api restart
;;
none)
warn "未检测到 systemd / OpenRC / SysVinit,改用后台进程方式运行(nohup + PID 文件)"
chmod +x "$APP_DIR/deploy/ctl.sh"
APP_DIR="$APP_DIR" SERVICE_USER="$SERVICE_USER" bash "$APP_DIR/deploy/ctl.sh" restart
# 尝试用 cron 的 @reboot 实现开机自启(没有 cron 就只提示)
if command -v crontab >/dev/null 2>&1; then
CRON_LINE="@reboot /bin/bash $APP_DIR/deploy/ctl.sh start >/dev/null 2>&1"
if crontab -l 2>/dev/null | grep -qF "$APP_DIR/deploy/ctl.sh start"; then
log "开机自启已存在(crontab @reboot)"
else
( crontab -l 2>/dev/null; echo "$CRON_LINE" ) | crontab -
log "已写入开机自启:crontab @reboot"
fi
else
warn "未找到 crontab,无法配置开机自启。重启后请手动执行:"
warn " bash $APP_DIR/deploy/ctl.sh start"
fi
;;
esac
log "等待 API 就绪…"
READY=0
@@ -303,6 +363,31 @@ fi
# ---------- 6. 完成 ----------
HOST_HINT="${DEPLOY_HOST:-<服务器IP>}"
# 按实际使用的初始化系统给出对应的运维命令(用变量拼装,避免嵌套 heredoc 出错)
case "$INIT_SYSTEM" in
systemd)
CMD_HINT=" systemctl status opendance-api # 服务状态
journalctl -u opendance-api -f # 实时日志
systemctl restart opendance-api # 重启"
;;
openrc)
CMD_HINT=" rc-service opendance-api status # 服务状态
rc-service opendance-api restart # 重启
tail -f /var/log/opendance-api.log # 实时日志"
;;
sysvinit)
CMD_HINT=" /etc/init.d/opendance-api status # 服务状态
/etc/init.d/opendance-api restart # 重启
tail -f /var/log/opendance-api.log # 实时日志"
;;
*)
CMD_HINT=" bash ${APP_DIR}/deploy/ctl.sh status # 服务状态
bash ${APP_DIR}/deploy/ctl.sh restart # 重启
bash ${APP_DIR}/deploy/ctl.sh logs # 实时日志"
;;
esac
cat <<EOF
============================================================
@@ -314,12 +399,10 @@ OpenDance Studio Web 服务端已部署(Linux 原生站点,无 Docker)
数据库: 使用现有实例 ${DB_HOST}:${DB_PORT:-3306}/${DB_NAME}
上传目录: ${STORAGE_DIR}
程序目录: ${APP_DIR}
Node 进程: 仅监听 ${API_HOST}:${API_PORT}(systemd 托管)
Node 进程: 仅监听 ${API_HOST}:${API_PORT}
进程管理: ${INIT_SYSTEM}
常用命令:
systemctl status opendance-api # 服务状态
journalctl -u opendance-api -f # 实时日志
systemctl restart opendance-api # 重启
nginx -t && systemctl reload nginx # 校验并重载 nginx
${CMD_HINT}
============================================================
EOF