fix(core): keep launchd service logs outside protected Downloads
This commit is contained in:
@@ -14,6 +14,7 @@ import time
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.launchd_logs import launchd_log_path, prepare_launchd_log
|
||||
from k1link.local_service_launchd import (
|
||||
MISSION_CORE_LAUNCH_AGENT_LABEL,
|
||||
MissionCoreLaunchAgentError,
|
||||
@@ -81,6 +82,7 @@ def main() -> int:
|
||||
raise MissionCoreLaunchAgentError("launch agent backup identity collision")
|
||||
if not backup.exists():
|
||||
_write_atomic(backup, previous)
|
||||
prepare_launchd_log(launchd_log_path("k1link-serve-launchd.log"))
|
||||
_write_atomic(plan.agent_path, plan.desired_payload)
|
||||
try:
|
||||
_reload_launch_agent(plan.agent_path)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Move only the two installed Worker tunnel startup journals to Library/Logs.
|
||||
|
||||
Preserves every SSH argument and all other launchd fields. Plan/apply is bound
|
||||
to both hashes; failed health acceptance restores the previous declaration.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import plistlib
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from manage_mission_core_launch_agent import _write_atomic
|
||||
|
||||
from k1link.launchd_logs import launchd_log_path, prepare_launchd_log
|
||||
|
||||
LABELS = {
|
||||
"observatory": "com.nodedc.observatory-worker-tunnel.local",
|
||||
"gaussian": "com.nodedc.gaussian-pipeline-tunnel.local",
|
||||
}
|
||||
|
||||
|
||||
def reload_agent(path, label):
|
||||
domain = f"gui/{os.getuid()}"
|
||||
target = f"{domain}/{label}"
|
||||
subprocess.run(["launchctl", "bootout", target], capture_output=True, timeout=20)
|
||||
deadline = time.monotonic() + 20
|
||||
while subprocess.run(["launchctl", "print", target], capture_output=True).returncode == 0:
|
||||
if time.monotonic() > deadline:
|
||||
raise RuntimeError("Previous tunnel did not unload")
|
||||
time.sleep(0.2)
|
||||
subprocess.run(["launchctl", "bootstrap", domain, str(path)], check=True, timeout=20)
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("action", choices=["plan", "apply"])
|
||||
p.add_argument("tunnel", choices=LABELS)
|
||||
p.add_argument("--expected-current-sha256")
|
||||
p.add_argument("--expected-desired-sha256")
|
||||
a = p.parse_args()
|
||||
label = LABELS[a.tunnel]
|
||||
agent = Path.home() / "Library/LaunchAgents" / (label + ".plist")
|
||||
if agent.is_symlink():
|
||||
raise ValueError("LaunchAgent cannot be a symlink")
|
||||
previous = agent.read_bytes()
|
||||
desired = plistlib.loads(previous)
|
||||
if desired["Label"] != label or desired["ProgramArguments"][0] != "/usr/bin/ssh":
|
||||
raise ValueError("Unexpected tunnel declaration")
|
||||
log = launchd_log_path(a.tunnel + "-worker-tunnel.log")
|
||||
desired.update(StandardOutPath=str(log), StandardErrorPath=str(log))
|
||||
payload = plistlib.dumps(desired, sort_keys=True)
|
||||
plan = dict(
|
||||
label=label,
|
||||
current_sha256=hashlib.sha256(previous).hexdigest(),
|
||||
desired_sha256=hashlib.sha256(payload).hexdigest(),
|
||||
log_path=str(log),
|
||||
)
|
||||
if a.action == "plan":
|
||||
print(json.dumps(plan))
|
||||
return
|
||||
if (
|
||||
a.expected_current_sha256 != plan["current_sha256"]
|
||||
or a.expected_desired_sha256 != plan["desired_sha256"]
|
||||
):
|
||||
raise ValueError("Tunnel plan changed before apply")
|
||||
prepare_launchd_log(log)
|
||||
backup = log.parent / (label + "." + plan["current_sha256"] + ".plist")
|
||||
if backup.exists() and backup.read_bytes() != previous:
|
||||
raise ValueError("Backup identity collision")
|
||||
_write_atomic(backup, previous)
|
||||
_write_atomic(agent, payload)
|
||||
try:
|
||||
reload_agent(agent, label)
|
||||
# ExitOnForwardFailure makes an occupied remote listener terminate SSH.
|
||||
# Check a stable exact launchd PID over several reconnect intervals.
|
||||
target = f"gui/{os.getuid()}/{label}"
|
||||
pid, stable = None, 0
|
||||
for _ in range(12):
|
||||
time.sleep(1)
|
||||
state = subprocess.check_output(["launchctl", "print", target], text=True)
|
||||
found = next(
|
||||
(x.strip() for x in state.splitlines() if x.strip().startswith("pid =")), None
|
||||
)
|
||||
stable = stable + 1 if found and found == pid and "state = running" in state else 0
|
||||
pid = found
|
||||
if stable >= 6:
|
||||
print(json.dumps({**plan, "applied": True, "pid": pid}))
|
||||
return
|
||||
raise RuntimeError("Tunnel process did not remain running")
|
||||
except BaseException:
|
||||
_write_atomic(agent, previous)
|
||||
reload_agent(agent, label)
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user