Skip to content

macOS: waiting on a foreign Process object for the launcher's server never sees its exit #520

Closed

Description

@Blind-Striker

What

On macOS, a consumer that hard-kills the server started by OpenCodeServer.StartAsync through its own Process object, and then waits on that object, never sees the exit. The pattern is Process.GetProcessById(server.ProcessId), then Kill(entireProcessTree: true), then WaitForExitAsync with a 10 s bound. The wait times out every time on the Intel Mac (macOS 15). The same app passes on Windows and Linux.

Evidence

The internal developer-experience round on the 2.0.32 nightlies, 2026-10-18 (the night round and both re-runs). App 03 restarts its own server mid-stream to test the event bus's reconnect.

It was first suspected to be the Stop-reap defect fixed in #518, but that is ruled out: the re-run on the nightly that contains #518 still times out in the same way, and StopAsync is not involved.

Not verified yet

The cause is not established. The candidates are:

  • The SDK side: the launcher's own exit watch reaps the child it spawned (it spawns with posix_spawn, not Process.Start). A separate Process object that the consumer obtains for that pid may then have no way to observe the exit on macOS.
  • The .NET side: how .NET's exit detection on macOS behaves for a process that the Process object did not start.

The next step is a minimal reproduction on macOS, with and without the SDK launcher in the picture. Then decide whether this is SDK behaviour, a documentation note (a consumer should end the server through the SDK, not through a foreign Process object), or a .NET report.

Source: internal developer-experience evaluation of the 2.0.32 nightlies, 2026-10-18 (app 03, macOS).

Activity

  1. added this to the 1.0 milestone on Oct 18, 2026
  2. Blind-Striker commented on Oct 23, 2026

    @Blind-Striker
    CollaboratorAuthor

    Mechanism found. It's an SDK defect on macOS, not a .NET one.

    Measured on an M1 Mac running macOS 27.0 (26A428), arm64, with .NET SDK 10.0.402. The original report came from an Intel Mac on macOS 15, which is no longer available.

    1. Kill(entireProcessTree: true) stops the tree before killing it. It sends SIGSTOP, "so it won't start additional children", and then SIGKILL (dotnet/runtime v10.0.0, Process.Unix.cs:110-112, KillTree). That is deliberate .NET behaviour, not a bug.
    2. On macOS, the launcher's exit watch wakes up on that stop. The watch blocks in waitid(P_PID, pid, WEXITED | WNOWAIT), and macOS returns from that call on the stop, with si_code = CLD_STOPPED, even though WSTOPPED was not requested. A C probe showed this in 4 of 4 runs.
    3. The watch then ends without reaping. Its waitpid(pid, WNOHANG) finds no exit yet and returns 0, so the watch records the exit as "unknown" and stops watching. Nothing reaps the child after that.
    4. The SIGKILL leaves the server as a zombie of the calling process, and it stays one.
    5. The consumer's Process object never sees the exit. That object did not start the process, so it checks for exit with kill(pid, 0), and a zombie still answers. WaitForExitAsync therefore never completes. A probe without the SDK, using the same watch logic, timed out 6 of 6 times.

    Controls:

    • With a plain SIGKILL and no stop, the same waitid returns at the exit and the child is reaped (8 of 8 runs).
    • Linux doesn't report stops to a WEXITED-only wait, which is consistent with the app passing on Linux and Windows.

    A second symptom: the launcher reports the server as exited, with an unknown status, at the SIGSTOP. At that moment the server is only stopped.

    Fix direction (to be specified): when the follow-up waitpid finds that the child hasn't exited, the child only stopped or continued. The watch must keep watching rather than end. The likely fallback is the watch's existing probe mode (kill(pid, 0) plus waitpid(WNOHANG)). A POSIX regression row would stop and then kill a launched child, and assert that the real exit (signal 9) is reported and the child is reaped. That row runs on the macOS CI leg.

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

    bugSomething isn't working

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions