How Kanata starts, stops, recovers, and stays healthy.
RuntimeCoordinator wires sub-coordinators and delegates all work.
ServiceLifecycleCoordinator is the ONLY start/stop/restart entry point.
ServiceHealthChecker polls launchctl status + TCP probe.
ServiceHealthChecker is the single source of truth for whether Kanata is running. It combines launchctl status with TCP probe results into a KanataServiceRuntimeSnapshot. No other code should check process state directly.ObservableObject — UI state lives in KanataViewModel, which reads snapshots via getCurrentUIState(). Annotated @MainActor.KanataDaemonManager.register(), kills orphaned processes before each start, kickstarts if registered but not running. Caches SMAppService pending status with a 5-second TTL to avoid blocking IPC in polling loops.func startKanata(reason: String) async -> Bool func stopKanata(reason: String) async -> Bool func restartKanata(reason: String) async -> Bool func currentRuntimeStatus() async -> RuntimeStatus
launchctl exit codes with TCP probes on port 6174 to produce a KanataServiceRuntimeSnapshot. Short-lived cache (2s TTL) deduplicates parallel health checks. Thread-safe via OSAllocatedUnfairLock.public struct KanataServiceRuntimeSnapshot { let managementState: WizardServiceManagementState let isRunning: Bool // launchctl says process exists let isResponding: Bool // TCP probe got a reply let inputCaptureReady: Bool // kanata capturing keys }
com.keypath.kanata LaunchDaemon via SMAppService. Handles stop/unregister, polls launchd-backed status, and debounces transient "enabled but no PID" samples to avoid false failure reports. Factory-based SMAppService creation provides a test seam.KanataDaemonManager state to avoid synchronous IPC in hot paths. Coordinates with ReloadSafetyMonitor and ProcessLifecycleManager.func triggerConfigReload() async -> ReloadResult
RuntimeCoordinator initialization.ServiceLifecycleCoordinator.startKanata() → kill orphans → KanataDaemonManager.register() → kickstart if needed → verify runningServiceLifecycleCoordinator.stopKanata() → AppContextService.stop() → KanataDaemonService.stopIfRunning()ConfigReloadCoordinator.triggerConfigReload() → check health → TCP command to port 6174 → ReloadResultServiceHealthChecker.checkKanataServiceHealth() → launchctl status + TCP probe → KanataServiceRuntimeSnapshotRecoveryCoordinator.attemptKeyboardRecovery() → kill → wait → restart VirtualHID → wait → restart KanataEngineClient reconnects on next reload attempt. Health checker marks isResponding = false until TCP probe succeeds.pgrep, no direct launchctl calls to check status. Use ServiceHealthChecker.checkKanataServiceHealth(). It combines PID + TCP and caches results.SMAppService.status == .enabled as "the daemon is registered" not "the daemon is running." Only the health checker knows if the process is alive.SMAppService.status is synchronous IPC that can block 10–30+ seconds under load. Cache within a validation cycle. ServiceLifecycleCoordinator uses a 5s TTL cache for pending checks.