fix: 修复更新重启工作目录

This commit is contained in:
jxxghp
2026-09-13 08:20:40 +08:00
parent 2edf20433a
commit 2d446e0f57
4 changed files with 40 additions and 5 deletions

View File

@@ -294,6 +294,12 @@ function source_control_generation() {
/entrypoint.sh --source-generation 2>/dev/null
}
# 更新会原子替换 /app;重入前切到稳定目录,避免新 Bash 进程继承已删除的工作目录。
function reexec_entrypoint() {
cd / || exit 1
exec /entrypoint.sh --post-update-reexec
}
function maybe_reexec_control_bundle() {
[ "${MOVIEPILOT_UPDATE_RESULT:-noop}" = "updated" ] || return 0
@@ -309,7 +315,7 @@ function maybe_reexec_control_bundle() {
"${next_control_generation}" \
"${MOVIEPILOT_BOOTSTRAP_REEXECUTED:-0}"; then
INFO "→ 检测到容器控制脚本更新,使用新版本继续本次启动。"
exec /entrypoint.sh --post-update-reexec
reexec_entrypoint
elif [ "$?" -eq 2 ]; then
ERROR "→ 容器控制脚本在单次启动中重复变化,已终止以避免重启循环。"
exit 1
@@ -505,7 +511,7 @@ if [ "${MOVIEPILOT_BOOTSTRAP_UPDATE_DONE:-0}" != "1" ] \
&& [ -f "${CONFIG_DIR}/temp/moviepilot-update/install.json" ]; then
if apply_pending_release_update_at_startup; then
INFO "→ 未完成的 Release 更新已安装,重新执行入口加载新代码。"
exec /entrypoint.sh --post-update-reexec
reexec_entrypoint
fi
fi
@@ -543,6 +549,10 @@ ensure_browser_kernel
# 证书管理
source "${MP_CONTROL_DIR:-/usr/local/lib/moviepilot/control}/cert.sh"
# /app 会被 Release worker 原子替换;常驻入口必须留在稳定目录,避免 worker 关闭 supervisor 后
# 当前 shell 继承已删除的旧工作目录,导致重入时 Bash 连续报告 getcwd 错误。
cd / || exit 1
# supervisord 常驻前台并统一托管 Nginx 与后端;带更新标记的 shutdown 会回到本入口消费更新包。
install -d -m 0755 /run/moviepilot
# Supervisor 的控制面只在容器内使用;未显式传入时生成本次容器启动专用的随机凭据,避免固定密码进入镜像。
@@ -584,13 +594,13 @@ while true; do
exit 1
fi
INFO "→ 更新代码已落盘,重新执行容器入口以加载新版本。"
exec /entrypoint.sh --post-update-reexec
reexec_entrypoint
fi
if [ -f "${ONE_SHOT_DEV_UPDATE_FLAG}" ]; then
if run_pending_dev_update_after_supervisor_shutdown; then
INFO "→ 更新包已安装,重新执行容器入口以加载新版本。"
exec /entrypoint.sh --post-update-reexec
reexec_entrypoint
fi
if [ -f "${ONE_SHOT_DEV_UPDATE_FLAG}" ]; then
ERROR "→ 更新请求未能完成且标记仍存在,停止启动。"

View File

@@ -25,6 +25,12 @@ if ! "${VENV_PATH}/bin/python3" -m app.cli apply-prepared-update; then
exit 1
fi
# /app 可能刚刚被替换,切回稳定目录后再调用 supervisor 控制面。
if ! cd /; then
ERROR "→ 更新后无法切回稳定工作目录。"
exit 1
fi
if ! mkdir -p "$(dirname "${RESTART_REQUEST_FILE}")"; then
ERROR "→ 无法记录更新后的重启请求。"
exit 1

View File

@@ -425,7 +425,8 @@ Python lifespan 会先撤销 readiness,再按组件声明的 `stop_order` 停
普通应用重启通过本地 `supervisorctl restart all` 同时重启 Nginx 和后端。确认安装 Release 时,
`SystemHelper` 只启动 root `moviepilot-update-worker`;worker 先替换 `/app`、`/public` 或站点资源,
再写入 `moviepilot.pending_supervisor_restart` 并执行 `supervisorctl shutdown`。外层 entrypoint 看到标记后
重新执行 launcher,加载新代码。Dev 更新仍通过一次性 Dev 标记关闭 Supervisor,再由 entrypoint 调用
重新执行 launcher,加载新代码。载荷替换期间外层 entrypoint、worker 的控制面调用和所有入口重入都使用 `/`
作为稳定工作目录,避免旧 `/app` 被删除后产生 `getcwd` 错误。Dev 更新仍通过一次性 Dev 标记关闭 Supervisor,再由 entrypoint 调用
`update.sh`。这样更新包不会因只重启受管进程而停留在暂存目录;整个过程不访问 Docker API,也不依赖
Docker restart policy。

View File

@@ -942,6 +942,24 @@ def test_release_update_worker_applies_before_supervisor_shutdown() -> None:
assert worker.index("apply-prepared-update") < worker.index("shutdown")
def test_update_reexec_uses_stable_working_directory() -> None:
"""更新替换 /app 后,入口重入和 worker 控制面调用必须离开旧工作目录。"""
entrypoint = (ROOT / "docker" / "entrypoint.sh").read_text(encoding="utf-8")
worker = (ROOT / "docker" / "update-worker.sh").read_text(encoding="utf-8")
reexec = entrypoint.split("function reexec_entrypoint()", 1)[1].split("}", 1)[0]
assert reexec.index("cd / || exit 1") < reexec.index("exec /entrypoint.sh")
assert entrypoint.count("exec /entrypoint.sh --post-update-reexec") == 1
assert entrypoint.count("reexec_entrypoint") == 5
supervisor_start = entrypoint.index("/usr/bin/supervisord -n")
assert entrypoint.rfind("cd / || exit 1", 0, supervisor_start) > entrypoint.rfind(
"cd /app || exit", 0, supervisor_start
)
worker_stable_cwd = worker.index("cd /", worker.index("apply-prepared-update"))
assert worker_stable_cwd < worker.index("supervisorctl")
@pytest.mark.parametrize(
("pyproject_changed", "lock_changed", "expected_route_calls", "expected_sync_calls"),
(