X-Git-Url: https://git.josue.xyz/?p=VSoRC%2F.git;a=blobdiff_plain;f=node_modules%2Fnode-pty%2Fsrc%2FwindowsPtyAgent.ts;fp=node_modules%2Fnode-pty%2Fsrc%2FwindowsPtyAgent.ts;h=cf8b65db61edb88f8b0ee78be5ca38e7b8f65e59;hp=0000000000000000000000000000000000000000;hb=e79e4a5a87f3e84f7c1777f10a954453a69bf540;hpb=4339da12467b75fb8b6ca831f4bf0081c485ed2c diff --git a/node_modules/node-pty/src/windowsPtyAgent.ts b/node_modules/node-pty/src/windowsPtyAgent.ts new file mode 100644 index 0000000..cf8b65d --- /dev/null +++ b/node_modules/node-pty/src/windowsPtyAgent.ts @@ -0,0 +1,313 @@ +/** + * Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License) + * Copyright (c) 2016, Daniel Imms (MIT License). + * Copyright (c) 2018, Microsoft Corporation (MIT License). + */ + +import * as os from 'os'; +import * as path from 'path'; +import { Socket } from 'net'; +import { ArgvOrCommandLine } from './types'; +import { fork } from 'child_process'; + +let conptyNative: IConptyNative; +let winptyNative: IWinptyNative; + +/** + * The amount of time to wait for additional data after the conpty shell process has exited before + * shutting down the socket. The timer will be reset if a new data event comes in after the timer + * has started. + */ +const FLUSH_DATA_INTERVAL = 20; + +/** + * This agent sits between the WindowsTerminal class and provides a common interface for both conpty + * and winpty. + */ +export class WindowsPtyAgent { + private _inSocket: Socket; + private _outSocket: Socket; + private _pid: number; + private _innerPid: number; + private _innerPidHandle: number; + private _closeTimeout: NodeJS.Timer; + private _exitCode: number | undefined; + + private _fd: any; + private _pty: number; + private _ptyNative: IConptyNative | IWinptyNative; + + public get inSocket(): Socket { return this._inSocket; } + public get outSocket(): Socket { return this._outSocket; } + public get fd(): any { return this._fd; } + public get innerPid(): number { return this._innerPid; } + public get pty(): number { return this._pty; } + + constructor( + file: string, + args: ArgvOrCommandLine, + env: string[], + cwd: string, + cols: number, + rows: number, + debug: boolean, + private _useConpty: boolean | undefined, + conptyInheritCursor: boolean = false + ) { + if (this._useConpty === undefined || this._useConpty === true) { + this._useConpty = this._getWindowsBuildNumber() >= 18309; + } + if (this._useConpty) { + if (!conptyNative) { + try { + conptyNative = require('../build/Release/conpty.node'); + } catch (outerError) { + try { + conptyNative = require('../build/Debug/conpty.node'); + } catch (innerError) { + console.error('innerError', innerError); + // Re-throw the exception from the Release require if the Debug require fails as well + throw outerError; + } + } + } + } else { + if (!winptyNative) { + try { + winptyNative = require('../build/Release/pty.node'); + } catch (outerError) { + try { + winptyNative = require('../build/Debug/pty.node'); + } catch (innerError) { + console.error('innerError', innerError); + // Re-throw the exception from the Release require if the Debug require fails as well + throw outerError; + } + } + } + } + this._ptyNative = this._useConpty ? conptyNative : winptyNative; + + // Sanitize input variable. + cwd = path.resolve(cwd); + + // Compose command line + const commandLine = argsToCommandLine(file, args); + + // Open pty session. + let term: IConptyProcess | IWinptyProcess; + if (this._useConpty) { + term = (this._ptyNative as IConptyNative).startProcess(file, cols, rows, debug, this._generatePipeName(), conptyInheritCursor); + } else { + term = (this._ptyNative as IWinptyNative).startProcess(file, commandLine, env, cwd, cols, rows, debug); + this._pid = (term as IWinptyProcess).pid; + this._innerPid = (term as IWinptyProcess).innerPid; + this._innerPidHandle = (term as IWinptyProcess).innerPidHandle; + } + + // Not available on windows. + this._fd = term.fd; + + // Generated incremental number that has no real purpose besides using it + // as a terminal id. + this._pty = term.pty; + + // Create terminal pipe IPC channel and forward to a local unix socket. + this._outSocket = new Socket(); + this._outSocket.setEncoding('utf8'); + this._outSocket.connect(term.conout, () => { + // TODO: Emit event on agent instead of socket? + + // Emit ready event. + this._outSocket.emit('ready_datapipe'); + }); + + this._inSocket = new Socket(); + this._inSocket.setEncoding('utf8'); + this._inSocket.connect(term.conin); + // TODO: Wait for ready event? + + if (this._useConpty) { + const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine, cwd, env, c => this._$onProcessExit(c) +); + this._innerPid = connect.pid; + } + } + + public resize(cols: number, rows: number): void { + if (this._useConpty) { + if (this._exitCode !== undefined) { + throw new Error('Cannot resize a pty that has already exited'); + } + this._ptyNative.resize(this._pty, cols, rows); + return; + } + this._ptyNative.resize(this._pid, cols, rows); + } + + public kill(): void { + this._inSocket.readable = false; + this._inSocket.writable = false; + this._outSocket.readable = false; + this._outSocket.writable = false; + // Tell the agent to kill the pty, this releases handles to the process + if (this._useConpty) { + this._getConsoleProcessList().then(consoleProcessList => { + consoleProcessList.forEach((pid: number) => { + try { + process.kill(pid); + } catch (e) { + // Ignore if process cannot be found (kill ESRCH error) + } + }); + (this._ptyNative as IConptyNative).kill(this._pty); + }); + } else { + (this._ptyNative as IWinptyNative).kill(this._pid, this._innerPidHandle); + // Since pty.kill closes the handle it will kill most processes by itself + // and process IDs can be reused as soon as all handles to them are + // dropped, we want to immediately kill the entire console process list. + // If we do not force kill all processes here, node servers in particular + // seem to become detached and remain running (see + // Microsoft/vscode#26807). + const processList: number[] = (this._ptyNative as IWinptyNative).getProcessList(this._pid); + processList.forEach(pid => { + try { + process.kill(pid); + } catch (e) { + // Ignore if process cannot be found (kill ESRCH error) + } + }); + } + } + + private _getConsoleProcessList(): Promise { + return new Promise(resolve => { + const agent = fork(path.join(__dirname, 'conpty_console_list_agent'), [ this._innerPid.toString() ]); + agent.on('message', message => { + clearTimeout(timeout); + resolve(message.consoleProcessList); + }); + const timeout = setTimeout(() => { + // Something went wrong, just send back the shell PID + agent.kill(); + resolve([ this._innerPid ]); + }, 5000); + }); + } + + public get exitCode(): number { + if (this._useConpty) { + return this._exitCode; + } + return (this._ptyNative as IWinptyNative).getExitCode(this._innerPidHandle); + } + + private _getWindowsBuildNumber(): number { + const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release()); + let buildNumber: number = 0; + if (osVersion && osVersion.length === 4) { + buildNumber = parseInt(osVersion[3]); + } + return buildNumber; + } + + private _generatePipeName(): string { + return `conpty-${Math.random() * 10000000}`; + } + + /** + * Triggered from the native side when a contpy process exits. + */ + private _$onProcessExit(exitCode: number): void { + this._exitCode = exitCode; + this._flushDataAndCleanUp(); + this._outSocket.on('data', () => this._flushDataAndCleanUp()); + } + + private _flushDataAndCleanUp(): void { + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + } + this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL); + } + + private _cleanUpProcess(): void { + this._inSocket.readable = false; + this._inSocket.writable = false; + this._outSocket.readable = false; + this._outSocket.writable = false; + this._outSocket.destroy(); + } +} + +// Convert argc/argv into a Win32 command-line following the escaping convention +// documented on MSDN (e.g. see CommandLineToArgvW documentation). Copied from +// winpty project. +export function argsToCommandLine(file: string, args: ArgvOrCommandLine): string { + if (isCommandLine(args)) { + if (args.length === 0) { + return file; + } + return `${argsToCommandLine(file, [])} ${args}`; + } + const argv = [file]; + Array.prototype.push.apply(argv, args); + let result = ''; + for (let argIndex = 0; argIndex < argv.length; argIndex++) { + if (argIndex > 0) { + result += ' '; + } + const arg = argv[argIndex]; + // if it is empty or it contains whitespace and is not already quoted + const hasLopsidedEnclosingQuote = xOr((arg[0] !== '"'), (arg[arg.length - 1] !== '"')); + const hasNoEnclosingQuotes = ((arg[0] !== '"') && (arg[arg.length - 1] !== '"')); + const quote = + arg === '' || + (arg.indexOf(' ') !== -1 || + arg.indexOf('\t') !== -1) && + ((arg.length > 1) && + (hasLopsidedEnclosingQuote || hasNoEnclosingQuotes)); + if (quote) { + result += '\"'; + } + let bsCount = 0; + for (let i = 0; i < arg.length; i++) { + const p = arg[i]; + if (p === '\\') { + bsCount++; + } else if (p === '"') { + result += repeatText('\\', bsCount * 2 + 1); + result += '"'; + bsCount = 0; + } else { + result += repeatText('\\', bsCount); + bsCount = 0; + result += p; + } + } + if (quote) { + result += repeatText('\\', bsCount * 2); + result += '\"'; + } else { + result += repeatText('\\', bsCount); + } + } + return result; +} + +function isCommandLine(args: ArgvOrCommandLine): args is string { + return typeof args === 'string'; +} + +function repeatText(text: string, count: number): string { + let result = ''; + for (let i = 0; i < count; i++) { + result += text; + } + return result; +} + +function xOr(arg1: boolean, arg2: boolean): boolean { + return ((arg1 && !arg2) || (!arg1 && arg2)); +}