Files
bun.sh/test/regression/issue/stdin-pause-resume.test.ts
robobun d3ff6a5e35 Fix process.stdin not receiving data after pause/resume (#23362)
## 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>
2025-10-07 22:04:40 -07:00

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");
});