mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-05 22:59:46 +08:00
fix(update): converge stale state and Docker transaction
This commit is contained in:
@@ -317,16 +317,13 @@ class SystemUpdateManager(metaclass=SingletonClass):
|
||||
}
|
||||
)
|
||||
changed = True
|
||||
elif item.get("state") == "installing" and self._is_install_applied(item, target):
|
||||
self._reset_item_after_install(item)
|
||||
changed = True
|
||||
elif (
|
||||
target == _RESOURCES
|
||||
and item.get("state") == "ready"
|
||||
item.get("state") in {"installing", "ready"}
|
||||
and self._is_install_applied(item, target)
|
||||
):
|
||||
# 容器替换或手工安装可能先让运行资源达到目标版本,需丢弃残留待安装包。
|
||||
self._discard_prepared_target(target)
|
||||
if item.get("state") == "ready":
|
||||
# 容器替换或手工安装可能先让运行版本达到目标,需丢弃残留待安装包。
|
||||
self._discard_prepared_target(target)
|
||||
self._reset_item_after_install(item)
|
||||
changed = True
|
||||
resources_changed = (
|
||||
@@ -344,7 +341,10 @@ class SystemUpdateManager(metaclass=SingletonClass):
|
||||
def _is_install_applied(self, item: dict[str, Any], target: SystemUpdateType) -> bool:
|
||||
"""判断启动器应用后的当前版本是否已经达到安装目标。"""
|
||||
if target == _APPLICATION:
|
||||
return bool(item.get("version")) and item["version"] == get_app_version()
|
||||
target_version = str(item.get("version") or "")
|
||||
return bool(target_version) and compare_version(
|
||||
get_app_version(), ">=", target_version
|
||||
) is True
|
||||
current_auth, current_indexer = get_resource_versions()
|
||||
checks = []
|
||||
if item.get("auth_version"):
|
||||
|
||||
@@ -12,7 +12,8 @@ export PATH
|
||||
SOURCE_CONTROL_DIR="${MOVIEPILOT_SOURCE_CONTROL_DIR:-/app/docker}"
|
||||
IMAGE_CONTROL_DIR="${MOVIEPILOT_IMAGE_CONTROL_DIR:-/usr/local/lib/moviepilot/control}"
|
||||
RUNTIME_ROOT="${MOVIEPILOT_RUNTIME_CONTROL_ROOT:-/run/moviepilot/control}"
|
||||
UPDATE_PENDING_FILE="${CONFIG_DIR:-/config}/temp/__update_pending__"
|
||||
# 与 update.sh 共用容器可写层中的 Dev 事务标记;容器重建时不保留旧半成品状态。
|
||||
UPDATE_PENDING_FILE="${MOVIEPILOT_UPDATE_PENDING_FILE:-/var/lib/moviepilot/update/__update_pending__}"
|
||||
UPDATE_PREVIOUS_APP="/app.__update_previous__"
|
||||
CONTROL_FILES=()
|
||||
CONTROL_REQUIRED_FILES=(entrypoint.sh update.sh browser.sh cert.sh)
|
||||
|
||||
@@ -28,7 +28,9 @@ UV_BIN="${UV_BIN:-/usr/local/bin/uv}"
|
||||
CONFIG_DIR="${CONFIG_DIR:-/config}"
|
||||
APP_DIR=/app
|
||||
PUBLIC_DIR=/public
|
||||
UPDATE_PENDING_FILE="${CONFIG_DIR}/temp/__update_pending__"
|
||||
# Dev 更新只需要跨同一容器重启恢复;标记必须和 /app、/public 位于同一容器可写层,
|
||||
# 避免容器重建后 /config 遗留旧标记阻断新镜像启动。
|
||||
UPDATE_PENDING_FILE="${MOVIEPILOT_UPDATE_PENDING_FILE:-/var/lib/moviepilot/update/__update_pending__}"
|
||||
UPDATE_PREVIOUS_APP="${APP_DIR}.__update_previous__"
|
||||
UPDATE_PREVIOUS_PUBLIC="${PUBLIC_DIR}.__update_previous__"
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ source 其他脚本,可能出现同一次启动混用新旧脚本的情况。
|
||||
|
||||
### 3.3 更新恢复时的代际选择
|
||||
|
||||
`/config/temp/__update_pending__` 的值会影响 launcher:
|
||||
`/var/lib/moviepilot/update/__update_pending__` 的值会影响 launcher:
|
||||
|
||||
| 状态 | 控制脚本选择原则 |
|
||||
| --- | --- |
|
||||
@@ -169,9 +169,13 @@ entrypoint 会删除该标记,并只在本次启动中把 `MOVIEPILOT_UPDATE_D
|
||||
```text
|
||||
/app.__update_previous__
|
||||
/public.__update_previous__
|
||||
/config/temp/__update_pending__
|
||||
/var/lib/moviepilot/update/__update_pending__
|
||||
```
|
||||
|
||||
Dev 更新标记与程序载荷都位于容器可写层,只用于跨同一容器重启恢复;容器重建时它会和旧的
|
||||
`/app`、`/public` 载荷一起丢弃,避免持久化卷残留旧事务标记而阻断新镜像启动。稳定版
|
||||
Release/资源更新的 `prepared.json`、`install.json` 和 `state.json` 仍保存在 `/config`,不受此规则影响。
|
||||
|
||||
状态含义与启动处理:
|
||||
|
||||
| 状态 | 含义 | 下次启动处理 |
|
||||
@@ -447,7 +451,7 @@ Docker restart policy。
|
||||
| `/config/app.env` | Docker 启动脚本和应用配置。 |
|
||||
| `/config/.cache/uv` | 默认 uv 持久缓存。 |
|
||||
| `/config/.browser/cloakbrowser` | 默认 CloakBrowser 持久缓存。 |
|
||||
| `/config/temp/__update_pending__` | 载荷切换事务状态。 |
|
||||
| `/var/lib/moviepilot/update/__update_pending__` | Dev 载荷切换事务状态;与 `/app`、`/public` 同属容器可写层。 |
|
||||
| `/app.__update_previous__` | 更新前后端备份。 |
|
||||
| `/public.__update_previous__` | 更新前前端备份。 |
|
||||
| `/config/temp/moviepilot-update/` | 后台下载的 Release/资源包及安装状态。 |
|
||||
@@ -460,7 +464,7 @@ Docker restart policy。
|
||||
|
||||
| 变量 | 默认值 | 影响 |
|
||||
| --- | --- | --- |
|
||||
| `CONFIG_DIR` | `/config` | 配置、缓存、更新状态和证书根目录。 |
|
||||
| `CONFIG_DIR` | `/config` | 配置、缓存、Release/资源更新状态和证书根目录。 |
|
||||
| `PUID` / `PGID` | `0` / `0` | 映射 `moviepilot` 运行用户。 |
|
||||
| `UMASK` | `000` | 后端进程文件权限掩码。 |
|
||||
| `PORT` | `3001` | 后端监听和 readiness 端口。 |
|
||||
|
||||
@@ -1186,6 +1186,7 @@ def test_failed_dependency_sync_does_not_replace_program_files(tmp_path: Path) -
|
||||
UV_BIN="$4"
|
||||
PIP_PROXY= PROXY_HOST=
|
||||
GITHUB_PROXY= CURL_OPTIONS=
|
||||
MOVIEPILOT_UPDATE_PENDING_FILE="$1/temp/__update_pending__"
|
||||
source {UPDATER!s}
|
||||
APP_DIR="$5"
|
||||
PUBLIC_DIR="$6"
|
||||
@@ -1282,6 +1283,7 @@ def test_pending_update_recovers_previous_payload_on_next_start(tmp_path: Path)
|
||||
CONFIG_DIR="$1"
|
||||
VENV_PATH="$2"
|
||||
UV_BIN="$7"
|
||||
MOVIEPILOT_UPDATE_PENDING_FILE="$1/temp/__update_pending__"
|
||||
PIP_PROXY= PROXY_HOST= GITHUB_PROXY= CURL_OPTIONS=
|
||||
source {UPDATER!s}
|
||||
APP_DIR="$3"
|
||||
@@ -1346,6 +1348,7 @@ def test_pending_dependency_update_keeps_current_payload_for_database_safety(
|
||||
script = textwrap.dedent(
|
||||
f"""\
|
||||
CONFIG_DIR="$1"
|
||||
MOVIEPILOT_UPDATE_PENDING_FILE="$1/temp/__update_pending__"
|
||||
source {UPDATER!s}
|
||||
APP_DIR="$2"
|
||||
PUBLIC_DIR="$3"
|
||||
@@ -1436,6 +1439,7 @@ def test_update_transaction_keeps_marker_when_backup_cleanup_fails(tmp_path: Pat
|
||||
script = textwrap.dedent(
|
||||
f"""\
|
||||
CONFIG_DIR="$1"
|
||||
MOVIEPILOT_UPDATE_PENDING_FILE="$2"
|
||||
UPDATE_PENDING_FILE="$2"
|
||||
UPDATE_PREVIOUS_APP="$3"
|
||||
UPDATE_PREVIOUS_PUBLIC="$4"
|
||||
@@ -1582,6 +1586,7 @@ def test_staged_payload_swap_failure_restores_previous_generation(tmp_path: Path
|
||||
CONFIG_DIR="$1"
|
||||
TMP_PATH="$2"
|
||||
PIP_PROXY= PROXY_HOST= GITHUB_PROXY= CURL_OPTIONS=
|
||||
MOVIEPILOT_UPDATE_PENDING_FILE="$1/temp/__update_pending__"
|
||||
source {UPDATER!s}
|
||||
APP_DIR="$3"
|
||||
PUBLIC_DIR="$4"
|
||||
|
||||
@@ -98,6 +98,18 @@ def test_plugin_runtime_updates_preserve_host_entrypoint() -> None:
|
||||
assert "cp -a /app/app/plugins/." not in perf_script
|
||||
|
||||
|
||||
def test_dev_update_marker_stays_with_container_payload() -> None:
|
||||
"""Dev 更新标记必须与容器载荷同层,避免持久化卷残留半完成事务。"""
|
||||
marker = "/var/lib/moviepilot/update/__update_pending__"
|
||||
update_script = _read(ROOT / "docker" / "update.sh")
|
||||
launcher_script = _read(ROOT / "docker" / "launcher.sh")
|
||||
|
||||
assert f"MOVIEPILOT_UPDATE_PENDING_FILE:-{marker}" in update_script
|
||||
assert f"MOVIEPILOT_UPDATE_PENDING_FILE:-{marker}" in launcher_script
|
||||
assert "/config/temp/__update_pending__" not in update_script
|
||||
assert "/config/temp/__update_pending__" not in launcher_script
|
||||
|
||||
|
||||
def test_update_script_keeps_new_host_entrypoint_during_runtime_migration(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -266,6 +266,61 @@ def test_interrupted_download_becomes_retryable_failure(monkeypatch, tmp_path):
|
||||
assert "中断" in status.error
|
||||
|
||||
|
||||
def test_installing_application_update_converges_when_runtime_is_newer(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
"""当前主程序高于残留目标版本时应清理状态并恢复自动检查。"""
|
||||
manager = _manager(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(update_module, "get_app_version", lambda: "v3.0.6")
|
||||
manager._write_item(
|
||||
"application",
|
||||
state="installing",
|
||||
version="v3.0.1",
|
||||
can_update=False,
|
||||
can_install=False,
|
||||
)
|
||||
checked = []
|
||||
monkeypatch.setattr(manager, "_check_application", lambda: checked.append(True))
|
||||
|
||||
status = manager.check("application")
|
||||
|
||||
application = next(item for item in status.updates if item.type == "application")
|
||||
assert checked == [True]
|
||||
assert application.state == "idle"
|
||||
assert application.version is None
|
||||
assert status.state == "idle"
|
||||
|
||||
|
||||
def test_ready_application_update_is_discarded_when_runtime_reaches_target(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
"""外部升级先于确认安装时应清理过期的主程序待安装包。"""
|
||||
manager = _manager(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(update_module, "get_app_version", lambda: "v3.1.0")
|
||||
manager._merge_prepared_manifest(
|
||||
{
|
||||
"version": "v3.1.0",
|
||||
"backend_archive": "/tmp/backend.zip",
|
||||
"frontend_archive": "/tmp/frontend.zip",
|
||||
}
|
||||
)
|
||||
manager._write_item(
|
||||
"application",
|
||||
state="ready",
|
||||
version="v3.1.0",
|
||||
can_update=False,
|
||||
can_install=True,
|
||||
)
|
||||
|
||||
status = manager.get_status()
|
||||
|
||||
application = next(item for item in status.updates if item.type == "application")
|
||||
assert application.state == "idle"
|
||||
assert application.version is None
|
||||
assert application.can_install is False
|
||||
assert not (manager._root / "prepared.json").exists()
|
||||
|
||||
|
||||
def test_ready_resource_update_clears_after_loaded_version_reaches_target(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
|
||||
Reference in New Issue
Block a user