"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 socket_utils_1 = require("../socket-utils");
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 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 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
            if ((((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));
    }
    async requestShutdown() {
        return this.sendToDaemonViaQueue({ type: 'REQUEST_SHUTDOWN' });
    }
    async getProjectGraphAndSourceMaps() {
        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(() => {
            messenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)((0, socket_utils_1.getFullOsSocketPath)())).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(() => {
            messenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)((0, socket_utils_1.getFullOsSocketPath)())).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 socket = (0, net_1.connect)((0, socket_utils_1.getFullOsSocketPath)(), () => {
                    socket.destroy();
                    resolve(true);
                });
                socket.once('error', () => {
                    resolve(false);
                });
            }
            catch (err) {
                resolve(false);
            }
        });
    }
    async sendToDaemonViaQueue(messageToDaemon, force) {
        return this.queue.sendToQueue(() => this.sendMessageToDaemon(messageToDaemon, force));
    }
    setUpConnection() {
        this.socketMessenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)((0, socket_utils_1.getFullOsSocketPath)())).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.`);
                (0, socket_utils_1.killSocketOrPath)();
            }
            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) {
        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);
        }
        // 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);
    }
}
