installed pty
[VSoRC/.git] / node_modules / node-pty / src / windowsPtyAgent.ts
1 /**
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).
5  */
6
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';
12
13 let conptyNative: IConptyNative;
14 let winptyNative: IWinptyNative;
15
16 /**
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
19  * has started.
20  */
21 const FLUSH_DATA_INTERVAL = 20;
22
23 /**
24  * This agent sits between the WindowsTerminal class and provides a common interface for both conpty
25  * and winpty.
26  */
27 export class WindowsPtyAgent {
28   private _inSocket: Socket;
29   private _outSocket: Socket;
30   private _pid: number;
31   private _innerPid: number;
32   private _innerPidHandle: number;
33   private _closeTimeout: NodeJS.Timer;
34   private _exitCode: number | undefined;
35
36   private _fd: any;
37   private _pty: number;
38   private _ptyNative: IConptyNative | IWinptyNative;
39
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; }
45
46   constructor(
47     file: string,
48     args: ArgvOrCommandLine,
49     env: string[],
50     cwd: string,
51     cols: number,
52     rows: number,
53     debug: boolean,
54     private _useConpty: boolean | undefined,
55     conptyInheritCursor: boolean = false
56   ) {
57     if (this._useConpty === undefined || this._useConpty === true) {
58       this._useConpty = this._getWindowsBuildNumber() >= 18309;
59     }
60     if (this._useConpty) {
61       if (!conptyNative) {
62         try {
63           conptyNative = require('../build/Release/conpty.node');
64         } catch (outerError) {
65           try {
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
70             throw outerError;
71           }
72         }
73       }
74     } else {
75       if (!winptyNative) {
76         try {
77           winptyNative = require('../build/Release/pty.node');
78         } catch (outerError) {
79           try {
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
84             throw outerError;
85           }
86         }
87       }
88     }
89     this._ptyNative = this._useConpty ? conptyNative : winptyNative;
90
91     // Sanitize input variable.
92     cwd = path.resolve(cwd);
93
94     // Compose command line
95     const commandLine = argsToCommandLine(file, args);
96
97     // Open pty session.
98     let term: IConptyProcess | IWinptyProcess;
99     if (this._useConpty) {
100       term = (this._ptyNative as IConptyNative).startProcess(file, cols, rows, debug, this._generatePipeName(), conptyInheritCursor);
101     } else {
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;
106     }
107
108     // Not available on windows.
109     this._fd = term.fd;
110
111     // Generated incremental number that has no real purpose besides  using it
112     // as a terminal id.
113     this._pty = term.pty;
114
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?
120
121       // Emit ready event.
122       this._outSocket.emit('ready_datapipe');
123     });
124
125     this._inSocket = new Socket();
126     this._inSocket.setEncoding('utf8');
127     this._inSocket.connect(term.conin);
128     // TODO: Wait for ready event?
129
130     if (this._useConpty) {
131       const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine, cwd, env, c => this._$onProcessExit(c)
132 );
133       this._innerPid = connect.pid;
134     }
135   }
136
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');
141       }
142       this._ptyNative.resize(this._pty, cols, rows);
143       return;
144     }
145     this._ptyNative.resize(this._pid, cols, rows);
146   }
147
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) => {
157           try {
158             process.kill(pid);
159           } catch (e) {
160             // Ignore if process cannot be found (kill ESRCH error)
161           }
162         });
163         (this._ptyNative as IConptyNative).kill(this._pty);
164       });
165     } else {
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 => {
175         try {
176           process.kill(pid);
177         } catch (e) {
178           // Ignore if process cannot be found (kill ESRCH error)
179         }
180       });
181     }
182   }
183
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);
190       });
191       const timeout = setTimeout(() => {
192         // Something went wrong, just send back the shell PID
193         agent.kill();
194         resolve([ this._innerPid ]);
195       }, 5000);
196     });
197   }
198
199   public get exitCode(): number {
200     if (this._useConpty) {
201       return this._exitCode;
202     }
203     return (this._ptyNative as IWinptyNative).getExitCode(this._innerPidHandle);
204   }
205
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]);
211     }
212     return buildNumber;
213   }
214
215   private _generatePipeName(): string {
216     return `conpty-${Math.random() * 10000000}`;
217   }
218
219   /**
220    * Triggered from the native side when a contpy process exits.
221    */
222   private _$onProcessExit(exitCode: number): void {
223     this._exitCode = exitCode;
224     this._flushDataAndCleanUp();
225     this._outSocket.on('data', () => this._flushDataAndCleanUp());
226   }
227
228   private _flushDataAndCleanUp(): void {
229     if (this._closeTimeout) {
230       clearTimeout(this._closeTimeout);
231     }
232     this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);
233   }
234
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();
241   }
242 }
243
244 // Convert argc/argv into a Win32 command-line following the escaping convention
245 // documented on MSDN (e.g. see CommandLineToArgvW documentation). Copied from
246 // winpty project.
247 export function argsToCommandLine(file: string, args: ArgvOrCommandLine): string {
248   if (isCommandLine(args)) {
249     if (args.length === 0) {
250       return file;
251     }
252     return `${argsToCommandLine(file, [])} ${args}`;
253   }
254   const argv = [file];
255   Array.prototype.push.apply(argv, args);
256   let result = '';
257   for (let argIndex = 0; argIndex < argv.length; argIndex++) {
258     if (argIndex > 0) {
259       result += ' ';
260     }
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] !== '"'));
265     const quote =
266       arg === '' ||
267       (arg.indexOf(' ') !== -1 ||
268       arg.indexOf('\t') !== -1) &&
269       ((arg.length > 1) &&
270       (hasLopsidedEnclosingQuote || hasNoEnclosingQuotes));
271     if (quote) {
272       result += '\"';
273     }
274     let bsCount = 0;
275     for (let i = 0; i < arg.length; i++) {
276       const p = arg[i];
277       if (p === '\\') {
278         bsCount++;
279       } else if (p === '"') {
280         result += repeatText('\\', bsCount * 2 + 1);
281         result += '"';
282         bsCount = 0;
283       } else {
284         result += repeatText('\\', bsCount);
285         bsCount = 0;
286         result += p;
287       }
288     }
289     if (quote) {
290       result += repeatText('\\', bsCount * 2);
291       result += '\"';
292     } else {
293       result += repeatText('\\', bsCount);
294     }
295   }
296   return result;
297 }
298
299 function isCommandLine(args: ArgvOrCommandLine): args is string {
300   return typeof args === 'string';
301 }
302
303 function repeatText(text: string, count: number): string {
304   let result = '';
305   for (let i = 0; i < count; i++) {
306     result += text;
307   }
308   return result;
309 }
310
311 function xOr(arg1: boolean, arg2: boolean): boolean {
312   return ((arg1 && !arg2) || (!arg1 && arg2));
313 }