Skip to content
This repository was archived by the owner on Oct 5, 2022. It is now read-only.

Commit e49c3c3

Browse files
Tyriarpaul-marechal
authored andcommittedJun 20, 2019
Fix kill under conpty
1 parent 33560c0 commit e49c3c3

10 files changed

+183
-67
lines changed
 

‎binding.gyp

+9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@
1515
'shlwapi.lib'
1616
]
1717
},
18+
{
19+
'target_name': 'conpty_console_list',
20+
'include_dirs' : [
21+
'<!(node -e "require(\'nan\')")'
22+
],
23+
'sources' : [
24+
'src/win/conpty_console_list.cc'
25+
]
26+
},
1827
{
1928
'target_name': 'pty',
2029
'include_dirs' : [

‎package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
],
3535
"scripts": {
3636
"tsc": "tsc",
37+
"watch": "tsc -w",
3738
"lint": "tslint 'src/**/*.ts'",
3839
"install": "node scripts/install.js",
3940
"postinstall": "node scripts/post-install.js",
@@ -46,11 +47,14 @@
4647
"devDependencies": {
4748
"@types/mocha": "^5.0.0",
4849
"@types/node": "^6.0.104",
50+
"@types/ps-list": "^6.0.0",
4951
"cross-env": "^5.1.4",
5052
"mocha": "^5.0.5",
5153
"pollUntil": "^1.0.3",
54+
"ps-list": "^6.0.0",
5255
"tslint": "^5.12.1",
5356
"tslint-consistent-codestyle": "^1.15.0",
54-
"typescript": "^3.2.2"
57+
"typescript": "^3.2.2",
58+
"windows-process-tree": "^0.2.3"
5559
}
5660
}

‎scripts/post-install.js

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ var RELEASE_DIR = path.join(__dirname, '..', 'build', 'Release');
55
var BUILD_FILES = [
66
path.join(RELEASE_DIR, 'conpty.node'),
77
path.join(RELEASE_DIR, 'conpty.pdb'),
8+
path.join(RELEASE_DIR, 'conpty_console_list.node'),
9+
path.join(RELEASE_DIR, 'conpty_console_list.pdb'),
810
path.join(RELEASE_DIR, 'pty.node'),
911
path.join(RELEASE_DIR, 'pty.pdb'),
1012
path.join(RELEASE_DIR, 'winpty-agent.exe'),

‎src/conpty_console_list_agent.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Copyright (c) 2019, Microsoft Corporation (MIT License).
3+
*
4+
* This module fetches the console process list for a particular PID. It must be
5+
* called from a different process (child_process.fork) as there can only be a
6+
* single console attached to a process.
7+
*/
8+
9+
import { loadNative } from './utils';
10+
11+
const getConsoleProcessList = loadNative('conpty_console_list').getConsoleProcessList;
12+
const shellPid = parseInt(process.argv[2], 10);
13+
const consoleProcessList = getConsoleProcessList(shellPid);
14+
process.send({ consoleProcessList });
15+
process.exit(0);

‎src/native.d.ts

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
interface IConptyNative {
66
startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string): IConptyProcess;
77
connect(ptyId: number, commandLine: string, cwd: string, env: string[], onExitCallback: (exitCode: number) => void): { pid: number };
8-
getProcessList(shellPid: number): number[];
98
resize(ptyId: number, cols: number, rows: number): void;
109
kill(ptyId: number): void;
1110
}

‎src/win/conpty.cc

-42
Original file line numberDiff line numberDiff line change
@@ -419,52 +419,11 @@ static NAN_METHOD(PtyKill) {
419419
}
420420
}
421421

422-
// Fetch the console process tree
423-
// auto processList = std::vector<DWORD>(64);
424-
// auto processCount = GetConsoleProcessList(&processList[0], processList.size());
425-
// if (processList.size() < processCount) {
426-
// processList.resize(processCount);
427-
// processCount = GetConsoleProcessList(&processList[0], processList.size());
428-
// }
429-
430-
// // Kill the console process tree
431-
// for (DWORD i = 0; i < processCount; i++) {
432-
// TerminateProcess(processList[i], 1);
433-
// }
434-
435-
// Close the shell handle
436422
CloseHandle(handle->hShell);
437423

438424
return info.GetReturnValue().SetUndefined();
439425
}
440426

