"use strict";
/* --------------------------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See License.txt in the project root for license information.
 * ------------------------------------------------------------------------------------------ */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
    for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SettingMonitor = exports.LanguageClient = exports.TransportKind = void 0;
const cp = require("child_process");
const fs = require("fs");
const path = require("path");
const vscode_1 = require("vscode");
const Is = require("../common/utils/is");
const client_1 = require("../common/client");
const processes_1 = require("./processes");
const node_1 = require("vscode-languageserver-protocol/node");
// Import SemVer functions individually to avoid circular dependencies in SemVer
const semverParse = require("semver/functions/parse");
const semverSatisfies = require("semver/functions/satisfies");
__exportStar(require("vscode-languageserver-protocol/node"), exports);
__exportStar(require("../common/api"), exports);
const REQUIRED_VSCODE_VERSION = '^1.67.0'; // do not change format, updated by `updateVSCode` script
var TransportKind;
(function (TransportKind) {
    TransportKind[TransportKind["stdio"] = 0] = "stdio";
    TransportKind[TransportKind["ipc"] = 1] = "ipc";
    TransportKind[TransportKind["pipe"] = 2] = "pipe";
    TransportKind[TransportKind["socket"] = 3] = "socket";
})(TransportKind = exports.TransportKind || (exports.TransportKind = {}));
var Transport;
(function (Transport) {
    function isSocket(value) {
        const candidate = value;
        return candidate && candidate.kind === TransportKind.socket && Is.number(candidate.port);
    }
    Transport.isSocket = isSocket;
})(Transport || (Transport = {}));
var Executable;
(function (Executable) {
    function is(value) {
        return Is.string(value.command);
    }
    Executable.is = is;
})(Executable || (Executable = {}));
var NodeModule;
(function (NodeModule) {
    function is(value) {
        return Is.string(value.module);
    }
    NodeModule.is = is;
})(NodeModule || (NodeModule = {}));
var StreamInfo;
(function (StreamInfo) {
    function is(value) {
        let candidate = value;
        return candidate && candidate.writer !== undefined && candidate.reader !== undefined;
    }
    StreamInfo.is = is;
})(StreamInfo || (StreamInfo = {}));
var ChildProcessInfo;
(function (ChildProcessInfo) {
    function is(value) {
        let candidate = value;
        return candidate && candidate.process !== undefined && typeof candidate.detached === 'boolean';
    }
    ChildProcessInfo.is = is;
})(ChildProcessInfo || (ChildProcessInfo = {}));
class LanguageClient extends client_1.BaseLanguageClient {
    constructor(arg1, arg2, arg3, arg4, arg5) {
        let id;
        let name;
        let serverOptions;
        let clientOptions;
        let forceDebug;
        if (Is.string(arg2)) {
            id = arg1;
            name = arg2;
            serverOptions = arg3;
            clientOptions = arg4;
            forceDebug = !!arg5;
        }
        else {
            id = arg1.toLowerCase();
            name = arg1;
            serverOptions = arg2;
            clientOptions = arg3;
            forceDebug = arg4;
        }
        if (forceDebug === undefined) {
            forceDebug = false;
        }
        super(id, name, clientOptions);
        this._serverOptions = serverOptions;
        this._forceDebug = forceDebug;
        this._isInDebugMode = forceDebug;
        try {
            this.checkVersion();
        }
        catch (error) {
            if (Is.string(error.message)) {
                this.outputChannel.appendLine(error.message);
            }
            throw error;
        }
    }
    checkVersion() {
        const codeVersion = semverParse(vscode_1.version);
        if (!codeVersion) {
            throw new Error(`No valid VS Code version detected. Version string is: ${vscode_1.version}`);
        }
        // Remove the insider pre-release since we stay API compatible.
        if (codeVersion.prerelease && codeVersion.prerelease.length > 0) {
            codeVersion.prerelease = [];
        }
        if (!semverSatisfies(codeVersion, REQUIRED_VSCODE_VERSION)) {
            throw new Error(`The language client requires VS Code version ${REQUIRED_VSCODE_VERSION} but received version ${vscode_1.version}`);
        }
    }
    get isInDebugMode() {
        return this._isInDebugMode;
    }
    async restart() {
        await this.stop();
        // We are in debug mode. Wait a little before we restart
        // so that the debug port can be freed. We can safely ignore
        // the disposable returned from start since it will call
        // stop on the same client instance.
        if (this.isInDebugMode) {
            await new Promise((resolve) => setTimeout(resolve, 1000));
            await this.start();
        }
        else {
            await this.start();
        }
    }
    stop(timeout = 2000) {
        return super.stop(timeout).finally(() => {
            if (this._serverProcess) {
                const toCheck = this._serverProcess;
                this._serverProcess = undefined;
                if (this._isDetached === undefined || !this._isDetached) {
                    this.checkProcessDied(toCheck);
                }
                this._isDetached = undefined;
            }
        });
    }
    checkProcessDied(childProcess) {
        if (!childProcess || childProcess.pid === undefined) {
            return;
        }
        setTimeout(() => {
            // Test if the process is still alive. Throws an exception if not
            try {
                if (childProcess.pid !== undefined) {
                    process.kill(childProcess.pid, 0);
                    (0, processes_1.terminate)(childProcess);
                }
            }
            catch (error) {
                // All is fine.
            }
        }, 2000);
    }
    handleConnectionClosed() {
        this._serverProcess = undefined;
        return super.handleConnectionClosed();
    }
    fillInitializeParams(params) {
        super.fillInitializeParams(params);
        if (params.processId === null) {
            params.processId = process.pid;
        }
    }
    createMessageTransports(encoding) {
        function getEnvironment(env, fork) {
            if (!env && !fork) {
                return undefined;
            }
            const result = Object.create(null);
            Object.keys(process.env).forEach(key => result[key] = process.env[key]);
            if (fork) {
                result['ELECTRON_RUN_AS_NODE'] = '1';
                result['ELECTRON_NO_ASAR'] = '1';
            }
            if (env) {
                Object.keys(env).forEach(key => result[key] = env[key]);
            }
            return result;
        }
        const debugStartWith = ['--debug=', '--debug-brk=', '--inspect=', '--inspect-brk='];
        const debugEquals = ['--debug', '--debug-brk', '--inspect', '--inspect-brk'];
        function startedInDebugMode() {
            let args = process.execArgv;
            if (args) {
                return args.some((arg) => {
                    return debugStartWith.some(value => arg.startsWith(value)) ||
                        debugEquals.some(value => arg === value);
                });
            }
            return false;
        }
        function assertStdio(process) {
            if (process.stdin === null || process.stdout === null || process.stderr === null) {
                throw new Error('Process created without stdio streams');
            }
        }
        const server = this._serverOptions;
        // We got a function.
        if (Is.func(server)) {
            return server().then((result) => {
                if (client_1.MessageTransports.is(result)) {
                    this._isDetached = !!result.detached;
                    return result;
                }
                else if (StreamInfo.is(result)) {
                    this._isDetached = !!result.detached;
                    return { reader: new node_1.StreamMessageReader(result.reader), writer: new node_1.StreamMessageWriter(result.writer) };
                }
                else {
                    let cp;
                    if (ChildProcessInfo.is(result)) {
                        cp = result.process;
                        this._isDetached = result.detached;
                    }
                    else {
                        cp = result;
                        this._isDetached = false;
                    }
                    cp.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                    return { reader: new node_1.StreamMessageReader(cp.stdout), writer: new node_1.StreamMessageWriter(cp.stdin) };
                }
            });
        }
        let json;
        let runDebug = server;
        if (runDebug.run || runDebug.debug) {
            if (this._forceDebug || startedInDebugMode()) {
                json = runDebug.debug;
                this._isInDebugMode = true;
            }
            else {
                json = runDebug.run;
                this._isInDebugMode = false;
            }
        }
        else {
            json = server;
        }
        return this._getServerWorkingDir(json.options).then(serverWorkingDir => {
            if (NodeModule.is(json) && json.module) {
                let node = json;
                let transport = node.transport || TransportKind.stdio;
                if (node.runtime) {
                    const args = [];
                    const options = node.options ?? Object.create(null);
                    if (options.execArgv) {
                        options.execArgv.forEach(element => args.push(element));
                    }
                    args.push(node.module);
                    if (node.args) {
                        node.args.forEach(element => args.push(element));
                    }
                    const execOptions = Object.create(null);
                    execOptions.cwd = serverWorkingDir;
                    execOptions.env = getEnvironment(options.env, false);
                    const runtime = this._getRuntimePath(node.runtime, serverWorkingDir);
                    let pipeName = undefined;
                    if (transport === TransportKind.ipc) {
                        // exec options not correctly typed in lib
                        execOptions.stdio = [null, null, null, 'ipc'];
                        args.push('--node-ipc');
                    }
                    else if (transport === TransportKind.stdio) {
                        args.push('--stdio');
                    }
                    else if (transport === TransportKind.pipe) {
                        pipeName = (0, node_1.generateRandomPipeName)();
                        args.push(`--pipe=${pipeName}`);
                    }
                    else if (Transport.isSocket(transport)) {
                        args.push(`--socket=${transport.port}`);
                    }
                    args.push(`--clientProcessId=${process.pid.toString()}`);
                    if (transport === TransportKind.ipc || transport === TransportKind.stdio) {
                        const serverProcess = cp.spawn(runtime, args, execOptions);
                        if (!serverProcess || !serverProcess.pid) {
                            return handleChildProcessStartError(serverProcess, `Launching server using runtime ${runtime} failed.`);
                        }
                        this._serverProcess = serverProcess;
                        serverProcess.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                        if (transport === TransportKind.ipc) {
                            serverProcess.stdout.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                            return Promise.resolve({ reader: new node_1.IPCMessageReader(serverProcess), writer: new node_1.IPCMessageWriter(serverProcess) });
                        }
                        else {
                            return Promise.resolve({ reader: new node_1.StreamMessageReader(serverProcess.stdout), writer: new node_1.StreamMessageWriter(serverProcess.stdin) });
                        }
                    }
                    else if (transport === TransportKind.pipe) {
                        return (0, node_1.createClientPipeTransport)(pipeName).then((transport) => {
                            const process = cp.spawn(runtime, args, execOptions);
                            if (!process || !process.pid) {
                                return handleChildProcessStartError(process, `Launching server using runtime ${runtime} failed.`);
                            }
                            this._serverProcess = process;
                            process.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                            process.stdout.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                            return transport.onConnected().then((protocol) => {
                                return { reader: protocol[0], writer: protocol[1] };
                            });
                        });
                    }
                    else if (Transport.isSocket(transport)) {
                        return (0, node_1.createClientSocketTransport)(transport.port).then((transport) => {
                            const process = cp.spawn(runtime, args, execOptions);
                            if (!process || !process.pid) {
                                return handleChildProcessStartError(process, `Launching server using runtime ${runtime} failed.`);
                            }
                            this._serverProcess = process;
                            process.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                            process.stdout.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                            return transport.onConnected().then((protocol) => {
                                return { reader: protocol[0], writer: protocol[1] };
                            });
                        });
                    }
                }
                else {
                    let pipeName = undefined;
                    return new Promise((resolve, reject) => {
                        const args = (node.args && node.args.slice()) ?? [];
                        if (transport === TransportKind.ipc) {
                            args.push('--node-ipc');
                        }
                        else if (transport === TransportKind.stdio) {
                            args.push('--stdio');
                        }
                        else if (transport === TransportKind.pipe) {
                            pipeName = (0, node_1.generateRandomPipeName)();
                            args.push(`--pipe=${pipeName}`);
                        }
                        else if (Transport.isSocket(transport)) {
                            args.push(`--socket=${transport.port}`);
                        }
                        args.push(`--clientProcessId=${process.pid.toString()}`);
                        const options = node.options ?? Object.create(null);
                        options.env = getEnvironment(options.env, true);
                        options.execArgv = options.execArgv || [];
                        options.cwd = serverWorkingDir;
                        options.silent = true;
                        if (transport === TransportKind.ipc || transport === TransportKind.stdio) {
                            const sp = cp.fork(node.module, args || [], options);
                            assertStdio(sp);
                            this._serverProcess = sp;
                            sp.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                            if (transport === TransportKind.ipc) {
                                sp.stdout.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                                resolve({ reader: new node_1.IPCMessageReader(this._serverProcess), writer: new node_1.IPCMessageWriter(this._serverProcess) });
                            }
                            else {
                                resolve({ reader: new node_1.StreamMessageReader(sp.stdout), writer: new node_1.StreamMessageWriter(sp.stdin) });
                            }
                        }
                        else if (transport === TransportKind.pipe) {
                            (0, node_1.createClientPipeTransport)(pipeName).then((transport) => {
                                const sp = cp.fork(node.module, args || [], options);
                                assertStdio(sp);
                                this._serverProcess = sp;
                                sp.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                                sp.stdout.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                                transport.onConnected().then((protocol) => {
                                    resolve({ reader: protocol[0], writer: protocol[1] });
                                }, reject);
                            }, reject);
                        }
                        else if (Transport.isSocket(transport)) {
                            (0, node_1.createClientSocketTransport)(transport.port).then((transport) => {
                                const sp = cp.fork(node.module, args || [], options);
                                assertStdio(sp);
                                this._serverProcess = sp;
                                sp.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                                sp.stdout.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                                transport.onConnected().then((protocol) => {
                                    resolve({ reader: protocol[0], writer: protocol[1] });
                                }, reject);
                            }, reject);
                        }
                    });
                }
            }
            else if (Executable.is(json) && json.command) {
                const command = json;
                const args = json.args !== undefined ? json.args.slice(0) : [];
                let pipeName = undefined;
                const transport = json.transport;
                if (transport === TransportKind.stdio) {
                    args.push('--stdio');
                }
                else if (transport === TransportKind.pipe) {
                    pipeName = (0, node_1.generateRandomPipeName)();
                    args.push(`--pipe=${pipeName}`);
                }
                else if (Transport.isSocket(transport)) {
                    args.push(`--socket=${transport.port}`);
                }
                else if (transport === TransportKind.ipc) {
                    throw new Error(`Transport kind ipc is not support for command executable`);
                }
                const options = Object.assign({}, command.options);
                options.cwd = options.cwd || serverWorkingDir;
                if (transport === undefined || transport === TransportKind.stdio) {
                    const serverProcess = cp.spawn(command.command, args, options);
                    if (!serverProcess || !serverProcess.pid) {
                        return handleChildProcessStartError(serverProcess, `Launching server using command ${command.command} failed.`);
                    }
                    serverProcess.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                    this._serverProcess = serverProcess;
                    this._isDetached = !!options.detached;
                    return Promise.resolve({ reader: new node_1.StreamMessageReader(serverProcess.stdout), writer: new node_1.StreamMessageWriter(serverProcess.stdin) });
                }
                else if (transport === TransportKind.pipe) {
                    return (0, node_1.createClientPipeTransport)(pipeName).then((transport) => {
                        const serverProcess = cp.spawn(command.command, args, options);
                        if (!serverProcess || !serverProcess.pid) {
                            return handleChildProcessStartError(serverProcess, `Launching server using command ${command.command} failed.`);
                        }
                        this._serverProcess = serverProcess;
                        this._isDetached = !!options.detached;
                        serverProcess.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                        serverProcess.stdout.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                        return transport.onConnected().then((protocol) => {
                            return { reader: protocol[0], writer: protocol[1] };
                        });
                    });
                }
                else if (Transport.isSocket(transport)) {
                    return (0, node_1.createClientSocketTransport)(transport.port).then((transport) => {
                        const serverProcess = cp.spawn(command.command, args, options);
                        if (!serverProcess || !serverProcess.pid) {
                            return handleChildProcessStartError(serverProcess, `Launching server using command ${command.command} failed.`);
                        }
                        this._serverProcess = serverProcess;
                        this._isDetached = !!options.detached;
                        serverProcess.stderr.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                        serverProcess.stdout.on('data', data => this.outputChannel.append(Is.string(data) ? data : data.toString(encoding)));
                        return transport.onConnected().then((protocol) => {
                            return { reader: protocol[0], writer: protocol[1] };
                        });
                    });
                }
            }
            return Promise.reject(new Error(`Unsupported server configuration ` + JSON.stringify(server, null, 4)));
        });
    }
    _getRuntimePath(runtime, serverWorkingDirectory) {
        if (path.isAbsolute(runtime)) {
            return runtime;
        }
        const mainRootPath = this._mainGetRootPath();
        if (mainRootPath !== undefined) {
            const result = path.join(mainRootPath, runtime);
            if (fs.existsSync(result)) {
                return result;
            }
        }
        if (serverWorkingDirectory !== undefined) {
            const result = path.join(serverWorkingDirectory, runtime);
            if (fs.existsSync(result)) {
                return result;
            }
        }
        return runtime;
    }
    _mainGetRootPath() {
        let folders = vscode_1.workspace.workspaceFolders;
        if (!folders || folders.length === 0) {
            return undefined;
        }
        let folder = folders[0];
        if (folder.uri.scheme === 'file') {
            return folder.uri.fsPath;
        }
        return undefined;
    }
    _getServerWorkingDir(options) {
        let cwd = options && options.cwd;
        if (!cwd) {
            cwd = this.clientOptions.workspaceFolder
                ? this.clientOptions.workspaceFolder.uri.fsPath
                : this._mainGetRootPath();
        }
        if (cwd) {
            // make sure the folder exists otherwise creating the process will fail
            return new Promise(s => {
                fs.lstat(cwd, (err, stats) => {
                    s(!err && stats.isDirectory() ? cwd : undefined);
                });
            });
        }
        return Promise.resolve(undefined);
    }
}
exports.LanguageClient = LanguageClient;
class SettingMonitor {
    constructor(_client, _setting) {
        this._client = _client;
        this._setting = _setting;
        this._listeners = [];
    }
    start() {
        vscode_1.workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this._listeners);
        this.onDidChangeConfiguration();
        return new vscode_1.Disposable(() => {
            if (this._client.needsStop()) {
                void this._client.stop();
            }
        });
    }
    onDidChangeConfiguration() {
        let index = this._setting.indexOf('.');
        let primary = index >= 0 ? this._setting.substr(0, index) : this._setting;
        let rest = index >= 0 ? this._setting.substr(index + 1) : undefined;
        let enabled = rest ? vscode_1.workspace.getConfiguration(primary).get(rest, false) : vscode_1.workspace.getConfiguration(primary);
        if (enabled && this._client.needsStart()) {
            this._client.start().catch((error) => this._client.error('Start failed after configuration change', error, 'force'));
        }
        else if (!enabled && this._client.needsStop()) {
            void this._client.stop().catch((error) => this._client.error('Stop failed after configuration change', error, 'force'));
        }
    }
}
exports.SettingMonitor = SettingMonitor;
function handleChildProcessStartError(process, message) {
    if (process === null) {
        return Promise.reject(message);
    }
    return new Promise((_, reject) => {
        process.on('error', (err) => {
            reject(`${message} ${err}`);
        });
        // the error event should always be run immediately,
        // but race on it just in case
        setImmediate(() => reject(message));
    });
}
