mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 05:44:44 +08:00
fix(runtime): validate process identity and preserve detached launch flags
This commit is contained in:
@@ -35,7 +35,7 @@ class SystemHelper(ConfigReloadMixin):
|
||||
__local_restart_log_file = (
|
||||
get_runtime_setting('LOG_PATH') / "moviepilot.restart.stdout.log"
|
||||
)
|
||||
__one_shot_dev_update_flag_file = (
|
||||
__one_shot_dev_update_flag_file: Path = (
|
||||
get_runtime_setting('TEMP_PATH') / "moviepilot.pending_dev_update"
|
||||
)
|
||||
__prepared_update_manifest = (
|
||||
@@ -46,11 +46,11 @@ class SystemHelper(ConfigReloadMixin):
|
||||
__supervisor_socket = Path("/run/moviepilot/supervisor.sock")
|
||||
__supervisor_update_worker = "moviepilot-update-worker"
|
||||
|
||||
def on_config_changed(self):
|
||||
def on_config_changed(self) -> None:
|
||||
"""配置变化后重新应用日志设置。"""
|
||||
logger.update_loggers()
|
||||
|
||||
def get_reload_name(self):
|
||||
def get_reload_name(self) -> str:
|
||||
"""返回配置重载日志使用的组件名称。"""
|
||||
return "日志设置"
|
||||
|
||||
@@ -69,7 +69,7 @@ class SystemHelper(ConfigReloadMixin):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _load_runtime_file(path: Path) -> Optional[dict]:
|
||||
def _load_runtime_file(path: Path) -> Optional[dict[str, object]]:
|
||||
"""安全读取本地进程运行状态文件。"""
|
||||
if not path.exists():
|
||||
return None
|
||||
@@ -88,12 +88,12 @@ class SystemHelper(ConfigReloadMixin):
|
||||
|
||||
pid = runtime.get("pid")
|
||||
create_time = runtime.get("create_time")
|
||||
if not pid:
|
||||
if not pid or not isinstance(pid, (str, int, float)):
|
||||
return False
|
||||
|
||||
try:
|
||||
pid = int(pid)
|
||||
except (TypeError, ValueError):
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return False
|
||||
|
||||
if pid != os.getpid():
|
||||
@@ -101,10 +101,13 @@ class SystemHelper(ConfigReloadMixin):
|
||||
|
||||
if create_time is None:
|
||||
return True
|
||||
if not isinstance(create_time, (str, int, float)):
|
||||
return False
|
||||
|
||||
try:
|
||||
current_process = psutil.Process(os.getpid())
|
||||
return abs(current_process.create_time() - float(create_time)) <= 2
|
||||
started_at: float = current_process.create_time()
|
||||
return abs(started_at - float(create_time)) <= 2
|
||||
except (psutil.Error, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
@@ -174,7 +177,7 @@ class SystemHelper(ConfigReloadMixin):
|
||||
"close_fds": True,
|
||||
"env": env,
|
||||
}
|
||||
if os.name == "nt":
|
||||
if sys.platform == "win32":
|
||||
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
||||
else:
|
||||
kwargs["start_new_session"] = True
|
||||
@@ -199,6 +202,7 @@ class SystemHelper(ConfigReloadMixin):
|
||||
) -> None:
|
||||
"""延迟调用本地 supervisor 控制命令,确保重启接口有机会完成响应。"""
|
||||
def run_command() -> None:
|
||||
"""执行已选定的 supervisor 操作并记录启动失败。"""
|
||||
command = [
|
||||
str(SystemHelper.__supervisorctl),
|
||||
"-c",
|
||||
@@ -296,7 +300,7 @@ class SystemHelper(ConfigReloadMixin):
|
||||
return False, message
|
||||
return True, "已安排 Dev 更新并重启"
|
||||
|
||||
def set_system_modified(self):
|
||||
def set_system_modified(self) -> None:
|
||||
"""
|
||||
设置系统已修改标志
|
||||
"""
|
||||
|
||||
78
tests/test_runtime_process_state.py
Normal file
78
tests/test_runtime_process_state.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""本地进程状态记录及跨平台子进程参数的兼容回归。"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from app import cli
|
||||
from app.runtime.state import SystemHelper
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("created_at", "expected"),
|
||||
[(None, True), (100, True), (102.0, True), (103, False), ("100", True), ("invalid", False), ([], False), ({}, False)],
|
||||
)
|
||||
def test_runtime_identity_checks_creation_time(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, created_at: object, expected: bool
|
||||
) -> None:
|
||||
"""创建时间可缺省或使用数字字符串;损坏记录不能冒充当前受管进程。"""
|
||||
state_path = tmp_path / "runtime.json"
|
||||
state_path.write_text(json.dumps({"pid": os.getpid(), "create_time": created_at}), encoding="utf-8")
|
||||
monkeypatch.setattr(SystemHelper, "_SystemHelper__local_backend_runtime_file", state_path)
|
||||
monkeypatch.setattr("app.runtime.state.psutil.Process", lambda _pid: SimpleNamespace(create_time=lambda: 100.0))
|
||||
|
||||
assert SystemHelper._is_local_cli_managed() is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pid", [None, [], {}, "invalid", 0, float("inf"), float("-inf"), float("nan")])
|
||||
def test_invalid_process_identity_is_rejected(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, pid: object
|
||||
) -> None:
|
||||
"""不可转换的 PID 和空 PID 均不建立受管进程身份。"""
|
||||
state_path = tmp_path / "runtime.json"
|
||||
state_path.write_text(json.dumps({"pid": pid}), encoding="utf-8")
|
||||
monkeypatch.setattr(SystemHelper, "_SystemHelper__local_backend_runtime_file", state_path)
|
||||
|
||||
assert SystemHelper._is_local_cli_managed() is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["win32", "linux", "darwin"])
|
||||
@pytest.mark.parametrize("entrypoint", ["cli", "restart"])
|
||||
def test_detached_process_flags_match_platform(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, platform: str, entrypoint: str
|
||||
) -> None:
|
||||
"""模拟平台并截获启动参数,验证 Windows 分离标志和 Unix 新会话保持一致。"""
|
||||
for attribute, name in (
|
||||
("_SystemHelper__local_restart_log_file", "restart.log"),
|
||||
("_SystemHelper__prepared_update_manifest", "install.json"),
|
||||
("_SystemHelper__one_shot_dev_update_flag_file", "pending-update"),
|
||||
):
|
||||
monkeypatch.setattr(SystemHelper, attribute, tmp_path / name)
|
||||
spawn = Mock(return_value=SimpleNamespace(pid=123))
|
||||
monkeypatch.setattr(subprocess, "Popen", spawn)
|
||||
monkeypatch.setattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0x200, raising=False)
|
||||
monkeypatch.setattr(subprocess, "DETACHED_PROCESS", 0x8, raising=False)
|
||||
|
||||
with monkeypatch.context() as platform_patch:
|
||||
platform_patch.setattr(sys, "platform", platform)
|
||||
if entrypoint == "cli":
|
||||
cli._spawn_process(["test-command"], cwd=tmp_path, log_file=None)
|
||||
else:
|
||||
SystemHelper._spawn_local_restart_helper()
|
||||
|
||||
spawn.assert_called_once()
|
||||
options = spawn.call_args.kwargs
|
||||
assert options["stdin"] == subprocess.DEVNULL
|
||||
assert options["close_fds"] is True
|
||||
if platform == "win32":
|
||||
assert options["creationflags"] == 0x208
|
||||
assert "start_new_session" not in options
|
||||
else:
|
||||
assert options["start_new_session"] is True
|
||||
assert "creationflags" not in options
|
||||
Reference in New Issue
Block a user