installed pty
[VSoRC/.git] / node_modules / node-pty / src / windowsPtyAgent.ts
diff --git a/node_modules/node-pty/src/windowsPtyAgent.ts b/node_modules/node-pty/src/windowsPtyAgent.ts
new file mode 100644 (file)
index 0000000..cf8b65d
--- /dev/null
@@ -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<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));
+}