'decnet status' used to psutil-scan for cmdlines matching hand-coded service launch args. That worked on dev boxes running workers via 'python -m decnet.cli ...' but missed the systemd reality on real hosts: units may be installed but not started, failed, or in auto-restart — all invisible to a cmdline grep. New behaviour: status calls `systemctl list-units --type=service --all --output=json 'decnet-*.service'` and renders the unit/load/active/ sub/description matrix. One view works for masters, agents, and mixed hosts — iterates over whatever 'decnet-*' units were installed by 'decnet init' / the enroll-bundle. Agent/master mode filtering is no longer needed in the CLI; the host literally does not have master-only units installed if it enrolled as an agent. The psutil path survives as a fallback for boxes without systemd (dev laptops, CI containers, minimal init systems) so the command stays useful there. Clearly labelled 'psutil fallback' in the table title so operators know which view they're looking at.
148 lines
5.4 KiB
Python
148 lines
5.4 KiB
Python
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()
|