UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

222 lines (221 loc) 11.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); // Must be the first import — see enable-compile-cache.ts. require("../../../utils/enable-compile-cache"); const node_perf_hooks_1 = require("node:perf_hooks"); node_perf_hooks_1.performance.mark(`plugin worker ${process.pid} code loading -- start`); const consume_messages_from_socket_1 = require("../../../utils/consume-messages-from-socket"); const logger_1 = require("../../../utils/logger"); const serializable_error_1 = require("../../../utils/serializable-error"); const daemon_message_1 = require("../../../daemon/message-types/daemon-message"); const messaging_1 = require("./messaging"); const worker_streaming_1 = require("./worker-streaming"); const fs_1 = require("fs"); const net_1 = require("net"); const analytics_1 = require("../../../analytics"); const daemon_environment_1 = require("../../../daemon/client/daemon-environment"); const sandbox_socket_hint_1 = require("../../../daemon/sandbox-socket-hint"); const permission_errors_1 = require("../../../utils/permission-errors"); const socket_refused_exit_code_1 = require("../../../utils/socket-refused-exit-code"); const is_sandbox_1 = require("../../../utils/is-sandbox"); require("../../../utils/perf-logging"); const environment = process.env; (0, analytics_1.startAnalytics)(); node_perf_hooks_1.performance.mark(`plugin worker ${process.pid} code loading -- end`); node_perf_hooks_1.performance.measure(`plugin worker ${process.pid} code loading`, `plugin worker ${process.pid} code loading -- start`, `plugin worker ${process.pid} code loading -- end`); global.NX_GRAPH_CREATION = true; global.NX_PLUGIN_WORKER = true; let plugin; const socketPath = process.argv[2]; const expectedPluginName = process.argv[3]; // The host's root, passed explicitly rather than re-resolved: a host that set // its root at runtime would resolve a different one here and drop every // legitimate message as foreign. const hostWorkspaceRoot = process.argv[4]; // Positional, so inserting an argument host-side shifts all of them — and the // symptom is silent and total: an undefined hostWorkspaceRoot makes every // message look foreign while the host waits out its plugin timeout. if (!socketPath || !expectedPluginName || !hostWorkspaceRoot) { console.error(`[plugin-worker] started with an incomplete argument list ` + `(socketPath=${socketPath}, pluginName=${expectedPluginName}, hostWorkspaceRoot=${hostWorkspaceRoot}). ` + `This is an Nx bug — please report it at https://github.com/nrwl/nx/issues.`); process.exit(1); } const CONNECT_TIMEOUT_MS = 30_000; let connectErrorTimeout = setErrorTimeout(CONNECT_TIMEOUT_MS, `The plugin worker for ${expectedPluginName} is exiting as it was not connected to within ${CONNECT_TIMEOUT_MS / 1000} seconds. ` + 'Plugin workers expect to receive a socket connection from the parent process shortly after being started. ' + 'If you are seeing this issue, please report it to the Nx team at https://github.com/nrwl/nx/issues.'); const server = (0, net_1.createServer)((socket) => { connectErrorTimeout?.clear(); // Make the host-facing socket available to plugin code running in this // worker so it can emit log / progress notifications without having // the socket threaded through every call site. (0, worker_streaming_1.setPluginWorkerHostSocket)(socket); logger_1.logger.verbose(`[plugin-worker] "${expectedPluginName}" (pid: ${process.pid}) connected`); // This handles cases where the host process was killed // after the worker connected but before the worker was // instructed to load the plugin. let loadErrorTimeout = setErrorTimeout(10_000, `Plugin Worker for ${expectedPluginName} is exiting as it did not receive a load message within 10 seconds of connecting. ` + 'This likely indicates that the host process was terminated before the worker could be instructed to load the plugin. ' + 'If you are seeing this issue, please report it to the Nx team at https://github.com/nrwl/nx/issues.'); socket.on('data', (0, consume_messages_from_socket_1.consumeMessagesFromSocket)((raw) => { const message = (0, consume_messages_from_socket_1.parseMessage)(raw); if (!(0, messaging_1.isPluginWorkerMessage)(message)) { return; } // Same check the daemon applies to its own socket. Dropped rather than thrown: // a stray foreign message must not kill a worker serving its host. The daemon // has a response channel and surfaces it to the client instead. try { (0, daemon_message_1.assertNotForeignWorkspaceMessage)(message, hostWorkspaceRoot, `The Nx plugin worker "${expectedPluginName}" (pid: ${process.pid})`); } catch (e) { logger_1.logger.verbose(`[plugin-worker] ignored a "${message.type}" message: ${e instanceof Error ? e.message : e}`); return; } return (0, messaging_1.consumeMessage)(socket, message, { load: async ({ plugin: pluginConfiguration, root, name, pluginPath, shouldRegisterTSTranspiler, }) => { loadErrorTimeout?.clear(); process.chdir(root); return withErrorHandling(async () => { const { loadResolvedNxPluginAsync } = await Promise.resolve(require(require.resolve('../load-resolved-plugin'))); // Register the ts-transpiler if we are pointing to a // plain ts file that's not part of a plugin project if (shouldRegisterTSTranspiler) { require('../transpiler').registerPluginTSTranspiler(); } plugin = await loadResolvedNxPluginAsync(pluginConfiguration, pluginPath, name); logger_1.logger.verbose(`[plugin-worker] "${name}" (pid: ${process.pid}) loaded successfully`); return { name: plugin.name, include: plugin.include, exclude: plugin.exclude, createNodesPattern: plugin.createNodes?.[0], hasCreateDependencies: 'createDependencies' in plugin && !!plugin.createDependencies, hasProcessProjectGraph: 'processProjectGraph' in plugin && !!plugin.processProjectGraph, hasCreateMetadata: 'createMetadata' in plugin && !!plugin.createMetadata, hasPreTasksExecution: 'preTasksExecution' in plugin && !!plugin.preTasksExecution, hasPostTasksExecution: 'postTasksExecution' in plugin && !!plugin.postTasksExecution, success: true, }; }); }, createNodes: async ({ configFiles, context }) => withErrorHandling(async () => { const result = await plugin.createNodes[1](configFiles, context); return { result, success: true }; }), createDependencies: async ({ context }) => withErrorHandling(async () => { const result = await plugin.createDependencies(context); return { dependencies: result, success: true }; }), createMetadata: async ({ graph, context }) => withErrorHandling(async () => { const result = await plugin.createMetadata(graph, context); return { metadata: result, success: true }; }), preTasksExecution: async ({ context }) => withErrorHandling(async () => { const mutations = await plugin.preTasksExecution?.(context); return { success: true, mutations }; }), postTasksExecution: async ({ context }) => withErrorHandling(() => plugin.postTasksExecution?.(context)), setWorkerEnv: (env) => withErrorHandling(() => { (0, daemon_environment_1.applyDaemonEnvFromClient)(env); }), }); }, (err) => { // The host link cannot resynchronize. Exit so the host's 'exit' handler // rejects the pending hooks instead of waiting on a reply that the // framing failure guarantees will never arrive. console.error(err.message); socket.destroy(); process.exit(1); })); // When the host disconnects, clean up and exit. socket.on('end', () => { socket.destroySoon(); try { (0, fs_1.unlinkSync)(socketPath); } catch (e) { } process.exit(0); }); }); server.on('error', (err) => { // Without this the host only sees "exited before the connection was // established"; the errno distinguishes a denied bind from EADDRINUSE. console.error(`[plugin-worker] "${expectedPluginName}" (pid: ${process.pid}) failed to listen on ${socketPath}: ${err.message}`); // A bind can fail for reasons permission does not explain (EADDRINUSE from a // leftover socket, ENOENT from a reaped socket dir), so print the hint only // when the errno proves a refusal or the environment says a sandbox is here. const refusedByOs = (0, permission_errors_1.isPermissionDenied)(err); if (refusedByOs || (0, is_sandbox_1.isSandbox)()) { console.error((0, sandbox_socket_hint_1.sandboxSocketHint)({ certain: refusedByOs }).join('\n')); } // The host cannot see this errno, and stderr may be lost with the process, so // the exit code is what tells it a refusal happened rather than a crash. process.exit(refusedByOs ? socket_refused_exit_code_1.SOCKET_REFUSED_EXIT_CODE : 1); }); // A worker killed without its 'end' handler leaves the socket behind. Colliding // takes a recycled host pid landing on the same counter *and* the same 4 random // bytes; the random component is what this unlink rests on, and on Windows it is // the only barrier, since the endpoint is a namespace object and the mode never // applies. A failed bind surfaces through the error handler above. try { (0, fs_1.unlinkSync)(socketPath); } catch { } server.listen(socketPath, () => { logger_1.logger.verbose(`[plugin-worker] "${expectedPluginName}" (pid: ${process.pid}) listening on ${socketPath}`); }); async function withErrorHandling(cb) { try { const result = await cb(); return result ?? { success: true }; } catch (e) { return { success: false, error: (0, serializable_error_1.createSerializableError)(e), }; } } function setErrorTimeout(timeoutMs, errorMessage) { if (environment.NX_PLUGIN_NO_TIMEOUTS === 'true') { return; } let cleared = false; const timeout = setTimeout(() => { if (!cleared) { console.error(errorMessage); process.exit(1); } }, timeoutMs).unref(); return { clear: () => { cleared = true; clearTimeout(timeout); }, }; } const cleanup = () => { server.close(); try { (0, fs_1.unlinkSync)(socketPath); } catch (e) { } }; const events = ['SIGINT', 'SIGTERM', 'SIGQUIT']; events.forEach((event) => process.once(event, () => { cleanup(); process.exit(0); })); // Cleanup only: process.exit() here would override the real exit code. process.once('exit', cleanup); const fatalHandler = (error) => { // Registering this handler suppresses Node's default reporting. console.error(error); cleanup(); process.exit(1); }; process.once('uncaughtException', fatalHandler); process.once('unhandledRejection', fatalHandler);