Skip to content

Commit ade74bf

Browse files
authored
Add nodeChildProcess to subprocess promises and make the subprocess a normal promise (#1255)
1 parent 29d9cfc commit ade74bf

47 files changed

Lines changed: 389 additions & 155 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/api.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ This can only be called inside a subprocess. This requires the [`gracefulCancel`
211211
## Return value
212212

213213
_TypeScript:_ [`ResultPromise`](typescript.md)\
214-
_Type:_ `Promise<object> | Subprocess`
214+
_Type:_ `Subprocess`
215215

216216
The return value of all [asynchronous methods](#methods) is both:
217217
- the [subprocess](#subprocess).
@@ -223,7 +223,9 @@ The return value of all [asynchronous methods](#methods) is both:
223223

224224
_TypeScript:_ [`Subprocess`](typescript.md)
225225

226-
[`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with the following methods and properties.
226+
Promise with the following Execa-specific methods and properties.
227+
228+
The underlying Node.js [`ChildProcess`](https://nodejs.org/api/child_process.html#class-childprocess) instance is available as [`subprocess.nodeChildProcess`](#subprocessnodechildprocess). This is an escape hatch for Node.js-specific APIs that Execa does not document, such as `.on()`, `.send()`, `.disconnect()`, `.ref()` and `.unref()`.
227229

228230
### subprocess\[Symbol.asyncIterator\]()
229231

@@ -335,6 +337,14 @@ This is `undefined` if the subprocess failed to spawn.
335337

336338
[More info.](termination.md#inter-process-termination)
337339

340+
### subprocess.nodeChildProcess
341+
342+
_Type:_ [`ChildProcess`](https://nodejs.org/api/child_process.html#class-childprocess)
343+
344+
Underlying Node.js child process instance.
345+
346+
This is intended as an escape hatch for Node.js-specific APIs that Execa does not document.
347+
338348
### subprocess.sendMessage(message, sendMessageOptions)
339349

340350
`message`: [`Message`](ipc.md#message-type)\

docs/execution.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ await timedExeca`npm run test`;
127127

128128
### Subprocess
129129

130-
The subprocess is returned as soon as it is spawned. It is a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with [additional methods and properties](api.md#subprocess).
130+
The subprocess is returned as soon as it is spawned. It is a `Promise` with [Execa-specific methods and properties](api.md#subprocess). The underlying Node.js [`ChildProcess`](https://nodejs.org/api/child_process.html#class-childprocess) is available as [`subprocess.nodeChildProcess`](api.md#subprocessnodechildprocess).
131131

132132
```js
133133
const subprocess = execa`npm run build`;
@@ -136,7 +136,7 @@ console.log(subprocess.pid);
136136

137137
### Result
138138

139-
The subprocess is also a `Promise` that resolves with the [`result`](api.md#result).
139+
The subprocess resolves with the [`result`](api.md#result).
140140

141141
```js
142142
const {stdout} = await execa`npm run build`;

lib/convert/duplex.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const createDuplex = ({subprocess, concurrentStreams, encoding}, {from, t
5252
subprocess,
5353
subprocessStdin,
5454
});
55-
onStdinFinished(subprocessStdin, duplex, subprocessStdout);
55+
onStdinFinished(subprocessStdin, duplex, subprocessStdout, subprocess);
5656
return duplex;
5757
};
5858

lib/convert/readable.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,24 @@ export const onStdoutFinished = async ({subprocessStdout, onStdoutDataDone, read
9696
}
9797
} catch (error) {
9898
await safeWaitForSubprocessStdin(subprocessStdin);
99-
destroyOtherReadable(readable, error);
99+
destroyOtherReadable(readable, await getPrematureCloseError(subprocess, error));
100100
}
101101
};
102102

103+
const getPrematureCloseError = async (subprocess, error) => {
104+
if (error.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
105+
return error;
106+
}
107+
108+
try {
109+
await subprocess;
110+
} catch (subprocessError) {
111+
return subprocessError;
112+
}
113+
114+
return error;
115+
};
116+
103117
// When `readable` aborts/errors, do the same on `subprocess.stdout`
104118
export const onReadableDestroy = async ({subprocessStdout, subprocess, waitReadableDestroy}, error) => {
105119
if (!await waitForConcurrentStreams(waitReadableDestroy, subprocess)) {

lib/convert/writable.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Writable} from 'node:stream';
22
import {callbackify} from 'node:util';
33
import {getToStream} from '../arguments/fd-options.js';
4+
import {isStreamAbort} from '../resolve/wait-stream.js';
45
import {addConcurrentStream, waitForConcurrentStreams} from './concurrent.js';
56
import {
67
safeWaitForSubprocessStdout,
@@ -23,7 +24,7 @@ export const createWritable = ({subprocess, concurrentStreams}, {to} = {}) => {
2324
highWaterMark: subprocessStdin.writableHighWaterMark,
2425
objectMode: subprocessStdin.writableObjectMode,
2526
});
26-
onStdinFinished(subprocessStdin, writable);
27+
onStdinFinished(subprocessStdin, writable, undefined, subprocess);
2728
return writable;
2829
};
2930

@@ -66,18 +67,36 @@ const onWritableFinal = async (subprocessStdin, subprocess, waitWritableFinal) =
6667
};
6768

6869
// When `subprocess.stdin` ends/aborts/errors, do the same on `writable`.
69-
export const onStdinFinished = async (subprocessStdin, writable, subprocessStdout) => {
70+
export const onStdinFinished = async (subprocessStdin, writable, subprocessStdout, subprocess) => {
7071
try {
7172
await waitForSubprocessStdin(subprocessStdin);
73+
await subprocess;
7274
if (writable.writable) {
7375
writable.end();
7476
}
7577
} catch (error) {
7678
await safeWaitForSubprocessStdout(subprocessStdout);
77-
destroyOtherWritable(writable, error);
79+
destroyOtherWritable(writable, await getSubprocessError(subprocess, error));
7880
}
7981
};
8082

83+
const getSubprocessError = async (subprocess, error) => {
84+
if (!shouldUseSubprocessError(error)) {
85+
return error;
86+
}
87+
88+
try {
89+
await subprocess;
90+
} catch (subprocessError) {
91+
return subprocessError;
92+
}
93+
94+
return error;
95+
};
96+
97+
const shouldUseSubprocessError = error => error === undefined
98+
|| isStreamAbort(error);
99+
81100
// When `writable` aborts/errors, do the same on `subprocess.stdin`
82101
export const onWritableDestroy = async ({subprocessStdin, subprocess, waitWritableFinal, waitWritableDestroy}, error) => {
83102
await waitForConcurrentStreams(waitWritableFinal, subprocess);

lib/ipc/forward.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,6 @@ const forwardEvents = ({ipcEmitter, anyProcess, channel, isSubprocess}) => {
5151
export const isConnected = anyProcess => {
5252
const ipcEmitter = IPC_EMITTERS.get(anyProcess);
5353
return ipcEmitter === undefined
54-
? anyProcess.channel !== null
54+
? anyProcess.channel !== undefined && anyProcess.channel !== null
5555
: ipcEmitter.connected;
5656
};

lib/ipc/get-each.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const getEachMessage = (subprocessInfo, options = {}) => {
2020
};
2121

2222
// Same but used internally
23-
export const loopOnMessages = ({anyProcess, channel, isSubprocess, ipc, shouldAwait, reference, signal}) => {
23+
export const loopOnMessages = ({anyProcess, waitProcess = anyProcess, channel, isSubprocess, ipc, shouldAwait, reference, signal}) => {
2424
validateIpcMethod({
2525
methodName: 'getEachMessage',
2626
isSubprocess,
@@ -42,6 +42,7 @@ export const loopOnMessages = ({anyProcess, channel, isSubprocess, ipc, shouldAw
4242
});
4343
return iterateOnMessages({
4444
anyProcess,
45+
waitProcess,
4546
channel,
4647
ipcEmitter,
4748
isSubprocess,
@@ -83,7 +84,7 @@ const abortOnStrictError = async ({ipcEmitter, isSubprocess, controller, state})
8384
} catch {}
8485
};
8586

86-
const iterateOnMessages = async function * ({anyProcess, channel, ipcEmitter, isSubprocess, shouldAwait, controller, state, reference}) {
87+
const iterateOnMessages = async function * ({anyProcess, waitProcess, channel, ipcEmitter, isSubprocess, shouldAwait, controller, state, reference}) {
8788
try {
8889
for await (const [message] of on(ipcEmitter, 'message', {signal: controller.signal})) {
8990
throwIfStrictError(state);
@@ -100,7 +101,7 @@ const iterateOnMessages = async function * ({anyProcess, channel, ipcEmitter, is
100101
}
101102

102103
if (shouldAwait) {
103-
await anyProcess;
104+
await waitProcess;
104105
}
105106
}
106107
};

lib/ipc/ipc-input.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {serialize} from 'node:v8';
2+
import {sendMessage} from './send.js';
23

34
// Validate the `ipcInput` option
45
export const validateIpcInputOption = ({ipcInput, ipc, serialization}) => {
@@ -35,10 +36,15 @@ const validateIpcInput = {
3536
};
3637

3738
// When the `ipcInput` option is set, it is sent as an initial IPC message to the subprocess
38-
export const sendIpcInput = async (subprocess, ipcInput) => {
39+
export const sendIpcInput = async (subprocess, ipcInput, ipc) => {
3940
if (ipcInput === undefined) {
4041
return;
4142
}
4243

43-
await subprocess.sendMessage(ipcInput);
44+
await sendMessage({
45+
anyProcess: subprocess,
46+
channel: subprocess.channel,
47+
isSubprocess: false,
48+
ipc,
49+
}, ipcInput);
4450
};

lib/ipc/methods.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {getEachMessage} from './get-each.js';
55
import {getCancelSignal} from './graceful.js';
66

77
// Add promise-based IPC methods in current process
8-
export const addIpcMethods = (subprocess, {ipc}) => {
9-
Object.assign(subprocess, getIpcMethods(subprocess, false, ipc));
8+
export const addIpcMethods = (target, subprocess, {ipc}) => {
9+
Object.assign(target, getIpcMethods(subprocess, false, ipc, target));
1010
};
1111

1212
// Get promise-based IPC in the subprocess
@@ -27,7 +27,7 @@ export const getIpcExport = () => {
2727
};
2828

2929
// Retrieve the `ipc` shared by both the current process and the subprocess
30-
const getIpcMethods = (anyProcess, isSubprocess, ipc) => ({
30+
const getIpcMethods = (anyProcess, isSubprocess, ipc, waitProcess = anyProcess) => ({
3131
sendMessage: sendMessage.bind(undefined, {
3232
anyProcess,
3333
channel: anyProcess.channel,
@@ -45,5 +45,6 @@ const getIpcMethods = (anyProcess, isSubprocess, ipc) => ({
4545
channel: anyProcess.channel,
4646
isSubprocess,
4747
ipc,
48+
waitProcess,
4849
}),
4950
});

lib/ipc/outgoing.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {createDeferred} from '../utils/deferred.js';
22
import {getFdSpecificValue} from '../arguments/specific.js';
3-
import {SUBPROCESS_OPTIONS} from '../arguments/fd-options.js';
43
import {validateStrictDeadlock} from './strict.js';
54

65
// When `sendMessage()` is ongoing, any `message` being received waits before being emitted.
@@ -35,13 +34,20 @@ export const waitForOutgoingMessages = async (anyProcess, ipcEmitter, wrappedMes
3534
};
3635

3736
const OUTGOING_MESSAGES = new WeakMap();
37+
const IPC_SUBPROCESS_OPTIONS = new WeakMap();
38+
39+
export const setIpcSubprocessOptions = (subprocess, options) => {
40+
IPC_SUBPROCESS_OPTIONS.set(subprocess, options);
41+
};
3842

3943
// Whether any `message` listener is setup
4044
export const hasMessageListeners = (anyProcess, ipcEmitter) => ipcEmitter.listenerCount('message') > getMinListenerCount(anyProcess);
4145

4246
// When `buffer` is `false`, we set up a `message` listener that should be ignored.
4347
// That listener is only meant to intercept `strict` acknowledgement responses.
44-
const getMinListenerCount = anyProcess => SUBPROCESS_OPTIONS.has(anyProcess)
45-
&& !getFdSpecificValue(SUBPROCESS_OPTIONS.get(anyProcess).options.buffer, 'ipc')
48+
const getMinListenerCount = anyProcess => getOptions(anyProcess) !== undefined
49+
&& !getFdSpecificValue(getOptions(anyProcess).buffer, 'ipc')
4650
? 1
4751
: 0;
52+
53+
const getOptions = anyProcess => IPC_SUBPROCESS_OPTIONS.get(anyProcess);

0 commit comments

Comments
 (0)