1
+ import * as os from 'os';
1
2
import { fork, ChildProcess } from 'child_process';
2
3
import { File } from 'stryker-api/core';
3
4
import { getLogger } from 'stryker-api/logging';
4
5
import { WorkerMessage, WorkerMessageKind, ParentMessage, autoStart, ParentMessageKind } from './messageProtocol';
5
- import { serialize , deserialize } from '../utils/objectUtils' ;
6
- import Task from '../utils/Task' ;
6
+ import { serialize, deserialize, kill, isErrnoException, padLeft } from '../utils/objectUtils';
7
+ import { Task, ExpirableTask } from '../utils/Task';
7
8
import LoggingClientContext from '../logging/LoggingClientContext';
9
+ import ChildProcessCrashedError from './ChildProcessCrashedError';
10
+ import OutOfMemoryError from './OutOfMemoryError';
11
+ import StringBuilder from '../utils/StringBuilder';
8
12
9
- export type ChildProxy < T > = {
10
- [ K in keyof T ] : ( ...args : any [ ] ) => Promise < any > ;
13
+ interface Func<TS extends any[], R> {
14
+ (...args: TS): R;
15
+ }
16
+ interface PromisifiedFunc<TS extends any[], R> {
17
+ (...args: TS): Promise<R >;
18
+ }
19
+ interface Constructor<T , TS extends any[]> {
20
+ new (...args: TS): T;
21
+ }
22
+ export type Promisified<T > = {
23
+ [K in keyof T]: T[K] extends PromisifiedFunc<any , any> ? T[K] : T[K] extends Func<infer TS, infer R> ? PromisifiedFunc<TS , R> : () => Promise<T [K]>;
11
24
};
12
25
26
+ const BROKEN_PIPE_ERROR_CODE = 'EPIPE';
27
+ const IPC_CHANNEL_CLOSED_ERROR_CODE = 'ERR_IPC_CHANNEL_CLOSED';
28
+ const TIMEOUT_FOR_DISPOSE = 2000;
29
+
13
30
export default class ChildProcessProxy<T > {
14
- readonly proxy : ChildProxy < T > = { } as ChildProxy < T > ;
31
+ readonly proxy: Promisified <T >;
15
32
16
33
private worker: ChildProcess;
17
34
private initTask: Task;
18
- private disposeTask : Task < void > ;
35
+ private disposeTask: ExpirableTask<void > | undefined;
36
+ private currentError: ChildProcessCrashedError | undefined;
19
37
private workerTasks: Task<any >[] = [];
20
38
private log = getLogger(ChildProcessProxy.name);
39
+ private stdoutAndStderrBuilder = new StringBuilder();
40
+ private isDisposed = false;
21
41
22
- private constructor ( requirePath : string , loggingContext : LoggingClientContext , plugins : string [ ] , private constructorFunction : { new ( ... params : any [ ] ) : T } , constructorParams : any [ ] ) {
23
- this . worker = fork ( require . resolve ( './ChildProcessProxyWorker' ) , [ autoStart ] , { silent : false , execArgv : [ ] } ) ;
42
+ private constructor(requirePath: string, loggingContext: LoggingClientContext, plugins: string[], workingDirectory: string , constructorParams: any[]) {
43
+ this.worker = fork(require.resolve('./ChildProcessProxyWorker'), [autoStart], { silent: true , execArgv: [] });
24
44
this.initTask = new Task();
45
+ this.log.debug('Starting %s in child process %s', requirePath, this.worker.pid);
25
46
this.send({
26
47
kind: WorkerMessageKind.Init,
27
48
loggingContext,
28
49
plugins,
29
50
requirePath,
30
- constructorArgs : constructorParams
51
+ constructorArgs: constructorParams,
52
+ workingDirectory
31
53
});
32
- this . listenToWorkerMessages ( ) ;
33
- this . initProxy ( ) ;
54
+ this.listenForMessages();
55
+ this.listenToStdoutAndStderr();
56
+ // This is important! Be sure to bind to `this`
57
+ this.handleUnexpectedExit = this.handleUnexpectedExit.bind(this);
58
+ this.handleError = this.handleError.bind(this);
59
+ this.worker.on('exit', this.handleUnexpectedExit);
60
+ this.worker.on('error', this.handleError);
61
+ this.proxy = this.initProxy();
34
62
}
35
63
36
64
/**
37
65
* Creates a proxy where each function of the object created using the constructorFunction arg is ran inside of a child process
38
66
*/
39
- static create < T , P1 > ( requirePath : string , loggingContext : LoggingClientContext , plugins : string [ ] , constructorFunction : { new ( arg : P1 ) : T } , arg : P1 ) : ChildProcessProxy < T > ;
40
- /**
41
- * Creates a proxy where each function of the object created using the constructorFunction arg is ran inside of a child process
42
- */
43
- static create < T , P1 , P2 > ( requirePath : string , loggingContext : LoggingClientContext , plugins : string [ ] , constructorFunction : { new ( arg : P1 , arg2 : P2 ) : T } , arg1 : P1 , arg2 : P2 ) : ChildProcessProxy < T > ;
44
- /**
45
- * Creates a proxy where each function of the object created using the constructorFunction arg is ran inside of a child process
46
- */
47
- static create < T > ( requirePath : string , loggingContext : LoggingClientContext , plugins : string [ ] , constructorFunction : { new ( ...params : any [ ] ) : T } , ...constructorArgs : any [ ] ) {
48
- return new ChildProcessProxy ( requirePath , loggingContext , plugins , constructorFunction , constructorArgs ) ;
67
+ static create<T , TS extends any[]>(requirePath: string, loggingContext: LoggingClientContext, plugins: string[], workingDirectory: string, _: Constructor<T , TS>, ...constructorArgs: TS):
68
+ ChildProcessProxy<T > {
69
+ return new ChildProcessProxy(requirePath, loggingContext, plugins, workingDirectory, constructorArgs);
49
70
}
50
71
51
72
private send(message: WorkerMessage) {
52
73
this.worker.send(serialize(message));
53
74
}
54
75
55
- private initProxy ( ) {
56
- Object . keys ( this . constructorFunction . prototype ) . forEach ( methodName => {
57
- this . proxyMethod ( methodName as keyof T ) ;
76
+ private initProxy(): Promisified<T > {
77
+ // This proxy is a genuine javascript `Proxy` class
78
+ // More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
79
+ const self = this;
80
+ return new Proxy({} as Promisified<T >, {
81
+ get(_, propertyKey) {
82
+ if (typeof propertyKey === 'string') {
83
+ return self.forward(propertyKey);
84
+ } else {
85
+ return undefined;
86
+ }
87
+ }
58
88
});
59
89
}
60
90
61
- private proxyMethod ( methodName : any ) {
62
- this . proxy [ ( methodName as keyof T ) ] = ( ...args : any [ ] ) => {
63
- const workerTask = new Task < any > ( ) ;
64
- this . initTask . promise . then ( ( ) => {
91
+ private forward(methodName: string) {
92
+ return (...args: any[]) => {
93
+ if (this.currentError) {
94
+ return Promise.reject(this.currentError);
95
+ } else {
96
+ const workerTask = new Task<any >();
65
97
const correlationId = this.workerTasks.push(workerTask) - 1;
66
- this . send ( {
67
- kind : WorkerMessageKind . Work ,
68
- correlationId,
69
- methodName,
70
- args
98
+ this.initTask.promise.then(() => {
99
+ this.send({
100
+ kind: WorkerMessageKind.Call,
101
+ correlationId,
102
+ methodName,
103
+ args
104
+ });
71
105
});
72
- } ) ;
73
- return workerTask . promise ;
106
+ return workerTask.promise ;
107
+ }
74
108
};
75
109
}
76
110
77
- private listenToWorkerMessages ( ) {
111
+ private listenForMessages () {
78
112
this.worker.on('message', (serializedMessage: string) => {
79
113
const message: ParentMessage = deserialize(serializedMessage, [File]);
80
114
switch (message.kind) {
@@ -83,12 +117,16 @@ export default class ChildProcessProxy<T> {
83
117
break;
84
118
case ParentMessageKind.Result:
85
119
this.workerTasks[message.correlationId].resolve(message.result);
120
+ delete this.workerTasks[message.correlationId];
86
121
break;
87
122
case ParentMessageKind.Rejection:
88
123
this.workerTasks[message.correlationId].reject(new Error(message.error));
124
+ delete this.workerTasks[message.correlationId];
89
125
break;
90
126
case ParentMessageKind.DisposeCompleted:
91
- this . disposeTask . resolve ( undefined ) ;
127
+ if (this.disposeTask) {
128
+ this.disposeTask.resolve(undefined);
129
+ }
92
130
break;
93
131
default:
94
132
this.logUnidentifiedMessage(message);
@@ -97,12 +135,88 @@ export default class ChildProcessProxy<T> {
97
135
});
98
136
}
99
137
138
+ private listenToStdoutAndStderr() {
139
+ const handleData = (data: Buffer | string) => {
140
+ const output = data.toString();
141
+ this.stdoutAndStderrBuilder.append(output);
142
+ if (this.log.isTraceEnabled()) {
143
+ this.log.trace(output);
144
+ }
145
+ };
146
+
147
+ if (this.worker.stdout) {
148
+ this.worker.stdout.on('data', handleData);
149
+ }
150
+
151
+ if (this.worker.stderr) {
152
+ this.worker.stderr.on('data', handleData);
153
+ }
154
+ }
155
+
156
+ private reportError(error: Error) {
157
+ this.workerTasks
158
+ .filter(task => !task.isCompleted)
159
+ .forEach(task => task.reject(error));
160
+ }
161
+
162
+ private handleUnexpectedExit(code: number, signal: string) {
163
+ this.isDisposed = true;
164
+ const output = this.stdoutAndStderrBuilder.toString();
165
+
166
+ if (processOutOfMemory()) {
167
+ this.currentError = new OutOfMemoryError(this.worker.pid, code);
168
+ this.log.warn(`Child process [pid ${this.currentError.pid}] ran out of memory. Stdout and stderr are logged on debug level.`);
169
+ this.log.debug(stdoutAndStderr());
170
+ } else {
171
+ this.currentError = new ChildProcessCrashedError(this.worker.pid, `Child process [pid ${this.worker.pid}] exited unexpectedly with exit code ${code} (${signal || 'without signal'}). ${stdoutAndStderr()}`, code, signal);
172
+ this.log.warn(this.currentError.message, this.currentError);
173
+ }
174
+
175
+ this.reportError(this.currentError);
176
+
177
+ function processOutOfMemory() {
178
+ return output.indexOf('JavaScript heap out of memory') >= 0;
179
+ }
180
+
181
+ function stdoutAndStderr() {
182
+ if (output.length) {
183
+ return `Last part of stdout and stderr was:${os.EOL}${padLeft(output)}`;
184
+ } else {
185
+ return 'Stdout and stderr were empty.';
186
+ }
187
+ }
188
+ }
189
+
190
+ private handleError(error: Error) {
191
+ if (this.innerProcessIsCrashed(error)) {
192
+ this.log.warn(`Child process [pid ${this.worker.pid}] has crashed. See other warning messages for more info.`, error);
193
+ this.reportError(new ChildProcessCrashedError(this.worker.pid, `Child process [pid ${this.worker.pid}] has crashed`, undefined, undefined, error));
194
+ } else {
195
+ this.reportError(error);
196
+ }
197
+ }
198
+
199
+ private innerProcessIsCrashed(error: any) {
200
+ return isErrnoException(error) && (error.code === BROKEN_PIPE_ERROR_CODE || error.code === IPC_CHANNEL_CLOSED_ERROR_CODE);
201
+ }
202
+
100
203
public dispose(): Promise<void > {
101
- this . disposeTask = new Task ( ) ;
102
- this . send ( { kind : WorkerMessageKind . Dispose } ) ;
103
- return this . disposeTask . promise
104
- . then ( ( ) => this . worker . kill ( ) )
105
- . catch ( ( ) => this . worker . kill ( ) ) ;
204
+ this.worker.removeListener('exit', this.handleUnexpectedExit);
205
+ if (this.isDisposed) {
206
+ return Promise.resolve();
207
+ } else {
208
+ this.log.debug('Disposing of worker process %s', this.worker.pid);
209
+ const killWorker = () => {
210
+ this.log.debug('Kill %s', this.worker.pid);
211
+ kill(this.worker.pid);
212
+ this.isDisposed = true;
213
+ };
214
+ this.disposeTask = new ExpirableTask(TIMEOUT_FOR_DISPOSE);
215
+ this.send({ kind: WorkerMessageKind.Dispose });
216
+ return this.disposeTask.promise
217
+ .then(killWorker)
218
+ .catch(killWorker);
219
+ }
106
220
}
107
221
108
222
private logUnidentifiedMessage(message: never) {
0 commit comments