-
Notifications
You must be signed in to change notification settings - Fork 414
/
child-pool.ts
168 lines (138 loc) · 4.56 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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { ChildProcess, fork } from 'child_process';
import * as path from 'path';
import { flatten } from 'lodash';
import { AddressInfo, createServer } from 'net';
import { killAsync } from './process-utils';
import { ParentCommand, ChildCommand } from '../interfaces';
import { parentSend } from '../utils';
const CHILD_KILL_TIMEOUT = 30_000;
export interface ChildProcessExt extends ChildProcess {
processFile?: string;
}
const getFreePort = async () => {
return new Promise(resolve => {
const server = createServer();
server.listen(0, () => {
const { port } = (server.address() as AddressInfo);
server.close(() => resolve(port));
});
});
};
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 getFreePort();
convertedArgs.push(`${argName}=${port}`);
}
}
return standard.concat(convertedArgs);
};
/**
* @see https://nodejs.org/api/process.html#process_exit_codes
*/
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 === ParentCommand.InitCompleted) {
resolve();
} else if (msg.cmd === ParentCommand.InitFailed) {
const err = new Error();
err.stack = msg.err.stack;
err.message = msg.err.message;
reject(err);
}
child.off('message', onMessageHandler);
child.off('close', onCloseHandler);
};
const onCloseHandler = (code: number, signal: number) => {
if (code > 128) {
code -= 128;
}
const msg = exitCodesErrors[code] || `Unknown exit code ${code}`;
reject(
new Error(`Error initializing child: ${msg} and signal ${signal}`),
);
child.off('message', onMessageHandler);
child.off('close', onCloseHandler);
};
child.on('message', onMessageHandler);
child.on('close', onCloseHandler);
});
await parentSend(child, { cmd: ChildCommand.Init, value: processFile });
await onComplete;
}
export class ChildPool {
retained: { [key: number]: ChildProcessExt } = {};
free: { [key: string]: ChildProcessExt[] } = {};
constructor(
private masterFile = path.join(process.cwd(), 'dist/cjs/classes/master.js'),
) {}
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);
child = fork(this.masterFile, [], { execArgv, stdio: 'pipe' });
child.processFile = processFile;
_this.retained[child.pid] = child;
child.on('exit', _this.remove.bind(_this, child));
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
await initChild(child, child.processFile);
return child;
}
release(child: ChildProcessExt): void {
delete this.retained[child.pid];
this.getFree(child.processFile).push(child);
}
remove(child: ChildProcessExt): void {
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',
): Promise<void> {
this.remove(child);
await killAsync(child, signal, CHILD_KILL_TIMEOUT);
}
async clean(): Promise<void> {
const children = Object.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(): ChildProcessExt[] {
return flatten(Object.values(this.free));
}
}