merge: testing → main (reconcile 2-week divergence)
This commit is contained in:
147
decnet/cli/lifecycle.py
Normal file
147
decnet/cli/lifecycle.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess # nosec B404
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
from rich.table import Table
|
||||
|
||||
from decnet.env import DECNET_INGEST_LOG_FILE
|
||||
|
||||
from . import utils as _utils
|
||||
from .gating import _agent_mode_active, _require_master_mode
|
||||
from .utils import console, log
|
||||
|
||||
|
||||
def register(app: typer.Typer) -> None:
|
||||
@app.command()
|
||||
def redeploy(
|
||||
log_file: str = typer.Option(DECNET_INGEST_LOG_FILE, "--log-file", "-f", help="Path to the DECNET log file"),
|
||||
) -> None:
|
||||
"""Check running DECNET services and relaunch any that are down."""
|
||||
log.info("redeploy: checking services")
|
||||
registry = _utils._service_registry(str(log_file))
|
||||
|
||||
table = Table(title="DECNET Services", show_lines=True)
|
||||
table.add_column("Service", style="bold cyan")
|
||||
table.add_column("Status")
|
||||
table.add_column("PID", style="dim")
|
||||
table.add_column("Action")
|
||||
|
||||
relaunched = 0
|
||||
for name, match_fn, launch_args in registry:
|
||||
pid = _utils._is_running(match_fn)
|
||||
if pid is not None:
|
||||
table.add_row(name, "[green]UP[/]", str(pid), "—")
|
||||
else:
|
||||
try:
|
||||
subprocess.Popen( # nosec B603
|
||||
launch_args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
table.add_row(name, "[red]DOWN[/]", "—", "[green]relaunched[/]")
|
||||
relaunched += 1
|
||||
except (FileNotFoundError, subprocess.SubprocessError) as exc:
|
||||
table.add_row(name, "[red]DOWN[/]", "—", f"[red]failed: {exc}[/]")
|
||||
|
||||
console.print(table)
|
||||
if relaunched:
|
||||
console.print(f"[green]{relaunched} service(s) relaunched.[/]")
|
||||
else:
|
||||
console.print("[green]All services running.[/]")
|
||||
|
||||
@app.command()
|
||||
def status() -> None:
|
||||
"""Show running deckies and the state of every ``decnet-*`` unit.
|
||||
|
||||
Prefers systemd (``systemctl list-units 'decnet-*.service'``) so
|
||||
agents, masters and mixed hosts all get one consistent view of
|
||||
what's installed, loaded, and active. Falls back to the psutil
|
||||
cmdline registry on boxes without systemd (dev laptops, CI
|
||||
containers, non-systemd init) so `decnet status` is still useful
|
||||
there.
|
||||
"""
|
||||
log.info("status command invoked")
|
||||
from decnet.engine import status as _status
|
||||
_status()
|
||||
|
||||
units = _utils._systemd_units()
|
||||
if units is not None:
|
||||
_render_systemd_units(units)
|
||||
else:
|
||||
_render_psutil_fallback()
|
||||
|
||||
def _render_systemd_units(units: list[dict]) -> None:
|
||||
svc_table = Table(title="DECNET Services (systemd)", show_lines=True)
|
||||
svc_table.add_column("Unit", style="bold cyan")
|
||||
svc_table.add_column("Load")
|
||||
svc_table.add_column("Active")
|
||||
svc_table.add_column("Sub")
|
||||
svc_table.add_column("Description", style="dim")
|
||||
|
||||
if not units:
|
||||
console.print(
|
||||
"[yellow]No decnet-* systemd units loaded. "
|
||||
"Run `sudo decnet init` to install them.[/]"
|
||||
)
|
||||
return
|
||||
|
||||
def _active_style(active: str) -> str:
|
||||
if active == "active":
|
||||
return "[green]active[/]"
|
||||
if active == "failed":
|
||||
return "[red]failed[/]"
|
||||
return f"[yellow]{active}[/]"
|
||||
|
||||
for u in sorted(units, key=lambda x: x.get("unit", "")):
|
||||
svc_table.add_row(
|
||||
u.get("unit", ""),
|
||||
u.get("load", ""),
|
||||
_active_style(u.get("active", "")),
|
||||
u.get("sub", ""),
|
||||
u.get("description", ""),
|
||||
)
|
||||
console.print(svc_table)
|
||||
|
||||
def _render_psutil_fallback() -> None:
|
||||
registry = _utils._service_registry(str(DECNET_INGEST_LOG_FILE))
|
||||
if _agent_mode_active():
|
||||
registry = [r for r in registry if r[0] not in {"Mutator", "Profiler", "API"}]
|
||||
svc_table = Table(
|
||||
title="DECNET Services (psutil fallback — systemd unavailable)",
|
||||
show_lines=True,
|
||||
)
|
||||
svc_table.add_column("Service", style="bold cyan")
|
||||
svc_table.add_column("Status")
|
||||
svc_table.add_column("PID", style="dim")
|
||||
|
||||
for name, match_fn, _launch_args in registry:
|
||||
pid = _utils._is_running(match_fn)
|
||||
if pid is not None:
|
||||
svc_table.add_row(name, "[green]UP[/]", str(pid))
|
||||
else:
|
||||
svc_table.add_row(name, "[red]DOWN[/]", "—")
|
||||
|
||||
console.print(svc_table)
|
||||
|
||||
@app.command()
|
||||
def teardown(
|
||||
all_: bool = typer.Option(False, "--all", help="Tear down all deckies and remove network"),
|
||||
id_: Optional[str] = typer.Option(None, "--id", help="Tear down a specific decky by name"),
|
||||
) -> None:
|
||||
"""Stop and remove deckies."""
|
||||
_require_master_mode("teardown")
|
||||
if not all_ and not id_:
|
||||
console.print("[red]Specify --all or --id <name>.[/]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
log.info("teardown command invoked all=%s id=%s", all_, id_)
|
||||
from decnet.engine import teardown as _teardown
|
||||
_teardown(decky_id=id_)
|
||||
log.info("teardown complete all=%s id=%s", all_, id_)
|
||||
|
||||
if all_:
|
||||
_utils._kill_all_services()
|
||||
Reference in New Issue
Block a user