Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/js/builtins/Ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@
// const dgram = require("node:dgram");
switch (serialized.type) {
case "net.Server": {
const server = new net.Server();
server.listen({ fd, exclusive: true }, () => {
emit(target, serialized.msg, server);
});
server.listen({ fd, exclusive: true });
// Not from the 'listening' callback (a timer): the first poll accepts the whole backlog before it fires.
emit(target, serialized.msg, server);

Check warning on line 60 in src/js/builtins/Ipc.ts

View check run for this annotation

Claude / Claude Code Review

Received net.Server now emits 'listening' after delivery (Node-compat divergence)

Moving `emit()` out of the `'listening'` callback means the received `net.Server` now fires `'listening'` *after* the user's `'message'` handler runs — so `server.on('listening', fn)` attached inside the handler will fire in Bun but never fires in Node (or pre-PR Bun), where the handle is delivered *from* the listening callback. Unusual to hit and `server.listening` is already `true` at delivery, so not blocking; if you want to close the gap, consume the pending event before emitting (e.g. `serv
Comment thread
robobun marked this conversation as resolved.
return;
}
case "net.Socket": {
Expand Down
83 changes: 77 additions & 6 deletions test/js/node/child_process/child_process_ipc_handle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,77 @@ process.on('message', (m, socket) => {
});
});

// The receiver's first poll of the adopted fd accepts everything already queued in its
// backlog, so the server has to reach the 'message' listener before that poll happens.
test.concurrent(
"connections queued before a net.Server handoff reach the receiver's 'connection' listener",
async () => {
using dir = tempDir("ipc-handle-busy-server", {
"parent.js": `
const { fork } = require('node:child_process');
const net = require('node:net');
const N = 20;
const child = fork('child.js');
const server = net.createServer();
let served = 0;
let report;

function finish(extra) {
console.log(JSON.stringify({ served, ...report, ...extra }));
child.kill();
process.exit(0);
}

child.on('exit', code => finish({ childExit: code }));
child.on('message', m => {
report = m;
// Connections accepted before the child held the server can never be served.
if (m.acceptedBeforeDelivery !== 0 || served === N) finish();
});

server.listen(0, '127.0.0.1', () => {
const { port } = server.address();
child.send('srv', server);
// send() duplicated the descriptor, so the socket keeps listening after the parent drops
// its own copy: the connects below queue up in the backlog with only the child able to
// accept them.
server.close();
for (let i = 0; i < N; i++) {
const client = net.connect(port, '127.0.0.1');
client.setEncoding('utf8');
let data = '';
client.on('data', c => (data += c));
client.on('end', () => {
if (data === 'C') served++;
if (served === N && report) finish();
});
client.on('error', e => finish({ clientError: e.message }));
}
});
`,
"child.js": `
process.on('message', (m, server) => {
server.on('connection', s => s.end('C'));
process.send({ acceptedBeforeDelivery: server._connections });
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "parent.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ out: JSON.parse(stdout.trim()), stderr }).toEqual({
out: { served: 20, acceptedBeforeDelivery: 0 },
stderr: "",
});
expect(exitCode).toBe(0);
},
);

test.concurrent("received net.Socket has connecting=false and remoteAddress synchronously", async () => {
using dir = tempDir("ipc-handle-connecting", {
"parent.js": `
Expand Down Expand Up @@ -608,10 +679,9 @@ process.on('disconnect', () => process.exit(sawQueued ? 0 : 3));
);

// The child sends a server and disconnects at once. node: process.connected drops immediately, a
// second disconnect() errors, and the parent still receives the server (its adoption completes a
// loop turn later, which must not lose it) as well as the message queued behind it. Order is not
// pinned: bun currently emits the late-adopted handle after 'disconnect', node before it.
test.concurrent("a handle sent right before the child's disconnect() is still delivered", async () => {
// second disconnect() errors, and the parent receives the server, then the message queued behind
// it (it is only sent once the handle is acked), then 'disconnect'.
test.concurrent("a handle sent right before the child's disconnect() is delivered, in order", async () => {
using dir = tempDir("ipc-handle-then-disconnect", {
"parent.js": `
const { fork } = require('node:child_process');
Expand All @@ -621,7 +691,8 @@ let childReport = '';
child.stderr.on('data', d => { childReport += d; });
child.on('message', (m, h) => { got.push(h ? 'handle:' + m : m); if (h) h.close(); });
child.on('disconnect', () => got.push('disconnect'));
child.on('exit', code => console.log(JSON.stringify({ got: got.sort(), code, child: JSON.parse(childReport) })));
// 'close' waits for 'exit', 'disconnect' and the end of the stderr pipe; 'exit' can beat the other two.
child.on('close', code => console.log(JSON.stringify({ got, code, child: JSON.parse(childReport) })));
`,
"child.js": `
const net = require('node:net');
Expand All @@ -647,7 +718,7 @@ const server = net.createServer().listen(0, '127.0.0.1', () => {
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ out: JSON.parse(stdout.trim()), stderr }).toEqual({
out: {
got: ["after-handle", "disconnect", "handle:srv"],
got: ["handle:srv", "after-handle", "disconnect"],
code: 0,
child: { connectedAfterDisconnect: false, secondDisconnect: "ERR_IPC_DISCONNECTED" },
},
Expand Down
Loading