Skip to content

Commit 2ce2d79

Browse files
committed
Improve taskkill usage
1 parent 5944b2d commit 2ce2d79

2 files changed

Lines changed: 156 additions & 6 deletions

File tree

lib/terminate/kill-descendants.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import process from 'node:process';
22
import {execFile} from 'node:child_process';
3+
import path from 'node:path/win32';
34

45
const isWindows = process.platform === 'win32';
56

@@ -40,15 +41,39 @@ const killDescendantsUnix = (subprocess, signal) => {
4041
};
4142

4243
// Windows has no process groups. Instead, `taskkill /T` recursively terminates the process tree.
43-
// It must run while the tree is still intact, so it is the only termination performed: killing the
44-
// direct subprocess first would orphan its descendants before `taskkill` could enumerate them.
45-
// Windows does not support signals, so the signal argument is ignored (`/F` terminates the tree).
46-
const killDescendantsWindows = subprocess => {
44+
// It must run while the tree is still intact, so direct subprocess termination is only used as a
45+
// fallback: killing the direct subprocess first would orphan its descendants before `taskkill` could enumerate them.
46+
// If `taskkill` is unavailable or fails, the fallback only terminates the direct subprocess.
47+
// `taskkill` ignores the signal (`/F` terminates the tree).
48+
const killDescendantsWindows = (subprocess, signal) => {
4749
if (subprocess.pid === undefined) {
4850
return false;
4951
}
5052

51-
// This is best-effort: any error (such as the subprocess having already exited) is ignored.
52-
execFile('taskkill', ['/pid', `${subprocess.pid}`, '/T', '/F'], () => {});
53+
const taskkillFile = getTaskkillFile();
54+
if (taskkillFile === undefined) {
55+
return subprocess.kill(signal);
56+
}
57+
58+
// This is best-effort: if `taskkill` fails, still try the direct subprocess.
59+
execFile(taskkillFile, ['/pid', `${subprocess.pid}`, '/T', '/F'], error => {
60+
if (error) {
61+
subprocess.kill(signal);
62+
}
63+
});
5364
return true;
5465
};
66+
67+
export const getTaskkillFile = () => {
68+
const windowsDirectory = [process.env.SystemRoot, process.env.windir]
69+
.find(directory => directory && isWindowsDriveAbsolutePath(directory));
70+
71+
return windowsDirectory === undefined
72+
? undefined
73+
: path.join(windowsDirectory, 'System32', 'taskkill.exe');
74+
};
75+
76+
const isWindowsDriveAbsolutePath = directory => {
77+
const {root} = path.parse(directory);
78+
return /^[a-z]:[/\\]/i.test(root);
79+
};

test/terminate/kill-descendants.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import childProcess from 'node:child_process';
2+
import {syncBuiltinESMExports} from 'node:module';
3+
import path from 'node:path/win32';
14
import process from 'node:process';
25
import {setTimeout} from 'node:timers/promises';
36
import test from 'ava';
47
import isRunning from 'is-running';
58
import {execa, execaSync} from '../../index.js';
9+
import {getTaskkillFile} from '../../lib/terminate/kill-descendants.js';
610
import {setFixtureDirectory} from '../helpers/fixtures-directory.js';
711

812
setFixtureDirectory();
@@ -88,3 +92,124 @@ test('Cannot use "killDescendants" option, sync', t => {
8892
execaSync('empty.js', {killDescendants: true});
8993
}, {message: /The "killDescendants: true" option cannot be used/});
9094
});
95+
96+
test.serial('taskkill is resolved from the Windows directory when available', t => {
97+
const {SystemRoot, windir} = process.env;
98+
t.teardown(() => {
99+
restoreEnvironment('SystemRoot', SystemRoot);
100+
restoreEnvironment('windir', windir);
101+
});
102+
103+
process.env.SystemRoot = 'C:\\Windows';
104+
process.env.windir = 'D:\\Windows';
105+
t.is(getTaskkillFile(), path.join('C:\\Windows', 'System32', 'taskkill.exe'));
106+
107+
process.env.SystemRoot = 'C:/Windows';
108+
t.is(getTaskkillFile(), path.join('C:/Windows', 'System32', 'taskkill.exe'));
109+
110+
process.env.SystemRoot = 'Windows';
111+
t.is(getTaskkillFile(), path.join('D:\\Windows', 'System32', 'taskkill.exe'));
112+
113+
process.env.SystemRoot = '\\Windows';
114+
t.is(getTaskkillFile(), path.join('D:\\Windows', 'System32', 'taskkill.exe'));
115+
116+
delete process.env.SystemRoot;
117+
t.is(getTaskkillFile(), path.join('D:\\Windows', 'System32', 'taskkill.exe'));
118+
119+
process.env.windir = 'Windows';
120+
t.is(getTaskkillFile(), undefined);
121+
122+
process.env.windir = '\\Windows';
123+
t.is(getTaskkillFile(), undefined);
124+
125+
process.env.windir = '\\\\server\\share\\Windows';
126+
t.is(getTaskkillFile(), undefined);
127+
128+
process.env.windir = '';
129+
t.is(getTaskkillFile(), undefined);
130+
131+
delete process.env.windir;
132+
t.is(getTaskkillFile(), undefined);
133+
});
134+
135+
test.serial('taskkill fallback uses direct subprocess kill when Windows directory is unavailable', async t => {
136+
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
137+
const {SystemRoot, windir} = process.env;
138+
t.teardown(() => {
139+
Object.defineProperty(process, 'platform', platformDescriptor);
140+
restoreEnvironment('SystemRoot', SystemRoot);
141+
restoreEnvironment('windir', windir);
142+
});
143+
144+
Object.defineProperty(process, 'platform', {value: 'win32'});
145+
process.env.SystemRoot = 'Windows';
146+
delete process.env.windir;
147+
148+
const {getKillFunction} = await import(`../../lib/terminate/kill-descendants.js?taskkill-fallback=${Date.now()}`);
149+
let killedWith;
150+
const subprocess = {
151+
pid: 123,
152+
kill(signal) {
153+
killedWith = signal;
154+
return true;
155+
},
156+
};
157+
158+
const kill = getKillFunction(subprocess, {killDescendants: true});
159+
t.true(kill('SIGTERM'));
160+
t.is(killedWith, 'SIGTERM');
161+
});
162+
163+
test.serial('taskkill fallback uses direct subprocess kill when taskkill cannot be spawned', async t => {
164+
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
165+
const originalExecFile = childProcess.execFile;
166+
const {SystemRoot, windir} = process.env;
167+
t.teardown(() => {
168+
Object.defineProperty(process, 'platform', platformDescriptor);
169+
childProcess.execFile = originalExecFile;
170+
syncBuiltinESMExports();
171+
restoreEnvironment('SystemRoot', SystemRoot);
172+
restoreEnvironment('windir', windir);
173+
});
174+
175+
Object.defineProperty(process, 'platform', {value: 'win32'});
176+
process.env.SystemRoot = 'C:\\MissingWindows';
177+
delete process.env.windir;
178+
179+
const taskkillFailure = Promise.withResolvers();
180+
childProcess.execFile = (file, arguments_, callback) => {
181+
t.is(file, path.join('C:\\MissingWindows', 'System32', 'taskkill.exe'));
182+
t.deepEqual(arguments_, ['/pid', '123', '/T', '/F']);
183+
queueMicrotask(() => {
184+
callback(new Error('spawn failed'));
185+
taskkillFailure.resolve();
186+
});
187+
};
188+
189+
syncBuiltinESMExports();
190+
191+
const {getKillFunction} = await import(`../../lib/terminate/kill-descendants.js?taskkill-spawn-fallback=${Date.now()}`);
192+
let killedWith;
193+
const subprocess = {
194+
pid: 123,
195+
kill(signal) {
196+
killedWith = signal;
197+
return true;
198+
},
199+
};
200+
201+
const kill = getKillFunction(subprocess, {killDescendants: true});
202+
t.true(kill('SIGTERM'));
203+
t.is(killedWith, undefined);
204+
205+
await taskkillFailure.promise;
206+
t.is(killedWith, 'SIGTERM');
207+
});
208+
209+
const restoreEnvironment = (name, value) => {
210+
if (value === undefined) {
211+
delete process.env[name];
212+
} else {
213+
process.env[name] = value;
214+
}
215+
};

0 commit comments

Comments
 (0)