441-
static NAN_METHOD(PtyGetProcessList) {
442-
Nan::HandleScope scope;
443-
444-
if (info.Length() != 1 ||
445-
!info[0]->IsNumber()) {
446-
Nan::ThrowError("Usage: pty.getProcessList(shellPid)");
447-
return;
448-
}
449-
450-
int shellPid = info[0]->Int32Value();
451-
452-
AttachConsole(shellPid);
453-
auto processList = std::vector<DWORD>(64);
454-
auto processCount = GetConsoleProcessList(&processList[0], processList.size());
455-
if (processList.size() < processCount) {
456-
processList.resize(processCount);
457-
processCount = GetConsoleProcessList(&processList[0], processList.size());
458-
}
459-
FreeConsole();
460-
461-
auto result = Nan::New<v8::Array>(processCount);
462-
for (DWORD i = 0; i < processCount; i++) {
463-
result->Set(i, Nan::New<v8::Number>(processList[i]));
464-
}
465-
info.GetReturnValue().Set(result);
466-
}
467-
468427
/**
469428
* Init
470429
*/
@@ -475,7 +434,6 @@ extern "C" void init(v8::Handle<v8::Object> target) {
475434
Nan::SetMethod(target, "connect", PtyConnect);
476435
Nan::SetMethod(target, "resize", PtyResize);
477436
Nan::SetMethod(target, "kill", PtyKill);
478-
Nan::SetMethod(target, "getProcessList", PtyGetProcessList);
479437
};
480438

481439
NODE_MODULE(pty, init);

‎src/win/conpty_console_list.cc

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Copyright (c) 2019, Microsoft Corporation (MIT License).
3+
*/
4+
5+
#include <nan.h>
6+
#include <windows.h>
7+
8+
static NAN_METHOD(ApiConsoleProcessList) {
9+
if (info.Length() != 1 ||
10+
!info[0]->IsNumber()) {
11+
Nan::ThrowError("Usage: getConsoleProcessList(shellPid)");
12+
return;
13+
}
14+
15+
const SHORT pid = info[0]->Uint32Value();
16+
17+
if (!FreeConsole()) {
18+
Nan::ThrowError("FreeConsole failed");
19+
}
20+
if (!AttachConsole(pid)) {
21+
Nan::ThrowError("AttachConsole failed");
22+
}
23+
auto processList = std::vector<DWORD>(64);
24+
auto processCount = GetConsoleProcessList(&processList[0], processList.size());
25+
if (processList.size() < processCount) {
26+
processList.resize(processCount);
27+
processCount = GetConsoleProcessList(&processList[0], processList.size());
28+
}
29+
FreeConsole();
30+
31+
v8::Local<v8::Array> result = Nan::New<v8::Array>();
32+
for (DWORD i = 0; i < processCount; i++) {
33+
result->Set(i, Nan::New<v8::Number>(processList[i]));
34+
}
35+
info.GetReturnValue().Set(result);
36+
}
37+
38+
extern "C" void init(v8::Handle<v8::Object> target) {
39+
Nan::HandleScope scope;
40+
Nan::SetMethod(target, "getConsoleProcessList", ApiConsoleProcessList);
41+
};
42+
43+
NODE_MODULE(pty, init);

‎src/windowsPtyAgent.ts

+40-15
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as path from 'path';
99
import { Socket } from 'net';
1010
import { ArgvOrCommandLine } from './types';
1111
import { loadNative } from './utils';
12+
import { fork } from 'child_process';
1213

