mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
## Summary
Fixed a race condition where calling `pause()` followed by `resume()` on
`process.stdin` would prevent data from being received, causing the
process to exit immediately instead of listening for input.
## Root Cause
The issue was in the pause/resume event handling logic in
`ProcessObjectInternals.ts`:
1. When `pause()` is called, the "pause" event handler schedules a
`disown()` call for the next tick
2. When `resume()` is called immediately after, it calls `own()` to
acquire a stream reader
3. On the next tick, the scheduled `disown()` from step 1 executes and
incorrectly releases the reader that was just acquired in step 2
This race condition left the stream without a reader, so no data could
be received.
## Solution
Added a `pendingDisown` flag that:
- Gets set to `true` when scheduling a disown operation
- Gets cleared to `false` when `own()` is called (during resume)
- Prevents the scheduled disown from executing if it has been cancelled
by a subsequent `own()` call
## Test Plan
- [x] Added regression tests in
`test/regression/issue/stdin-pause-resume.test.ts`
- [x] Verified fix with original reproduction case
- [x] Existing stdin/tty tests still pass
(`tty-readstream-ref-unref.test.ts`,
`tty-reopen-after-stdin-eof.test.ts`)
## Reproduction
Before this fix, the following code would exit immediately:
```ts
process.stdin.on("data", chunk => {
process.stdout.write(chunk);
});
process.stdin.pause();
process.stdin.resume();
```
After the fix, it correctly waits for and processes input.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
65 lines
1.5 KiB
TypeScript
65 lines
1.5 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { bunEnv, bunExe } from "harness";
|
|
|
|
test("process.stdin should work after pause() and resume()", async () => {
|
|
const script = `
|
|
process.stdin.on("data", chunk => {
|
|
process.stdout.write(chunk);
|
|
});
|
|
process.stdin.pause();
|
|
process.stdin.resume();
|
|
`;
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "-e", script],
|
|
env: bunEnv,
|
|
stdin: "pipe",
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
proc.stdin.write("hello\n");
|
|
proc.stdin.end();
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(stderr).toBe("");
|
|
expect(exitCode).toBe(0);
|
|
expect(stdout).toBe("hello\n");
|
|
});
|
|
|
|
test("process.stdin should receive data after multiple pause/resume cycles", async () => {
|
|
const script = `
|
|
let received = '';
|
|
process.stdin.on("data", chunk => {
|
|
received += chunk.toString();
|
|
});
|
|
|
|
process.stdin.pause();
|
|
process.stdin.resume();
|
|
process.stdin.pause();
|
|
process.stdin.resume();
|
|
|
|
process.stdin.on("end", () => {
|
|
console.log("RECEIVED:", received);
|
|
});
|
|
`;
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "-e", script],
|
|
env: bunEnv,
|
|
stdin: "pipe",
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
proc.stdin.write("test data");
|
|
proc.stdin.end();
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(stderr).toBe("");
|
|
expect(exitCode).toBe(0);
|
|
expect(stdout).toContain("RECEIVED: test data");
|
|
});
|