From 5f895fb4f529f4bf86d01388c2aa358ed105d8a5 Mon Sep 17 00:00:00 2001 From: xiami762 <> Date: Wed, 15 Apr 2026 15:55:59 +0800 Subject: [PATCH] fix(cli): use config backend port when stopping services on start/restart When runtime pid records are missing or legacy-only, start_all and restart_all now resolve the backend stop port from the current CLI ServiceConfig instead of the static default. Extract _resolve_stop_ports and _stop_all_locked for shared stop logic under the service lock. Made-with: Cursor --- flocks/cli/service_manager.py | 49 ++++++++++++++++------- tests/cli/test_service_manager.py | 65 +++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/flocks/cli/service_manager.py b/flocks/cli/service_manager.py index 88e0cf13..fb6c2628 100644 --- a/flocks/cli/service_manager.py +++ b/flocks/cli/service_manager.py @@ -1045,15 +1045,42 @@ def _run_legacy_task_migration(root: Path, console) -> None: console.print("[flocks] 旧任务迁移失败,请检查日志。") +def _resolve_stop_ports( + paths: RuntimePaths, + config: ServiceConfig | None = None, +) -> tuple[int, int]: + """Resolve frontend/backend ports for stop flows. + + When a runtime record is missing or uses the legacy pid-only format, + ``start`` and ``restart`` should fall back to the current CLI config + rather than the static default ports. + """ + frontend_default = config.frontend_port if config is not None else ServiceConfig.frontend_port + backend_default = config.backend_port if config is not None else ServiceConfig.backend_port + return ( + _effective_frontend_port(paths, frontend_default), + _recorded_port(paths.backend_pid, backend_default), + ) + + +def _stop_all_locked( + paths: RuntimePaths, + console, + *, + config: ServiceConfig | None = None, +) -> None: + """Stop frontend then backend while reusing the caller's lock.""" + fe_port, be_port = _resolve_stop_ports(paths, config) + _resolve_upgrade_runtime(console, frontend_port=fe_port, attempt_recover=False) + stop_one(fe_port, paths.frontend_pid, "WebUI", console) + stop_one(be_port, paths.backend_pid, "后端", console) + + def stop_all(console) -> None: """Stop frontend then backend using ports persisted in runtime records.""" paths = ensure_runtime_dirs() with service_lock(paths): - fe_port = _effective_frontend_port(paths, ServiceConfig.frontend_port) - be_port = _recorded_port(paths.backend_pid, ServiceConfig.backend_port) - _resolve_upgrade_runtime(console, frontend_port=fe_port, attempt_recover=False) - stop_one(fe_port, paths.frontend_pid, "WebUI", console) - stop_one(be_port, paths.backend_pid, "后端", console) + _stop_all_locked(paths, console) def _start_all_without_stop(config: ServiceConfig, console) -> None: @@ -1070,11 +1097,7 @@ def start_all(config: ServiceConfig, console) -> None: """Ensure backend and frontend are restarted with a clean state.""" paths = ensure_runtime_dirs() with service_lock(paths): - fe_port = _effective_frontend_port(paths, config.frontend_port) - be_port = _recorded_port(paths.backend_pid, ServiceConfig.backend_port) - _resolve_upgrade_runtime(console, frontend_port=fe_port, attempt_recover=False) - stop_one(fe_port, paths.frontend_pid, "WebUI", console) - stop_one(be_port, paths.backend_pid, "后端", console) + _stop_all_locked(paths, console, config=config) _start_all_without_stop(config, console) @@ -1082,11 +1105,7 @@ def restart_all(config: ServiceConfig, console) -> None: """Restart backend and frontend.""" paths = ensure_runtime_dirs() with service_lock(paths): - fe_port = _effective_frontend_port(paths, config.frontend_port) - be_port = _recorded_port(paths.backend_pid, ServiceConfig.backend_port) - _resolve_upgrade_runtime(console, frontend_port=fe_port, attempt_recover=False) - stop_one(fe_port, paths.frontend_pid, "WebUI", console) - stop_one(be_port, paths.backend_pid, "后端", console) + _stop_all_locked(paths, console, config=config) _start_all_without_stop(config, console) diff --git a/tests/cli/test_service_manager.py b/tests/cli/test_service_manager.py index 79d2edf6..dc60fd58 100644 --- a/tests/cli/test_service_manager.py +++ b/tests/cli/test_service_manager.py @@ -479,6 +479,71 @@ def test_restart_all_stops_then_starts_under_lock(monkeypatch) -> None: ] +def test_start_all_uses_config_backend_port_when_pid_record_missing(monkeypatch, tmp_path: Path) -> None: + paths = service_manager.RuntimePaths( + root=tmp_path, + run_dir=tmp_path / "run", + log_dir=tmp_path / "logs", + backend_pid=tmp_path / "run" / "backend.pid", + frontend_pid=tmp_path / "run" / "webui.pid", + backend_log=tmp_path / "logs" / "backend.log", + frontend_log=tmp_path / "logs" / "webui.log", + ) + paths.run_dir.mkdir(parents=True) + config = service_manager.ServiceConfig(frontend_port=5175, backend_port=9100) + calls: list[tuple[int, Path, str]] = [] + + monkeypatch.setattr(service_manager, "ensure_runtime_dirs", lambda: paths) + monkeypatch.setattr(service_manager, "service_lock", lambda _paths: _record_call([], "service_lock")) + monkeypatch.setattr(service_manager, "_resolve_upgrade_runtime", lambda *_args, **_kwargs: None) + monkeypatch.setattr( + service_manager, + "stop_one", + lambda port, pid_file, name, _console: calls.append((port, pid_file, name)), + ) + monkeypatch.setattr(service_manager, "_start_all_without_stop", lambda *_args, **_kwargs: None) + + service_manager.start_all(config, console=None) + + assert calls == [ + (5175, paths.frontend_pid, "WebUI"), + (9100, paths.backend_pid, "后端"), + ] + + +def test_restart_all_uses_config_backend_port_with_legacy_pid_record(monkeypatch, tmp_path: Path) -> None: + paths = service_manager.RuntimePaths( + root=tmp_path, + run_dir=tmp_path / "run", + log_dir=tmp_path / "logs", + backend_pid=tmp_path / "run" / "backend.pid", + frontend_pid=tmp_path / "run" / "webui.pid", + backend_log=tmp_path / "logs" / "backend.log", + frontend_log=tmp_path / "logs" / "webui.log", + ) + paths.run_dir.mkdir(parents=True) + paths.backend_pid.write_text("12345", encoding="utf-8") + config = service_manager.ServiceConfig(frontend_port=5176, backend_port=9200) + calls: list[tuple[int, Path, str]] = [] + + monkeypatch.setattr(service_manager, "ensure_runtime_dirs", lambda: paths) + monkeypatch.setattr(service_manager, "service_lock", lambda _paths: _record_call([], "service_lock")) + monkeypatch.setattr(service_manager, "_resolve_upgrade_runtime", lambda *_args, **_kwargs: None) + monkeypatch.setattr( + service_manager, + "stop_one", + lambda port, pid_file, name, _console: calls.append((port, pid_file, name)), + ) + monkeypatch.setattr(service_manager, "_start_all_without_stop", lambda *_args, **_kwargs: None) + + service_manager.restart_all(config, console=None) + + assert calls == [ + (5176, paths.frontend_pid, "WebUI"), + (9200, paths.backend_pid, "后端"), + ] + + def test_start_all_stops_on_failure_before_restart(monkeypatch) -> None: paths = service_manager.RuntimePaths( root=Path("/tmp"),