Skip to content

Commit cefffac

Browse files
committed
net: add parent-owned pipe and socket endpoints
Signed-off-by: Chris King <kingces95@gmail.com>
1 parent b2b7405 commit cefffac

24 files changed

Lines changed: 1507 additions & 5 deletions

doc/api/child_process.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,9 @@ pipes between the parent and child. The value is one of the following:
10551055
file descriptor is duplicated in the child process to the fd that
10561056
corresponds to the index in the `stdio` array. The stream must have an
10571057
underlying descriptor (file streams do not start until the `'open'` event has
1058-
occurred).
1058+
occurred). Pipe endpoints returned by [`net.createPipe()`][] may be passed
1059+
here. Readable endpoints returned by [`net.createPipe()`][] must not be
1060+
flowing when passed here.
10591061
**NOTE:** While it is technically possible to pass `stdin` as a writable or
10601062
`stdout`/`stderr` as readable, it is not recommended.
10611063
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
14411443
[`'exit'`][] event, since multiple processes might share the same stdio
14421444
streams. The `'close'` event will always emit after [`'exit'`][] was
14431445
already emitted, or [`'error'`][] if the child process failed to spawn.
1446+
Readable stdio streams created by Node.js are resumed after the child process
1447+
exits so they can be fully consumed and closed before the `'close'` event is
1448+
emitted. Endpoints created by [`net.createPipe()`][] are an exception to this
1449+
rule and are not resumed by the child process. Their stream lifecycle remains
1450+
owned by the parent process, and consequently the child process `'close'` event
1451+
does not wait for such streams to close.
14441452
14451453
If the process exited, `code` is the final exit code of the process, otherwise
14461454
`null`. If the process terminated due to receipt of a signal, `signal` is the
@@ -2374,6 +2382,7 @@ or [`child_process.fork()`][].
23742382
[`maxBuffer` and Unicode]: #maxbuffer-and-unicode
23752383
[`net.Server`]: net.md#class-netserver
23762384
[`net.Socket`]: net.md#class-netsocket
2385+
[`net.createPipe()`]: net.md#netcreatepipe
23772386
[`options.detached`]: #optionsdetached
23782387
[`process.disconnect()`]: process.md#processdisconnect
23792388
[`process.env`]: process.md#processenv

doc/api/net.md

Lines changed: 175 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
99
<!-- source_link=lib/net.js -->
1010

11-
The `node:net` module provides an asynchronous network API for creating stream-based
12-
TCP or [IPC][] servers ([`net.createServer()`][]) and clients
13-
([`net.createConnection()`][]).
11+
The `node:net` module provides an asynchronous network API for creating
12+
stream-based TCP or [IPC][] servers ([`net.createServer()`][]) and clients
13+
([`net.createConnection()`][]), and operating system pipe pairs
14+
([`net.createPipe()`][]) and socket pairs ([`net.createSocketPair()`][]).
1415

1516
It can be accessed using:
1617

