fix(service): supervise canonical Mission Core lifecycle

This commit is contained in:
DCCONSTRUCTIONS
2026-08-25 21:58:22 +03:00
parent fb5bf943c9
commit f50e0077c5
10 changed files with 1095 additions and 6 deletions
+221
View File
@@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""Plan/apply the user-owned Mission Core LaunchAgent with rollback acceptance."""
from __future__ import annotations
import argparse
import hashlib
import http.client
import json
import os
import subprocess
import tempfile
import time
from contextlib import suppress
from pathlib import Path
from k1link.local_service_launchd import (
MISSION_CORE_LAUNCH_AGENT_LABEL,
MissionCoreLaunchAgentError,
plan_mission_core_launch_agent,
)
_LAUNCHCTL_TRANSITION_TIMEOUT_SECONDS = 30.0
_LAUNCHCTL_TRANSITION_POLL_SECONDS = 0.1
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=("plan", "apply", "status"))
parser.add_argument("--repository-root", type=Path, required=True)
parser.add_argument(
"--agent-path",
type=Path,
default=Path.home()
/ "Library/LaunchAgents/com.nodedc.mission-core.local.plist",
)
parser.add_argument("--expected-current-sha256")
parser.add_argument("--expected-desired-sha256")
arguments = parser.parse_args()
if arguments.action == "status":
print(json.dumps(_status(), sort_keys=True))
return 0
plan = plan_mission_core_launch_agent(
repository_root=arguments.repository_root,
agent_path=arguments.agent_path,
)
if arguments.action == "plan":
print(json.dumps(plan.to_dict(), indent=2, sort_keys=True))
return 0
if (
arguments.expected_current_sha256 != plan.current_sha256
or arguments.expected_desired_sha256 != plan.desired_sha256
):
raise MissionCoreLaunchAgentError("launch agent plan changed before apply")
previous = plan.agent_path.read_bytes()
backup_root = (
arguments.repository_root.expanduser().resolve()
/ ".runtime/mission-core/launch-agent-backups"
)
backup_root.mkdir(mode=0o700, parents=True, exist_ok=True)
backup = backup_root / f"{plan.current_sha256}.plist"
if backup.exists() and hashlib.sha256(backup.read_bytes()).hexdigest() != plan.current_sha256:
raise MissionCoreLaunchAgentError("launch agent backup identity collision")
if not backup.exists():
_write_atomic(backup, previous)
_write_atomic(plan.agent_path, plan.desired_payload)
try:
_reload_launch_agent(plan.agent_path)
accepted = _wait_for_health(60.0)
if not accepted:
raise MissionCoreLaunchAgentError("reloaded Mission Core did not become healthy")
except BaseException:
_write_atomic(plan.agent_path, previous)
_reload_launch_agent(plan.agent_path)
if not _wait_for_health(60.0):
raise MissionCoreLaunchAgentError(
"launch agent apply failed and rollback did not recover health"
) from None
raise
result = {
**plan.to_dict(),
"applied": True,
"backup_path": str(backup),
"health_accepted": True,
"status": _status(),
}
print(json.dumps(result, indent=2, sort_keys=True))
return 0
def _write_atomic(path: Path, payload: bytes) -> None:
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "wb", closefd=True) as stream:
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
except BaseException:
with suppress(OSError):
os.close(descriptor)
with suppress(FileNotFoundError):
os.unlink(temporary)
raise
def _reload_launch_agent(path: Path) -> None:
"""Reload only after launchd proves the previous job is fully absent.
``launchctl bootout`` can return while the job is still represented by an
``xpcproxy`` transition. An immediate bootstrap is then rejected even
though the plist is valid. Waiting on the exact label avoids that race and
keeps apply/rollback symmetric.
"""
domain = f"gui/{os.getuid()}"
target = f"{domain}/{MISSION_CORE_LAUNCH_AGENT_LABEL}"
bootout = subprocess.run(
["launchctl", "bootout", f"{domain}/{MISSION_CORE_LAUNCH_AGENT_LABEL}"],
check=False,
capture_output=True,
text=True,
timeout=30.0,
)
if bootout.returncode != 0 and _launch_agent_loaded(target):
raise MissionCoreLaunchAgentError(
f"launchctl bootout failed with exit code {bootout.returncode}"
)
_wait_until_launch_agent_unloaded(target)
completed = subprocess.run(
["launchctl", "bootstrap", domain, str(path)],
check=False,
capture_output=True,
text=True,
timeout=30.0,
)
if completed.returncode != 0:
raise MissionCoreLaunchAgentError(
"launchctl bootstrap rejected the Mission Core agent "
f"with exit code {completed.returncode}"
)
def _launch_agent_loaded(target: str) -> bool:
completed = subprocess.run(
["launchctl", "print", target],
check=False,
capture_output=True,
text=True,
timeout=5.0,
)
return completed.returncode == 0
def _wait_until_launch_agent_unloaded(target: str) -> None:
deadline = time.monotonic() + _LAUNCHCTL_TRANSITION_TIMEOUT_SECONDS
while time.monotonic() < deadline:
if not _launch_agent_loaded(target):
return
time.sleep(_LAUNCHCTL_TRANSITION_POLL_SECONDS)
raise MissionCoreLaunchAgentError(
"Mission Core launch agent did not finish bootout before the deadline"
)
def _wait_for_health(timeout_seconds: float) -> bool:
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
if _healthy():
return True
time.sleep(0.25)
return False
def _healthy() -> bool:
connection = http.client.HTTPConnection("127.0.0.1", 8000, timeout=1.0)
try:
connection.request("GET", "/api/health", headers={"Connection": "close"})
response = connection.getresponse()
payload = response.read(64 * 1024 + 1)
except (OSError, TimeoutError, http.client.HTTPException):
return False
finally:
connection.close()
if response.status != 200 or len(payload) > 64 * 1024:
return False
try:
document = json.loads(payload)
except (UnicodeDecodeError, json.JSONDecodeError):
return False
return bool(
isinstance(document, dict)
and document.get("ok") is True
and document.get("status") == "ok"
and document.get("service") == "mission-core-control-plane"
)
def _status() -> dict[str, object]:
completed = subprocess.run(
[
"launchctl",
"print",
f"gui/{os.getuid()}/{MISSION_CORE_LAUNCH_AGENT_LABEL}",
],
check=False,
capture_output=True,
text=True,
timeout=5.0,
)
return {
"label": MISSION_CORE_LAUNCH_AGENT_LABEL,
"launchd_loaded": completed.returncode == 0,
"health_ok": _healthy(),
}
if __name__ == "__main__":
raise SystemExit(main())