From cefffacb7fa7921f0ac6117aa36595a005e9fc39 Mon Sep 17 00:00:00 2001 From: Chris King Date: Sun, 9 Aug 2026 14:36:34 -1000 Subject: [PATCH] net: add parent-owned pipe and socket endpoints Signed-off-by: Chris King --- doc/api/child_process.md | 11 +- doc/api/net.md | 178 ++++++++++++- lib/internal/child_process.js | 98 ++++++++ lib/internal/net.js | 3 + lib/net.js | 44 ++++ src/env_properties.h | 1 + src/pipe_wrap.cc | 46 ++++ src/pipe_wrap.h | 1 + src/process_wrap.cc | 3 +- src/tcp_wrap.cc | 54 ++++ src/tcp_wrap.h | 1 + test/common/net-create-pipe.js | 60 +++++ test/common/net-create-socket-pair.js | 65 +++++ .../test-child-process-leased-pipe-errors.js | 237 ++++++++++++++++++ .../test-child-process-leased-pipe-unit.js | 180 +++++++++++++ .../test-child-process-leased-pipe.js | 76 ++++++ .../test-child-process-send-socket-pair.js | 71 ++++++ ...-child-process-socket-pair-stdio-errors.js | 63 +++++ .../test-child-process-validate-stdio.js | 40 +++ test/parallel/test-net-create-pipe-docs.js | 77 ++++++ test/parallel/test-net-create-pipe-finally.js | 45 ++++ test/parallel/test-net-create-pipe.js | 22 ++ .../test-net-create-socket-pair-docs.js | 93 +++++++ test/parallel/test-net-create-socket-pair.js | 43 ++++ 24 files changed, 1507 insertions(+), 5 deletions(-) create mode 100644 test/common/net-create-pipe.js create mode 100644 test/common/net-create-socket-pair.js create mode 100644 test/parallel/test-child-process-leased-pipe-errors.js create mode 100644 test/parallel/test-child-process-leased-pipe-unit.js create mode 100644 test/parallel/test-child-process-leased-pipe.js create mode 100644 test/parallel/test-child-process-send-socket-pair.js create mode 100644 test/parallel/test-child-process-socket-pair-stdio-errors.js create mode 100644 test/parallel/test-net-create-pipe-docs.js create mode 100644 test/parallel/test-net-create-pipe-finally.js create mode 100644 test/parallel/test-net-create-pipe.js create mode 100644 test/parallel/test-net-create-socket-pair-docs.js create mode 100644 test/parallel/test-net-create-socket-pair.js diff --git a/doc/api/child_process.md b/doc/api/child_process.md index 913542c2dc06..ab4b42971f97 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -1055,7 +1055,9 @@ pipes between the parent and child. The value is one of the following: file descriptor is duplicated in the child process to the fd that corresponds to the index in the `stdio` array. The stream must have an underlying descriptor (file streams do not start until the `'open'` event has - occurred). + occurred). Pipe endpoints returned by [`net.createPipe()`][] may be passed + here. Readable endpoints returned by [`net.createPipe()`][] must not be + flowing when passed here. **NOTE:** While it is technically possible to pass `stdin` as a writable or `stdout`/`stderr` as readable, it is not recommended. Readable and writable streams are designed with distinct behaviors, and using @@ -1441,6 +1443,12 @@ streams of a child process have been closed. This is distinct from the [`'exit'`][] event, since multiple processes might share the same stdio streams. The `'close'` event will always emit after [`'exit'`][] was already emitted, or [`'error'`][] if the child process failed to spawn. +Readable stdio streams created by Node.js are resumed after the child process +exits so they can be fully consumed and closed before the `'close'` event is +emitted. Endpoints created by [`net.createPipe()`][] are an exception to this +rule and are not resumed by the child process. Their stream lifecycle remains +owned by the parent process, and consequently the child process `'close'` event +does not wait for such streams to close. If the process exited, `code` is the final exit code of the process, otherwise `null`. If the process terminated due to receipt of a signal, `signal` is the @@ -2374,6 +2382,7 @@ or [`child_process.fork()`][]. [`maxBuffer` and Unicode]: #maxbuffer-and-unicode [`net.Server`]: net.md#class-netserver [`net.Socket`]: net.md#class-netsocket +[`net.createPipe()`]: net.md#netcreatepipe [`options.detached`]: #optionsdetached [`process.disconnect()`]: process.md#processdisconnect [`process.env`]: process.md#processenv diff --git a/doc/api/net.md b/doc/api/net.md index 81a25eee0f1b..421c1e0c2015 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -8,9 +8,10 @@ -The `node:net` module provides an asynchronous network API for creating stream-based -TCP or [IPC][] servers ([`net.createServer()`][]) and clients -([`net.createConnection()`][]). +The `node:net` module provides an asynchronous network API for creating +stream-based TCP or [IPC][] servers ([`net.createServer()`][]) and clients +([`net.createConnection()`][]), and operating system pipe pairs +([`net.createPipe()`][]) and socket pairs ([`net.createSocketPair()`][]). It can be accessed using: @@ -2382,6 +2383,170 @@ Use `nc` to connect to a Unix domain socket server: nc -U /tmp/echo.sock ``` +Endpoint pairs created by [`net.createPipe()`][] and +[`net.createSocketPair()`][] are owned by the current process. Use normal +stream idioms such as `end()` to finish writing and stream consumption to drain +a readable endpoint. Use `resume()` when an unread readable endpoint should be +drained without observing its data, and use `destroy()` when an endpoint is no +longer needed without being naturally ended or drained. + +## `net.createSocketPair()` + + + +* Returns: {net.Socket\[]} + * {net.Socket} The first socket. + * {net.Socket} The second socket. + +The `net.createSocketPair()` method creates a connected pair of operating +system sockets. The returned [`net.Socket`][] instances are owned by the current +process and may be used to exchange bytes in either direction without binding a +server or connecting a client. Either socket may be passed to a Node.js child +process over an IPC channel using [`subprocess.send()`][]. + +```cjs +const { spawn } = require('node:child_process'); +const { createSocketPair } = require('node:net'); + +const [left, right] = createSocketPair(); + +const child = spawn(process.execPath, ['-e', ` + process.on('message', (message, socket) => { + socket.on('data', (chunk) => { + socket.write(chunk.toString().toUpperCase()); + }); + socket.resume(); + process.send('ready'); + }); +`], { + stdio: ['ignore', 'inherit', 'inherit', 'ipc'], +}); + +child.once('message', () => { + left.write('hello'); +}); + +left.once('data', (chunk) => { + console.log(chunk.toString()); // Prints: HELLO + left.destroy(); + child.kill(); +}); + +child.send('socket', right, { keepOpen: false }); +``` + +```mjs +import { spawn } from 'node:child_process'; +import { createSocketPair } from 'node:net'; + +const [left, right] = createSocketPair(); + +const child = spawn(process.execPath, ['-e', ` + process.on('message', (message, socket) => { + socket.on('data', (chunk) => { + socket.write(chunk.toString().toUpperCase()); + }); + socket.resume(); + process.send('ready'); + }); +`], { + stdio: ['ignore', 'inherit', 'inherit', 'ipc'], +}); + +child.once('message', () => { + left.write('hello'); +}); + +left.once('data', (chunk) => { + console.log(chunk.toString()); // Prints: HELLO + left.destroy(); + child.kill(); +}); + +child.send('socket', right, { keepOpen: false }); +``` + +## `net.createPipe()` + + + +* Returns: {Object} + * `readable` {net.Socket} The readable end of the pipe. + * `writable` {net.Socket} The writable end of the pipe. + +The `net.createPipe()` method creates an operating system pipe pair. The +returned `readable` and `writable` streams are owned by the current process and +may be passed to [`child_process.spawn()`][] using the [`stdio`][] option. + +When a pipe endpoint is passed to [`child_process.spawn()`][], the child process +leases the endpoint until the child process exits. A pipe endpoint may be leased +to only one child process at a time. Pipe endpoints created by this module are +not supported by synchronous child process APIs such as +[`child_process.spawnSync()`][]. + +Readable pipe endpoints must not be flowing when they are passed to +[`child_process.spawn()`][]. The child process [`'close'` +event][child-process-close] does not wait for leased endpoints to close and +does not resume them after the child process exits. + +When a `readable` endpoint is passed as child stdin or as another child fd, the +child leases a readable handle. When a `writable` endpoint is passed as child +stdout, stderr, or another child fd, the child leases a writable handle. A +`readable` endpoint may not be passed as child stdout or stderr, and a +`writable` endpoint may not be passed as child stdin. + +```cjs +const { spawn } = require('node:child_process'); +const { createPipe } = require('node:net'); +const { text } = require('node:stream/consumers'); + +const { readable, writable } = createPipe(); +const child = spawn(process.execPath, ['-e', ` + const fs = require('node:fs'); + const buffer = Buffer.alloc(1); + const count = fs.readSync(0, buffer, 0, 1, null); + fs.writeSync(1, buffer.subarray(0, count)); +`], { + stdio: [readable, 'pipe', 'inherit'], +}); + +const output = text(child.stdout); +writable.end('abc'); + +child.on('close', async () => { + console.log(await output); // Prints: a + console.log(await text(readable)); // Prints: bc +}); +``` + +```mjs +import { spawn } from 'node:child_process'; +import { createPipe } from 'node:net'; +import { text } from 'node:stream/consumers'; + +const { readable, writable } = createPipe(); +const child = spawn(process.execPath, ['-e', ` + const fs = require('node:fs'); + const buffer = Buffer.alloc(1); + const count = fs.readSync(0, buffer, 0, 1, null); + fs.writeSync(1, buffer.subarray(0, count)); +`], { + stdio: [readable, 'pipe', 'inherit'], +}); + +const output = text(child.stdout); +writable.end('abc'); + +child.on('close', async () => { + console.log(await output); // Prints: a + console.log(await text(readable)); // Prints: bc +}); +``` + ## `net.getDefaultAutoSelectFamily()`