Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
9e6d07a357 fix(ipc): implement sendHandle for passing net.Socket via IPC
Enable the `sendHandle` argument in `process.send()` / `child.send()`
for passing TCP socket handles between parent and forked child processes.

The IPC infrastructure for fd passing already existed at the Zig level
(SCM_RIGHTS, ack/nack protocol), but the JavaScript serialization layer
was disabled (`serialize()` returned null, `parseHandle()` threw for
net.Socket).

Changes:
- Ipc.ts `serialize()`: Implement handle type detection and serialization
  for net.Server and net.Socket, extracting the internal native handle
  and producing the NODE_HANDLE protocol message
- Ipc.ts `parseHandle()`: Implement net.Socket deserialization by
  creating a new Socket and connecting it to the received fd
- ipc.zig `doSend()`: Add TCPSocket and TLSSocket fd extraction
  alongside the existing Listener (net.Server) support

Closes #6743

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 21:26:44 +00:00
3 changed files with 156 additions and 44 deletions

View File

@@ -1025,8 +1025,14 @@ pub fn doSend(ipc: ?*SendQueue, globalObject: *jsc.JSGlobalObject, callFrame: *j
},
.none => {},
}
} else {
//
} else if (bun.jsc.API.TCPSocket.fromJS(handle)) |tcp_socket| {
log("got tcp socket", .{});
const fd = tcp_socket.socket.fd();
zig_handle = .init(fd, handle);
} else if (bun.jsc.API.TLSSocket.fromJS(handle)) |tls_socket| {
log("got tls socket", .{});
const fd = tls_socket.socket.fd();
zig_handle = .init(fd, handle);
}
}

View File

@@ -145,65 +145,41 @@
* @param {{ keepOpen?: boolean } | undefined} options
* @returns {[unknown, Serialized] | null}
*/
export function serialize(_message, _handle, _options) {
// sending file descriptors is not supported yet
return null; // send the message without the file descriptor
/*
export function serialize(message, handle, options) {
const net = require("node:net");
const dgram = require("node:dgram");
if (handle instanceof net.Server) {
// this one doesn't need a close function, but the fd needs to be kept alive until it is sent
const server = handle as unknown as (typeof net)["Server"] & { _handle: Bun.TCPSocketListener<unknown> };
return [server._handle, { cmd: "NODE_HANDLE", message, type: "net.Server" }];
return [handle._handle, { cmd: "NODE_HANDLE", message, type: "net.Server" }];
} else if (handle instanceof net.Socket) {
const new_message: { cmd: "NODE_HANDLE"; message: unknown; type: "net.Socket"; key?: string } = {
const serialized_message: { cmd: "NODE_HANDLE"; message: unknown; type: "net.Socket"; key?: string } = {
cmd: "NODE_HANDLE",
message,
type: "net.Socket",
};
const socket = handle as unknown as (typeof net)["Socket"] & {
_handle: Bun.Socket;
server: (typeof net)["Server"] | null;
setTimeout(timeout: number): void;
};
if (!socket._handle) return null; // failed
if (!handle._handle) return null;
// If the socket was created by net.Server
if (socket.server) {
// The worker should keep track of the socket
new_message.key = socket.server._connectionKey;
const firstTime = !this[kChannelHandle].sockets.send[message.key];
const socketList = getSocketList("send", this, message.key);
// The server should no longer expose a .connection property
// and when asked to close it should query the socket status from
// the workers
if (firstTime) socket.server._setupWorker(socketList);
// If the socket was created by net.Server, track the connection key
if (handle.server) {
serialized_message.key = handle.server._connectionKey;
// Act like socket is detached
if (!options?.keepOpen) socket.server._connections--;
if (!options?.keepOpen) handle.server._connections--;
}
const internal_handle = socket._handle;
const internal_handle = handle._handle;
// Remove handle from socket object, it will be closed when the socket
// will be sent
// Remove handle from socket object, it will be closed when the socket is sent
if (!options?.keepOpen) {
// we can use a $newZigFunction to have it unset the callback
internal_handle.onread = nop;
socket._handle = null;
socket.setTimeout(0);
handle._handle = null;
handle.setTimeout(0);
}
return [internal_handle, new_message];
} else if (handle instanceof dgram.Socket) {
// this one doesn't need a close function, but the fd needs to be kept alive until it is sent
throw new Error("todo serialize dgram.Socket");
return [internal_handle, serialized_message];
} else {
const dgram = require("node:dgram");
if (handle instanceof dgram.Socket) {
throw new Error("dgram.Socket handle passing is not yet supported");
}
throw $ERR_INVALID_HANDLE_TYPE();
}
*/
}
/**
* @param {Serialized} serialized
@@ -224,7 +200,12 @@ export function parseHandle(target, serialized, fd) {
return;
}
case "net.Socket": {
throw new Error("TODO case net.Socket");
const socket = new net.Socket();
socket.once("connect", () => {
emit(target, serialized.message, socket);
});
socket.connect({ fd });
return;
}
case "dgram.Socket": {
throw new Error("TODO case dgram.Socket");

View File

@@ -0,0 +1,125 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
describe.each(["json", "advanced"])("IPC sendHandle - mode %s", mode => {
test("parent can send net.Socket handle to forked child", async () => {
using dir = tempDir("ipc-sendhandle", {
"parent.js": `
const net = require("net");
const { fork } = require("child_process");
const path = require("path");
const server = net.createServer();
server.on("connection", (socket) => {
const worker = fork(path.join(process.cwd(), "child.js"), [], {
serialization: "${mode}",
});
worker.on("message", (message) => {
if (message === "ready") {
worker.send("handle-incoming", socket);
}
});
worker.on("exit", () => {
server.close(() => {});
});
});
server.listen(0, () => {
const port = server.address().port;
const client = net.connect(port, "127.0.0.1", () => {
client.on("data", (chunk) => {
process.stdout.write(chunk.toString());
client.destroy();
process.exit(0);
});
});
});
setTimeout(() => { process.exit(2); }, 8000);
`,
"child.js": `
process.send("ready");
process.on("message", (msg, sendHandle) => {
if (sendHandle) {
sendHandle.write("Hello from child!", () => {
process.disconnect();
});
} else {
process.stderr.write("sendHandle was undefined\\n");
process.exit(1);
}
});
setTimeout(() => { process.exit(2); }, 8000);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "parent.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("Hello from child!");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
}, 15000);
test("sendHandle is undefined when handle argument is not provided", async () => {
using dir = tempDir("ipc-no-handle", {
"parent.js": `
const { fork } = require("child_process");
const path = require("path");
const worker = fork(path.join(process.cwd(), "child.js"), [], {
serialization: "${mode}",
});
worker.on("message", (message) => {
if (message === "ready") {
worker.send("just-a-message");
} else {
process.stdout.write(message);
worker.kill();
}
});
worker.on("exit", () => {
process.exit(0);
});
setTimeout(() => { process.exit(2); }, 8000);
`,
"child.js": `
process.send("ready");
process.on("message", (msg, sendHandle) => {
if (sendHandle === undefined) {
process.send("handle-is-undefined");
} else {
process.send("handle-is-" + typeof sendHandle);
}
});
setTimeout(() => { process.exit(2); }, 8000);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "parent.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("handle-is-undefined");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
}, 15000);
});