Repository navigation
fix: restart the macOS service in one launchd operation - #58
Merged
Merged
Conversation
Restart sent "launchctl stop", which only signals the process, and then an immediate "launchctl start". A start that arrives while the old process is still exiting is ignored, and the clean exit that follows is not relaunched (KeepAlive only covers failed exits), so restart could leave the service stopped while reporting success. Restart now runs "launchctl kickstart -kp gui/<uid>/<label>", falling back to the user domain only when launchd reports the job is not in the GUI domain. launchd sends SIGTERM, waits for the old process to exit (escalating to SIGKILL after the job's exit timeout), starts the new one, and prints its PID. Restart succeeds only if that PID is new and alive; an uninstalled or unloaded job gets an explicit error. All launchctl calls go through an injectable runner that keeps stdout separate from diagnostics, so the restart paths are tested without touching launchd. Closes #49. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull request
Summary
On macOS,
-restartsentlaunchctl stop, which only signals the process, and then an immediatelaunchctl start. A start that arrives while the old process is still exiting is ignored, and the clean exit that follows is not relaunched because the plist only keeps the job alive after a failed exit (KeepAlive.SuccessfulExit=false). Restart could therefore leave the service stopped while printing "restarted".Restartrunslaunchctl kickstart -kp gui/<uid>/<label>: launchd sends SIGTERM, waits for the old process to exit (escalating to SIGKILL after the job's exit timeout), starts the new one, and prints its PID. It falls back touser/<uid>only when launchd reports the job is not in the GUI domain (exit status 113).-reinstall(automatic recovery stays with enhancement: recover an installed but unregistered macOS LaunchAgent #51).launchctl print, whose output the manual says not to rely on.launchctlcalls go through an injectable runner that keeps stdout separate from diagnostics, so the PID printed by-pis not treated as an error and the restart paths are tested without touching launchd.-restartrow describes the macOS behavior.Closes #49.
User impact
-restarton macOS reliably replaces the running process, starts a stopped job, and fails with a clear message instead of reporting success when no new process starts. The old process still shuts down gracefully (SIGTERM), so sockets and locks are released as before. Linux is unchanged (systemctl restartwas already synchronous).Readiness after the restart (sockets bound) is not checked here; that remains #53. Status error handling remains #52.
Validation
launchctl: running job, stopped job, GUI-to-user domain fallback, job not loaded, kickstart error propagation, PID not replaced, replacement exited, unexpected output, and uninstalled job. With the original stop/startRestartrestored, these tests fail.go test -race ./...withSSH_AGENT_PROXY_REQUIRE_OPENSSH=1; twenty race-enabled repetitions of the launchd tests.go vet ./...(alsoGOOS=linuxfor the service package), formatting,git diff --check, and CGO-disabled builds for darwin/linux × amd64/arm64.io.example.sap-kickstart-test, sameRunAtLoadandKeepAlive.SuccessfulExit=false), removed afterwards; the real job was not touched:stop+start: twice in a row the job ended up not running, reproducing the bug; launchd also throttled further starts.kickstart -kp, three times back to back: exit 0, stdout exactly the new PID, empty stderr, old process gone, new process alive, no throttling.Checklist
go test -race ./...passes locally, or the reason it was not run is documented.🤖 Generated with Claude Code