现象: ./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 与测试数量。
187 lines
6.2 KiB
Bash
Executable File
187 lines
6.2 KiB
Bash
Executable File
#!/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
|