Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
e3b331a708 Fix child_process.unref() not keeping parent alive with active IPC channel
Fixes #23686

When child_process.unref() was called on a forked process with an
active IPC channel, the parent process would exit immediately without
waiting to receive messages from the child.

Per Node.js documentation: "Calling subprocess.unref() will allow the
parent process to exit independently of the child unless there is an
established IPC channel between the child and the parent."

This means that even after unref(), an active IPC connection should
keep the parent process alive to allow bidirectional communication.

Changes:
- Modified SendQueue.shouldRef() in ipc.zig to return true when the
  queue is empty but the socket is still connected, ensuring IPC keeps
  the event loop alive
- Updated hasPendingActivityNonThreadsafe() in subprocess.zig to check
  if IPC keepalive is active rather than just checking if ipc_data
  exists
- Added comments to jsRef() and jsUnref() clarifying that IPC keepalive
  is managed independently and not affected by unref()
- Added regression test that verifies both parent and child can exchange
  messages before exiting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 05:30:09 +00:00
3 changed files with 70 additions and 3 deletions

View File

@@ -170,8 +170,10 @@ pub fn hasExited(this: *const Subprocess) bool {
}
pub fn hasPendingActivityNonThreadsafe(this: *const Subprocess) bool {
if (this.ipc_data != null) {
return true;
if (this.ipc_data) |*ipc_data| {
if (ipc_data.keep_alive.isActive()) {
return true;
}
}
if (this.hasPendingActivityStdio()) {
@@ -266,6 +268,9 @@ pub fn jsRef(this: *Subprocess) void {
this.stderr.ref();
}
// Note: IPC keepalive is managed independently and keeps the event loop alive
// as long as the socket is connected, per Node.js behavior
this.updateHasPendingActivity();
}
@@ -285,6 +290,10 @@ pub fn jsUnref(this: *Subprocess) void {
this.stderr.unref();
}
// Note: IPC keepalive is NOT affected by unref(), per Node.js behavior:
// "unless there is an established IPC channel between the child and the parent"
// The IPC connection will keep the parent alive until it's closed
this.updateHasPendingActivity();
}

View File

@@ -647,7 +647,12 @@ pub const SendQueue = struct {
}
fn shouldRef(this: *SendQueue) bool {
if (this.waiting_for_ack != null) return true; // waiting to receive an ack/nack from the other side
if (this.queue.items.len == 0) return false; // nothing to send
if (this.queue.items.len == 0) {
// Keep the event loop alive if the socket is connected.
// Per Node.js behavior, IPC always keeps the parent alive "unless there is an established IPC channel"
// This means after unref(), IPC still keeps the loop alive (unref only affects process/stdio).
return this.isConnected();
}
const first = &this.queue.items[0];
if (first.data.cursor > 0) return true; // send in progress, waiting on writable
if (this.write_in_progress) return true; // send in progress (windows), waiting on writable

View File

@@ -0,0 +1,53 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("child_process.unref() should allow parent to exit when IPC is established", async () => {
using dir = tempDir("test-23686", {
"parent.js": `
import {fork} from "node:child_process";
const child = fork("./child.js");
child.on('message', (msg) => {
console.log(msg);
});
child.unref();
child.send('Hello from parent');
`,
"child.js": `
process.on('message', (msg) => {
console.log(msg);
process.send('Hello from child');
process.disconnect();
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "parent.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
// Set a timeout to kill the process if it hangs
const timeout = setTimeout(() => {
proc.kill();
}, 3000);
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
clearTimeout(timeout);
// The parent should exit cleanly, not hang
expect(exitCode).toBe(0);
// We should see both messages
expect(stdout).toContain("Hello from parent");
expect(stdout).toContain("Hello from child");
// No errors
expect(stderr).toBe("");
}, 5000);