Files
opendance-web/deploy/ctl.sh
T
opendance ea58520fc2 fix(deploy): .env 中带空格的值未加引号导致 install.sh 以 127 失败
现象:
  ./deploy/.env: line 53: Miku.pmx: command not found
  [install] 第 53 行执行失败(退出码 127)

根因:deploy/.env.example 的 MODEL_CONVERT_PMX=Hatsune Miku.pmx 值含空格
却没加引号,install.sh 用 `set -a; . deploy/.env` 载入时被切成
「赋值 + 命令」,Miku.pmx 被当成命令执行。

修复:
1. 模板改为 MODEL_CONVERT_PMX="Hatsune Miku.pmx"(占位符值同样加引号);
2. 新增 deploy/env-lib.sh:.env 语法预检,install.sh / ctl.sh 在 source
   之前检查,报错给出「哪一行 / 原文 / 改法」, 与反引号建议单引号;
3. install.sh 的 ERR trap 补上出错文件名(lineno 属于被 source 的 .env,
   只报行号无法定位);
4. 新增 test/env-file.spec.ts 12 项回归测试,锁住「模板可被 shell 原样载入」;
5. 版本升到 0.2.1,README(.md/.txt)/DSH.md 补 .env 写法约定、FAQ 与测试数量。
2026-09-15 22:26:58 +08:00

187 lines
6.2 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
SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
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
# 载入部署环境变量(先做语法预检:.env 会被 shell 真的执行)
if [ -f "$APP_DIR/deploy/.env" ]; then
if [ -f "$SCRIPT_DIR/env-lib.sh" ]; then
# shellcheck source=deploy/env-lib.sh
. "$SCRIPT_DIR/env-lib.sh"
opendance_validate_env_file "$APP_DIR/deploy/.env" \
|| die "$APP_DIR/deploy/.env 语法预检未通过(见上):给对应行的值加上引号后重试。"
fi
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