mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-10 18:06:48 +08:00
fix(docker): keep current payload during dependency recovery
This commit is contained in:
@@ -595,6 +595,15 @@ usermod -o -u "${PUID}" moviepilot
|
||||
# 启动前优先确认主运行环境仍然健康,避免插件依赖污染导致服务直接起不来。
|
||||
ensure_backend_runtime_dependencies
|
||||
|
||||
# 依赖阶段恢复会保留当前程序,待自愈成功后再清理旧代际备份和事务标记。
|
||||
if [ "${UPDATE_RECOVERY_BLOCKED:-false}" = "true" ]; then
|
||||
if finalize_update_transaction; then
|
||||
INFO "→ 当前程序依赖已恢复,已清理保留当前程序的更新事务"
|
||||
else
|
||||
WARN "→ 当前程序依赖已恢复,但旧代际备份清理失败,将保留事务标记重试"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 缓存路径解析必须晚于依赖自愈,确保有效性探针使用当前运行版本。
|
||||
if ! resolve_browser_cache_dir; then
|
||||
exit 1
|
||||
|
||||
+6
-1
@@ -107,7 +107,7 @@ function pending_recovery_control_dir() {
|
||||
|
||||
state="$(pending_update_state 2>/dev/null || true)"
|
||||
case "${state}" in
|
||||
prepared|dependencies)
|
||||
prepared)
|
||||
[ -e "${UPDATE_PREVIOUS_APP}" ] || return 1
|
||||
if source_bundle_is_trusted "${previous_control_dir}"; then
|
||||
printf '%s\n' "${previous_control_dir}"
|
||||
@@ -118,6 +118,11 @@ function pending_recovery_control_dir() {
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
dependencies|blocked)
|
||||
# 依赖阶段可能已切换到包含新迁移的当前 /app;必须使用当前或镜像控制脚本,
|
||||
# 让 update.sh 保留新代际,而不是先执行旧代控制脚本触发数据库不兼容回退。
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
+24
-3
@@ -66,6 +66,7 @@ UV_OPTIONS=()
|
||||
MOVIEPILOT_UPDATE_RESULT="noop"
|
||||
UPDATE_RECOVERY_REQUIRED="false"
|
||||
UPDATE_RECOVERY_COMPLETED="false"
|
||||
UPDATE_RECOVERY_BLOCKED="false"
|
||||
DEPENDENCY_SYNC_ATTEMPTED="false"
|
||||
PACKAGE_ROUTE_READY="false"
|
||||
|
||||
@@ -268,10 +269,30 @@ function recover_pending_update() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
WARN "→ 检测到未完成的容器更新事务,正在恢复旧版本"
|
||||
if [ "${state}" = "dependencies" ]; then
|
||||
DEPENDENCY_SYNC_ATTEMPTED="true"
|
||||
if [ "${state}" = "dependencies" ] || [ "${state}" = "blocked" ]; then
|
||||
if [ ! -e "${APP_DIR}" ] || [ ! -e "${PUBLIC_DIR}" ]; then
|
||||
# 目录切换中断可能留下不完整的当前载荷;这种情况仍需恢复完整旧代际。
|
||||
if [ ! -e "${UPDATE_PREVIOUS_APP}" ] || [ ! -e "${UPDATE_PREVIOUS_PUBLIC}" ]; then
|
||||
ERROR "→ 当前与更新前载荷均不完整,无法安全恢复"
|
||||
UPDATE_RECOVERY_REQUIRED="true"
|
||||
return 1
|
||||
fi
|
||||
WARN "→ 当前更新载荷不完整,恢复更新前版本"
|
||||
else
|
||||
# 依赖阶段可能已经切换到包含新迁移的 /app;此时不能恢复旧代际,
|
||||
# 否则数据库已经前进时会把旧程序交给未知的 Alembic revision。
|
||||
WARN "→ 更新事务停留在依赖阶段,保留当前程序并等待启动前依赖自愈,不回退旧版本"
|
||||
if [ "${state}" = "dependencies" ] && ! set_update_pending blocked; then
|
||||
ERROR "→ 无法记录保留当前程序的更新恢复状态"
|
||||
return 1
|
||||
fi
|
||||
UPDATE_RECOVERY_BLOCKED="true"
|
||||
UPDATE_RECOVERY_COMPLETED="true"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
WARN "→ 检测到未完成的容器更新事务,正在恢复旧版本"
|
||||
rollback_update_transaction || return 1
|
||||
UPDATE_RECOVERY_COMPLETED="true"
|
||||
INFO "→ 未完成的容器更新事务已恢复"
|
||||
|
||||
@@ -74,6 +74,10 @@ profile,依赖名称、版本和 source 语义全部由 `pyproject.toml` 与 `
|
||||
源码更新事务中的依赖同步和失败回滚使用同一 profile 选择入口。否则 V3t 在恢复时可能被普通默认组
|
||||
覆盖,得到“3.14t 解释器 + 标准原生依赖”的无效组合。
|
||||
|
||||
如果启动时发现更新事务停留在 `dependencies` 阶段,恢复流程保留当前 `/app`,只执行启动前依赖自愈,
|
||||
不会自动切回旧代际。这样即使当前程序已经完成过数据库迁移,也不会把包含新结构的数据库交给缺少对应
|
||||
Alembic revision 文件的旧程序;依赖自愈成功后再清理旧代际备份和事务标记。
|
||||
|
||||
### 4.2 插件安装后的宿主恢复
|
||||
|
||||
插件与主程序共享虚拟环境。插件依赖安装前后都会采集宿主健康快照,只对安装后新增的异常执行补偿:
|
||||
|
||||
@@ -1237,7 +1237,7 @@ def test_pending_update_recovers_previous_payload_on_next_start(tmp_path: Path)
|
||||
(previous_public / "index.html").write_text("old-front", encoding="utf-8")
|
||||
config_dir = tmp_path / "config"
|
||||
(config_dir / "temp").mkdir(parents=True)
|
||||
(config_dir / "temp" / "__update_pending__").write_text("dependencies\n", encoding="utf-8")
|
||||
(config_dir / "temp" / "__update_pending__").write_text("prepared\n", encoding="utf-8")
|
||||
uv_bin = tmp_path / "uv"
|
||||
uv_log = tmp_path / "uv.log"
|
||||
uv_bin.write_text(
|
||||
@@ -1288,9 +1288,114 @@ def test_pending_update_recovers_previous_payload_on_next_start(tmp_path: Path)
|
||||
)
|
||||
|
||||
assert result.stdout == "old|old-front|cleared|true\n"
|
||||
assert uv_log.read_text(encoding="utf-8").startswith(
|
||||
f"sync --project {live_app} --locked --inexact --no-dev"
|
||||
assert not uv_log.exists()
|
||||
|
||||
|
||||
def test_pending_dependency_update_keeps_current_payload_for_database_safety(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""依赖阶段中断时保留当前载荷,避免新数据库回退到旧迁移链。"""
|
||||
live_app = tmp_path / "app"
|
||||
live_public = tmp_path / "public"
|
||||
previous_app = tmp_path / "previous-app"
|
||||
previous_public = tmp_path / "previous-public"
|
||||
(live_app / "app").mkdir(parents=True)
|
||||
live_public.mkdir()
|
||||
(previous_app / "app").mkdir(parents=True)
|
||||
previous_public.mkdir()
|
||||
(live_app / "app" / "new.py").write_text("new", encoding="utf-8")
|
||||
(live_public / "index.html").write_text("new-front", encoding="utf-8")
|
||||
(previous_app / "app" / "old.py").write_text("old", encoding="utf-8")
|
||||
(previous_public / "index.html").write_text("old-front", encoding="utf-8")
|
||||
config_dir = tmp_path / "config"
|
||||
pending_file = config_dir / "temp" / "__update_pending__"
|
||||
pending_file.parent.mkdir(parents=True)
|
||||
pending_file.write_text("dependencies\n", encoding="utf-8")
|
||||
script = textwrap.dedent(
|
||||
f"""\
|
||||
CONFIG_DIR="$1"
|
||||
source {UPDATER!s}
|
||||
APP_DIR="$2"
|
||||
PUBLIC_DIR="$3"
|
||||
UPDATE_PREVIOUS_APP="$4"
|
||||
UPDATE_PREVIOUS_PUBLIC="$5"
|
||||
INFO() {{ :; }}
|
||||
WARN() {{ :; }}
|
||||
ERROR() {{ :; }}
|
||||
recover_pending_update
|
||||
printf '%s|%s|%s|%s|%s|%s\n' \
|
||||
"$([[ -f "${{APP_DIR}}/app/new.py" ]] && printf new || printf missing)" \
|
||||
"$([[ -f "${{PUBLIC_DIR}}/index.html" ]] && head -n1 "${{PUBLIC_DIR}}/index.html" || printf missing)" \
|
||||
"$(tr -d '\r\n' < "${{UPDATE_PENDING_FILE}}")" \
|
||||
"${{UPDATE_RECOVERY_COMPLETED}}" \
|
||||
"${{UPDATE_RECOVERY_BLOCKED}}" \
|
||||
"$([[ -f "${{UPDATE_PREVIOUS_APP}}/app/old.py" ]] && printf retained || printf absent)"
|
||||
"""
|
||||
)
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
script,
|
||||
"pending-dependency-recovery-test",
|
||||
str(config_dir),
|
||||
str(live_app),
|
||||
str(live_public),
|
||||
str(previous_app),
|
||||
str(previous_public),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
assert result.stdout == "new|new-front|blocked|true|true|retained\n"
|
||||
|
||||
|
||||
def test_launcher_uses_current_control_for_dependency_recovery(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""依赖阶段 pending 不得先选旧代控制脚本触发回退。"""
|
||||
source = tmp_path / "source"
|
||||
image = tmp_path / "image"
|
||||
previous = tmp_path / "previous-app"
|
||||
config = tmp_path / "config"
|
||||
_write_bundle(source, "new")
|
||||
_write_bundle(image, "image")
|
||||
_write_bundle(previous / "docker", "old")
|
||||
pending_file = config / "temp" / "__update_pending__"
|
||||
pending_file.parent.mkdir(parents=True)
|
||||
pending_file.write_text("dependencies\n", encoding="utf-8")
|
||||
script = textwrap.dedent(
|
||||
f"""\
|
||||
source {LAUNCHER!s}
|
||||
SOURCE_CONTROL_DIR="$1"
|
||||
IMAGE_CONTROL_DIR="$2"
|
||||
RUNTIME_ROOT="$3"
|
||||
UPDATE_PENDING_FILE="$4"
|
||||
UPDATE_PREVIOUS_APP="$5"
|
||||
source_bundle_is_trusted() {{ control_bundle_generation "$1" > /dev/null; }}
|
||||
launcher_main
|
||||
"""
|
||||
)
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
script,
|
||||
"dependency-launcher-test",
|
||||
str(source),
|
||||
str(image),
|
||||
str(tmp_path / "run"),
|
||||
str(pending_file),
|
||||
str(previous),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
assert result.stdout == "new\n"
|
||||
|
||||
|
||||
def test_update_transaction_keeps_marker_when_backup_cleanup_fails(tmp_path: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user