Skip to content

Fix stopping disabled KeepAlive service - #1252

Merged
malpern merged 1 commit into
masterfrom
fix/helper-stop-disabled-service-pid
Jul 30, 2026
Merged

Fix stopping disabled KeepAlive service#1252
malpern merged 1 commit into
masterfrom
fix/helper-stop-disabled-service-pid

Conversation

@malpern

@malpern malpern commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • capture the live Kanata service PID before disabling its KeepAlive launchd job
  • signal that exact PID after disable so launchd cannot respawn it
  • advance helper compatibility to 1.3.2 and app build to 11
  • preserve lifecycle ordering with a focused lint regression test

Why

Installed release acceptance showed that launchctl disable succeeds but makes launchctl kill system/com.keypath.kanata unable to resolve the still-running process. The CLI therefore reported failure and TCP stayed up.

Tests

  • TEST_FILTER=HelperServiceKeepAliveLifecycleLintTests TIMEOUT_SECONDS=600 ./Scripts/run-tests-safe.sh
  • TEST_FILTER=HelperVersionContractTests KEYPATH_TEST_PREBUILD=0 TIMEOUT_SECONDS=180 ./Scripts/run-tests-safe.sh
  • ./Scripts/build-helper.sh
  • git diff --check

Release gate

This PR is required before publishing KeyPath 1.0.1. Publication remains paused until the merged build is notarized and the installed CLI proves stop/TCP-down/start/TCP-up.

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown

Correctness/reliability concern — PID capture → signal has a TOCTOU window

In stopKanataService (Sources/KeyPathHelper/HelperService.swift), the new flow is:

  1. processID = Self.serviceProcessID(Self.kanataServiceID) (via launchctl print)
  2. launchctl disable <target> (up to 15s timeout)
  3. /bin/kill -TERM <processID>

Between steps 1 and 3 there's a window where the captured PID could become stale (process exits on its own, e.g. crash) and get reused by an unrelated process — macOS PID reuse is rare but not impossible, especially if disable takes any nontrivial time or the system has high process churn. Unlike the old launchctl kill SIGTERM system/<label> call, which resolves through the launchd service label at signal-time, /bin/kill -TERM <pid> has no such safety net — it blindly signals whatever process currently holds that PID. Since the helper runs privileged, a stale/reused PID means kill -TERM could terminate an unrelated process on the system.

Suggest re-validating that processID still corresponds to the kanata service (e.g., re-check via launchctl print immediately before the kill, or verify the process's executable path/name via ps -p <pid> -o comm=) right before signaling, rather than trusting a PID captured before the disable round-trip.

Minor: the lifecycle lint test (HelperServiceKeepAliveLifecycleLintTests) only asserts textual ordering of the relevant snippets in the function body — it doesn't exercise the actual PID-reuse race, so it wouldn't catch a regression here even if the safeguard above were added and later removed.

No other issues found in the plist/version bumps or docs update — they're consistent with the code change.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e23327c9dc

ℹ️ 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".

"Failed to identify the KeyPath Kanata service process"
)
}
let result = Self.run("/bin/kill", ["-TERM", processID], timeout: 15)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate the PID after disabling the job

If Kanata exits or restarts between the initial launchctl print and the disable operation, processID identifies the previous process rather than the current service instance. The raw root-level kill can then either return ESRCH while a replacement remains alive—causing stop to re-enable the job and fail—or, after PID reuse, signal an unrelated process. Re-read and validate the job's current PID after disabling it, or use another service-owned mechanism that preserves process identity, before signaling.

Useful? React with 👍 / 👎.

@malpern
malpern merged commit 7e41642 into master Jul 30, 2026
8 checks passed
@malpern
malpern deleted the fix/helper-stop-disabled-service-pid branch July 30, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant