Skip to content

Commit e44f173

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

24 files changed

Lines changed: 1458 additions & 5 deletions

doc/api/child_process.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,10 @@ 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. Socket endpoints returned by [`net.createSocketPair()`][] may be passed
1060+
here only at positions greater than or equal to `3`. A readable pipe endpoint
1061+
returned by [`net.createPipe()`][] must not be flowing when it is passed here.
10591062
**NOTE:** While it is technically possible to pass `stdin` as a writable or
10601063
`stdout`/`stderr` as readable, it is not recommended.
10611064
Readable and writable streams are designed with distinct behaviors, and using
@@ -1441,6 +1444,12 @@ streams of a child process have been closed. This is distinct from the
14411444
[`'exit'`][] event, since multiple processes might share the same stdio
14421445
streams. The `'close'` event will always emit after [`'exit'`][] was
14431446
already emitted, or [`'error'`][] if the child process failed to spawn.
1447+
Readable stdio streams created by Node.js are resumed after the child process
1448+
exits so they can be fully consumed and closed before the `'close'` event is
1449+
emitted. Endpoints created by [`net.createPipe()`][] are an exception to this
1450+
rule and are not resumed by the child process. Their stream lifecycle remains
1451+
owned by the parent process, and consequently the child process `'close'` event
1452+
does not wait for such streams to close.
14441453
14451454
If the process exited, `code` is the final exit code of the process, otherwise
14461455
`null`. If the process terminated due to receipt of a signal, `signal` is the
@@ -2374,6 +2383,8 @@ or [`child_process.fork()`][].
23742383
[`maxBuffer` and Unicode]: #maxbuffer-and-unicode
23752384
[`net.Server`]: net.md#class-netserver
23762385
[`net.Socket`]: net.md#class-netsocket
2386+
[`net.createPipe()`]: net.md#netcreatepipe
2387+
[`net.createSocketPair()`]: net.md#netcreatesocketpair
23772388
[`options.detached`]: #optionsdetached
23782389
[`process.disconnect()`]: process.md#processdisconnect
23792390
[`process.env`]: process.md#processenv

doc/api/net.md

Lines changed: 148 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,144 @@ Use `nc` to connect to a Unix domain socket server:
23822383
nc -U /tmp/echo.sock
23832384
```
23842385

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

23872526
<!-- YAML
@@ -2585,6 +2724,8 @@ console.log('listening on', server.address().port);
25852724
[`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted
25862725
[`EventEmitter`]: events.md#class-eventemitter
25872726
[`child_process.fork()`]: child_process.md#child_processforkmodulepath-args-options
2727+
[`child_process.spawn()`]: child_process.md#child_processspawncommand-args-options
2728+
[`child_process.spawnSync()`]: child_process.md#child_processspawnsynccommand-args-options
25882729
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
25892730
[`dns.lookup()` hints]: dns.md#supported-getaddrinfo-flags
25902731
[`net.Server`]: #class-netserver
@@ -2597,7 +2738,9 @@ console.log('listening on', server.address().port);
25972738
[`net.createConnection(options)`]: #netcreateconnectionoptions-connectlistener
25982739
[`net.createConnection(path)`]: #netcreateconnectionpath-connectlistener
25992740
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
2741+
[`net.createPipe()`]: #netcreatepipe
26002742
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
2743+
[`net.createSocketPair()`]: #netcreatesocketpair
26012744
[`net.getDefaultAutoSelectFamily()`]: #netgetdefaultautoselectfamily
26022745
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: #netgetdefaultautoselectfamilyattempttimeout
26032746
[`netPromises.listen()`]: #netpromiseslistenoptions
@@ -2630,13 +2773,15 @@ console.log('listening on', server.address().port);
26302773
[`socket.setTimeout()`]: #socketsettimeouttimeout-callback
26312774
[`socket.setTimeout(timeout)`]: #socketsettimeouttimeout-callback
26322775
[`stream.getDefaultHighWaterMark()`]: stream.md#streamgetdefaulthighwatermarkobjectmode
2776+
[`stdio`]: child_process.md#optionsstdio
26332777
[`worker_threads`]: worker_threads.md
26342778
[`writable.destroy()`]: stream.md#writabledestroyerror
26352779
[`writable.destroyed`]: stream.md#writabledestroyed
26362780
[`writable.end()`]: stream.md#writableendchunk-encoding-callback
26372781
[`writable.writableLength`]: stream.md#writablewritablelength
26382782
[dot-decimal notation]: https://en.wikipedia.org/wiki/Dot-decimal_notation
26392783
[half-closed]: https://tools.ietf.org/html/rfc1122
2784+
[child-process-close]: child_process.md#event-close
26402785
[stream_writable_write]: stream.md#writablewritechunk-encoding-callback
26412786
[unspecified IPv4 address]: https://en.wikipedia.org/wiki/0.0.0.0
26422787
[unspecified IPv6 address]: https://en.wikipedia.org/wiki/IPv6_address#Unspecified_address

0 commit comments

Comments
 (0)