1+ import * as os from 'os';
12import { fork, ChildProcess } from 'child_process';
23import { File } from 'stryker-api/core';
34import { getLogger } from 'stryker-api/logging';
45import { 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';
78import LoggingClientContext from '../logging/LoggingClientContext';
9+ import ChildProcessCrashedError from './ChildProcessCrashedError';
10+ import OutOfMemoryError from './OutOfMemoryError';
11+ import StringBuilder from '../utils/StringBuilder';
812
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]>;
1124};
1225
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+
1330export default class ChildProcessProxy<T > {
14- readonly proxy : ChildProxy < T > = { } as ChildProxy < T > ;
31+ readonly proxy: Promisified <T >;
1532
1633 private worker: ChildProcess;
1734 private initTask: Task;
18- private disposeTask : Task < void > ;
35+ private disposeTask: ExpirableTask<void > | undefined;
36+ private currentError: ChildProcessCrashedError | undefined;
1937 private workerTasks: Task<any >[] = [];
2038 private log = getLogger(ChildProcessProxy.name);
39+ private stdoutAndStderrBuilder = new StringBuilder();
40+ private isDisposed = false;
2141
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: [] });
2444 this.initTask = new Task();
45+ this.log.debug('Starting %s in child process %s', requirePath, this.worker.pid);
2546 this.send({
2647 kind: WorkerMessageKind.Init,
2748 loggingContext,
2849 plugins,
2950 requirePath,
30- constructorArgs : constructorParams
51+ constructorArgs: constructorParams,
52+ workingDirectory
3153 });
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();
3462 }
3563
3664 /**
3765 * Creates a proxy where each function of the object created using the constructorFunction arg is ran inside of a child process
3866 */
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);
4970 }
5071
5172 private send(message: WorkerMessage) {
5273 this.worker.send(serialize(message));
5374 }
5475
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+ }
5888 });
5989 }
6090
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 >();
6597 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+ });
71105 });
72- } ) ;
73- return workerTask . promise ;
106+ return workerTask.promise ;
107+ }
74108 };
75109 }
76110
77- private listenToWorkerMessages ( ) {
111+ private listenForMessages () {
78112 this.worker.on('message', (serializedMessage: string) => {
79113 const message: ParentMessage = deserialize(serializedMessage, [File]);
80114 switch (message.kind) {
@@ -83,12 +117,16 @@ export default class ChildProcessProxy<T> {
83117 break;
84118 case ParentMessageKind.Result:
85119 this.workerTasks[message.correlationId].resolve(message.result);
120+ delete this.workerTasks[message.correlationId];
86121 break;
87122 case ParentMessageKind.Rejection:
88123 this.workerTasks[message.correlationId].reject(new Error(message.error));
124+ delete this.workerTasks[message.correlationId];
89125 break;
90126 case ParentMessageKind.DisposeCompleted:
91- this . disposeTask . resolve ( undefined ) ;
127+ if (this.disposeTask) {
128+ this.disposeTask.resolve(undefined);
129+ }
92130 break;
93131 default:
94132 this.logUnidentifiedMessage(message);
@@ -97,12 +135,88 @@ export default class ChildProcessProxy<T> {
97135 });
98136 }
99137
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+
100203 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+ }
106220 }
107221
108222 private logUnidentifiedMessage(message: never) {
0 commit comments