UNPKG

@netlify/build

Version:
121 lines (120 loc) 5.53 kB
import process from 'process'; import { promisify } from 'util'; import { pEvent } from 'p-event'; import { v4 as uuidv4 } from 'uuid'; import { jsonToError, errorToJson } from '../error/build.js'; import { addErrorInfo } from '../error/info.js'; import { logSendingEventToChild, logSentEventToChild, logReceivedEventFromChild, logSendingEventToParent, } from '../log/messages/ipc.js'; // Send event from child to parent process then wait for response // We need to fire them in parallel because `process.send()` can be slow // to await, i.e. child might send response before parent start listening for it export const callChild = async function ({ childProcess, eventName, payload, logs, verbose }) { const callId = uuidv4(); const [response] = await Promise.all([ getEventFromChild(childProcess, callId), sendEventToChild({ childProcess, callId, eventName, payload, logs, verbose }), ]); logReceivedEventFromChild(logs, verbose); return response; }; // Receive event from child to parent process // Wait for either: // - `message` event with a specific `callId` // - `message` event with an `error` `callId` indicating an exception in the // child process // - child process `exit` // In the later two cases, we propagate the error. // We need to make `p-event` listeners are properly cleaned up too. export const getEventFromChild = async function (childProcess, callId) { if (childProcessHasExited(childProcess)) { throw getChildExitError('Could not receive event from child process because it already exited.'); } const messagePromise = pEvent(childProcess, 'message', { filter: (data) => data?.[0] === callId }); const errorPromise = pEvent(childProcess, 'message', { filter: (data) => data?.[0] === 'error' }); const exitPromise = pEvent(childProcess, 'exit', { multiArgs: true }); try { return await Promise.race([getMessage(messagePromise), getError(errorPromise), getExit(exitPromise)]); } finally { messagePromise.cancel(); errorPromise.cancel(); exitPromise.cancel(); } }; const childProcessHasExited = function (childProcess) { return !childProcess.connected || childProcess.signalCode !== null || childProcess.exitCode !== null; }; const getMessage = async function (messagePromise) { const [, response] = await messagePromise; return response; }; const getError = async function (errorPromise) { const [, error] = await errorPromise; throw jsonToError(error); }; const getExit = async function (exitPromise) { const [exitCode, signal] = await exitPromise; throw getChildExitError(`Plugin exited with exit code ${exitCode} and signal ${signal}.`); }; // Plugins should not terminate processes explicitly: // - It prevents specifying error messages to the end users // - It makes it impossible to distinguish between bugs (such as infinite loops) and user errors // - It complicates child process orchestration. For example if an async operation // of a previous event handler is still running, it would be aborted if another // is terminating the process. const getChildExitError = function (message) { const error = new Error(`${message}\n${EXIT_WARNING}`); addErrorInfo(error, { type: 'ipc' }); return error; }; const EXIT_WARNING = `The plugin might have exited due to a bug terminating the process, such as an infinite loop. The plugin might also have explicitly terminated the process, for example with process.exit(). Plugin methods should instead: - on success: return - on failure: call utils.build.failPlugin() or utils.build.failBuild()`; // Send event from parent to child process const sendEventToChild = async function ({ childProcess, callId, eventName, payload, logs, verbose }) { logSendingEventToChild(logs, verbose); const payloadA = serializePayload(payload); await promisify(childProcess.send.bind(childProcess))([callId, eventName, payloadA]); logSentEventToChild(logs, verbose); }; // Respond to events from parent to child process. // This runs forever until `childProcess.kill()` is called. // We need to use `new Promise()` and callbacks because this runs forever. export const getEventsFromParent = function (callback) { return new Promise((resolve, reject) => { process.on('message', async (message) => { try { const [callId, eventName, payload] = message; const payloadA = parsePayload(payload); return await callback(callId, eventName, payloadA); } catch (error) { reject(error); } }); }); }; // Send event from child to parent process export const sendEventToParent = async function (callId, payload, verbose, error) { logSendingEventToParent(verbose, error); await promisify(process.send.bind(process))([callId, payload]); }; // Error static properties are not serializable through `child_process` // (which uses `v8.serialize()` under the hood) so we need to convert from/to // plain objects. const serializePayload = function ({ error = {}, error: { name } = {}, ...payload }) { if (name === undefined) { return payload; } const errorA = errorToJson(error); return { ...payload, error: errorA }; }; const parsePayload = function ({ error = {}, error: { name } = {}, ...payload }) { if (name === undefined) { return payload; } const errorA = jsonToError(error); return { ...payload, error: errorA }; };