fix: macOS use launchctl bootout/bootstrap instead of deprecated load #42

Merged
razvandimescu merged 1 commits from fix/launchctl-bootout-bootstrap into main 2026-04-08 21:54:22 +08:00
razvandimescu commented 2026-04-08 21:45:43 +08:00 (Migrated from github.com)

Summary

Replace the deprecated launchctl load -w / unload -w API with the modern bootout / bootstrap pair across all three call sites in install_service_macos and uninstall_service_macos.

The bug

launchctl load -w returns exit code 0 even when it cannot actually reload a service whose label is already in launchd's in-memory state. It prints Load failed: 5: Input/output error to stderr but exits 0, so the calling code (install_service_macos) interprets it as success and continues.

The consequence: every numa install rewrites the on-disk plist to point at a new binary path, but launchctl's in-memory state still references the binary that was first loaded. The daemon stays on the old binary forever, despite "successful" install output.

This affects every macOS user upgrading via Homebrew. Without this fix, brew users who brew upgrade numa from v0.10.0 to v0.10.1 would silently keep running v0.10.0 — neither the cross-platform CA trust fix (#41) nor the self-referential backup fix (#40) would actually take effect on their machines until they manually ran:

sudo launchctl bootout system /Library/LaunchDaemons/com.numa.dns.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/com.numa.dns.plist

Diagnosed during this evening's dogfood session — every sudo numa install we ran produced the Load failed: 5 line while exiting cleanly. The error message itself says Try running launchctl bootstrap as root for richer errors — macOS is literally telling us to use the modern API.

The fix

Use bootout + bootstrap symmetrically across three call sites:

Call site Before After
install_service_macos (load) launchctl load -w PLIST launchctl bootout system PLIST (best-effort cleanup) → launchctl bootstrap system PLIST
install_service_macos (rollback when port 53 in use) launchctl unload PLIST launchctl bootout system PLIST
uninstall_service_macos launchctl unload -w PLIST (after remove_file) launchctl bootout system PLIST (BEFORE remove_file, so the file still exists as the specifier)

The deprecated load/unload commands have been deprecated since macOS 10.10 (Yosemite, 2014). Every supported macOS version has the modern API.

Test plan

  • CI green across Linux, macOS, Windows (Linux/Windows: compile-check only since the changes are inside #[cfg(target_os = "macos")]; macOS: full build + test)
  • Manual: sudo ./target/release/numa install no longer prints Load failed: 5: Input/output error
  • Manual: re-running sudo numa install over an existing install completes cleanly with no error
  • Manual: PR #40's Existing DNS backup preserved and PR #41's Trusted Numa CA in system keychain still fire correctly (this fix is orthogonal to both)

Why this is its own PR

Different concern from #40 (DNS backup integrity) and #41 (CA trust). Different code path (install_service_macos/uninstall_service_macos vs install_macos/install_windows/install_linux). Easier to revert independently if needed.

Why this should ship with v0.10.1

Without it, brew users upgrading from v0.10.0 to v0.10.1 will silently fail to actually load the new binary, negating both #41 and #40 for the upgrade path. v0.10.1 should be deferred until this lands.

🤖 Generated with Claude Code

## Summary Replace the deprecated `launchctl load -w` / `unload -w` API with the modern `bootout` / `bootstrap` pair across all three call sites in `install_service_macos` and `uninstall_service_macos`. ## The bug `launchctl load -w` returns exit code 0 even when it cannot actually reload a service whose label is already in launchd's in-memory state. It prints `Load failed: 5: Input/output error` to stderr but exits 0, so the calling code (`install_service_macos`) interprets it as success and continues. The consequence: every `numa install` rewrites the on-disk plist to point at a new binary path, but launchctl's in-memory state still references the binary that was first loaded. The daemon stays on the old binary forever, despite "successful" install output. **This affects every macOS user upgrading via Homebrew.** Without this fix, brew users who `brew upgrade numa` from v0.10.0 to v0.10.1 would silently keep running v0.10.0 — neither the cross-platform CA trust fix (#41) nor the self-referential backup fix (#40) would actually take effect on their machines until they manually ran: ```sh sudo launchctl bootout system /Library/LaunchDaemons/com.numa.dns.plist sudo launchctl bootstrap system /Library/LaunchDaemons/com.numa.dns.plist ``` Diagnosed during this evening's dogfood session — every `sudo numa install` we ran produced the `Load failed: 5` line while exiting cleanly. The error message itself says `Try running launchctl bootstrap as root for richer errors` — macOS is literally telling us to use the modern API. ## The fix Use `bootout` + `bootstrap` symmetrically across three call sites: | Call site | Before | After | |---|---|---| | `install_service_macos` (load) | `launchctl load -w PLIST` | `launchctl bootout system PLIST` (best-effort cleanup) → `launchctl bootstrap system PLIST` | | `install_service_macos` (rollback when port 53 in use) | `launchctl unload PLIST` | `launchctl bootout system PLIST` | | `uninstall_service_macos` | `launchctl unload -w PLIST` (after `remove_file`) | `launchctl bootout system PLIST` (BEFORE `remove_file`, so the file still exists as the specifier) | The deprecated `load`/`unload` commands have been deprecated since macOS 10.10 (Yosemite, 2014). Every supported macOS version has the modern API. ## Test plan - [x] CI green across Linux, macOS, Windows (Linux/Windows: compile-check only since the changes are inside `#[cfg(target_os = "macos")]`; macOS: full build + test) - [x] Manual: `sudo ./target/release/numa install` no longer prints `Load failed: 5: Input/output error` - [x] Manual: re-running `sudo numa install` over an existing install completes cleanly with no error - [x] Manual: PR #40's `Existing DNS backup preserved` and PR #41's `Trusted Numa CA in system keychain` still fire correctly (this fix is orthogonal to both) ## Why this is its own PR Different concern from #40 (DNS backup integrity) and #41 (CA trust). Different code path (`install_service_macos`/`uninstall_service_macos` vs `install_macos`/`install_windows`/`install_linux`). Easier to revert independently if needed. ## Why this should ship with v0.10.1 Without it, brew users upgrading from v0.10.0 to v0.10.1 will silently fail to actually load the new binary, negating both #41 and #40 for the upgrade path. v0.10.1 should be deferred until this lands. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.