Skip to content

fix(core): resolve spawn on exit not close to avoid bg-child hang#29108

Open
divitkashyap wants to merge 1 commit into
anomalyco:devfrom
divitkashyap:fix/bash-background-hang-20902
Open

fix(core): resolve spawn on exit not close to avoid bg-child hang#29108
divitkashyap wants to merge 1 commit into
anomalyco:devfrom
divitkashyap:fix/bash-background-hang-20902

Conversation

@divitkashyap
Copy link
Copy Markdown

Issue for this PR

Closes #20902

Type of change

  • Bug fix

What does this PR do?

The cross-spawn spawner (packages/core/src/cross-spawn-spawner.ts) only settled its exit Deferred on the Node close event. close waits for all stdio descriptors to drain — so if the spawned command forks a background grandchild that inherits stdout/stderr (e.g. cmd &, daemonized servers, the Gradle daemon, long-lived stdio MCP servers), close never fires and the shell tool hangs until the configured timeout (~2 minutes).

The fix is to settle on whichever of exit or close fires first. exit fires when the direct child exits regardless of inherited-fd lifetime — which is what callers of handle.exitCode actually want. close is kept as a fallback in case exit never fires for some reason. The exit-code/signal payload comes from whichever event ran first, which matches what Node passes to both.

How did you verify your code works?

  • Added a regression test in packages/opencode/test/tool/shell.test.ts that runs sleep 30 & disown; echo backgrounded through the shell tool and asserts it returns in well under the timeout. Without the fix it hangs the full 15s test timeout; with the fix it returns in ~1s.
  • Ran the full packages/opencode/test/tool/shell.test.ts suite (24 tests pass) and packages/core/test/effect/cross-spawn-spawner.test.ts (24 tests pass) locally.
  • Did not run the full repo typecheck — that pipeline is slow and the change is isolated to one well-typed function.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

The cross-spawn spawner only resolved its exit Deferred on the Node
`close` event, which waits for all stdio descriptors to drain. When a
spawned command forks a background grandchild that inherits those
descriptors (e.g. `cmd &`, daemonized servers, gradle daemon, MCP
stdio servers), `close` never fires and the shell tool hangs until
the configured timeout (~2 minutes).

Settle the exit Deferred on whichever of `exit` / `close` fires first.
`exit` fires when the direct child process exits regardless of
inherited-fd lifetime, which is what callers actually care about.

Closes anomalyco#20902
@divitkashyap
Copy link
Copy Markdown
Author

@thdxr would appreciate a look when you have a moment — small, contained fix with a regression test. Happy to iterate on the approach if you'd prefer something different.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bash tool hangs when command spawns background child processes

1 participant