From b813b63ffb50091aba4bc45bd8d4f7d24d676779 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 5 Jun 2025 23:17:04 +0000 Subject: [PATCH] Implement partial IPC socket handle passing support --- src/bun.js/bindings/webcore/HTTPHeaderMap.cpp | 2 +- src/bun.js/ipc.zig | 10 +- src/js/builtins/Ipc.ts | 58 ++----- test-ipc-server.js | 32 ++++ .../test-child-process-fork-net-server.js | 159 ++++++++++++++++++ 5 files changed, 210 insertions(+), 51 deletions(-) create mode 100644 test-ipc-server.js create mode 100644 test/js/node/test/parallel/test-child-process-fork-net-server.js diff --git a/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp b/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp index 9b95c823de..35bb6151d9 100644 --- a/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp +++ b/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp @@ -40,7 +40,7 @@ static StringView extractCookieName(const StringView& cookie) { auto nameEnd = cookie.find('='); if (nameEnd == notFound) - return String(); + return StringView(); return cookie.substring(0, nameEnd); } diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig index caaec23e3c..7b53c54cbe 100644 --- a/src/bun.js/ipc.zig +++ b/src/bun.js/ipc.zig @@ -1003,8 +1003,14 @@ pub fn doSend(ipc: ?*SendQueue, globalObject: *JSC.JSGlobalObject, callFrame: *J }, .none => {}, } - } else { - // + } else if (bun.JSC.API.TCPSocket.fromJS(handle)) |socket| { + // Handle regular TCP sockets + const fd = socket.socket.fd(); + zig_handle = .init(fd, handle); + } else if (bun.JSC.API.TLSSocket.fromJS(handle)) |socket| { + // Handle TLS sockets - not supported yet + _ = socket; + // TODO: Handle TLS sockets } } diff --git a/src/js/builtins/Ipc.ts b/src/js/builtins/Ipc.ts index 8971d6290f..dcc6afd778 100644 --- a/src/js/builtins/Ipc.ts +++ b/src/js/builtins/Ipc.ts @@ -145,65 +145,27 @@ * @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 +const nop = () => {}; - /* +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 }; - return [server._handle, { cmd: "NODE_HANDLE", message, type: "net.Server" }]; + return [handle._handle, { cmd: "NODE_HANDLE", message: message, type: "net.Server" }]; } else if (handle instanceof net.Socket) { - const new_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 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); - - // Act like socket is detached - if (!options?.keepOpen) socket.server._connections--; - } - - const internal_handle = socket._handle; - - // Remove handle from socket object, it will be closed when the socket - // will be 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); - } - return [internal_handle, new_message]; + // net.Socket support is not yet implemented + // TODO: Implement proper socket handle passing with connection tracking + return null; } 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"); } else { - throw $ERR_INVALID_HANDLE_TYPE(); + const err = new Error("Invalid handle type"); + err.name = "ERR_INVALID_HANDLE_TYPE"; + throw err; } - */ } /** * @param {Serialized} serialized diff --git a/test-ipc-server.js b/test-ipc-server.js new file mode 100644 index 0000000000..3f0ccdc29b --- /dev/null +++ b/test-ipc-server.js @@ -0,0 +1,32 @@ +const net = require("node:net"); +const { fork } = require("node:child_process"); + +if (process.argv[2] === "child") { + // Child process + process.on("message", (msg, server) => { + console.log("Child received:", msg, "server:", server); + if (server) { + console.log("Server is a net.Server:", server instanceof net.Server); + server.on("connection", socket => { + console.log("Child: Got connection"); + socket.end("Hello from child!"); + }); + } + process.exit(0); + }); +} else { + // Parent process + const server = net.createServer(); + server.listen(0, () => { + console.log("Parent: Server listening on port", server.address().port); + + const child = fork(__filename, ["child"]); + child.on("exit", code => { + console.log("Child exited with code", code); + server.close(); + }); + + console.log("Parent: Sending server to child"); + child.send({ what: "server" }, server); + }); +} diff --git a/test/js/node/test/parallel/test-child-process-fork-net-server.js b/test/js/node/test/parallel/test-child-process-fork-net-server.js new file mode 100644 index 0000000000..3a3f01c6d6 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-fork-net-server.js @@ -0,0 +1,159 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fork = require('child_process').fork; +const net = require('net'); +const debug = require('util').debuglog('test'); + +const Countdown = require('../common/countdown'); + +if (process.argv[2] === 'child') { + + let serverScope; + + // TODO(@jasnell): The message event is not called consistently + // across platforms. Need to investigate if it can be made + // more consistent. + const onServer = (msg, server) => { + if (msg.what !== 'server') return; + process.removeListener('message', onServer); + + serverScope = server; + + // TODO(@jasnell): This is apparently not called consistently + // across platforms. Need to investigate if it can be made + // more consistent. + server.on('connection', (socket) => { + debug('CHILD: got connection'); + process.send({ what: 'connection' }); + socket.destroy(); + }); + + // Start making connection from parent. + debug('CHILD: server listening'); + process.send({ what: 'listening' }); + }; + + process.on('message', onServer); + + // TODO(@jasnell): The close event is not called consistently + // across platforms. Need to investigate if it can be made + // more consistent. + const onClose = (msg) => { + if (msg.what !== 'close') return; + process.removeListener('message', onClose); + + serverScope.on('close', common.mustCall(() => { + process.send({ what: 'close' }); + })); + serverScope.close(); + }; + + process.on('message', onClose); + + process.send({ what: 'ready' }); +} else { + + const child = fork(process.argv[1], ['child']); + + child.on('exit', common.mustCall((code, signal) => { + const message = `CHILD: died with ${code}, ${signal}`; + assert.strictEqual(code, 0, message); + })); + + // Send net.Server to child and test by connecting. + function testServer(callback) { + + // Destroy server execute callback when done. + const countdown = new Countdown(2, () => { + server.on('close', common.mustCall(() => { + debug('PARENT: server closed'); + child.send({ what: 'close' }); + })); + server.close(); + }); + + // We expect 4 connections and close events. + const connections = new Countdown(4, () => countdown.dec()); + const closed = new Countdown(4, () => countdown.dec()); + + // Create server and send it to child. + const server = net.createServer(); + + // TODO(@jasnell): The specific number of times the connection + // event is emitted appears to be variable across platforms. + // Need to investigate why and whether it can be made + // more consistent. + server.on('connection', (socket) => { + debug('PARENT: got connection'); + socket.destroy(); + connections.dec(); + }); + + server.on('listening', common.mustCall(() => { + debug('PARENT: server listening'); + child.send({ what: 'server' }, server); + })); + server.listen(0); + + // Handle client messages. + // TODO(@jasnell): The specific number of times the message + // event is emitted appears to be variable across platforms. + // Need to investigate why and whether it can be made + // more consistent. + const messageHandlers = (msg) => { + if (msg.what === 'listening') { + // Make connections. + let socket; + for (let i = 0; i < 4; i++) { + socket = net.connect(server.address().port, common.mustCall(() => { + debug('CLIENT: connected'); + })); + socket.on('close', common.mustCall(() => { + closed.dec(); + debug('CLIENT: closed'); + })); + } + + } else if (msg.what === 'connection') { + // Child got connection + connections.dec(); + } else if (msg.what === 'close') { + child.removeListener('message', messageHandlers); + callback(); + } + }; + + child.on('message', messageHandlers); + } + + const onReady = common.mustCall((msg) => { + if (msg.what !== 'ready') return; + child.removeListener('message', onReady); + testServer(common.mustCall()); + }); + + // Create server and send it to child. + child.on('message', onReady); +}