06-成品-受限宿主守护进程防回收启动法
一、适用场景
把服务交给别人的进程管理器托管时(群晖 DSM 套件中心、各类面板、容器外的 init 包装、CI runner 等),出现以下任一症状:
- 点「启动」立刻弹宿主标准错误(DSM 的「无法启动,请稍后再试一次」);
- 手动跑得好好的,交给宿主拉就起不来,且看不到任何报错;
- 进程确实起来了,但宿主仍判定"未运行",或 start 返回后进程就消失了。
共同点:脚本运行在一个被裁剪过的执行环境里(受限用户、PATH 不完整、无登录 shell、父进程会回收子进程),而这类约束在本地开发机上永远复现不出来。
二、操作步骤
-
解释器/运行时一律绝对路径扫描,不要用
command -v。受限用户的 PATH 通常不含/usr/local/bin、也不含其它套件的 bin 目录。
sh for p in /usr/local/bin/python3.13 /usr/bin/python3 \ /var/packages/python313/target/bin/python3.13 \ /var/packages/*/target/bin/python3* ; do [ -x "$p" ] || continue ver=$("$p" -c 'import sys;print("%d%02d"%sys.version_info[:2])' 2>/dev/null) || continue ... done
取版本最高且 ≥ 门槛者;command -v只作最后兜底。
锚点:程序源代码/deploy/synology/spk/scripts/start-stop-statusL20-37。 -
命中后自动补
LD_LIBRARY_PATH:社群源/自装运行时依赖自身 lib 目录,裸二进制在受限环境里找不到libpython会静默失败(不报错、直接退出)。
sh PYDIR=$(dirname "$PYTHON"); PYROOT=$(dirname "$PYDIR") export LD_LIBRARY_PATH="$PYROOT/lib:$PYROOT/lib/python3.13/lib-dynload:$LD_LIBRARY_PATH"
锚点:L51-54。 -
未命中时输出可诊断的失败:把每个候选的真实报错逐条收集进日志,而不是一句"未找到 Python"。
锚点:L39-49([START][ERR] … $diag)。 -
用
setsid脱离宿主进程组(无setsid回退nohup),并断开标准输入:
sh setsid "$PYTHON" -u app.py >> "$LOG" 2>&1 < /dev/null &
锚点:L84-89。 -
就绪判定以「端口真正可连」为准,不用
kill -0:
sh port_probe() { "$PYTHON" -c 'import socket,sys s=socket.socket(); s.settimeout(1.0) try: s.connect(("127.0.0.1", int(sys.argv[1]))); s.close(); sys.exit(0) except Exception: sys.exit(1)' "$1" >/dev/null 2>&1; }
复用已探明的解释器,不依赖宿主是否装了 curl。
锚点:L67-74。 -
轮询等待必须有上限(本例 40 次 × 1s),并区分三种结局:端口就绪(
0) / 进程已退出(2) / 超时未监听(1),各自写不同的[START][ERR]并exit 1。
锚点:L91-113。 -
遵循宿主的退出码约定:DSM
status为 运行中exit 0/ 未运行exit 3(返回 1 会被判成异常);提供log动作回显日志路径,宿主 UI 的「日志」按钮才读得到。
锚点:L127-139。 -
交付前体检:跑
scripts/daemon_start_guard_check.py <脚本路径>,逐项核对五道加固 + 超时上限 + 退出码约定。
三、注意边界
- 这套加固针对的是宿主环境约束,不是产品缺陷。不要为了让脚本跑通就去改产品行为或降功能。
- 端口从应用自身配置读取(本例
grep '^PORT=' "$APP/config.env"),不要硬编码;硬编码会和配置漂移后互相欺骗。锚点 L56-58。 - 诊断信息里不要写真名、单位、内网真实拓扑;
uname/id/目录权限/最近日志足够定位。 - 真机复现不了时,把"加了诊断输出"本身当作交付:下一轮失败日志就能定位,而不是继续猜。
- 日志要
>>(追加)且启动前mkdir -p日志目录,否则首次启动就因目录不存在而写不进去。锚点 L60。
四、反例避雷
- ✗ 用
command -v python3探测 → 受限用户 PATH 里根本没有,直接判"未找到",而实际是装了的。 - ✗ 只补 PATH 不补
LD_LIBRARY_PATH→ 裸二进制加载libpython失败,表现为"命令存在但一执行就没输出"。这是 DSM 上「无法启动」的头号根因。 - ✗
nohup python app.py &就完事 → 进程仍在宿主进程组内,start 动作一返回就被回收,宿主随后判"未运行"。必须setsid。 - ✗ 用
kill -0判就绪 → 进程活着 ≠ 服务已监听。端口没起来就返回成功,宿主显示"运行中"但用户打不开页面,比直接报错更难查。 - ✗ 轮询
while true不设上限 → 应用启动卡住时,宿主的 start 动作永久挂起,UI 转圈到超时。 - ✗
status未运行时exit 1→ 违反 DSM 约定(应为 3),宿主可能把"正常停止"显示成异常状态。 - ✗ 失败只写
exit 1不写日志 → 宿主 UI 只有一个笼统错误框,等于没有信息。必须把解释器路径、环境变量、uname、id、最近日志尾巴都落盘。
五、代码锚点 / 实战战绩
锚点:
- 程序源代码/deploy/synology/spk/scripts/start-stop-status(143 行)
- 解释器绝对路径扫描 L20-37;兜底 L37
- 诊断式失败 L39-49
- LD_LIBRARY_PATH 补全 L51-54
- 端口读取 L56-58;日志目录 mkdir -p L60
- is_running() L62-64;port_probe() L67-74
- setsid/nohup 启动 L84-89
- 就绪轮询与三态判定 L91-113
- stop L115-126;status 0/3 L127-134;log L135-139
- 附包只读体检:scripts/daemon_start_guard_check.py
战绩:
- 2026-10-15:用户截图「无法启动,请稍后再试一次」→ 按本流程连修三处(LD_LIBRARY_PATH 缺失、未脱离进程组、kill -0 判就绪),start-stop-status 由 3090 B 重写为 5425 B,并加入诊断落盘。
- 同日:preinst 不再声明依赖(避免精确包名不匹配被拦截,见 06-成品-群晖SPK打包与格式排查法);deploy/synology/hfms_start.sh 同步改为绝对路径扫描。
- 2026-10-16:附包体检器首跑 PASS——五道加固 + 就绪轮询上限 + status 退出码约定全部命中。
- 待真机复测:本流程在目标机的最终成效仍需用户在 DSM 上重新安装并点启动确认;脚本已强化诊断输出,若再失败可直接从「日志」按钮读到 [START][ERR] 与 Python traceback。
六、关联笔记
- 与
06-成品-部署与开机自启SOP互补:它解决"自己的机器上怎么常驻与开机自启"(Windows HKCU Run / Linux start.sh / 单实例 netstat 守护);本技能解决"交给别人的进程管理器托管时怎么不被环境掐死"。 - 与
06-成品-群晖SPK打包与格式排查法配套:那个管"包能不能装上",本技能管"装上后能不能起来"。 - 与
04-开发-沙箱环境写文件限制规避法同理:环境侧限制不通过改产品来绕过,只能通过适配层吸收。
七、版本记录
- v1.0(2026-10-16):首版。据 2026-10-15 群晖 DSM 「无法启动」三轮排查实战固化,含五道加固 + 三态就绪判定 + 诊断落盘,附只读体检器
scripts/daemon_start_guard_check.py;真机成效待复测已如实标注。