UNPKG

execa

Version:
130 lines (108 loc) • 4.38 kB
import {once} from 'node:events'; import {createDeferred} from '../utils/deferred.js'; import {incrementMaxListeners} from '../utils/max-listeners.js'; import {sendMessage} from './send.js'; import {throwOnMissingStrict, throwOnStrictDisconnect, throwOnStrictDeadlockError} from './validation.js'; import {getIpcEmitter} from './forward.js'; import {hasMessageListeners} from './outgoing.js'; // When using the `strict` option, wrap the message with metadata during `sendMessage()` export const handleSendStrict = ({anyProcess, channel, isSubprocess, message, strict}) => { if (!strict) { return message; } const ipcEmitter = getIpcEmitter(anyProcess, channel, isSubprocess); const hasListeners = hasMessageListeners(anyProcess, ipcEmitter); return { id: count++, type: REQUEST_TYPE, message, hasListeners, }; }; // This is a plain number, not a `BigInt`, since it must be serializable with `serialization: 'json'` let count = 0; // Handles when both processes are calling `sendMessage()` with `strict` at the same time. // If neither process is listening, this would create a deadlock. We detect it and throw. export const validateStrictDeadlock = (outgoingMessages, wrappedMessage) => { if (!isStrictRequest(wrappedMessage) || wrappedMessage.hasListeners) { return; } for (const {id} of outgoingMessages) { if (id !== undefined) { STRICT_RESPONSES[id].deferred.resolve({isDeadlock: true, hasListeners: false}); } } }; // The other process then sends the acknowledgment back as a response export const handleStrictRequest = async ({wrappedMessage, anyProcess, channel, isSubprocess, ipcEmitter}) => { if (!isStrictRequest(wrappedMessage)) { return wrappedMessage; } const {id, message} = wrappedMessage; // The acknowledgment cannot be sent anymore, but the message is still delivered if (!anyProcess.connected) { return message; } const response = {id, type: RESPONSE_TYPE, message: hasMessageListeners(anyProcess, ipcEmitter)}; try { await sendMessage({ anyProcess, channel, isSubprocess, ipc: true, }, response); } catch (error) { // When the other process disconnects right after sending the message, it cannot receive the acknowledgment anymore. Like above, the message is still delivered. if (error.cause?.code !== 'EPIPE') { ipcEmitter.emit('strict:error', error); } } return message; }; // Reception of the acknowledgment response. // Ids are shared by all processes, so the response must come from the process the message was sent to. Otherwise, a subprocess could acknowledge a message sent to another one. export const handleStrictResponse = (wrappedMessage, anyProcess) => { if (wrappedMessage?.type !== RESPONSE_TYPE) { return false; } const {id, message: hasListeners} = wrappedMessage; const strictResponse = STRICT_RESPONSES[id]; if (strictResponse?.anyProcess === anyProcess) { strictResponse.deferred.resolve({isDeadlock: false, hasListeners}); } return true; }; // Wait for the other process to receive the message from `sendMessage()` export const waitForStrictResponse = async (wrappedMessage, anyProcess, isSubprocess) => { const deferred = createDeferred(); STRICT_RESPONSES[wrappedMessage.id] = {deferred, anyProcess}; const controller = new AbortController(); try { const {isDeadlock, hasListeners} = await Promise.race([ deferred, throwOnDisconnect(anyProcess, isSubprocess, controller), ]); if (isDeadlock) { throwOnStrictDeadlockError(isSubprocess); } if (!hasListeners) { throwOnMissingStrict(isSubprocess); } } finally { controller.abort(); delete STRICT_RESPONSES[wrappedMessage.id]; } }; const STRICT_RESPONSES = {__proto__: null}; const throwOnDisconnect = async (anyProcess, isSubprocess, {signal}) => { incrementMaxListeners(anyProcess, 1, signal); await once(anyProcess, 'disconnect', {signal}); throwOnStrictDisconnect(isSubprocess); }; const REQUEST_TYPE = 'execa:ipc:request'; const RESPONSE_TYPE = 'execa:ipc:response'; // `id` and `hasListeners` are always defined in messages sent by `sendMessage()` with the `strict` option. // Messages with the same `type` but without them are user messages, which must be delivered as is. const isStrictRequest = wrappedMessage => wrappedMessage?.type === REQUEST_TYPE && Number.isInteger(wrappedMessage.id) && typeof wrappedMessage.hasListeners === 'boolean';