fix(gateway): defer macOS launchd reload when run inside the gateway tree (#47842)
When refresh_launchd_plist_if_needed() runs from inside the gateway's own launchd process tree (agent-initiated self-update via the terminal tool), a direct launchctl bootout tears down the service's process group — including the CLI doing the refresh — before the follow-up bootstrap can run. The gateway is left unloaded and KeepAlive can't revive it (#43842). Detect in-service execution via gateway.status.get_running_pid() + _is_pid_ancestor_of_current_process(), and delegate the bootout->bootstrap to a detached (start_new_session=True) helper that survives the process-group teardown. The normal out-of-tree CLI path is unchanged. Fixes #43842.
This commit is contained in:
parent
4d39a603d1
commit
e48803daec
2 changed files with 155 additions and 2 deletions
|
|
@ -7,6 +7,7 @@ Handles: hermes gateway [run|start|stop|restart|status|install|uninstall|setup]
|
|||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
|
|
@ -3467,14 +3468,60 @@ def refresh_launchd_plist_if_needed() -> bool:
|
|||
|
||||
plist_path.write_text(new_plist, encoding="utf-8")
|
||||
label = get_launchd_label()
|
||||
domain = _launchd_domain()
|
||||
target = f"{domain}/{label}"
|
||||
|
||||
# If this refresh is running INSIDE the gateway's own launchd process tree
|
||||
# (e.g. the agent triggered a self-update via its terminal tool), a direct
|
||||
# `launchctl bootout` tears down the service's process group — which
|
||||
# includes THIS CLI — before the follow-up `bootstrap` can run. The gateway
|
||||
# then stays unloaded and KeepAlive can't revive it (#43842). Detect that
|
||||
# case and hand the reload to a detached session that survives the bootout.
|
||||
gateway_pid = None
|
||||
try:
|
||||
from gateway.status import get_running_pid
|
||||
gateway_pid = get_running_pid()
|
||||
except Exception:
|
||||
gateway_pid = None
|
||||
|
||||
if (
|
||||
gateway_pid is not None
|
||||
and _is_pid_ancestor_of_current_process(gateway_pid)
|
||||
and hasattr(os, "setsid") # POSIX-only; launchd is macOS so always true here
|
||||
):
|
||||
# Delegate to a new session: `start_new_session=True` detaches the
|
||||
# helper from the gateway's process group, so the bootout that kills
|
||||
# the gateway (and us) does not kill the helper before it bootstraps.
|
||||
reload_script = (
|
||||
f"sleep 2; "
|
||||
f"launchctl bootout {shlex.quote(target)} 2>/dev/null; "
|
||||
f"sleep 1; "
|
||||
f"launchctl bootstrap {shlex.quote(domain)} {shlex.quote(str(plist_path))} 2>/dev/null"
|
||||
)
|
||||
try:
|
||||
subprocess.Popen(
|
||||
["/bin/bash", "-c", reload_script],
|
||||
start_new_session=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Deferred launchd reload could not be spawned: %s", e)
|
||||
return False
|
||||
print(
|
||||
"↻ Updated gateway launchd service definition; reload deferred to a "
|
||||
"detached helper (refresh ran inside the gateway process tree)"
|
||||
)
|
||||
return True
|
||||
|
||||
# Bootout/bootstrap so launchd picks up the new definition
|
||||
subprocess.run(
|
||||
["launchctl", "bootout", f"{_launchd_domain()}/{label}"],
|
||||
["launchctl", "bootout", target],
|
||||
check=False,
|
||||
timeout=90,
|
||||
)
|
||||
subprocess.run(
|
||||
["launchctl", "bootstrap", _launchd_domain(), str(plist_path)],
|
||||
["launchctl", "bootstrap", domain, str(plist_path)],
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue