Skip to content

launchctl bootout --wait is a usage error on macOS 15, so boot-service install/update/restart fails #11995

Description

@keithce

Summary

On macOS 15, launchctl bootout --wait <service-target> is a usage error, so the boot service's stop step never runs. The old job keeps the label, and the strict bootstrap that follows fails with EIO. The result is that t3 service install, t3 update, and the post-update restart all fail on this OS, leaving the previous server running.

Environment

  • macOS 15.7.9 (build 24G830), Darwin 24.6.0
  • Darwin Bootstrapper Version 7.0.0
  • t3 0.0.41-nightly.20260915.1752

Reproduction

$ launchctl bootout --wait gui/501/com.t3tools.t3code.service
Unrecognized target specifier.
Usage: launchctl bootout <domain-target> [service-path1, service-path2, ...] | <service-target>
<service-target> takes a form of <domain-target>/<service-id>.
Please refer to `man launchctl` for explanation of the <domain-target> specifiers.
$ echo $?
64

$ launchctl bootout gui/501/com.t3tools.t3code.service
$ echo $?
0

With a service already installed, t3 update <version> --yes then reports:

CliUpdateError: t3@<version> is installed but the background service could not be
updated: Background setup failed while starting the service (exit code 5).

and ~/.t3/userdata/logs/boot-service.log shows the two steps in order:

Background setup failed while stopping the installed launch agent (exit code 64).
Background setup failed while starting the service (exit code 5).
Background setup failed while restarting the service after a failed update (exit code 5).

Cause

launchdManager in apps/server/src/cloud/bootService.ts passes --wait to bootout in both its stop and deactivate steps:

args: ["bootout", "--wait", serviceTarget],

The accompanying comment reads "--wait (present on modern macOS, absent from the man page)". It is not present on macOS 15: launchctl rejects the whole invocation as a usage error rather than ignoring the flag. Because the step is optional: true, the failure is logged and the flow continues to bootstrap, which is exactly the EIO case the flag was added to avoid.

Suggested fix

Try the flag, fall back to a plain bootout, and wait for the label to leave the domain either way, so both old and new launchctl behave:

launchctl bootout --wait "$target" 2>/dev/null || launchctl bootout "$target" 2>/dev/null
i=0
while launchctl print "$target" >/dev/null 2>&1 && [ "$i" -lt 90 ]; do sleep 1; i=$((i + 1)); done

I am running that locally as a patch and t3 update now installs and restarts the service cleanly, including a downgrade back to the previous version. Happy to send it as a PR if you would like it in this shape.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions