UNPKG

nx

Version:

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

1,004 lines (1,003 loc) • 41.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.daemonClient = exports.DaemonClient = void 0; exports.isDaemonEnabled = isDaemonEnabled; const child_process_1 = require("child_process"); const promises_1 = require("fs/promises"); const net_1 = require("net"); const node_fs_1 = require("node:fs"); const node_v8_1 = require("node:v8"); const path_1 = require("path"); const perf_hooks_1 = require("perf_hooks"); const configuration_1 = require("../../config/configuration"); const nx_json_1 = require("../../config/nx-json"); const native_1 = require("../../native"); const error_types_1 = require("../../project-graph/error-types"); const project_graph_1 = require("../../project-graph/project-graph"); const consume_messages_from_socket_1 = require("../../utils/consume-messages-from-socket"); const delayed_spinner_1 = require("../../utils/delayed-spinner"); const is_ci_1 = require("../../utils/is-ci"); const output_1 = require("../../utils/output"); const promised_based_queue_1 = require("../../utils/promised-based-queue"); const workspace_root_1 = require("../../utils/workspace-root"); const cache_1 = require("../cache"); const is_nx_version_mismatch_1 = require("../is-nx-version-mismatch"); const logger_1 = require("../logger"); const flush_sync_generator_changes_to_disk_1 = require("../message-types/flush-sync-generator-changes-to-disk"); 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 get_nx_workspace_files_1 = require("../message-types/get-nx-workspace-files"); const get_registered_sync_generators_1 = require("../message-types/get-registered-sync-generators"); const get_sync_generator_changes_1 = require("../message-types/get-sync-generator-changes"); const hash_glob_1 = require("../message-types/hash-glob"); const nx_console_1 = require("../message-types/nx-console"); const register_project_graph_listener_1 = require("../message-types/register-project-graph-listener"); const run_tasks_execution_hooks_1 = require("../message-types/run-tasks-execution-hooks"); const task_history_1 = require("../message-types/task-history"); const update_workspace_context_1 = require("../message-types/update-workspace-context"); const tmp_dir_1 = require("../tmp-dir"); const daemon_socket_messenger_1 = require("./daemon-socket-messenger"); const DAEMON_ENV_REQUIRED_SETTINGS = { NX_PROJECT_GLOB_CACHE: 'false', NX_CACHE_PROJECTS_CONFIG: 'false', }; const DAEMON_ENV_OVERRIDABLE_SETTINGS = { 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 = {})); const WAIT_FOR_SERVER_CONFIG = { delayMs: 10, maxAttempts: 6000, // 6000 * 10ms = 60 seconds }; class DaemonClient { constructor() { this._daemonStatus = DaemonStatus.DISCONNECTED; this._waitForDaemonReady = null; this._daemonReady = null; this._out = null; this._err = null; this.fileWatcherReconnecting = false; this.fileWatcherCallbacks = new Map(); this.fileWatcherConfigs = new Map(); this.projectGraphListenerReconnecting = false; this.projectGraphListenerCallbacks = new Map(); 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; // Clean up file watcher and project graph listener connections this.fileWatcherMessenger?.close(); this.fileWatcherMessenger = undefined; this.projectGraphListenerMessenger?.close(); this.projectGraphListenerMessenger = undefined; 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'); } } parseMessage(message) { return (0, consume_messages_from_socket_1.isJsonMessage)(message) ? JSON.parse(message) : (0, node_v8_1.deserialize)(Buffer.from(message, 'binary')); } 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, cwd) { return this.sendToDaemonViaQueue({ type: 'HASH_TASKS', runnerOptions, env: process.env.NX_USE_V8_SERIALIZER !== 'false' ? structuredClone(process.env) : env, tasks, taskGraph, cwd, }); } 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; } } // Generate unique ID for this callback const callbackId = Math.random().toString(36).substring(2, 11); // Store callback and config for reconnection this.fileWatcherCallbacks.set(callbackId, callback); this.fileWatcherConfigs.set(callbackId, config); await this.queue.sendToQueue(async () => { // If we already have a connection, just register the new config if (this.fileWatcherMessenger) { this.fileWatcherMessenger.sendMessage({ type: 'REGISTER_FILE_WATCHER', config, }); return; } await this.startDaemonIfNecessary(); const socketPath = this.getSocketPath(); this.fileWatcherMessenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)(socketPath)).listen((message) => { try { const parsedMessage = this.parseMessage(message); // Notify all callbacks for (const cb of this.fileWatcherCallbacks.values()) { cb(null, parsedMessage); } } catch (e) { for (const cb of this.fileWatcherCallbacks.values()) { cb(e, null); } } }, () => { // Connection closed - trigger reconnection logger_1.clientLogger.log(`[FileWatcher] Socket closed, triggering reconnection`); this.fileWatcherMessenger = undefined; for (const cb of this.fileWatcherCallbacks.values()) { cb('reconnecting', null); } this.reconnectFileWatcher(); }, (err) => { if (err instanceof daemon_socket_messenger_1.VersionMismatchError) { for (const cb of this.fileWatcherCallbacks.values()) { cb('closed', null); } process.exit(1); } for (const cb of this.fileWatcherCallbacks.values()) { cb(err, null); } }); this.fileWatcherMessenger.sendMessage({ type: 'REGISTER_FILE_WATCHER', config, }); }); // Return unregister function return () => { this.fileWatcherCallbacks.delete(callbackId); this.fileWatcherConfigs.delete(callbackId); // If no more callbacks, close the connection if (this.fileWatcherCallbacks.size === 0) { this.fileWatcherMessenger?.close(); this.fileWatcherMessenger = undefined; } }; } async reconnectFileWatcher() { // Guard against concurrent reconnection attempts if (this.fileWatcherReconnecting) { return; } if (this.fileWatcherCallbacks.size === 0) { return; // No callbacks to reconnect } this.fileWatcherReconnecting = true; logger_1.clientLogger.log(`[FileWatcher] Starting reconnection for ${this.fileWatcherCallbacks.size} callbacks`); // Wait for daemon server to be available before trying to reconnect let serverAvailable; try { serverAvailable = await this.waitForServerToBeAvailable({ ignoreVersionMismatch: false, }); } catch (err) { // Version mismatch - pass error to callbacks so they can handle it logger_1.clientLogger.log(`[FileWatcher] Error during reconnection: ${err.message}`); this.fileWatcherReconnecting = false; for (const cb of this.fileWatcherCallbacks.values()) { cb(err, null); } return; } if (!serverAvailable) { // Failed to reconnect after all attempts - notify as closed logger_1.clientLogger.log(`[FileWatcher] Failed to reconnect - server unavailable`); this.fileWatcherReconnecting = false; for (const cb of this.fileWatcherCallbacks.values()) { cb('closed', null); } return; } try { // Try to reconnect const socketPath = this.getSocketPath(); this.fileWatcherMessenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)(socketPath)).listen((message) => { try { const parsedMessage = this.parseMessage(message); for (const cb of this.fileWatcherCallbacks.values()) { cb(null, parsedMessage); } } catch (e) { for (const cb of this.fileWatcherCallbacks.values()) { cb(e, null); } } }, () => { // Connection closed - trigger reconnection again this.fileWatcherMessenger = undefined; // Reset reconnection flag before triggering reconnection this.fileWatcherReconnecting = false; for (const cb of this.fileWatcherCallbacks.values()) { cb('reconnecting', null); } this.reconnectFileWatcher(); }, (err) => { if (err instanceof daemon_socket_messenger_1.VersionMismatchError) { for (const cb of this.fileWatcherCallbacks.values()) { cb('closed', null); } process.exit(1); } // Other errors during reconnection - let retry loop handle }); // Re-register all stored configs for (const cfg of this.fileWatcherConfigs.values()) { this.fileWatcherMessenger.sendMessage({ type: 'REGISTER_FILE_WATCHER', config: cfg, }); } // Successfully reconnected - notify callbacks logger_1.clientLogger.log(`[FileWatcher] Reconnected successfully`); for (const cb of this.fileWatcherCallbacks.values()) { cb('reconnected', null); } this.fileWatcherReconnecting = false; } catch (e) { // Failed to reconnect - notify as closed logger_1.clientLogger.log(`[FileWatcher] Reconnection failed: ${e.message}`); this.fileWatcherReconnecting = false; for (const cb of this.fileWatcherCallbacks.values()) { cb('closed', null); } } } async registerProjectGraphRecomputationListener(callback) { // Generate unique ID for this callback const callbackId = Math.random().toString(36).substring(2, 11); // Store callback this.projectGraphListenerCallbacks.set(callbackId, callback); await this.queue.sendToQueue(async () => { // If we already have a connection, just send the registration if (this.projectGraphListenerMessenger) { this.projectGraphListenerMessenger.sendMessage({ type: register_project_graph_listener_1.REGISTER_PROJECT_GRAPH_LISTENER, }); return; } await this.startDaemonIfNecessary(); const socketPath = this.getSocketPath(); this.projectGraphListenerMessenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)(socketPath)).listen((message) => { try { const parsedMessage = this.parseMessage(message); // Notify all callbacks for (const cb of this.projectGraphListenerCallbacks.values()) { cb(null, parsedMessage); } } catch (e) { for (const cb of this.projectGraphListenerCallbacks.values()) { cb(e, null); } } }, () => { // Connection closed - trigger reconnection logger_1.clientLogger.log(`[ProjectGraphListener] Socket closed, triggering reconnection`); this.projectGraphListenerMessenger = undefined; for (const cb of this.projectGraphListenerCallbacks.values()) { cb('reconnecting', null); } this.reconnectProjectGraphListener(); }, (err) => { if (err instanceof daemon_socket_messenger_1.VersionMismatchError) { for (const cb of this.projectGraphListenerCallbacks.values()) { cb('closed', null); } process.exit(1); } for (const cb of this.projectGraphListenerCallbacks.values()) { cb(err, null); } }); this.projectGraphListenerMessenger.sendMessage({ type: register_project_graph_listener_1.REGISTER_PROJECT_GRAPH_LISTENER, }); }); // Return unregister function return () => { this.projectGraphListenerCallbacks.delete(callbackId); // If no more callbacks, close the connection if (this.projectGraphListenerCallbacks.size === 0) { this.projectGraphListenerMessenger?.close(); this.projectGraphListenerMessenger = undefined; } }; } async reconnectProjectGraphListener() { // Guard against concurrent reconnection attempts if (this.projectGraphListenerReconnecting) { return; } if (this.projectGraphListenerCallbacks.size === 0) { return; // No callbacks to reconnect } this.projectGraphListenerReconnecting = true; logger_1.clientLogger.log(`[ProjectGraphListener] Starting reconnection for ${this.projectGraphListenerCallbacks.size} callbacks`); // Wait for daemon server to be available before trying to reconnect let serverAvailable; try { serverAvailable = await this.waitForServerToBeAvailable({ ignoreVersionMismatch: false, }); } catch (err) { // Version mismatch - pass error to callbacks so they can handle it logger_1.clientLogger.log(`[ProjectGraphListener] Error during reconnection: ${err.message}`); this.projectGraphListenerReconnecting = false; for (const cb of this.projectGraphListenerCallbacks.values()) { cb(err, null); } return; } if (!serverAvailable) { // Failed to reconnect after all attempts - notify as closed logger_1.clientLogger.log(`[ProjectGraphListener] Failed to reconnect - server unavailable`); this.projectGraphListenerReconnecting = false; for (const cb of this.projectGraphListenerCallbacks.values()) { cb('closed', null); } return; } try { const socketPath = this.getSocketPath(); // Try to reconnect this.projectGraphListenerMessenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)(socketPath)).listen((message) => { try { const parsedMessage = this.parseMessage(message); for (const cb of this.projectGraphListenerCallbacks.values()) { cb(null, parsedMessage); } } catch (e) { for (const cb of this.projectGraphListenerCallbacks.values()) { cb(e, null); } } }, () => { // Connection closed - trigger reconnection again this.projectGraphListenerMessenger = undefined; // Reset reconnection flag before triggering reconnection this.projectGraphListenerReconnecting = false; for (const cb of this.projectGraphListenerCallbacks.values()) { cb('reconnecting', null); } this.reconnectProjectGraphListener(); }, (err) => { if (err instanceof daemon_socket_messenger_1.VersionMismatchError) { for (const cb of this.projectGraphListenerCallbacks.values()) { cb('closed', null); } process.exit(1); } // Other errors during reconnection - let retry loop handle }); // Re-register this.projectGraphListenerMessenger.sendMessage({ type: register_project_graph_listener_1.REGISTER_PROJECT_GRAPH_LISTENER, }); // Successfully reconnected - notify callbacks logger_1.clientLogger.log(`[ProjectGraphListener] Reconnected successfully`); for (const cb of this.projectGraphListenerCallbacks.values()) { cb('reconnected', null); } this.projectGraphListenerReconnecting = false; } catch (e) { // Failed to reconnect - notify as closed logger_1.clientLogger.log(`[ProjectGraphListener] Reconnection failed: ${e.message}`); this.projectGraphListenerReconnecting = false; for (const cb of this.projectGraphListenerCallbacks.values()) { cb('closed', null); } } } 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, reject) => { 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) { if (err instanceof daemon_socket_messenger_1.VersionMismatchError) { reject(err); // Let version mismatch bubble up } 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; let serverAvailable; try { serverAvailable = await this.isServerAvailable(); } catch (err) { // Version mismatch - treat as server not available, start new one if (err instanceof daemon_socket_messenger_1.VersionMismatchError) { serverAvailable = false; } else { throw err; } } if (!serverAvailable) { daemonPid = await this.startInBackground(); } this.setUpConnection(); this._daemonStatus = DaemonStatus.CONNECTED; this._daemonReady(); daemonPid ??= (0, cache_1.getDaemonProcessIdSync)(); // Fire-and-forget - don't block daemon connection by waiting for metrics registration this.registerDaemonProcessWithMetricsService(daemonPid); } else if (this._daemonStatus == DaemonStatus.CONNECTING) { await this._waitForDaemonReady; const daemonPid = (0, cache_1.getDaemonProcessIdSync)(); // Fire-and-forget - don't block daemon connection by waiting for metrics registration 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 { // Connection closed while we had pending work - try to reconnect this._daemonStatus = DaemonStatus.DISCONNECTED; this.handleConnectionError(daemonProcessException('Daemon process terminated and closed the connection')); } }, (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()); } this.currentReject(error); }); } async handleConnectionError(error) { logger_1.clientLogger.log(`[Reconnect] Connection error detected: ${error.message}`); // Create a new ready promise for new requests to wait on this._waitForDaemonReady = new Promise((resolve) => (this._daemonReady = resolve)); // Set status to CONNECTING so new requests will wait for reconnection this._daemonStatus = DaemonStatus.CONNECTING; let serverAvailable; try { serverAvailable = await this.waitForServerToBeAvailable({ ignoreVersionMismatch: false, }); } catch (err) { if (err instanceof daemon_socket_messenger_1.VersionMismatchError) { // New daemon has different version - reject with error so caller can handle if (this.currentReject) { this.currentReject(err); } return; } throw err; } if (serverAvailable) { logger_1.clientLogger.log(`[Reconnect] Reconnection successful, re-establishing connection`); // Server is back up, establish connection and signal ready this.establishConnection(); // Resend the pending message if one exists if (this.currentMessage && this.currentResolve && this.currentReject) { // Retry the message directly (not through the queue) to resolve the // pending promise that the original queue entry is waiting on. // This allows the original queue entry to complete naturally. const msg = this.currentMessage; const res = this.currentResolve; const rej = this.currentReject; this.sendMessageToDaemon(msg).then(res, rej); } } else { // Failed to reconnect after all attempts, reject the pending request if (this.currentReject) { this.currentReject(error); } } } establishConnection() { this._daemonStatus = DaemonStatus.DISCONNECTED; this.setUpConnection(); this._daemonStatus = DaemonStatus.CONNECTED; this._daemonReady(); } /** * Wait for daemon server to be available. * Used for reconnection - throws VersionMismatchError if daemon version differs. */ async waitForServerToBeAvailable(options) { let attempts = 0; logger_1.clientLogger.log(`[Client] Waiting for server (max: ${WAIT_FOR_SERVER_CONFIG.maxAttempts} attempts, ${WAIT_FOR_SERVER_CONFIG.delayMs}ms interval)`); while (attempts < WAIT_FOR_SERVER_CONFIG.maxAttempts) { await new Promise((resolve) => setTimeout(resolve, WAIT_FOR_SERVER_CONFIG.delayMs)); attempts++; try { if (await this.isServerAvailable()) { logger_1.clientLogger.log(`[Client] Server available after ${attempts} attempts`); return true; } } catch (err) { if (err instanceof daemon_socket_messenger_1.VersionMismatchError) { if (!options.ignoreVersionMismatch) { throw err; } // Keep waiting - old cache file may exist } else { throw err; } } } logger_1.clientLogger.log(`[Client] Server not available after ${WAIT_FOR_SERVER_CONFIG.maxAttempts} attempts`); return false; } 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 } } 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) { 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'); logger_1.clientLogger.log(`[Client] Starting new daemon server in background`); 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: { ...DAEMON_ENV_OVERRIDABLE_SETTINGS, ...process.env, ...DAEMON_ENV_REQUIRED_SETTINGS, }, }); backgroundProcess.unref(); /** * Ensure the server is actually available to connect to via IPC before resolving */ const serverAvailable = await this.waitForServerToBeAvailable({ ignoreVersionMismatch: true, }); if (serverAvailable) { logger_1.clientLogger.log(`[Client] Daemon server started, pid=${backgroundProcess.pid}`); return backgroundProcess.pid; } else { throw daemonProcessException('Failed to start or connect to the Nx Daemon process.'); } } 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); } }