Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bde8296
fix: add backend service watchdog recovery
Jul 1, 2026
b16d714
Refactor service supervisor control plane
Jul 1, 2026
10a18dc
Refactor supervisor control and process adapters
Jul 2, 2026
8563950
fix(updater): coordinate handoff with supervisor
Jul 2, 2026
04c800e
fix(supervisor): resume services after failed upgrade
Jul 2, 2026
0e8cad1
chore(cli): clarify daemon status output
Jul 2, 2026
f689e91
fix(cli): clean trusted orphan service ports
Jul 2, 2026
4db629b
fix(cli): preserve daemon lifecycle compatibility
Jul 2, 2026
b69a681
fix(cli): remove supervisor log alias
Jul 2, 2026
a92bb37
fix(cli): clean orphan service port owners
Jul 2, 2026
517da1e
chore(cli): simplify startup status summary
Jul 2, 2026
4005df4
chore(cli): print daemon startup step immediately
Jul 2, 2026
533d8a5
chore(cli): group daemon in status services
Jul 2, 2026
112cedf
fix(daemon): suppress disconnected control clients
Jul 2, 2026
21cd80f
chore(cli): show stopped services
Jul 2, 2026
5f538b5
refactor(cli): serve webui through supervised backend
Jul 3, 2026
dcfdc95
fix(cli): support supervisor control on Windows
Jul 3, 2026
81ee06f
fix(cli): improve Windows supervisor startup
Jul 3, 2026
0be6198
fix(cli): recover upgrade handoff on start
Jul 3, 2026
3227f6b
Rename daemon logs and drop wheel force-include
Jul 3, 2026
26c8f67
test(updater): add manual webui branch upgrade test
Jul 3, 2026
fc17ab5
fix(cli): simplify service start upgrade cleanup
Jul 3, 2026
8d74380
fix(updater): defer supervisor handoff restart
Jul 3, 2026
e1b5124
Merge remote-tracking branch 'origin/refactor/supervisor-control-adap…
Jul 3, 2026
e26e64a
Merge remote-tracking branch 'origin/dev' into refactor/supervisor-co…
Jul 3, 2026
7479b67
test(docker): remove obsolete runtime install assertion
Jul 3, 2026
6cd7620
fix(installer): build webui after dependency install
Jul 3, 2026
2f3d6c8
fix(doctor): detect current service status
Jul 3, 2026
fb12b0e
fix(server): harden startup and auth security
Jul 3, 2026
77b1e31
fix(server): bound login rate limiter state
Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions flocks/cli/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,25 @@ def _print_service_diagnosis() -> None:
status_lines = build_status_lines()
except Exception as error:
console.print(f"[yellow]服务状态检查失败:{error}[/yellow]")
console.print("[yellow]服务不正常,请执行 `flocks restart`[/yellow]")
console.print("[yellow]运行状态异常,请执行 `flocks restart`[/yellow]")
return

for line in status_lines:
console.print(line)

if _service_status_is_healthy(status_lines):
console.print("[green]服务正常[/green]")
console.print("[green]运行状态正常[/green]")
else:
console.print("[yellow]服务不正常,请执行 `flocks restart`[/yellow]")
console.print("[yellow]运行状态异常,请执行 `flocks restart`[/yellow]")


def _service_status_is_healthy(status_lines: list[str]) -> bool:
"""Return whether backend and WebUI both look healthy from status lines."""
"""Return whether the current or legacy service status looks healthy."""
daemon_running = any("daemon:" in line and "state=running" in line for line in status_lines)
flocks_healthy = any("flocks:" in line and "state=healthy" in line for line in status_lines)
if daemon_running and flocks_healthy:
return True

