nx
Version:
1,004 lines (1,003 loc) • 41.9 kB
JavaScript
"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);
}
}