@@ -2382,6 +2383,170 @@ Use `nc` to connect to a Unix domain socket server:
23822383
nc -U /tmp/echo.sock
23832384
```
23842385

2386+
Endpoint pairs created by [`net.createPipe()`][] and
2387+
[`net.createSocketPair()`][] are owned by the current process. Use normal
2388+
stream idioms such as `end()` to finish writing and stream consumption to drain
2389+
a readable endpoint. Use `resume()` when an unread readable endpoint should be
2390+
drained without observing its data, and use `destroy()` when an endpoint is no
2391+
longer needed without being naturally ended or drained.
2392+
2393+
## `net.createSocketPair()`
2394+
2395+
<!-- YAML
2396+
added: REPLACEME
2397+
-->
2398+
2399+
* Returns: {net.Socket\[]}
2400+
* {net.Socket} The first socket.
2401+
* {net.Socket} The second socket.
2402+
2403+
The `net.createSocketPair()` method creates a connected pair of operating
2404+
system sockets. The returned [`net.Socket`][] instances are owned by the current
2405+
process and may be used to exchange bytes in either direction without binding a
2406+
server or connecting a client. Either socket may be passed to a Node.js child
2407+
process over an IPC channel using [`subprocess.send()`][].
2408+
2409+
```cjs
2410+
const { spawn } = require('node:child_process');
2411+
const { createSocketPair } = require('node:net');
2412+
2413+
const [left, right] = createSocketPair();
2414+
2415+
const child = spawn(process.execPath, ['-e', `
2416+
process.on('message', (message, socket) => {
2417+
socket.on('data', (chunk) => {
2418+
socket.write(chunk.toString().toUpperCase());
2419+
});
2420+
socket.resume();
2421+
process.send('ready');
2422+
});
2423+
`], {
2424+
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
2425+
});
2426+
2427+
child.once('message', () => {
2428+
left.write('hello');
2429+
});
2430+
2431+
left.once('data', (chunk) => {
2432+
console.log(chunk.toString()); // Prints: HELLO
2433+
left.destroy();
2434+
child.kill();
2435+
});
2436+
2437+
child.send('socket', right, { keepOpen: false });
2438+
```
2439+
2440+
```mjs
2441+
import { spawn } from 'node:child_process';
2442+
import { createSocketPair } from 'node:net';
2443+
2444+
const [left, right] = createSocketPair();
2445+
2446+
const child = spawn(process.execPath, ['-e', `
2447+
process.on('message', (message, socket) => {
2448+
socket.on('data', (chunk) => {
2449+
socket.write(chunk.toString().toUpperCase());
2450+
});
2451+
socket.resume();
2452+
process.send('ready');
2453+
});
2454+
`], {
2455+
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
2456+
});
2457+
2458+
child.once('message', () => {
2459+
left.write('hello');
2460+
});
2461+
2462+
left.once('data', (chunk) => {
2463+
console.log(chunk.toString()); // Prints: HELLO
2464+
left.destroy();
2465+
child.kill();
2466+
});
2467+
2468+
child.send('socket', right, { keepOpen: false });
2469+
```
2470+
2471+
## `net.createPipe()`
2472+
2473+
<!-- YAML
2474+
added: REPLACEME
2475+
-->
2476+
2477+
* Returns: {Object}
2478+
* `readable` {net.Socket} The readable end of the pipe.
2479+
* `writable` {net.Socket} The writable end of the pipe.
2480+
2481+
The `net.createPipe()` method creates an operating system pipe pair. The
2482+
returned `readable` and `writable` streams are owned by the current process and
2483+
may be passed to [`child_process.spawn()`][] using the [`stdio`][] option.
2484+
2485+
When a pipe endpoint is passed to [`child_process.spawn()`][], the child process
2486+
leases the endpoint until the child process exits. A pipe endpoint may be leased
2487+
to only one child process at a time. Pipe endpoints created by this module are
2488+
not supported by synchronous child process APIs such as
2489+
[`child_process.spawnSync()`][].
2490+
2491+
Readable pipe endpoints must not be flowing when they are passed to
2492+
[`child_process.spawn()`][]. The child process [`'close'`
2493+
event][child-process-close] does not wait for leased endpoints to close and
2494+
does not resume them after the child process exits.
2495+
2496+
When a `readable` endpoint is passed as child stdin or as another child fd, the
2497+
child leases a readable handle. When a `writable` endpoint is passed as child
2498+
stdout, stderr, or another child fd, the child leases a writable handle. A
2499+
`readable` endpoint may not be passed as child stdout or stderr, and a
2500+
`writable` endpoint may not be passed as child stdin.
2501+
2502+
```cjs
2503+
const { spawn } = require('node:child_process');
2504+
const { createPipe } = require('node:net');
2505+
const { text } = require('node:stream/consumers');
2506+
2507+
const { readable, writable } = createPipe();
2508+
const child = spawn(process.execPath, ['-e', `
2509+
const fs = require('node:fs');
2510+
const buffer = Buffer.alloc(1);
2511+
const count = fs.readSync(0, buffer, 0, 1, null);
2512+
fs.writeSync(1, buffer.subarray(0, count));
2513+
`], {
2514+
stdio: [readable, 'pipe', 'inherit'],
2515+
});
2516+
2517+
const output = text(child.stdout);
2518+
writable.end('abc');
2519+
2520+
child.on('close', async () => {
2521+
console.log(await output); // Prints: a
2522+
console.log(await text(readable)); // Prints: bc
2523+
});
2524+
```
2525+
2526+
```mjs
2527+
import { spawn } from 'node:child_process';
2528+
import { createPipe } from 'node:net';
2529+
import { text } from 'node:stream/consumers';
2530+
2531+
const { readable, writable } = createPipe();
2532+
const child = spawn(process.execPath, ['-e', `
2533+
const fs = require('node:fs');
2534+
const buffer = Buffer.alloc(1);
2535+
const count = fs.readSync(0, buffer, 0, 1, null);
2536+
fs.writeSync(1, buffer.subarray(0, count));
2537+
`], {
2538+
stdio: [readable, 'pipe', 'inherit'],
2539+
});
2540+
2541+
const output = text(child.stdout);
2542+
writable.end('abc');
2543+
2544+
child.on('close', async () => {
2545+
console.log(await output); // Prints: a
2546+
console.log(await text(readable)); // Prints: bc
2547+
});
2548+
```
2549+
23852550
## `net.getDefaultAutoSelectFamily()`
23862551

23872552
<!-- YAML
@@ -2585,6 +2750,8 @@ console.log('listening on', server.address().port);
25852750
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
25862751
[`EventEmitter`]: events.md#class-eventemitter
25872752
[`child_process.fork()`]: child_process.md#child_processforkmodulepath-args-options
2753+
[`child_process.spawn()`]: child_process.md#child_processspawncommand-args-options
2754+
[`child_process.spawnSync()`]: child_process.md#child_processspawnsynccommand-args-options
25882755
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
25892756
[`dns.lookup()` hints]: dns.md#supported-getaddrinfo-flags
25902757
[`net.Server`]: #class-netserver
@@ -2597,7 +2764,9 @@ console.log('listening on', server.address().port);
25972764
[`net.createConnection(options)`]: #netcreateconnectionoptions-connectlistener
25982765
[`net.createConnection(path)`]: #netcreateconnectionpath-connectlistener
25992766
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
2767+
[`net.createPipe()`]: #netcreatepipe
26002768
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
2769+
[`net.createSocketPair()`]: #netcreatesocketpair
26012770
[`net.getDefaultAutoSelectFamily()`]: #netgetdefaultautoselectfamily
26022771
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: #netgetdefaultautoselectfamilyattempttimeout
26032772
[`netPromises.listen()`]: #netpromiseslistenoptions
@@ -2612,6 +2781,7 @@ console.log('listening on', server.address().port);
26122781
[`server.listen(path)`]: #serverlistenpath-backlog-callback
26132782
[`server.listen(port)`]: #serverlistenport-host-backlog-callback
26142783
[`server.maxConnections`]: #servermaxconnections
2784+
[`subprocess.send()`]: child_process.md#subprocesssendmessage-sendhandle-options-callback
26152785
[`socket(7)`]: https://man7.org/linux/man-pages/man7/socket.7.html
26162786
[`socket.connect()`]: #socketconnect
26172787
[`socket.connect(options)`]: #socketconnectoptions-connectlistener
@@ -2630,13 +2800,15 @@ console.log('listening on', server.address().port);
26302800
[`socket.setTimeout()`]: #socketsettimeouttimeout-callback
26312801
[`socket.setTimeout(timeout)`]: #socketsettimeouttimeout-callback
26322802
[`stream.getDefaultHighWaterMark()`]: stream.md#streamgetdefaulthighwatermarkobjectmode
2803+
[`stdio`]: child_process.md#optionsstdio
26332804
[`worker_threads`]: worker_threads.md
26342805
[`writable.destroy()`]: stream.md#writabledestroyerror
26352806
[`writable.destroyed`]: stream.md#writabledestroyed
26362807
[`writable.end()`]: stream.md#writableendchunk-encoding-callback
26372808
[`writable.writableLength`]: stream.md#writablewritablelength
26382809
[dot-decimal notation]: https://en.wikipedia.org/wiki/Dot-decimal_notation
26392810
[half-closed]: https://tools.ietf.org/html/rfc1122
2811+
[child-process-close]: child_process.md#event-close
26402812
[stream_writable_write]: stream.md#writablewritechunk-encoding-callback
26412813
[unspecified IPv4 address]: https://en.wikipedia.org/wiki/0.0.0.0
26422814
[unspecified IPv6 address]: https://en.wikipedia.org/wiki/IPv6_address#Unspecified_address

0 commit comments

Comments
 (0)