Follow-up trim to the salvaged fix from #124669 (@lorenzozanee): add the why-comments on the two one-line branch changes so the next reader does not "simplify" them back. - profile_lifecycle: `stop` both parks the profile and drops it from the host's served set, so a later `restart` finds no host serving it and fell through to the standalone restart path. Treating a parked restart as `start` unparks and hot-serves via the host; the `find_gateway_pids()` guard keeps a live --force gateway's own restart ownership. - _cmd_status: gateway.parked records intent, not runtime. Read the runtime snapshot before the parked short-circuit so a --force-started live gateway is reported beside the marker; a parked profile with nothing running still stops at the parked line. Sibling surfaces checked: dashboard POST /api/gateway/restart?profile=X spawns `hermes -p X gateway restart` (same profile_lifecycle, fixed by the same change); /api/status lists live gateways independently of parked_profiles; tui_gateway has no parked-marker consumer.
116 lines
5.7 KiB
Python
116 lines
5.7 KiB
Python
"""Named-profile lifecycle inside the host gateway, never a service-manager action."""
|
|
from __future__ import annotations
|
|
|
|
from hermes_constants import get_hermes_home, get_default_hermes_root
|
|
from hermes_cli.profiles import parked_marker_path, profile_is_parked, profile_is_standalone, profiles_to_serve
|
|
|
|
|
|
def _confirmed(answer, key, name):
|
|
return isinstance(answer, dict) and answer.get(key) == name and not answer.get("error")
|
|
|
|
|
|
def _failure(answer):
|
|
if isinstance(answer, dict):
|
|
return answer.get("error") or "host operation is still pending"
|
|
return "host control socket did not answer"
|
|
|
|
|
|
def host_scope_for_all_verb(owner):
|
|
"""Home a ``--all`` verb acts as: the live host owner's, else the default root.
|
|
|
|
``--all`` names the ONE host gateway, whichever profile invoked the CLI — but the active home
|
|
decides the service name, the Windows task/launcher and the detached child's ``--profile``, so
|
|
with the host DOWN ``-p X gateway start|restart --all`` re-launched X's own gateway, which the
|
|
named-profile guard then refused (exit 78): the host stayed down after a Desktop update.
|
|
"""
|
|
from hermes_cli.gateway_migrate import _home_env
|
|
return _home_env(owner.home if owner is not None else get_default_hermes_root())
|
|
|
|
|
|
def profile_lifecycle(command: str, args) -> bool:
|
|
"""True when a named-profile command was handled (including an unconfirmed request)."""
|
|
from hermes_cli import gateway as gw
|
|
from gateway.control_socket import request_unserve_profile, request_serve_profile_hot
|
|
|
|
name = gw._current_profile_name()
|
|
if not name or name == "default" or getattr(args, "all", False) or getattr(args, "force", False):
|
|
return False
|
|
home = get_hermes_home()
|
|
if profile_is_standalone(home):
|
|
# gateway.standalone wins: the host never serves (or parks) an opted-out profile, so its
|
|
# verbs keep addressing its own gateway process even while a stale host record lists it.
|
|
return False
|
|
marker = parked_marker_path(home)
|
|
if command == "restart" and profile_is_parked(home) and not gw.find_gateway_pids():
|
|
# `stop` parked the profile AND dropped it from the host's served set, so the unserve/serve
|
|
# path below finds no host serving it and would fall through to a standalone restart.
|
|
# Unpark like `start` does; a live --force gateway keeps its own restart (pids non-empty).
|
|
command = "start"
|
|
if command == "start":
|
|
if not profile_is_parked(home):
|
|
return False
|
|
marker.unlink()
|
|
owner = gw._host_multiplexer_for_all_verb()
|
|
if owner is None:
|
|
from hermes_cli.gateway_multiplex_served import live_default_gateway_pid
|
|
if live_default_gateway_pid() is None:
|
|
return False # Unpark even when today's normal start path must start the host first.
|
|
host_home = owner.home if owner is not None else get_default_hermes_root()
|
|
answer = request_serve_profile_hot(host_home, name)
|
|
if _confirmed(answer, "served", name):
|
|
print(f"Profile '{name}' served by the host gateway.")
|
|
else:
|
|
print(f"Profile '{name}' unparked, but serving was not confirmed: {_failure(answer)}.")
|
|
print("The host retries on its next rescan (within 30s). Check gateway status.")
|
|
return True
|
|
|
|
# A separate --force gateway still owns its normal process/service lifecycle.
|
|
if gw.find_gateway_pids():
|
|
return False
|
|
owner = gw._served_by_another_host_gateway()
|
|
if owner is None and not gw.named_profile_served_by_running_multiplexer():
|
|
return False
|
|
host_home = owner.home if owner is not None else get_default_hermes_root()
|
|
if command == "stop":
|
|
# Persist intent BEFORE teardown: the periodic rescan must not re-add this profile.
|
|
marker.touch()
|
|
answer = request_unserve_profile(host_home, name)
|
|
if _confirmed(answer, "unserved", name):
|
|
print(f"Profile '{name}' parked; its bots and cron are stopped. "
|
|
f"Start again with: hermes -p {name} gateway start")
|
|
else:
|
|
print(f"Profile '{name}' parked, but immediate stop was not confirmed: {_failure(answer)}.")
|
|
print("The host drops it on its next rescan (within 30s).")
|
|
return True
|
|
|
|
answer = request_unserve_profile(host_home, name)
|
|
if not _confirmed(answer, "unserved", name):
|
|
print(f"Profile '{name}' restart was not confirmed: {_failure(answer)}.")
|
|
return True
|
|
answer = request_serve_profile_hot(host_home, name)
|
|
if _confirmed(answer, "served", name):
|
|
print(f"Profile '{name}' restarted by the host gateway.")
|
|
else:
|
|
print(f"Profile '{name}' stopped, but serving was not confirmed: {_failure(answer)}.")
|
|
print("The host retries on its next rescan (within 30s). Check gateway status.")
|
|
return True
|
|
|
|
|
|
def print_parked_status() -> bool:
|
|
"""A parked satellite is still installed, but is not a running gateway."""
|
|
from hermes_cli import gateway as gw
|
|
name = gw._current_profile_name()
|
|
if name and name != "default" and profile_is_parked(get_hermes_home()):
|
|
print(f"Profile '{name}': parked (hermes -p {name} gateway start)")
|
|
return True
|
|
if not name or name == "default":
|
|
parked = [profile for profile, home in profiles_to_serve(True, include_parked=True)
|
|
if profile != "default" and profile_is_parked(home)]
|
|
if parked:
|
|
owner = gw.host_multiplexer_serving()
|
|
if owner is not None:
|
|
print(f"Served profiles: {', '.join(owner.profiles)}")
|
|
for profile in parked:
|
|
print(f"Profile '{profile}': parked (hermes -p {profile} gateway start)")
|
|
return False
|