--- /dev/null
+/**
+ * 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<number[]> {
+ return new Promise<number[]>(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));
+}