-
Notifications
You must be signed in to change notification settings - Fork 354
/
child-pool.ts
145 lines (119 loc) · 3.85 KB
/
child-pool.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
import { ChildProcess, fork } from 'child_process';
import * as path from 'path';
import { values, flatten } from 'lodash';
import * as getPort from 'get-port';
import * as fs from 'fs';
import { promisify } from 'util';
import { killAsync } from './process-utils';
const stat = promisify(fs.stat);
const CHILD_KILL_TIMEOUT = 30_000;
export interface ChildProcessExt extends ChildProcess {
processFile?: string;
}
const convertExecArgv = async (execArgv: string[]): Promise<string[]> => {
const standard: string[] = [];
const convertedArgs: string[] = [];
for (let i = 0; i < execArgv.length; i++) {
const arg = execArgv[i];
if (arg.indexOf('--inspect') === -1) {
standard.push(arg);
} else {
const argName = arg.split('=')[0];
const port = await getPort();
convertedArgs.push(`${argName}=${port}`);
}
}
return standard.concat(convertedArgs);
};
const exitCodesErrors: { [index: number]: string } = {
1: 'Uncaught Fatal Exception',
2: 'Unused',
3: 'Internal JavaScript Parse Error',
4: 'Internal JavaScript Evaluation Failure',
5: 'Fatal Error',
6: 'Non-function Internal Exception Handler',
7: 'Internal Exception Handler Run-Time Failure',
8: 'Unused',
9: 'Invalid Argument',
10: 'Internal JavaScript Run-Time Failure',
12: 'Invalid Debug Argument',
13: 'Unfinished Top-Level Await',
};
async function initChild(child: ChildProcess, processFile: string) {
const onComplete = new Promise<void>((resolve, reject) => {
const onMessageHandler = (msg: any) => {
if (msg.cmd === 'init-complete') {
resolve();
child.off('message', onMessageHandler);
}
};
child.on('message', onMessageHandler);
child.on('close', (code, signal) => {
if (code > 128) {
code -= 128;
}
const msg = exitCodesErrors[code] || `Unknown exit code ${code}`;
reject(
new Error(`Error initializing child: ${msg} and signal ${signal}`),
);
});
});
await new Promise(resolve =>
child.send({ cmd: 'init', value: processFile }, resolve),
);
await onComplete;
}
export class ChildPool {
retained: { [key: number]: ChildProcessExt } = {};
free: { [key: string]: ChildProcessExt[] } = {};
async retain(processFile: string): Promise<ChildProcessExt> {
const _this = this;
let child = _this.getFree(processFile).pop();
if (child) {
_this.retained[child.pid] = child;
return child;
}
const execArgv = await convertExecArgv(process.execArgv);
let masterFile = path.join(__dirname, './master.js');
try {
await stat(masterFile); // would throw if file not exists
} catch (_) {
masterFile = path.join(process.cwd(), 'dist/classes/master.js');
await stat(masterFile);
}
child = fork(masterFile, [], { execArgv });
child.processFile = processFile;
_this.retained[child.pid] = child;
child.on('exit', _this.remove.bind(_this, child));
await initChild(child, child.processFile);
return child;
}
release(child: ChildProcessExt) {
delete this.retained[child.pid];
this.getFree(child.processFile).push(child);
}
remove(child: ChildProcessExt) {
delete this.retained[child.pid];
const free = this.getFree(child.processFile);
const childIndex = free.indexOf(child);
if (childIndex > -1) {
free.splice(childIndex, 1);
}
}
async kill(child: ChildProcess, signal: 'SIGTERM' | 'SIGKILL' = 'SIGKILL') {
this.remove(child);
await killAsync(child, signal, CHILD_KILL_TIMEOUT);
}
async clean() {
const children = values(this.retained).concat(this.getAllFree());
this.retained = {};
this.free = {};
await Promise.all(children.map(c => this.kill(c, 'SIGTERM')));
}
getFree(id: string): ChildProcessExt[] {
return (this.free[id] = this.free[id] || []);
}
getAllFree() {
return flatten(values(this.free));
}
}