UNPKG

nx

Version:

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

669 lines (668 loc) • 26.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.daemonClient = exports.DaemonClient = void 0; exports.isDaemonEnabled = isDaemonEnabled; const workspace_root_1 = require("../../utils/workspace-root"); const child_process_1 = require("child_process"); const node_fs_1 = require("node:fs"); const promises_1 = require("fs/promises"); const net_1 = require("net"); const path_1 = require("path"); const perf_hooks_1 = require("perf_hooks"); const output_1 = require("../../utils/output"); const tmp_dir_1 = require("../tmp-dir"); const is_ci_1 = require("../../utils/is-ci"); const nx_json_1 = require("../../config/nx-json"); const configuration_1 = require("../../config/configuration"); const promised_based_queue_1 = require("../../utils/promised-based-queue"); const daemon_socket_messenger_1 = require("./daemon-socket-messenger"); const cache_1 = require("../cache"); const is_nx_version_mismatch_1 = require("../is-nx-version-mismatch"); const error_types_1 = require("../../project-graph/error-types"); const native_1 = require("../../native"); const get_nx_workspace_files_1 = require("../message-types/get-nx-workspace-files"); const get_context_file_data_1 = require("../message-types/get-context-file-data"); const get_files_in_directory_1 = require("../message-types/get-files-in-directory"); const hash_glob_1 = require("../message-types/hash-glob"); const task_history_1 = require("../message-types/task-history"); const get_sync_generator_changes_1 = require("../message-types/get-sync-generator-changes"); const get_registered_sync_generators_1 = require("../message-types/get-registered-sync-generators"); const update_workspace_context_1 = require("../message-types/update-workspace-context"); const flush_sync_generator_changes_to_disk_1 = require("../message-types/flush-sync-generator-changes-to-disk"); const delayed_spinner_1 = require("../../utils/delayed-spinner"); const run_tasks_execution_hooks_1 = require("../message-types/run-tasks-execution-hooks"); const register_project_graph_listener_1 = require("../message-types/register-project-graph-listener"); const nx_console_1 = require("../message-types/nx-console"); const node_v8_1 = require("node:v8"); const consume_messages_from_socket_1 = require("../../utils/consume-messages-from-socket"); const project_graph_1 = require("../../project-graph/project-graph"); const DAEMON_ENV_SETTINGS = { NX_PROJECT_GLOB_CACHE: 'false', NX_CACHE_PROJECTS_CONFIG: 'false', NX_VERBOSE_LOGGING: 'true', NX_PERF_LOGGING: 'true', NX_NATIVE_LOGGING: 'nx=debug', }; var DaemonStatus; (function (DaemonStatus) { DaemonStatus[DaemonStatus["CONNECTING"] = 0] = "CONNECTING"; DaemonStatus[DaemonStatus["DISCONNECTED"] = 1] = "DISCONNECTED"; DaemonStatus[DaemonStatus["CONNECTED"] = 2] = "CONNECTED"; })(DaemonStatus || (DaemonStatus = {})); class DaemonClient { constructor() { this._daemonStatus = DaemonStatus.DISCONNECTED; this._waitForDaemonReady = null; this._daemonReady = null; this._out = null; this._err = null; try { this.nxJson = (0, configuration_1.readNxJson)(); } catch (e) { this.nxJson = null; } this.reset(); } enabled() { if (this._enabled === undefined) { const useDaemonProcessOption = this.nxJson?.useDaemonProcess; const env = process.env.NX_DAEMON; // env takes precedence // option=true,env=false => no daemon // option=false,env=undefined => no daemon // option=false,env=false => no daemon // option=undefined,env=undefined => daemon // option=true,env=true => daemon // option=false,env=true => daemon // CI=true,env=undefined => no daemon // CI=true,env=false => no daemon // CI=true,env=true => daemon // docker=true,env=undefined => no daemon // docker=true,env=false => no daemon // docker=true,env=true => daemon // WASM => no daemon because file watching does not work // version mismatch => no daemon because the installed nx version differs from the running one if ((0, is_nx_version_mismatch_1.isNxVersionMismatch)() || (((0, is_ci_1.isCI)() || isDocker()) && env !== 'true') || (0, tmp_dir_1.isDaemonDisabled)() || nxJsonIsNotPresent() || (useDaemonProcessOption === undefined && env === 'false') || (useDaemonProcessOption === true && env === 'false') || (useDaemonProcessOption === false && env === undefined) || (useDaemonProcessOption === false && env === 'false')) { this._enabled = false; } else if (native_1.IS_WASM) { output_1.output.warn({ title: 'The Nx Daemon is unsupported in WebAssembly environments. Some things may be slower than or not function as expected.', }); this._enabled = false; } else { this._enabled = true; } } return this._enabled; } reset() { this.socketMessenger?.close(); this.socketMessenger = null; this.queue = new promised_based_queue_1.PromisedBasedQueue(); this.currentMessage = null; this.currentResolve = null; this.currentReject = null; this._enabled = undefined; this._out?.close(); this._err?.close(); this._out = null; this._err = null; this._daemonStatus = DaemonStatus.DISCONNECTED; this._waitForDaemonReady = new Promise((resolve) => (this._daemonReady = resolve)); } getSocketPath() { const daemonProcessJson = (0, cache_1.readDaemonProcessJsonCache)(); if (daemonProcessJson?.socketPath) { return daemonProcessJson.socketPath; } else { throw daemonProcessException('Unable to connect to daemon: no socket path available'); } } async requestShutdown() { return this.sendToDaemonViaQueue({ type: 'REQUEST_SHUTDOWN' }); } async getProjectGraphAndSourceMaps() { (0, project_graph_1.preventRecursionInGraphConstruction)(); let spinner; // If the graph takes a while to load, we want to show a spinner. spinner = new delayed_spinner_1.DelayedSpinner('Calculating the project graph on the Nx Daemon').scheduleMessageUpdate('Calculating the project graph on the Nx Daemon is taking longer than expected. Re-run with NX_DAEMON=false to see more details.', { ciDelay: 60_000, delay: 30_000 }); try { const response = await this.sendToDaemonViaQueue({ type: 'REQUEST_PROJECT_GRAPH', }); return { projectGraph: response.projectGraph, sourceMaps: response.sourceMaps, }; } catch (e) { if (e.name === error_types_1.DaemonProjectGraphError.name) { throw error_types_1.ProjectGraphError.fromDaemonProjectGraphError(e); } else { throw e; } } finally { spinner?.cleanup(); } } async getAllFileData() { return await this.sendToDaemonViaQueue({ type: 'REQUEST_FILE_DATA' }); } hashTasks(runnerOptions, tasks, taskGraph, env) { return this.sendToDaemonViaQueue({ type: 'HASH_TASKS', runnerOptions, env: process.env.NX_USE_V8_SERIALIZER !== 'false' ? structuredClone(process.env) : env, tasks, taskGraph, }); } async registerFileWatcher(config, callback) { try { await this.getProjectGraphAndSourceMaps(); } catch (e) { if (config.allowPartialGraph && e instanceof error_types_1.ProjectGraphError) { // we are fine with partial graph } else { throw e; } } let messenger; await this.queue.sendToQueue(async () => { await this.startDaemonIfNecessary(); const socketPath = this.getSocketPath(); messenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)(socketPath)).listen((message) => { try { const parsedMessage = (0, consume_messages_from_socket_1.isJsonMessage)(message) ? JSON.parse(message) : (0, node_v8_1.deserialize)(Buffer.from(message, 'binary')); callback(null, parsedMessage); } catch (e) { callback(e, null); } }, () => { callback('closed', null); }, (err) => callback(err, null)); return messenger.sendMessage({ type: 'REGISTER_FILE_WATCHER', config }); }); return () => { messenger?.close(); }; } async registerProjectGraphRecomputationListener(callback) { let messenger; await this.queue.sendToQueue(async () => { await this.startDaemonIfNecessary(); const socketPath = this.getSocketPath(); messenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)(socketPath)).listen((message) => { try { const parsedMessage = (0, consume_messages_from_socket_1.isJsonMessage)(message) ? JSON.parse(message) : (0, node_v8_1.deserialize)(Buffer.from(message, 'binary')); callback(null, parsedMessage); } catch (e) { callback(e, null); } }, () => { callback('closed', null); }, (err) => callback(err, null)); return messenger.sendMessage({ type: register_project_graph_listener_1.REGISTER_PROJECT_GRAPH_LISTENER, }); }); return () => { messenger?.close(); }; } processInBackground(requirePath, data) { return this.sendToDaemonViaQueue({ type: 'PROCESS_IN_BACKGROUND', requirePath, data, // This method is sometimes passed data that cannot be serialized with v8 // so we force JSON serialization here }, 'json'); } recordOutputsHash(outputs, hash) { return this.sendToDaemonViaQueue({ type: 'RECORD_OUTPUTS_HASH', data: { outputs, hash, }, }); } outputsHashesMatch(outputs, hash) { return this.sendToDaemonViaQueue({ type: 'OUTPUTS_HASHES_MATCH', data: { outputs, hash, }, }); } glob(globs, exclude) { const message = { type: 'GLOB', globs, exclude, }; return this.sendToDaemonViaQueue(message); } multiGlob(globs, exclude) { const message = { type: 'MULTI_GLOB', globs, exclude, }; return this.sendToDaemonViaQueue(message); } getWorkspaceContextFileData() { const message = { type: get_context_file_data_1.GET_CONTEXT_FILE_DATA, }; return this.sendToDaemonViaQueue(message); } getWorkspaceFiles(projectRootMap) { const message = { type: get_nx_workspace_files_1.GET_NX_WORKSPACE_FILES, projectRootMap, }; return this.sendToDaemonViaQueue(message); } getFilesInDirectory(dir) { const message = { type: get_files_in_directory_1.GET_FILES_IN_DIRECTORY, dir, }; return this.sendToDaemonViaQueue(message); } hashGlob(globs, exclude) { const message = { type: hash_glob_1.HASH_GLOB, globs, exclude, }; return this.sendToDaemonViaQueue(message); } hashMultiGlob(globGroups) { const message = { type: hash_glob_1.HASH_MULTI_GLOB, globGroups: globGroups, }; return this.sendToDaemonViaQueue(message); } getFlakyTasks(hashes) { const message = { type: task_history_1.GET_FLAKY_TASKS, hashes, }; return this.sendToDaemonViaQueue(message); } async getEstimatedTaskTimings(targets) { const message = { type: task_history_1.GET_ESTIMATED_TASK_TIMINGS, targets, }; return this.sendToDaemonViaQueue(message); } recordTaskRuns(taskRuns) { const message = { type: task_history_1.RECORD_TASK_RUNS, taskRuns, }; return this.sendToDaemonViaQueue(message); } getSyncGeneratorChanges(generators) { const message = { type: get_sync_generator_changes_1.GET_SYNC_GENERATOR_CHANGES, generators, }; return this.sendToDaemonViaQueue(message); } flushSyncGeneratorChangesToDisk(generators) { const message = { type: flush_sync_generator_changes_to_disk_1.FLUSH_SYNC_GENERATOR_CHANGES_TO_DISK, generators, }; return this.sendToDaemonViaQueue(message); } getRegisteredSyncGenerators() { const message = { type: get_registered_sync_generators_1.GET_REGISTERED_SYNC_GENERATORS, }; return this.sendToDaemonViaQueue(message); } updateWorkspaceContext(createdFiles, updatedFiles, deletedFiles) { const message = { type: update_workspace_context_1.UPDATE_WORKSPACE_CONTEXT, createdFiles, updatedFiles, deletedFiles, }; return this.sendToDaemonViaQueue(message); } async runPreTasksExecution(context) { const message = { type: run_tasks_execution_hooks_1.PRE_TASKS_EXECUTION, context, }; return this.sendToDaemonViaQueue(message); } async runPostTasksExecution(context) { const message = { type: run_tasks_execution_hooks_1.POST_TASKS_EXECUTION, context, }; return this.sendToDaemonViaQueue(message); } getNxConsoleStatus() { const message = { type: nx_console_1.GET_NX_CONSOLE_STATUS, }; return this.sendToDaemonViaQueue(message); } setNxConsolePreferenceAndInstall(preference) { const message = { type: nx_console_1.SET_NX_CONSOLE_PREFERENCE_AND_INSTALL, preference, }; return this.sendToDaemonViaQueue(message); } async isServerAvailable() { return new Promise((resolve) => { try { const socketPath = this.getSocketPath(); if (!socketPath) { resolve(false); return; } const socket = (0, net_1.connect)(socketPath, () => { socket.destroy(); resolve(true); }); socket.once('error', () => { resolve(false); }); } catch (err) { resolve(false); } }); } async startDaemonIfNecessary() { if (this._daemonStatus == DaemonStatus.CONNECTED) { return; } // Ensure daemon is running and socket path is available if (this._daemonStatus == DaemonStatus.DISCONNECTED) { this._daemonStatus = DaemonStatus.CONNECTING; let daemonPid = null; if (!(await this.isServerAvailable())) { daemonPid = await this.startInBackground(); } this.setUpConnection(); this._daemonStatus = DaemonStatus.CONNECTED; this._daemonReady(); daemonPid ??= (0, cache_1.getDaemonProcessIdSync)(); await this.registerDaemonProcessWithMetricsService(daemonPid); } else if (this._daemonStatus == DaemonStatus.CONNECTING) { await this._waitForDaemonReady; const daemonPid = (0, cache_1.getDaemonProcessIdSync)(); await this.registerDaemonProcessWithMetricsService(daemonPid); } } async sendToDaemonViaQueue(messageToDaemon, force) { return this.queue.sendToQueue(() => this.sendMessageToDaemon(messageToDaemon, force)); } setUpConnection() { const socketPath = this.getSocketPath(); this.socketMessenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)(socketPath)).listen((message) => this.handleMessage(message), () => { // it's ok for the daemon to terminate if the client doesn't wait on // any messages from the daemon if (this.queue.isEmpty()) { this.reset(); } else { output_1.output.error({ title: 'Daemon process terminated and closed the connection', bodyLines: [ 'Please rerun the command, which will restart the daemon.', `If you get this error again, check for any errors in the daemon process logs found in: ${tmp_dir_1.DAEMON_OUTPUT_LOG_FILE}`, ], }); this._daemonStatus = DaemonStatus.DISCONNECTED; this.currentReject?.(daemonProcessException('Daemon process terminated and closed the connection')); process.exit(1); } }, (err) => { if (!err.message) { return this.currentReject(daemonProcessException(err.toString())); } let error; if (err.message.startsWith('connect ENOENT')) { error = daemonProcessException('The Daemon Server is not running'); } else if (err.message.startsWith('connect ECONNREFUSED')) { error = daemonProcessException(`A server instance had not been fully shut down. Please try running the command again.`); } else if (err.message.startsWith('read ECONNRESET')) { error = daemonProcessException(`Unable to connect to the daemon process.`); } else { error = daemonProcessException(err.toString()); } return this.currentReject(error); }); } async sendMessageToDaemon(message, force) { await this.startDaemonIfNecessary(); // An open promise isn't enough to keep the event loop // alive, so we set a timeout here and clear it when we hear // back const keepAlive = setTimeout(() => { }, 10 * 60 * 1000); return new Promise((resolve, reject) => { perf_hooks_1.performance.mark('sendMessageToDaemon-start'); this.currentMessage = message; this.currentResolve = resolve; this.currentReject = reject; this.socketMessenger.sendMessage(message, force); }).finally(() => { clearTimeout(keepAlive); }); } async registerDaemonProcessWithMetricsService(daemonPid) { if (!daemonPid) { return; } try { const { getProcessMetricsService } = await Promise.resolve().then(() => require('../../tasks-runner/process-metrics-service')); getProcessMetricsService().registerDaemonProcess(daemonPid); } catch { // don't error, this is a secondary concern that should not break task execution } } retryMessageAfterNewDaemonStarts() { const [msg, res, rej] = [ this.currentMessage, this.currentResolve, this.currentReject, ]; // If we get to this point the daemon is about to close, and we don't // want to halt on our daemon terminated unexpectedly condition, // so we decrement the promise queue to make it look empty. this.queue.decrementQueueCounter(); if (msg) { setTimeout(() => { // We wait a bit to allow the server to finish shutting down before // retrying the message, which will start a new daemon. Part of // the process of starting up the daemon clears this.currentMessage etc // so we need to store them before waiting. this.sendToDaemonViaQueue(msg).then(res, rej); }, 50); } else { throw new Error('Daemon client attempted to retry a message without a current message'); } } handleMessage(serializedResult) { try { perf_hooks_1.performance.mark('result-parse-start-' + this.currentMessage.type); const parsedResult = (0, consume_messages_from_socket_1.isJsonMessage)(serializedResult) ? JSON.parse(serializedResult) : (0, node_v8_1.deserialize)(Buffer.from(serializedResult, 'binary')); perf_hooks_1.performance.mark('result-parse-end-' + this.currentMessage.type); perf_hooks_1.performance.measure('deserialize daemon response - ' + this.currentMessage.type, 'result-parse-start-' + this.currentMessage.type, 'result-parse-end-' + this.currentMessage.type); if (parsedResult.error) { if ('message' in parsedResult.error && (parsedResult.error.message === 'NX_VERSION_CHANGED' || parsedResult.error.message === 'LOCK_FILES_CHANGED')) { this.retryMessageAfterNewDaemonStarts(); } else { this.currentReject(parsedResult.error); } } else { perf_hooks_1.performance.measure(`${this.currentMessage.type} round trip`, 'sendMessageToDaemon-start', 'result-parse-end-' + this.currentMessage.type); return this.currentResolve(parsedResult); } } catch (e) { const endOfResponse = serializedResult.length > 300 ? serializedResult.substring(serializedResult.length - 300) : serializedResult; this.currentReject(daemonProcessException([ 'Could not deserialize response from Nx daemon.', `Message: ${e.message}`, '\n', `Received:`, endOfResponse, '\n', ].join('\n'))); } } async startInBackground() { if (global.NX_PLUGIN_WORKER) { throw new Error('Fatal Error: Something unexpected has occurred. Plugin Workers should not start a new daemon process. Please report this issue.'); } (0, node_fs_1.mkdirSync)(tmp_dir_1.DAEMON_DIR_FOR_CURRENT_WORKSPACE, { recursive: true }); if (!(0, node_fs_1.existsSync)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE)) { (0, node_fs_1.writeFileSync)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE, ''); } this._out = await (0, promises_1.open)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE, 'a'); this._err = await (0, promises_1.open)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE, 'a'); const backgroundProcess = (0, child_process_1.spawn)(process.execPath, [(0, path_1.join)(__dirname, `../server/start.js`)], { cwd: workspace_root_1.workspaceRoot, stdio: ['ignore', this._out.fd, this._err.fd], detached: true, windowsHide: false, shell: false, env: { ...process.env, ...DAEMON_ENV_SETTINGS, }, }); backgroundProcess.unref(); /** * Ensure the server is actually available to connect to via IPC before resolving */ let attempts = 0; return new Promise((resolve, reject) => { const id = setInterval(async () => { if (await this.isServerAvailable()) { clearInterval(id); resolve(backgroundProcess.pid); } else if (attempts > 6000) { // daemon fails to start, the process probably exited // we print the logs and exit the client reject(daemonProcessException('Failed to start or connect to the Nx Daemon process.')); } else { attempts++; } }, 10); }); } async stop() { try { const pid = (0, cache_1.getDaemonProcessIdSync)(); if (pid) { process.kill(pid, 'SIGTERM'); } } catch (err) { if (err.code !== 'ESRCH') { output_1.output.error({ title: err?.message || 'Something unexpected went wrong when stopping the daemon server', }); } } (0, tmp_dir_1.removeSocketDir)(); } } exports.DaemonClient = DaemonClient; exports.daemonClient = new DaemonClient(); function isDaemonEnabled() { return exports.daemonClient.enabled(); } function isDocker() { try { (0, node_fs_1.statSync)('/.dockerenv'); return true; } catch { try { return (0, node_fs_1.readFileSync)('/proc/self/cgroup', 'utf8')?.includes('docker'); } catch { } return false; } } function nxJsonIsNotPresent() { return !(0, nx_json_1.hasNxJson)(workspace_root_1.workspaceRoot); } function daemonProcessException(message) { try { let log = (0, node_fs_1.readFileSync)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE).toString().split('\n'); if (log.length > 20) { log = log.slice(log.length - 20); } const error = new Error([ message, '', 'Messages from the log:', ...log, '\n', `More information: ${tmp_dir_1.DAEMON_OUTPUT_LOG_FILE}`, ].join('\n')); error.internalDaemonError = true; return error; } catch (e) { return new Error(message); } }