From 98b16bda8d2e62c76f9924ec177a4e624c9baa3b Mon Sep 17 00:00:00 2001 From: qqcomeup Date: Tue, 14 Jul 2026 12:31:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Docker=20=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=AE=8C=E6=88=90=E6=97=A5=E5=BF=97=20(#6112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/entrypoint.sh | 40 +++++++++++ tests/test_docker_entrypoint_permissions.py | 73 +++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 5398c76a6..fea389d1d 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -20,6 +20,8 @@ function WARN() { echo -e "${WARN} ${1}" } +ENTRYPOINT_START_TIME="$(date +%s)" + function normalize_env_value() { printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' } @@ -57,6 +59,42 @@ function run_package_command() { fi } +function wait_backend_ready() { + local entrypoint_start_time="${1:-$(date +%s)}" + local backend_start_time="${2:-$(date +%s)}" + local python_pid="${3:-}" + local backend_port="${PORT:-3001}" + local web_port="${NGINX_PORT:-3000}" + local timeout="${MOVIEPILOT_BACKEND_READY_TIMEOUT:-300}" + local ready_url="http://127.0.0.1:${backend_port}/api/v1/system/global?token=moviepilot" + local deadline + if ! [[ "${timeout}" =~ ^[0-9]+$ ]] || [ "$((10#${timeout}))" -le 0 ]; then + WARN "→ MOVIEPILOT_BACKEND_READY_TIMEOUT=${timeout} 无效,使用默认 300 秒。" + timeout=300 + else + timeout=$((10#${timeout})) + fi + deadline=$(( $(date +%s) + timeout )) + + while [ "$(date +%s)" -lt "${deadline}" ]; do + if [ -n "${python_pid}" ] && ! kill -0 "${python_pid}" >/dev/null 2>&1; then + WARN "→ 后端服务启动完成探测已停止:后端进程已退出。" + return 1 + fi + + if curl -fsS --max-time 2 "${ready_url}" >/dev/null 2>&1; then + local now + now="$(date +%s)" + INFO "→ MoviePilot Web 已可访问,启动总耗时 $(( now - entrypoint_start_time )) 秒,后端就绪耗时 $(( now - backend_start_time )) 秒,后端端口 ${backend_port},前端端口 ${web_port}。" + return 0 + fi + sleep 1 + done + + WARN "→ 后端服务启动完成探测超时,已等待 ${timeout} 秒,后端端口 ${backend_port},继续等待进程日志..." + return 1 +} + # 环境变量补全 # 优先级: 系统环境变量 -> .env 文件 (即使为空字符串) -> 预设默认值 # 精准适配 Python 端 set_key (quote_mode="always", 单引号包裹, \' 转义) @@ -480,12 +518,14 @@ umask "${UMASK}" # 启动后端服务 INFO "→ 启动后端服务..." +BACKEND_START_TIME="$(date +%s)" if [ "${START_NOGOSU:-false}" = "true" ]; then "${VENV_PATH}/bin/python3" app/main.py > /dev/stdout 2> /dev/stderr & else gosu moviepilot:moviepilot "${VENV_PATH}/bin/python3" app/main.py > /dev/stdout 2> /dev/stderr & fi PYTHON_PID=$! +wait_backend_ready "${ENTRYPOINT_START_TIME}" "${BACKEND_START_TIME}" "${PYTHON_PID}" & # 等待 Python 进程退出。 # 如果收到信号,trap 会中断 wait,并执行 graceful_exit。 diff --git a/tests/test_docker_entrypoint_permissions.py b/tests/test_docker_entrypoint_permissions.py index 2ae66557b..f5862ef6a 100644 --- a/tests/test_docker_entrypoint_permissions.py +++ b/tests/test_docker_entrypoint_permissions.py @@ -78,6 +78,26 @@ def _run_permission_case(tmp_path: Path, body: str, env: dict[str, str] | None = return chown_log.read_text(encoding="utf-8") if chown_log.exists() else "" +def _run_entrypoint_case(tmp_path: Path, body: str, env: dict[str, str] | None = None) -> str: + functions = _write_entrypoint_functions(tmp_path) + case_env = { + **os.environ, + "ENTRYPOINT_FUNCTIONS": str(functions), + } + if env: + case_env.update(env) + + script = textwrap.dedent( + f"""\ + set -euo pipefail + source "${{ENTRYPOINT_FUNCTIONS}}" + {body} + """ + ) + result = subprocess.run(["bash", "-c", script], check=True, env=case_env, text=True, capture_output=True) + return result.stdout + + def test_image_paths_are_not_chowned_by_default_regardless_of_owner(tmp_path: Path) -> None: log = _run_permission_case( tmp_path, @@ -170,3 +190,56 @@ def test_runtime_writable_paths_are_still_corrected(tmp_path: Path) -> None: assert not any(line.startswith("-R ") and ".cloakbrowser" in line for line in lines) assert not any(f"{tmp_path}/app " in line for line in lines) assert not any(f"{tmp_path}/public" in line for line in lines) + + +def test_backend_ready_log_uses_configured_ports(tmp_path: Path) -> None: + curl_log = tmp_path / "curl.log" + output = _run_entrypoint_case( + tmp_path, + """ + INFO() { printf '[INFO] %s\\n' "$1"; } + curl() { + printf '%s\\n' "$*" > "${CURL_LOG}" + return 0 + } + PORT=4321 NGINX_PORT=8765 wait_backend_ready 1 2 "$$" + """, + env={"CURL_LOG": str(curl_log)}, + ) + + assert curl_log.read_text(encoding="utf-8") == ( + "-fsS --max-time 2 http://127.0.0.1:4321/api/v1/system/global?token=moviepilot\n" + ) + assert "MoviePilot Web 已可访问" in output + assert "后端就绪耗时" in output + assert "后端端口 4321" in output + assert "前端端口 8765" in output + + +def test_backend_ready_timeout_falls_back_to_default_for_invalid_value(tmp_path: Path) -> None: + output = _run_entrypoint_case( + tmp_path, + """ + WARN() { printf '[WARN] %s\\n' "$1"; } + curl() { return 1; } + MOVIEPILOT_BACKEND_READY_TIMEOUT=invalid wait_backend_ready 1 2 999999 || true + """, + ) + + assert "MOVIEPILOT_BACKEND_READY_TIMEOUT=invalid 无效,使用默认 300 秒" in output + assert "后端服务启动完成探测已停止:后端进程已退出" in output + + +def test_backend_ready_timeout_accepts_leading_zero_decimal(tmp_path: Path) -> None: + output = _run_entrypoint_case( + tmp_path, + """ + INFO() { printf '[INFO] %s\\n' "$1"; } + WARN() { printf '[WARN] %s\\n' "$1"; } + curl() { return 0; } + MOVIEPILOT_BACKEND_READY_TIMEOUT=08 wait_backend_ready 1 2 "$$" + """, + ) + + assert "MOVIEPILOT_BACKEND_READY_TIMEOUT=08 无效" not in output + assert "MoviePilot Web 已可访问" in output