1314
let conptyNative: IConptyNative;
1415
let winptyNative: IWinptyNative;
@@ -130,24 +131,48 @@ export class WindowsPtyAgent {
130131
this._outSocket.writable = false;
131132
// Tell the agent to kill the pty, this releases handles to the process
132133
if (this._useConpty) {
133-
(this._ptyNative as IConptyNative).kill(this._pty);
134+
this._getConsoleProcessList().then(consoleProcessList => {
135+
consoleProcessList.forEach((pid: number) => {
136+
try {
137+
process.kill(pid);
138+
} catch (e) {
139+
// Ignore if process cannot be found (kill ESRCH error)
140+
}
141+
});
142+
(this._ptyNative as IConptyNative).kill(this._pty);
143+
});
134144
} else {
135145
(this._ptyNative as IWinptyNative).kill(this._pid, this._innerPidHandle);
146+
// Since pty.kill closes the handle it will kill most processes by itself
147+
// and process IDs can be reused as soon as all handles to them are
148+
// dropped, we want to immediately kill the entire console process list.
149+
// If we do not force kill all processes here, node servers in particular
150+
// seem to become detached and remain running (see
151+
// Microsoft/vscode#26807).
152+
const processList: number[] = (this._ptyNative as IWinptyNative).getProcessList(this._pid);
153+
processList.forEach(pid => {
154+
try {
155+
process.kill(pid);
156+
} catch (e) {
157+
// Ignore if process cannot be found (kill ESRCH error)
158+
}
159+
});
136160
}
137-
// Since pty.kill closes the handle it will kill most processes by itself
138-
// and process IDs can be reused as soon as all handles to them are
139-
// dropped, we want to immediately kill the entire console process list.
140-
// If we do not force kill all processes here, node servers in particular
141-
// seem to become detached and remain running (see
142-
// Microsoft/vscode#26807).
143-
const processList: number[] = this._ptyNative.getProcessList(this._useConpty ? this._innerPid : this._pid);
144-
console.log('processList', processList.join(', '));
145-
processList.forEach(pid => {
146-
try {
147-
process.kill(pid);
148-
} catch (e) {
149-
// Ignore if process cannot be found (kill ESRCH error)
150-
}
161+
}
162+
163+
private _getConsoleProcessList(): Promise<number[]> {
164+
return new Promise<number[]>(resolve => {
165+
const agent = fork(path.join(__dirname, 'conpty_console_list_agent'), [ this._innerPid.toString() ]);
166+
agent.on('message', message => {
167+
clearTimeout(timeout);
168+
resolve(message.consoleProcessList);
169+
});
170+
const timeout = setTimeout(() => {
171+
// Something went wrong, just send back the shell PID
172+
console.error('Could not fetch console process list');
173+
agent.kill();
174+
resolve([ this._innerPid ]);
175+
}, 5000);
151176
});
152177
}
153178

‎src/windowsTerminal.test.ts

+68-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,48 @@ import * as fs from 'fs';
88
import * as assert from 'assert';
99
import { WindowsTerminal } from './windowsTerminal';
1010
import * as path from 'path';
11+
import { getProcessList } from 'windows-process-tree';
12+
import * as psList from 'ps-list';
13+
14+
interface IProcessState {
15+
// Whether the PID must exist or must not exist
16+
[pid: number]: boolean;
17+
}
18+
19+
function pollForProcessState(desiredState: IProcessState, intervalMs: number = 100, timeoutMs: number = 2000): Promise<void> {
20+
return new Promise<void>(resolve => {
21+
let tries = 0;
22+
const interval = setInterval(() => {
23+
psList({ all: true }).then(ps => {
24+
let success = true;
25+
const pids = Object.keys(desiredState).map(k => parseInt(k, 10));
26+
pids.forEach(pid => {
27+
if (desiredState[pid]) {
28+
if (!ps.some(p => p.pid === pid)) {
29+
success = false;
30+
}
31+
} else {
32+
if (ps.some(p => p.pid === pid)) {
33+
success = false;
34+
}
35+
}
36+
});
37+
if (success) {
38+
clearInterval(interval);
39+
resolve();
40+
return;
41+
}
42+
tries++;
43+
if (tries * intervalMs >= timeoutMs) {
44+
clearInterval(interval);
45+
const processListing = pids.map(k => `${k}: ${desiredState[k]}`).join('\n');
46+
assert.fail(`Bad process state, expected:\n${processListing}`);
47+
resolve();
48+
}
49+
});
50+
}, intervalMs);
51+
});
52+
}
1153

1254
if (process.platform === 'win32') {
1355
describe('WindowsTerminal', () => {
@@ -18,15 +60,34 @@ if (process.platform === 'win32') {
1860
// Add done call to deferred function queue to ensure the kill call has completed
1961
(<any>term)._defer(done);
2062
});
21-
it('should kill the process tree', (done) => {
63+
it('should kill the process tree', function (done: Mocha.Done): void {
64+
this.timeout(5000);
65+
2266
const term = new WindowsTerminal('cmd.exe', [], {});
2367
// Start a sub-process
24-
term.write('powershell.exe');
25-
const proc = cp.execSync(`tasklist /fi "PID eq ${term.pid}"`);
26-
const index = proc.toString().indexOf(term.pid.toString());
27-
console.log('******* ' + index + ', ' + proc.toString());
28-
// term.pid
29-
// term.kill();
68+
term.write('powershell.exe\r');
69+
term.write('notepad.exe\r');
70+
term.write('node.exe\r');
71+
setTimeout(() => {
72+
getProcessList(term.pid, list => {
73+
assert.equal(list.length, 4);
74+
assert.equal(list[0].name, 'cmd.exe');
75+
assert.equal(list[1].name, 'powershell.exe');
76+
assert.equal(list[2].name, 'notepad.exe');
77+
assert.equal(list[3].name, 'node.exe');
78+
term.kill();
79+
const desiredState: IProcessState = {};
80+
desiredState[list[0].pid] = false;
81+
desiredState[list[1].pid] = false;
82+
desiredState[list[2].pid] = true;
83+
desiredState[list[3].pid] = false;
84+
pollForProcessState(desiredState).then(() => {
85+
// Kill notepad before done
86+
process.kill(list[2].pid);
87+
done();
88+
});
89+
});
90+
}, 1000);
3091
});
3192
});
3293

‎tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"outDir": "lib",
77
"sourceMap": true,
88
"lib": [
9-
"es5"
9+
"es2015"
1010
],
1111
"alwaysStrict": true,
1212
"noImplicitAny": true,

0 commit comments

Comments
 (0)
This repository has been archived.