backend_running = any("后端运行中" in line for line in status_lines)
webui_running = any("WebUI 运行中" in line for line in status_lines)
return backend_running and webui_running
Expand Down
167 changes: 83 additions & 84 deletions flocks/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"""

import asyncio
import os
import secrets as secrets_lib
import sys
from pathlib import Path
Expand All @@ -31,10 +30,15 @@
task_app,
)
from flocks.cli.commands.update import update_command
from flocks.cli.service_manager import (
from flocks.cli.service_config import (
ServiceConfig,
ServiceConfigError,
build_service_config,
restart_defaults_from_status_payload,
)
from flocks.cli.service_control import read_supervisor_status
from flocks.cli.service_manager import (
ServiceError,
read_runtime_record,
resolve_flocks_cli_command,
restart_all,
runtime_paths,
Expand All @@ -43,6 +47,7 @@
start_all,
stop_all,
)
from flocks.cli.service_supervisor import run_service_daemon
from flocks.config.config import Config
from flocks.utils.log import Log, LogLevel

Expand Down Expand Up @@ -142,6 +147,8 @@ def main_callback(
def _service_config(
no_browser: bool = False,
skip_webui_build: bool = False,
host: Optional[str] = None,
port: Optional[int] = None,
server_host: Optional[str] = None,
server_port: Optional[int] = None,
webui_host: Optional[str] = None,
Expand All @@ -153,87 +160,36 @@ def _service_config(
) -> ServiceConfig:
"""Build service config from environment and CLI toggles."""
global_config = Config.get_global()
return ServiceConfig(
backend_host=_resolve_host(
cli_value=server_host,
env_names=("FLOCKS_SERVER_HOST", "FLOCKS_BACKEND_HOST"),
default=default_server_host or global_config.server_host,
),
backend_port=_resolve_port(
cli_value=server_port,
env_names=("FLOCKS_SERVER_PORT", "FLOCKS_BACKEND_PORT"),
default=default_server_port or global_config.server_port,
label="server",
),
frontend_host=_resolve_host(
cli_value=webui_host,
env_names=("FLOCKS_WEBUI_HOST", "FLOCKS_FRONTEND_HOST"),
default=default_webui_host or "127.0.0.1",
),
frontend_port=_resolve_port(
cli_value=webui_port,
env_names=("FLOCKS_WEBUI_PORT", "FLOCKS_FRONTEND_PORT"),
default=default_webui_port or 5173,
label="webui",
),
return build_service_config(
no_browser=no_browser,
skip_frontend_build=skip_webui_build,
skip_webui_build=skip_webui_build,
public_host=host,
public_port=port,
server_host=server_host,
server_port=server_port,
webui_host=webui_host,
webui_port=webui_port,
default_server_host=default_server_host or global_config.server_host,
default_server_port=default_server_port or global_config.server_port,
default_webui_host=default_webui_host or "127.0.0.1",
default_webui_port=default_webui_port or 5173,
)


def _resolve_host(cli_value: Optional[str], env_names: tuple[str, ...], default: str) -> str:
"""Resolve a host value from CLI, environment, and default values."""
if cli_value is not None:
return cli_value
for env_name in env_names:
env_value = os.getenv(env_name)
if env_value:
return env_value
return default


def _resolve_port(
cli_value: Optional[int],
env_names: tuple[str, ...],
default: int,
label: str,
) -> int:
"""Resolve a port value from CLI, environment, and default values."""
if cli_value is not None:
return cli_value
for env_name in env_names:
env_value = os.getenv(env_name)
if not env_value:
continue
try:
return int(env_value)
except ValueError as error:
raise ServiceError(f"{label} port from {env_name} must be an integer.") from error
return default


def _restart_runtime_defaults() -> dict[str, Any]:
"""Load host/port defaults from the last recorded service runtime."""
paths = runtime_paths()
backend = read_runtime_record(paths.backend_pid)
frontend = read_runtime_record(paths.frontend_pid)
defaults: dict[str, Any] = {}
if backend is not None:
if backend.host:
defaults["default_server_host"] = backend.host
if backend.port is not None:
defaults["default_server_port"] = backend.port
if frontend is not None:
if frontend.host:
defaults["default_webui_host"] = frontend.host
if frontend.port is not None:
defaults["default_webui_port"] = frontend.port
return defaults
"""Load host/port defaults from the running supervisor when available."""
try:
status = read_supervisor_status(paths=runtime_paths(), timeout=1.0)
except Exception:
return {}
return restart_defaults_from_status_payload(getattr(status, "raw", status))


def _restart_service_config(
no_browser: bool = False,
skip_webui_build: bool = False,
host: Optional[str] = None,
port: Optional[int] = None,
server_host: Optional[str] = None,
server_port: Optional[int] = None,
webui_host: Optional[str] = None,
Expand All @@ -243,6 +199,8 @@ def _restart_service_config(
return _service_config(
no_browser=no_browser,
skip_webui_build=skip_webui_build,
host=host,
port=port,
server_host=server_host,
server_port=server_port,
webui_host=webui_host,
Expand All @@ -263,21 +221,25 @@ def start(
skip_webui_build: bool = typer.Option(
False,
"--skip-webui-build",
help="Skip `npm run build` before starting WebUI",
help="Skip WebUI static asset build before starting Flocks service",
),
host: Optional[str] = typer.Option(None, "--host", "-h", help="Public service host"),
port: Optional[int] = typer.Option(None, "--port", "-p", help="Public service port"),
server_host: Optional[str] = typer.Option(None, "--server-host", help="Backend server host"),
server_port: Optional[int] = typer.Option(None, "--server-port", help="Backend server port"),
webui_host: Optional[str] = typer.Option(None, "--webui-host", help="WebUI host"),
webui_port: Optional[int] = typer.Option(None, "--webui-port", help="WebUI port"),
):
"""
Start backend and WebUI in daemon mode
Start Flocks service in daemon mode.
"""
try:
start_all(
_service_config(
no_browser=no_browser,
skip_webui_build=skip_webui_build,
host=host,
port=port,
server_host=server_host,
server_port=server_port,
webui_host=webui_host,
Expand All @@ -292,7 +254,7 @@ def start(
@app.command()
def stop():
"""
Stop backend and WebUI
Stop Flocks service.
"""
try:
stop_all(console)
Expand All @@ -306,36 +268,40 @@ def restart(
skip_webui_build: bool = typer.Option(
False,
"--skip-webui-build",
help="Skip `npm run build` before starting WebUI",
help="Skip WebUI static asset build before starting Flocks service",
),
host: Optional[str] = typer.Option(None, "--host", "-h", help="Public service host"),
port: Optional[int] = typer.Option(None, "--port", "-p", help="Public service port"),
server_host: Optional[str] = typer.Option(None, "--server-host", help="Backend server host"),
server_port: Optional[int] = typer.Option(None, "--server-port", help="Backend server port"),
webui_host: Optional[str] = typer.Option(None, "--webui-host", help="WebUI host"),
webui_port: Optional[int] = typer.Option(None, "--webui-port", help="WebUI port"),
):
"""
Restart backend and WebUI
Restart Flocks service.
"""
try:
restart_all(
_restart_service_config(
no_browser=no_browser,
skip_webui_build=skip_webui_build,
host=host,
port=port,
server_host=server_host,
server_port=server_port,
webui_host=webui_host,
webui_port=webui_port,
),
console,
)
except ServiceError as error:
except (ServiceConfigError, ServiceError) as error:
_handle_service_error(error)


@app.command()
def status():
"""
Show backend and WebUI status
Show Flocks service status.
"""
try:
show_status(console)
Expand All @@ -345,13 +311,13 @@ def status():

@app.command()
def logs(
backend: bool = typer.Option(False, "--backend", help="Only show backend logs"),
webui: bool = typer.Option(False, "--webui", help="Only show WebUI logs"),
backend: bool = typer.Option(False, "--backend", help="Only show service logs"),
webui: bool = typer.Option(False, "--webui", help="Only show service logs"),
follow: bool = typer.Option(True, "--follow/--no-follow", help="Follow logs in real time"),
lines: int = typer.Option(50, "--lines", "-n", min=0, help="Number of recent lines to show"),
):
"""
Show backend and WebUI logs
Show Flocks service logs.
"""
try:
show_logs(console, backend=backend, webui=webui, follow=follow, lines=lines)
Expand Down Expand Up @@ -403,6 +369,39 @@ def serve(
)


@app.command(name="service-daemon", hidden=True)
def service_daemon(
server_host: str = typer.Option("127.0.0.1", "--server-host", help="Backend server host"),
server_port: int = typer.Option(5173, "--server-port", help="Public service port"),
webui_host: str = typer.Option("127.0.0.1", "--webui-host", help="WebUI host"),
webui_port: int = typer.Option(5173, "--webui-port", help="WebUI port"),
legacy_server_host: Optional[str] = typer.Option(None, "--legacy-server-host", help="Legacy backend host"),
legacy_server_port: Optional[int] = typer.Option(8000, "--legacy-server-port", help="Legacy backend port"),
server_port_migration_hint: bool = typer.Option(
False,
"--server-port-migration-hint",
help="Print server-port migration hint in parent CLI",
),
skip_webui_build: bool = typer.Option(False, "--skip-webui-build", help="Skip WebUI static asset build"),
):
"""
Run the Flocks service supervisor daemon.
"""
run_service_daemon(
ServiceConfig(
backend_host=server_host,
backend_port=server_port,
frontend_host=webui_host,
frontend_port=webui_port,
legacy_backend_host=legacy_server_host,
legacy_backend_port=legacy_server_port,
server_port_migration_hint=server_port_migration_hint,
no_browser=True,
skip_frontend_build=skip_webui_build,
),
)


@app.command()
def tui(
directory: Optional[Path] = typer.Option(None, "--directory", "-d", help="Project directory"),
Expand Down
Loading