nx
Version:
311 lines (310 loc) • 13.8 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadRemoteNxPlugin = loadRemoteNxPlugin;
const child_process_1 = require("child_process");
const path = require("path");
const net_1 = require("net");
const socket_utils_1 = require("../../../daemon/socket-utils");
const consume_messages_from_socket_1 = require("../../../utils/consume-messages-from-socket");
const messaging_1 = require("./messaging");
const installation_directory_1 = require("../../../utils/installation-directory");
const resolve_plugin_1 = require("../resolve-plugin");
const cleanupFunctions = new Set();
const pluginNames = new Map();
const PLUGIN_TIMEOUT_HINT_TEXT = 'As a last resort, you can set NX_PLUGIN_NO_TIMEOUTS=true to bypass this timeout.';
const MINUTES = 10;
const MAX_MESSAGE_WAIT = process.env.NX_PLUGIN_NO_TIMEOUTS === 'true'
? // Registering a timeout prevents the process from exiting
// if the call to a plugin happens to be the only thing
// keeping the process alive. As such, even if timeouts are disabled
// we need to register one. 2147483647 is the max timeout
// that Node.js allows, and is equivalent to 24.8 days....
// This does mean that the NX_PLUGIN_NO_TIMEOUTS env var
// would still timeout after 24.8 days, but that seems
// like a reasonable compromise.
2147483647
: 1000 * 60 * MINUTES; // 10 minutes
const nxPluginWorkerCache = (global['nxPluginWorkerCache'] ??= new Map());
async function loadRemoteNxPlugin(plugin, root) {
const cacheKey = JSON.stringify({ plugin, root });
if (nxPluginWorkerCache.has(cacheKey)) {
return [nxPluginWorkerCache.get(cacheKey), () => { }];
}
const moduleName = typeof plugin === 'string' ? plugin : plugin.plugin;
const { name, pluginPath, shouldRegisterTSTranspiler } = await (0, resolve_plugin_1.resolveNxPlugin)(moduleName, root, (0, installation_directory_1.getNxRequirePaths)(root));
const { worker, socket } = await startPluginWorker();
const pendingPromises = new Map();
const exitHandler = createWorkerExitHandler(worker, pendingPromises);
const cleanupFunction = () => {
worker.off('exit', exitHandler);
socket.destroy();
nxPluginWorkerCache.delete(cacheKey);
};
cleanupFunctions.add(cleanupFunction);
const pluginPromise = new Promise((res, rej) => {
(0, messaging_1.sendMessageOverSocket)(socket, {
type: 'load',
payload: { plugin, root, name, pluginPath, shouldRegisterTSTranspiler },
});
// logger.verbose(`[plugin-worker] started worker: ${worker.pid}`);
const loadTimeout = setTimeout(() => {
rej(new Error(`Loading "${typeof plugin === 'string' ? plugin : plugin.plugin}" timed out after ${MINUTES} minutes. ${PLUGIN_TIMEOUT_HINT_TEXT}`));
}, MAX_MESSAGE_WAIT);
socket.on('data', (0, consume_messages_from_socket_1.consumeMessagesFromSocket)(createWorkerHandler(worker, pendingPromises, (val) => {
if (loadTimeout)
clearTimeout(loadTimeout);
res(val);
}, rej, socket)));
worker.on('exit', exitHandler);
});
nxPluginWorkerCache.set(cacheKey, pluginPromise);
return [pluginPromise, cleanupFunction];
}
/**
* Creates a message handler for the given worker.
* @param worker Instance of plugin-worker
* @param pending Set of pending promises
* @param onload Resolver for RemotePlugin promise
* @param onloadError Rejecter for RemotePlugin promise
* @returns Function to handle messages from the worker
*/
function createWorkerHandler(worker, pending, onload, onloadError, socket) {
let pluginName;
let txId = 0;
return function (raw) {
const message = JSON.parse(raw);
if (!(0, messaging_1.isPluginWorkerResult)(message)) {
return;
}
return (0, messaging_1.consumeMessage)(socket, message, {
'load-result': (result) => {
if (result.success) {
const { name, createNodesPattern, include, exclude } = result;
pluginName = name;
pluginNames.set(worker, pluginName);
onload({
name,
include,
exclude,
createNodes: createNodesPattern
? [
createNodesPattern,
(configFiles, ctx) => {
const tx = pluginName + worker.pid + ':createNodes:' + txId++;
return registerPendingPromise(tx, pending, () => {
(0, messaging_1.sendMessageOverSocket)(socket, {
type: 'createNodes',
payload: { configFiles, context: ctx, tx },
});
}, {
plugin: pluginName,
operation: 'createNodes',
});
},
]
: undefined,
createDependencies: result.hasCreateDependencies
? (ctx) => {
const tx = pluginName + worker.pid + ':createDependencies:' + txId++;
return registerPendingPromise(tx, pending, () => {
(0, messaging_1.sendMessageOverSocket)(socket, {
type: 'createDependencies',
payload: { context: ctx, tx },
});
}, {
plugin: pluginName,
operation: 'createDependencies',
});
}
: undefined,
createMetadata: result.hasCreateMetadata
? (graph, ctx) => {
const tx = pluginName + worker.pid + ':createMetadata:' + txId++;
return registerPendingPromise(tx, pending, () => {
(0, messaging_1.sendMessageOverSocket)(socket, {
type: 'createMetadata',
payload: { graph, context: ctx, tx },
});
}, {
plugin: pluginName,
operation: 'createMetadata',
});
}
: undefined,
preTasksExecution: result.hasPreTasksExecution
? (context) => {
const tx = pluginName + worker.pid + ':preTasksExecution:' + txId++;
return registerPendingPromise(tx, pending, () => {
(0, messaging_1.sendMessageOverSocket)(socket, {
type: 'preTasksExecution',
payload: { tx, context },
});
}, {
plugin: pluginName,
operation: 'preTasksExecution',
});
}
: undefined,
postTasksExecution: result.hasPostTasksExecution
? (context) => {
const tx = pluginName + worker.pid + ':postTasksExecution:' + txId++;
return registerPendingPromise(tx, pending, () => {
(0, messaging_1.sendMessageOverSocket)(socket, {
type: 'postTasksExecution',
payload: { tx, context },
});
}, {
plugin: pluginName,
operation: 'postTasksExecution',
});
}
: undefined,
});
}
else if (result.success === false) {
onloadError(result.error);
}
},
createDependenciesResult: ({ tx, ...result }) => {
const { resolver, rejector } = pending.get(tx);
if (result.success) {
resolver(result.dependencies);
}
else if (result.success === false) {
rejector(result.error);
}
},
createNodesResult: ({ tx, ...result }) => {
const { resolver, rejector } = pending.get(tx);
if (result.success) {
resolver(result.result);
}
else if (result.success === false) {
rejector(result.error);
}
},
createMetadataResult: ({ tx, ...result }) => {
const { resolver, rejector } = pending.get(tx);
if (result.success) {
resolver(result.metadata);
}
else if (result.success === false) {
rejector(result.error);
}
},
preTasksExecutionResult: ({ tx, ...result }) => {
const { resolver, rejector } = pending.get(tx);
if (result.success) {
resolver(result.mutations);
}
else if (result.success === false) {
rejector(result.error);
}
},
postTasksExecutionResult: ({ tx, ...result }) => {
const { resolver, rejector } = pending.get(tx);
if (result.success) {
resolver();
}
else if (result.success === false) {
rejector(result.error);
}
},
});
};
}
function createWorkerExitHandler(worker, pendingPromises) {
return () => {
for (const [_, pendingPromise] of pendingPromises) {
pendingPromise.rejector(new Error(`Plugin worker ${pluginNames.get(worker) ?? worker.pid} exited unexpectedly with code ${worker.exitCode}`));
}
};
}
function registerPendingPromise(tx, pending, callback, context) {
let resolver, rejector, timeout;
const promise = new Promise((res, rej) => {
rejector = rej;
resolver = res;
timeout = setTimeout(() => {
rej(new Error(`${context.plugin} timed out after ${MINUTES} minutes during ${context.operation}. ${PLUGIN_TIMEOUT_HINT_TEXT}`));
}, MAX_MESSAGE_WAIT);
callback();
}).finally(() => {
pending.delete(tx);
if (timeout)
clearTimeout(timeout);
});
pending.set(tx, {
promise,
resolver,
rejector,
});
return promise;
}
global.nxPluginWorkerCount ??= 0;
async function startPluginWorker() {
// this should only really be true when running unit tests within
// the Nx repo. We still need to start the worker in this case,
// but its typescript.
const isWorkerTypescript = path.extname(__filename) === '.ts';
const workerPath = path.join(__dirname, 'plugin-worker');
const env = {
...process.env,
...(isWorkerTypescript
? {
// Ensures that the worker uses the same tsconfig as the main process
TS_NODE_PROJECT: path.join(__dirname, '../../../../tsconfig.lib.json'),
}
: {}),
};
const ipcPath = (0, socket_utils_1.getPluginOsSocketPath)([process.pid, global.nxPluginWorkerCount++].join('-'));
const worker = (0, child_process_1.spawn)(process.execPath, [
...(isWorkerTypescript ? ['--require', 'ts-node/register'] : []),
workerPath,
ipcPath,
], {
stdio: 'inherit',
env,
detached: true,
shell: false,
windowsHide: true,
});
worker.unref();
let attempts = 0;
return new Promise((resolve, reject) => {
const id = setInterval(async () => {
const socket = await isServerAvailable(ipcPath);
if (socket) {
socket.unref();
clearInterval(id);
resolve({
worker,
socket,
});
}
else if (attempts > 10000) {
// daemon fails to start, the process probably exited
// we print the logs and exit the client
reject('Failed to start plugin worker.');
}
else {
attempts++;
}
}, 10);
});
}
function isServerAvailable(ipcPath) {
return new Promise((resolve) => {
try {
const socket = (0, net_1.connect)(ipcPath, () => {
resolve(socket);
});
socket.once('error', () => {
resolve(false);
});
}
catch (err) {
resolve(false);
}
});
}
;