2 * Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
3 * Copyright (c) 2016, Daniel Imms (MIT License).
4 * Copyright (c) 2018, Microsoft Corporation (MIT License).
7 import * as os from 'os';
8 import * as path from 'path';
9 import { Socket } from 'net';
10 import { ArgvOrCommandLine } from './types';
11 import { fork } from 'child_process';
13 let conptyNative: IConptyNative;
14 let winptyNative: IWinptyNative;
17 * The amount of time to wait for additional data after the conpty shell process has exited before
18 * shutting down the socket. The timer will be reset if a new data event comes in after the timer
21 const FLUSH_DATA_INTERVAL = 20;
24 * This agent sits between the WindowsTerminal class and provides a common interface for both conpty
27 export class WindowsPtyAgent {
28 private _inSocket: Socket;
29 private _outSocket: Socket;
31 private _innerPid: number;
32 private _innerPidHandle: number;
33 private _closeTimeout: NodeJS.Timer;
34 private _exitCode: number | undefined;
38 private _ptyNative: IConptyNative | IWinptyNative;
40 public get inSocket(): Socket { return this._inSocket; }
41 public get outSocket(): Socket { return this._outSocket; }
42 public get fd(): any { return this._fd; }
43 public get innerPid(): number { return this._innerPid; }
44 public get pty(): number { return this._pty; }
48 args: ArgvOrCommandLine,
54 private _useConpty: boolean | undefined,
55 conptyInheritCursor: boolean = false
57 if (this._useConpty === undefined || this._useConpty === true) {
58 this._useConpty = this._getWindowsBuildNumber() >= 18309;
60 if (this._useConpty) {
63 conptyNative = require('../build/Release/conpty.node');
64 } catch (outerError) {
66 conptyNative = require('../build/Debug/conpty.node');
67 } catch (innerError) {
68 console.error('innerError', innerError);
69 // Re-throw the exception from the Release require if the Debug require fails as well
77 winptyNative = require('../build/Release/pty.node');
78 } catch (outerError) {
80 winptyNative = require('../build/Debug/pty.node');
81 } catch (innerError) {
82 console.error('innerError', innerError);
83 // Re-throw the exception from the Release require if the Debug require fails as well
89 this._ptyNative = this._useConpty ? conptyNative : winptyNative;
91 // Sanitize input variable.
92 cwd = path.resolve(cwd);
94 // Compose command line
95 const commandLine = argsToCommandLine(file, args);
98 let term: IConptyProcess | IWinptyProcess;
99 if (this._useConpty) {
100 term = (this._ptyNative as IConptyNative).startProcess(file, cols, rows, debug, this._generatePipeName(), conptyInheritCursor);
102 term = (this._ptyNative as IWinptyNative).startProcess(file, commandLine, env, cwd, cols, rows, debug);
103 this._pid = (term as IWinptyProcess).pid;
104 this._innerPid = (term as IWinptyProcess).innerPid;
105 this._innerPidHandle = (term as IWinptyProcess).innerPidHandle;
108 // Not available on windows.
111 // Generated incremental number that has no real purpose besides using it
113 this._pty = term.pty;
115 // Create terminal pipe IPC channel and forward to a local unix socket.
116 this._outSocket = new Socket();
117 this._outSocket.setEncoding('utf8');
118 this._outSocket.connect(term.conout, () => {
119 // TODO: Emit event on agent instead of socket?
122 this._outSocket.emit('ready_datapipe');
125 this._inSocket = new Socket();
126 this._inSocket.setEncoding('utf8');
127 this._inSocket.connect(term.conin);
128 // TODO: Wait for ready event?
130 if (this._useConpty) {
131 const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine, cwd, env, c => this._$onProcessExit(c)
133 this._innerPid = connect.pid;
137 public resize(cols: number, rows: number): void {
138 if (this._useConpty) {
139 if (this._exitCode !== undefined) {
140 throw new Error('Cannot resize a pty that has already exited');
142 this._ptyNative.resize(this._pty, cols, rows);
145 this._ptyNative.resize(this._pid, cols, rows);
148 public kill(): void {
149 this._inSocket.readable = false;
150 this._inSocket.writable = false;
151 this._outSocket.readable = false;
152 this._outSocket.writable = false;
153 // Tell the agent to kill the pty, this releases handles to the process
154 if (this._useConpty) {
155 this._getConsoleProcessList().then(consoleProcessList => {
156 consoleProcessList.forEach((pid: number) => {
160 // Ignore if process cannot be found (kill ESRCH error)
163 (this._ptyNative as IConptyNative).kill(this._pty);
166 (this._ptyNative as IWinptyNative).kill(this._pid, this._innerPidHandle);
167 // Since pty.kill closes the handle it will kill most processes by itself
168 // and process IDs can be reused as soon as all handles to them are
169 // dropped, we want to immediately kill the entire console process list.
170 // If we do not force kill all processes here, node servers in particular
171 // seem to become detached and remain running (see
172 // Microsoft/vscode#26807).
173 const processList: number[] = (this._ptyNative as IWinptyNative).getProcessList(this._pid);
174 processList.forEach(pid => {
178 // Ignore if process cannot be found (kill ESRCH error)
184 private _getConsoleProcessList(): Promise<number[]> {
185 return new Promise<number[]>(resolve => {
186 const agent = fork(path.join(__dirname, 'conpty_console_list_agent'), [ this._innerPid.toString() ]);
187 agent.on('message', message => {
188 clearTimeout(timeout);
189 resolve(message.consoleProcessList);
191 const timeout = setTimeout(() => {
192 // Something went wrong, just send back the shell PID
194 resolve([ this._innerPid ]);
199 public get exitCode(): number {
200 if (this._useConpty) {
201 return this._exitCode;
203 return (this._ptyNative as IWinptyNative).getExitCode(this._innerPidHandle);
206 private _getWindowsBuildNumber(): number {
207 const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release());
208 let buildNumber: number = 0;
209 if (osVersion && osVersion.length === 4) {
210 buildNumber = parseInt(osVersion[3]);
215 private _generatePipeName(): string {
216 return `conpty-${Math.random() * 10000000}`;
220 * Triggered from the native side when a contpy process exits.
222 private _$onProcessExit(exitCode: number): void {
223 this._exitCode = exitCode;
224 this._flushDataAndCleanUp();
225 this._outSocket.on('data', () => this._flushDataAndCleanUp());
228 private _flushDataAndCleanUp(): void {
229 if (this._closeTimeout) {
230 clearTimeout(this._closeTimeout);
232 this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);
235 private _cleanUpProcess(): void {
236 this._inSocket.readable = false;
237 this._inSocket.writable = false;
238 this._outSocket.readable = false;
239 this._outSocket.writable = false;
240 this._outSocket.destroy();
244 // Convert argc/argv into a Win32 command-line following the escaping convention
245 // documented on MSDN (e.g. see CommandLineToArgvW documentation). Copied from
247 export function argsToCommandLine(file: string, args: ArgvOrCommandLine): string {
248 if (isCommandLine(args)) {
249 if (args.length === 0) {
252 return `${argsToCommandLine(file, [])} ${args}`;
255 Array.prototype.push.apply(argv, args);
257 for (let argIndex = 0; argIndex < argv.length; argIndex++) {
261 const arg = argv[argIndex];
262 // if it is empty or it contains whitespace and is not already quoted
263 const hasLopsidedEnclosingQuote = xOr((arg[0] !== '"'), (arg[arg.length - 1] !== '"'));
264 const hasNoEnclosingQuotes = ((arg[0] !== '"') && (arg[arg.length - 1] !== '"'));
267 (arg.indexOf(' ') !== -1 ||
268 arg.indexOf('\t') !== -1) &&
270 (hasLopsidedEnclosingQuote || hasNoEnclosingQuotes));
275 for (let i = 0; i < arg.length; i++) {
279 } else if (p === '"') {
280 result += repeatText('\\', bsCount * 2 + 1);
284 result += repeatText('\\', bsCount);
290 result += repeatText('\\', bsCount * 2);
293 result += repeatText('\\', bsCount);
299 function isCommandLine(args: ArgvOrCommandLine): args is string {
300 return typeof args === 'string';
303 function repeatText(text: string, count: number): string {
305 for (let i = 0; i < count; i++) {
311 function xOr(arg1: boolean, arg2: boolean): boolean {
312 return ((arg1 && !arg2) || (!arg1 && arg2));