Skip to content

Commit e874053

Browse files
committed
fix: Force terminate the deno subprocess and handle scenarios when trying to terminate the worker before it has been setup
1 parent f99fb95 commit e874053

File tree

6 files changed

+211
-4
lines changed

6 files changed

+211
-4
lines changed

package-lock.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"tslib": "2.0.0",
5454
"gulp": "^4.0.0",
5555
"del": "^3.0.0",
56-
"typedoc": "^0.17.8"
56+
"typedoc": "^0.17.8",
57+
"ps-list": "7.2.0"
5758
},
5859
"dependencies": {
5960
"ws": "^7.3.1",

src/DenoWorker.spec.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { readFileSync } from 'fs';
33
import path from 'path';
44
import { URL } from 'url';
55
import { MessageChannel, MessagePort } from './MessageChannel';
6+
import psList from 'ps-list';
67

78
console.log = jest.fn();
89
jest.setTimeout(10000);
@@ -13,6 +14,8 @@ describe('DenoWorker', () => {
1314
const echoScript = readFileSync(echoFile, { encoding: 'utf-8' });
1415
const pingFile = path.resolve(__dirname, './test/ping.js');
1516
const pingScript = readFileSync(pingFile, { encoding: 'utf-8' });
17+
const infiniteFile = path.resolve(__dirname, './test/infinite.js');
18+
const infiniteScript = readFileSync(infiniteFile, { encoding: 'utf-8' });
1619

1720
afterEach(() => {
1821
if (worker) {
@@ -285,4 +288,158 @@ describe('DenoWorker', () => {
285288
expect(final).toEqual('pong');
286289
});
287290
});
291+
292+
describe('terminate()', () => {
293+
it('should kill the deno process when terminated immediately', async () => {
294+
let denoProcesses = await getDenoProcesses();
295+
expect(denoProcesses).toEqual([]);
296+
297+
worker = new DenoWorker(echoScript, {
298+
permissions: {
299+
allowNet: [`https://google.com`],
300+
},
301+
});
302+
worker.terminate();
303+
304+
denoProcesses = await getDenoProcesses();
305+
expect(denoProcesses).toEqual([]);
306+
});
307+
308+
it('should kill the deno process when terminated after the initial connection', async () => {
309+
let denoProcesses = await getDenoProcesses();
310+
expect(denoProcesses).toEqual([]);
311+
312+
worker = new DenoWorker(echoScript, {
313+
permissions: {
314+
allowNet: [`https://google.com`],
315+
},
316+
});
317+
318+
let ret: any;
319+
let resolve: any;
320+
let promise = new Promise((res, rej) => {
321+
resolve = res;
322+
});
323+
worker.onmessage = (e) => {
324+
ret = e.data;
325+
resolve();
326+
};
327+
328+
worker.postMessage({
329+
type: 'echo',
330+
message: 'Hello',
331+
});
332+
333+
await promise;
334+
335+
worker.terminate();
336+
337+
denoProcesses = await getDenoProcesses();
338+
expect(denoProcesses).toEqual([]);
339+
});
340+
341+
it('should kill the deno process when terminated while sending data', async () => {
342+
let denoProcesses = await getDenoProcesses();
343+
expect(denoProcesses).toEqual([]);
344+
345+
worker = new DenoWorker(echoScript, {
346+
permissions: {
347+
allowNet: [`https://google.com`],
348+
},
349+
});
350+
351+
let ret: any;
352+
let resolve: any;
353+
let promise = new Promise((res, rej) => {
354+
resolve = res;
355+
});
356+
worker.onmessage = (e) => {
357+
ret = e.data;
358+
resolve();
359+
};
360+
361+
worker.terminate();
362+
363+
worker.postMessage({
364+
type: 'echo',
365+
message: 'Hello',
366+
});
367+
368+
denoProcesses = await getDenoProcesses();
369+
expect(denoProcesses).toEqual([]);
370+
});
371+
372+
it('should kill the deno process when terminated while recieving data', async () => {
373+
let denoProcesses = await getDenoProcesses();
374+
expect(denoProcesses).toEqual([]);
375+
376+
worker = new DenoWorker(echoScript, {
377+
permissions: {
378+
allowNet: [`https://google.com`],
379+
},
380+
});
381+
382+
let ret: any;
383+
let resolve: any;
384+
let promise = new Promise((res, rej) => {
385+
resolve = res;
386+
});
387+
worker.onmessage = (e) => {
388+
ret = e.data;
389+
console.log('Message');
390+
resolve();
391+
};
392+
393+
worker.postMessage({
394+
type: 'echo',
395+
message: 'Hello',
396+
});
397+
398+
await Promise.resolve();
399+
400+
worker.terminate();
401+
402+
denoProcesses = await getDenoProcesses();
403+
expect(denoProcesses).toEqual([]);
404+
});
405+
406+
it('should kill the deno process when terminated while the script is in an infinite loop', async () => {
407+
let denoProcesses = await getDenoProcesses();
408+
expect(denoProcesses).toEqual([]);
409+
410+
worker = new DenoWorker(infiniteScript, {
411+
permissions: {
412+
allowNet: [`https://google.com`],
413+
},
414+
});
415+
416+
let ret: any;
417+
let resolve: any;
418+
let promise = new Promise((res, rej) => {
419+
resolve = res;
420+
});
421+
worker.onmessage = (e) => {
422+
ret = e.data;
423+
resolve();
424+
};
425+
426+
await promise;
427+
428+
worker.postMessage({
429+
type: 'echo',
430+
message: 'Hello',
431+
});
432+
433+
worker.terminate();
434+
435+
denoProcesses = await getDenoProcesses();
436+
expect(denoProcesses).toEqual([]);
437+
});
438+
});
288439
});
440+
441+
async function getDenoProcesses() {
442+
const list = await psList();
443+
const denoProcesses = list.filter((p) => /^deno/.test(p.name));
444+
return denoProcesses;
445+
}

src/DenoWorker.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from './MessageTarget';
1717
import { MessagePort } from './MessageChannel';
1818
import { Stream, Readable, Duplex } from 'stream';
19+
import { forceKill } from './Utils';
1920

2021
const DEFAULT_DENO_BOOTSTRAP_SCRIPT_PATH = __dirname.endsWith('src')
2122
? resolve(__dirname, '../deno/index.ts')
@@ -217,6 +218,7 @@ export class DenoWorker {
217218

218219
this._httpServer.listen({ host: '127.0.0.1', port: 0 }, () => {
219220
if (this._terminated) {
221+
this._httpServer.close();
220222
return;
221223
}
222224
const addr = this._httpServer.address();
@@ -240,6 +242,7 @@ export class DenoWorker {
240242
let runArgs = [] as string[];
241243

242244
addOption(runArgs, '--reload', this._options.reload);
245+
// addOption(runArgs, '--unstable', true);
243246

244247
if (this._options.permissions) {
245248
addOption(
@@ -342,12 +345,12 @@ export class DenoWorker {
342345
terminate() {
343346
this._terminated = true;
344347
if (this._process) {
345-
this._process.kill();
348+
// this._process.kill();
349+
forceKill(this._process.pid);
346350
this._process = null;
347351
}
348352
if (this._httpServer) {
349353
this._httpServer.close();
350-
this._httpServer = null;
351354
}
352355
if (this._server) {
353356
this._server.close();
@@ -387,6 +390,9 @@ export class DenoWorker {
387390
data: any,
388391
transfer?: Transferrable[]
389392
) {
393+
if (this._terminated) {
394+
return;
395+
}
390396
this._handleTransferrables(transfer);
391397
const structuredData = serializeStructure(data, transfer);
392398
if (channel !== null) {

src/Utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { MessageChannel, MessagePort } from './MessageChannel';
2+
import { execSync } from 'child_process';
23

34
export function polyfillMessageChannel() {
45
const anyGlobalThis = globalThis as any;
@@ -7,3 +8,32 @@ export function polyfillMessageChannel() {
78
anyGlobalThis.MessagePort = MessagePort;
89
}
910
}
11+
12+
/**
13+
* Forcefully kills the process with the given ID.
14+
* On Linux/Unix, this means sending the process the SIGKILL signal.
15+
* On Windows, this means using the taskkill executable to kill the process.
16+
* @param pid The ID of the process to kill.
17+
*/
18+
export function forceKill(pid: number) {
19+
const isWindows = /^win/.test(process.platform);
20+
if (isWindows) {
21+
return killWindows(pid);
22+
} else {
23+
return killUnix(pid);
24+
}
25+
}
26+
27+
function killWindows(pid: number) {
28+
execSync(`taskkill /PID ${pid} /T /F`);
29+
}
30+
31+
function killUnix(pid: number) {
32+
const signal = 'SIGKILL';
33+
process.kill(pid, signal);
34+
}
35+
36+
export interface ExecResult {
37+
stdout: string;
38+
stdin: string;
39+
}

src/test/infinite.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
self.postMessage('test');
2+
3+
self.onmessage = (e) => {
4+
while (true) {
5+
console.log('Running...');
6+
}
7+
};

0 commit comments

Comments
 (0)