fix(gateway): deregister transient reload label after one-shot job + test

Follow-up on the #69500 salvage: 'launchctl submit' jobs remain
registered with launchd after they exit, so every plist reload leaked
one dead '<label>.reload.<pid>.<ts>' label. The helper script now ends
with 'launchctl remove' of its own transient label, and the recovery
test asserts the self-removal is present.
This commit is contained in:
teknium1 2026-07-24 12:53:15 -07:00 committed by Teknium
parent ea94fc24ac
commit 8b45a8b0d4
2 changed files with 13 additions and 2 deletions

View file

@ -4134,6 +4134,9 @@ def refresh_launchd_plist_if_needed() -> bool:
# gateway is still draining (default agent.restart_drain_timeout=180s),
# so a fixed ~10s window is too short — bound by that budget instead.
_reload_budget = int(max(30.0, _get_restart_drain_timeout()))
# Label for the transient one-shot job (see `launchctl submit` below).
# Unique per reload so concurrent/repeated reloads never collide.
submit_label = f"{label}.reload.{os.getpid()}.{int(time.time())}"
reload_script = (
f"sleep 2; "
f"launchctl bootout {shlex.quote(target)} 2>/dev/null; "
@ -4148,7 +4151,12 @@ def refresh_launchd_plist_if_needed() -> bool:
f"done; "
f"if ! launchctl list {shlex.quote(label)} >/dev/null 2>&1; then "
f" echo \"[$(date '+%Y-%m-%d %H:%M:%S %z')] FAILED launchd reload for {shlex.quote(target)} — service NOT registered after {_reload_budget}s of retries\" >> {shlex.quote(str(reload_log_path))}; "
f"fi"
f"fi; "
# Submitted jobs stay registered with launchd after the script
# exits; without this, every reload leaks one dead label. Removing
# our own label is the documented way to end a one-shot submit job
# (it SIGTERMs the job, but this is the final statement anyway).
f"launchctl remove {shlex.quote(submit_label)} 2>/dev/null"
)
try:
# Spawn the reload helper via `launchctl submit` (a transient
@ -4162,7 +4170,6 @@ def refresh_launchd_plist_if_needed() -> bool:
# `launchctl submit` creates a wholly independent transient launchd
# job that launchd manages separately from the gateway, so bootout
# of the gateway job cannot reach the helper.
submit_label = f"{label}.reload.{os.getpid()}.{int(time.time())}"
subprocess.Popen(
[
"launchctl", "submit",

View file

@ -752,6 +752,10 @@ class TestLaunchdServiceRecovery:
script = cmd[bash_idx + 2]
assert "bootout" in script and "bootstrap" in script
assert str(plist_path) in script
# The one-shot job must deregister its own transient label at the end,
# otherwise every reload leaks a dead label in launchd.
submit_label = cmd[cmd.index("-l") + 1]
assert f"launchctl remove {submit_label}" in script
def test_refresh_uses_direct_reload_when_not_inside_gateway_tree(self, tmp_path, monkeypatch):
"""Normal CLI-initiated refresh (outside the service tree) keeps the