Fix CLI service lifecycle through SMAppService - #1253
Conversation
|
Two points worth a look in
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e8ed17089f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| private func registerDaemon() async throws { | ||
| let service = makeSMService() | ||
| do { | ||
| try service.register() |
There was a problem hiding this comment.
Recover stale enabled registrations before polling
When SMAppService.status is .enabled but the daemon is no longer loaded or running, calling register() directly does not perform the existing unregister/re-register recovery or kickstart implemented by KanataDaemonManager.register(). In that degraded state, keypath-cli service start will poll for 20 seconds and return false while leaving Kanata stopped; inspect the fresh registration state and route this through the canonical recovery path instead.
AGENTS.md reference: AGENTS.md:L313-L316
Useful? React with 👍 / 👎.
|
Correctness/reliability gap:
func start() async throws {
try await registerDaemon()
await pidCache.invalidateCache()
lastObservedState = .unknown
AppLogger.shared.info("✅ ... Start requested successfully")
}It only checks that
This directly conflicts with the repo's own invariant (AGENTS.md): "Mutating installer actions must be postcondition-verified before returning success... any action that can stop/restart/re-register Kanata must verify runtime readiness (running + TCP responding) or explicit pending-approval state before reporting success." It's also the same class of bug this PR's linked doc ( Test coverage reflects the gap: No other blocking issues found in the diff. |
|
Addressed both lifecycle review findings in 69a0c2a:
|
|
Reviewed the diff. One likely correctness bug plus a test-coverage gap in Bug: final postcondition check can gate forever on stale SMAppService registration, even after the privileged fallback succeeds.
let status = await currentRegistrationStatus()
guard status == .notRegistered || status == .notFound else { return false }
// only then polls process state
guard await waitForStoppedPostcondition(maxAttempts: 20) else {
throw KanataDaemonServiceError.stopFailed(...)
}But per this PR's own doc update, the privileged helper fallback "no longer tries to emulate the owning app's SMAppService lifecycle" — i.e. it kills the process without touching SMAppService registration. If macOS is slow to reflect the unregister (the exact stale-job scenario this PR targets), Test gap: Minor perf note: |
|
Addressed the follow-up stop-state concern in 88f470aa4. The postcondition now distinguishes registration still present from a removed registration with a lingering process. Privileged cleanup is followed by another owning-API unregister only when registration remains; the final gate still requires both registration removal and process exit, so a merely killed process cannot be reported as a durable stop. The tests now exercise the real registration/process branches (no composite postcondition bypass), including stale registration and process-only lingering cases. Focused suite: 19/19 green; pinned formatting: green. |
|
Correction: the follow-up fix commit is 928dae5 (the behavior and 19/19 validation described above are unchanged). |
|
Reviewed the diff. One reliability concern in the new Asymmetric/short timeout before escalating to the privileged helper. If kanata's normal shutdown (releasing HID resources, etc.) routinely takes more than ~1s, this will make No other correctness issues found; the SMAppService register/unregister state machine, |
|
Addressed the shutdown-grace concern in 76a85e1: ordinary SMAppService stop now waits six seconds, slightly longer than the launchd plist's five-second exit timeout, before invoking privileged cleanup. Start retains its 20-second readiness window. The real lingering-process fallback test now exercises that full grace period; focused suite remains 19/19 green and pinned formatting passes. |
|
One correctness inconsistency worth a look in In the escalation path, the first unregister retry swallows errors and re-polls the real postcondition: try? await unregisterDaemon()
stoppedPostcondition = await waitForStoppedPostcondition()but the unregister issued right after the privileged fallback succeeds does not: try await forceStopStaleDaemon()
...
if stoppedPostcondition == .registrationPresent {
try await unregisterDaemon() // <-- unguarded here
}
stoppedPostcondition = await waitForStoppedPostcondition(maxAttempts: 20)If that last unregister throws a transient/benign error (e.g. an SMAppService IPC hiccup, or the job briefly reporting "not found" right after the privileged bootout), Everything else in the diff (the new |
|
Addressed the transient post-fallback unregister concern in ec081c0: that cleanup call is now best-effort, and the final registration+process postcondition remains authoritative. Removed the test-only unregister-error suppression and added a test where the third unregister throws after cleanup but the real final state is healthy. Focused suite: 20/20 green; pinned formatting: green. |
|
Reviewed the diff. One correctness concern in The method opens with an unguarded All the other unregister calls in the method are wrapped in Suggest wrapping the initial call in Everything else (start()'s registration/approval/readiness handling, the stopped/running postcondition polling, the DEBUG-only test overrides) looks sound and well covered by the new integration tests. |
|
Addressed the initial-unregister concern and completed the same state-authoritative contract for all lifecycle mutations in 888372d: initial unregister, register, and privileged cleanup delivery errors no longer preempt a fresh final-state check. Added coverage for initial unregister failure, register-throws-after-enabling, and privileged-cleanup error with successful final recovery. Focused suite: 23/23 green; pinned formatting: green. |
|
Reviewed the diff. The new SMAppService-owned start/stop/restart lifecycle in Two non-blocking points worth confirming before merge:
No correctness bugs found in the postcondition/retry state machine itself — the |
|
Final integration confirmation:
All required checks are green; proceeding to merge and exact-merge release acceptance. |
Summary
Validation
Release scope
This is the final lifecycle correction required before the 1.0.1 Sparkle release candidate is rebuilt and notarized. Physical HID proof remains a separate lab gate and is not claimed here.