Supported OS-owned lifetime for one-shot macOS work after its controller exits

I’m designing a macOS utility that needs to run a bounded, one-shot diagnostic and collect its result.

The lifetime problem is:

  1. A controller asks for the diagnostic to start.
  2. The diagnostic may begin successfully.
  3. The controller can then fail immediately — potentially before it has retained the child’s PID or established output collection.
  4. The diagnostic may close stdin/stdout/stderr while continuing to run.
  5. The diagnostic must not become an unmanaged orphan if the controller disappears.

A focused test case uses Foundation.Process:

  • Controller launches Child.
  • Controller immediately exits with _exit(42), without waiting for Child or retaining its process identifier.
  • Child calls setsid(), closes stdin/stdout/stderr, stays alive for up to 120 seconds, then exits.

What I’m trying to establish is the supported macOS lifecycle boundary, rather than inventing a cleanup scheme around PIDs.

Is there a supported per-user launchd or other OS-managed mechanism where the OS assumes responsibility for the job before the diagnostic process can start, such that the requesting controller may subsequently fail without leaving an unmanaged process?

Specifically:

  • At what point does the OS own the lifetime relative to registration and process creation?
  • What supported mechanism bounds or terminates the job if the requesting controller disappears?
  • Is there a supported hard execution-time limit, rather than an idle-time concept?
  • How are descendants or process groups contained, and does calling setsid() conflict with that containment?
  • What completion or failure information remains available to a later controller after the original requester has died?
  • Are there important per-user, privilege, sandboxing, signing, or macOS-version limitations?

I’m not reporting an Apple bug and I’m not asking for a custom recovery implementation. I’m trying to identify the documented supported lifetime mechanism and its guarantees and limitations before choosing an architecture.

Environment:

  • macOS 26.6.2
  • Apple silicon
  • Foundation.Process is used by the focused reproducer

Apple Developer Technical Support suggested I start a new thread for this specific question in Processes & Concurrency.

Answered by DTS Engineer in 905649022
So I’m trying to establish whether a per-user launchd job

If there were such a thing, it would be in launchd. However, launchd doesn’t have anything that’ll meet your specific requirements.

You can use an XPC service to tie the lifetime of the ‘child’ to the lifetime of the ‘parent’ (in quotes because there is not a child/parent relationship in this case) but launchd will automatically terminate the service when the ‘parent’ terminates.

You can avoid that by using a launchd agent, but in that case there’s no way to tell launchd to limit the runtime of the job [1]. You could add code to the program itself to do that — finally a use for SIGALRM (-: — but such code can never be 100% effective.

Moreover, using a launchd job has other consequences:

  • You have to figure out a way to remove it when you’re done, or live with the fact that the job will be present always [2].
  • The system alerts the user to apps installing potentially long-running programs like this (see System Settings > Login Items & Extensions > App Background Activity).

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Well, that’s not quite true. You can apply the HardResourceLimits > CPU key, but that’s specified in terms of CPU time rather than ‘wall’ time, which means it’ll never kick in if the job blocks indefinitely.

[2] This isn’t a huge problem because the process can terminate, after which the only resource consumed by the job is some memory within launchd.

Thanks for bringing this to the forums.

I read through your post a couple of times and I’m still not sure I understand your issue. Most folks in your situation simply spawn the child process (via fork/exec*, posix_spawn, NSTask, Process, or Subprocess). Then one of two things happens:

  • If the child terminates first, the parent reaps it.
  • If the parent terminates first, the child gets reparented but that’s fine because eventually the child will terminate.

It sounds like you’re concerned about the parent terminating and then the child getting stuck, resulting in no one being around to clean up. Is that right? Or have I misunderstood something?

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks Quinn — yes, that is close, but the specific concern is slightly narrower.

I understand that if the parent exits first, the child can be reparented and may later terminate normally.

The question is what supported macOS mechanism, if any, can own and bound the diagnostic’s lifetime independently of the requesting controller.

In the focused sample, the controller may exit immediately after launch, before retaining the child PID or establishing output collection. The child may also create a new session, close stdin/stdout/stderr, and continue running.

So I’m trying to establish whether a per-user launchd job or another supported OS-managed mechanism can:

  • take responsibility before the diagnostic starts;
  • keep that responsibility if the requesting controller dies;
  • bound or terminate the diagnostic if it exceeds its intended execution window; and
  • provide some supported completion/failure state after the original controller is gone.

If ordinary child reparenting is the supported model and macOS does not provide that stronger per-job lifetime/containment guarantee, that would also answer the question.

Thanks for helping me narrow it down.

Accepted Answer
So I’m trying to establish whether a per-user launchd job

If there were such a thing, it would be in launchd. However, launchd doesn’t have anything that’ll meet your specific requirements.

You can use an XPC service to tie the lifetime of the ‘child’ to the lifetime of the ‘parent’ (in quotes because there is not a child/parent relationship in this case) but launchd will automatically terminate the service when the ‘parent’ terminates.

You can avoid that by using a launchd agent, but in that case there’s no way to tell launchd to limit the runtime of the job [1]. You could add code to the program itself to do that — finally a use for SIGALRM (-: — but such code can never be 100% effective.

Moreover, using a launchd job has other consequences:

  • You have to figure out a way to remove it when you’re done, or live with the fact that the job will be present always [2].
  • The system alerts the user to apps installing potentially long-running programs like this (see System Settings > Login Items & Extensions > App Background Activity).

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Well, that’s not quite true. You can apply the HardResourceLimits > CPU key, but that’s specified in terms of CPU time rather than ‘wall’ time, which means it’ll never kick in if the job blocks indefinitely.

[2] This isn’t a huge problem because the process can terminate, after which the only resource consumed by the job is some memory within launchd.

Thanks Quinn — that answers the exact question I was trying to pin down.

So for this requirement, there isn’t a supported launchd mechanism that both allows the work to survive its initiating controller and gives launchd responsibility for a hard wall-clock lifetime bound.

An XPC service ties lifetime back to the initiating process, while a LaunchAgent can outlive it but doesn’t provide the wall-time termination guarantee I was looking for. That’s enough for me to rule out the OS-owned lifetime model I was considering and design around that constraint instead.

Really appreciate the clear explanation.

Supported OS-owned lifetime for one-shot macOS work after its controller exits
 
 
Q