Skip to content

Commit ef25999

Browse files
authored
fix: gracefully close vscode on sigint (#208)
Right now, if the test process is killed, VS Code can remain open. Instead tear it down gracefully.
1 parent 83f719c commit ef25999

File tree

14 files changed

+98
-17
lines changed

14 files changed

+98
-17
lines changed

.husky/pre-commit

100644100755
File mode changed.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### 2.3.1 | 2022-04-04
4+
5+
- Gracefully kill VS Code if SIGINT is received
6+
37
### 2.3.0 | 2022-02-27
48

59
- Automatically use the most recent version matching `engines.vscode` in extensions' package.json

killTree.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh
2+
3+
ROOT_PID=$1
4+
SIGNAL=$2
5+
6+
terminateTree() {
7+
for cpid in $(/usr/bin/pgrep -P $1); do
8+
terminateTree $cpid
9+
done
10+
kill -$SIGNAL $1 > /dev/null 2>&1
11+
}
12+
13+
terminateTree $ROOT_PID

lib/runTest.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as cp from 'child_process';
77
import * as path from 'path';
88
import { downloadAndUnzipVSCode, DownloadVersion, DownloadPlatform, defaultCachePath } from './download';
99
import { ProgressReporter } from './progress';
10+
import { killTree } from './util';
1011

1112
export interface TestOptions {
1213
/**
@@ -171,16 +172,37 @@ function hasArg(argName: string, argList: readonly string[]) {
171172
return argList.some((a) => a === `--${argName}` || a.startsWith(`--${argName}=`));
172173
}
173174

175+
const SIGINT = 'SIGINT';
176+
174177
async function innerRunTests(
175178
executable: string,
176179
args: string[],
177180
testRunnerEnv?: {
178181
[key: string]: string | undefined;
179182
}
180183
): Promise<number> {
181-
return new Promise<number>((resolve, reject) => {
182-
const fullEnv = Object.assign({}, process.env, testRunnerEnv);
183-
const cmd = cp.spawn(executable, args, { env: fullEnv });
184+
const fullEnv = Object.assign({}, process.env, testRunnerEnv);
185+
const cmd = cp.spawn(executable, args, { env: fullEnv });
186+
let exitRequested = false;
187+
const ctrlc1 = () => {
188+
process.removeListener(SIGINT, ctrlc1);
189+
process.on(SIGINT, ctrlc2);
190+
console.log('Closing VS Code gracefully. Press Ctrl+C to force close.');
191+
exitRequested = true;
192+
cmd.kill(SIGINT); // this should cause the returned promise to resolve
193+
};
194+
195+
const ctrlc2 = () => {
196+
console.log('Closing VS Code forcefully.');
197+
process.removeListener(SIGINT, ctrlc2);
198+
exitRequested = true;
199+
killTree(cmd.pid!, true);
200+
};
201+
202+
const prom = new Promise<number>((resolve, reject) => {
203+
if (cmd.pid) {
204+
process.on(SIGINT, ctrlc1);
205+
}
184206

185207
cmd.stdout.on('data', (d) => process.stdout.write(d));
186208
cmd.stderr.on('data', (d) => process.stderr.write(d));
@@ -213,7 +235,21 @@ async function innerRunTests(
213235
}
214236

215237
cmd.on('close', onProcessClosed);
216-
217238
cmd.on('exit', onProcessClosed);
218239
});
240+
241+
let code: number;
242+
try {
243+
code = await prom;
244+
} finally {
245+
process.removeListener(SIGINT, ctrlc1);
246+
process.removeListener(SIGINT, ctrlc2);
247+
}
248+
249+
// exit immediately if we handled a SIGINT and no one else did
250+
if (exitRequested && process.listenerCount(SIGINT) === 0) {
251+
process.exit(1);
252+
}
253+
254+
return code;
219255
}

lib/util.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { ChildProcess, spawn } from 'child_process';
7+
import { readFileSync } from 'fs';
8+
import * as createHttpProxyAgent from 'http-proxy-agent';
9+
import * as https from 'https';
10+
import * as createHttpsProxyAgent from 'https-proxy-agent';
611
import * as path from 'path';
712
import { URL } from 'url';
8-
import * as https from 'https';
9-
import * as request from './request';
1013
import { DownloadPlatform } from './download';
11-
import * as createHttpsProxyAgent from 'https-proxy-agent';
12-
import * as createHttpProxyAgent from 'http-proxy-agent';
13-
import { readFileSync } from 'fs';
14-
import { getProfileArguments, TestOptions } from './runTest';
14+
import * as request from './request';
15+
import { TestOptions, getProfileArguments } from './runTest';
1516

1617
export let systemDefaultPlatform: DownloadPlatform;
1718

@@ -27,8 +28,8 @@ switch (process.platform) {
2728
process.arch === 'arm64'
2829
? 'win32-arm64-archive'
2930
: process.arch === 'ia32'
30-
? 'win32-archive'
31-
: 'win32-x64-archive';
31+
? 'win32-archive'
32+
: 'win32-x64-archive';
3233
break;
3334
default:
3435
systemDefaultPlatform =
@@ -232,3 +233,31 @@ export function onceWithoutRejections<T, Args extends unknown[]>(fn: (...args: A
232233
return value;
233234
};
234235
}
236+
237+
export function killTree(processId: number, force: boolean) {
238+
let cp: ChildProcess;
239+
240+
if (process.platform === 'win32') {
241+
const windir = process.env['WINDIR'] || 'C:\\Windows';
242+
243+
// when killing a process in Windows its child processes are *not* killed but become root processes.
244+
// Therefore we use TASKKILL.EXE
245+
cp = spawn(path.join(windir, 'System32', 'taskkill.exe'), [
246+
...force ? ['/F'] : [],
247+
'/T',
248+
'/PID',
249+
processId.toString(),
250+
], { stdio: 'inherit' });
251+
} else {
252+
// on linux and OS X we kill all direct and indirect child processes as well
253+
cp = spawn('sh', [
254+
path.resolve(__dirname, '../killTree.sh'),
255+
processId.toString(),
256+
force ? '9' : '15',
257+
], { stdio: 'inherit' });
258+
}
259+
260+
return new Promise<void>((resolve, reject) => {
261+
cp.on('error', reject).on('exit', resolve);
262+
});
263+
}

sample/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"vscode:prepublish": "yarn run compile",
2626
"compile": "tsc -p ./",
2727
"watch": "tsc -watch -p ./",
28-
"test": "tsc -p ./ && node ./out/sample/test/runTest.js"
28+
"test": "cd .. && tsc && cd sample tsc && node ./out/test/runTest.js"
2929
},
3030
"devDependencies": {
3131
"@types/glob": "^7.1.3",

sample/test/runTest.ts renamed to sample/src/test/runTest.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as path from 'path';
22
import * as cp from 'child_process';
33

4-
import { runTests, downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath } from '../../lib/index';
4+
import { runTests, downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath } from '../../..';
55

66
async function go() {
77
try {
@@ -17,7 +17,7 @@ async function go() {
1717
});
1818

1919
const extensionTestsPath2 = path.resolve(__dirname, './suite2');
20-
const testWorkspace = path.resolve(__dirname, '../../../test-fixtures/fixture1');
20+
const testWorkspace = path.resolve(__dirname, '../../src/test-fixtures/fixture1');
2121

2222
/**
2323
* Running another test suite on a specific workspace
File renamed without changes.
File renamed without changes.

sample/tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
"skipLibCheck": true
99
},
1010
"include": [
11-
"src",
12-
"test"
11+
"src"
1312
],
1413
"exclude": ["node_modules", ".vscode-test"]
1514
}

0 commit comments

Comments
 (0)