-
Notifications
You must be signed in to change notification settings - Fork 253
/
Copy pathwindowsPtyAgent.ts
322 lines (293 loc) · 10.1 KB
/
windowsPtyAgent.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
/**
* 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 fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { Socket } from 'net';
import { ArgvOrCommandLine } from './types';
import { fork } from 'child_process';
import { ConoutConnection } from './windowsConoutConnection';
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 = 1000;
/**
* 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 = 0;
private _innerPid: number = 0;
private _closeTimeout: NodeJS.Timer | undefined;
private _exitCode: number | undefined;
private _conoutSocketWorker: ConoutConnection;
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;
}
// 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');
// The conout socket must be ready out on another thread to avoid deadlocks
this._conoutSocketWorker = new ConoutConnection(term.conout);
this._conoutSocketWorker.onReady(() => {
this._conoutSocketWorker.connectSocket(this._outSocket);
});
this._outSocket.on('connect', () => {
this._outSocket.emit('ready_datapipe');
});
const inSocketFD = fs.openSync(term.conin, 'w');
this._inSocket = new Socket({
fd: inSocketFD,
readable: false,
writable: true
});
this._inSocket.setEncoding('utf8');
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 clear(): void {
if (this._useConpty) {
(this._ptyNative as IConptyNative).clear(this._pty);
}
}
public kill(): void {
this._inSocket.readable = false;
this._outSocket.readable = 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 {
// Because pty.kill closes the handle, it will kill most processes by itself.
// Process IDs can be reused as soon as all handles to them are
// dropped, so 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);
(this._ptyNative as IWinptyNative).kill(this._pid, this._innerPid);
processList.forEach(pid => {
try {
process.kill(pid);
} catch (e) {
// Ignore if process cannot be found (kill ESRCH error)
}
});
}
this._conoutSocketWorker.dispose();
}
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 | undefined {
if (this._useConpty) {
return this._exitCode;
}
const winptyExitCode = (this._ptyNative as IWinptyNative).getExitCode(this._innerPid);
return winptyExitCode === -1 ? undefined : winptyExitCode;
}
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._outSocket.readable = 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));
}