UNPKG

6.7 kBPlain TextView Raw
1import path from 'path';
2import { fileURLToPath } from 'url';
3
4import { errorToString } from '@stryker-mutator/util';
5import log4js from 'log4js';
6import { createInjector } from 'typed-inject';
7import { commonTokens, PluginContext, Injector } from '@stryker-mutator/api/plugin';
8
9import { LogConfigurator } from '../logging/index.js';
10import { deserialize, serialize } from '../utils/string-utils.js';
11import { coreTokens, provideLogger, PluginCreator } from '../di/index.js';
12import { PluginLoader } from '../di/plugin-loader.js';
13
14import { CallMessage, ParentMessage, ParentMessageKind, WorkerMessage, WorkerMessageKind, InitMessage } from './message-protocol.js';
15
16export interface ChildProcessContext extends PluginContext {
17 [coreTokens.pluginCreator]: PluginCreator;
18}
19
20export class ChildProcessProxyWorker {
21 private log?: log4js.Logger;
22
23 public realSubject: any;
24
25 constructor(private readonly injectorFactory: typeof createInjector) {
26 // Make sure to bind the methods in order to ensure the `this` pointer
27 this.handleMessage = this.handleMessage.bind(this);
28
29 // Start listening before sending the spawned message
30 process.on('message', this.handleMessage);
31 this.send({ kind: ParentMessageKind.Ready });
32 }
33
34 private send(value: ParentMessage) {
35 if (process.send) {
36 const str = serialize(value);
37 process.send(str);
38 }
39 }
40 private handleMessage(serializedMessage: unknown) {
41 const message = deserialize<WorkerMessage>(String(serializedMessage));
42 switch (message.kind) {
43 case WorkerMessageKind.Init:
44 // eslint-disable-next-line @typescript-eslint/no-floating-promises -- No handle needed, handleInit has try catch
45 this.handleInit(message);
46 this.removeAnyAdditionalMessageListeners(this.handleMessage);
47 break;
48 case WorkerMessageKind.Call:
49 // eslint-disable-next-line @typescript-eslint/no-floating-promises -- No handle needed, handleCall has try catch
50 this.handleCall(message);
51 this.removeAnyAdditionalMessageListeners(this.handleMessage);
52 break;
53 case WorkerMessageKind.Dispose:
54 const sendCompleted = () => {
55 this.send({ kind: ParentMessageKind.DisposeCompleted });
56 };
57 LogConfigurator.shutdown().then(sendCompleted).catch(sendCompleted);
58 break;
59 }
60 }
61
62 private async handleInit(message: InitMessage) {
63 try {
64 LogConfigurator.configureChildProcess(message.loggingContext);
65 this.log = log4js.getLogger(ChildProcessProxyWorker.name);
66 this.handlePromiseRejections();
67
68 // Load plugins in the child process
69 const pluginInjector = provideLogger(this.injectorFactory())
70 .provideValue(commonTokens.options, message.options)
71 .provideValue(commonTokens.fileDescriptions, message.fileDescriptions);
72 const pluginLoader = pluginInjector.injectClass(PluginLoader);
73 const { pluginsByKind } = await pluginLoader.load(message.pluginModulePaths);
74 const injector: Injector<ChildProcessContext> = pluginInjector
75 .provideValue(coreTokens.pluginsByKind, pluginsByKind)
76 .provideClass(coreTokens.pluginCreator, PluginCreator);
77
78 const childModule = await import(message.modulePath);
79 const RealSubjectClass = childModule[message.namedExport];
80 const workingDir = path.resolve(message.workingDirectory);
81 if (process.cwd() !== workingDir) {
82 this.log.debug(`Changing current working directory for this process to ${workingDir}`);
83 process.chdir(workingDir);
84 }
85 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
86 this.realSubject = injector.injectClass(RealSubjectClass);
87 this.send({ kind: ParentMessageKind.Initialized });
88 } catch (err) {
89 this.send({
90 error: errorToString(err),
91 kind: ParentMessageKind.InitError,
92 });
93 }
94 }
95
96 private async handleCall(message: CallMessage) {
97 try {
98 const result = await this.doCall(message);
99 this.send({
100 correlationId: message.correlationId,
101 kind: ParentMessageKind.CallResult,
102 result,
103 });
104 } catch (err) {
105 this.send({
106 correlationId: message.correlationId,
107 error: errorToString(err),
108 kind: ParentMessageKind.CallRejection,
109 });
110 }
111 }
112
113 private doCall(message: CallMessage): PromiseLike<Record<string, unknown>> | Record<string, unknown> | undefined {
114 if (typeof this.realSubject[message.methodName] === 'function') {
115 return this.realSubject[message.methodName](...message.args);
116 } else {
117 return this.realSubject[message.methodName];
118 }
119 }
120
121 /**
122 * Remove any addition message listeners that might me eavesdropping.
123 * the @ngtools/webpack plugin listens to messages and throws an error whenever it could not handle a message
124 * @see https://github.com/angular/angular-cli/blob/f776d3cf7982b64734c57fe4407434e9f4ec09f7/packages/%40ngtools/webpack/src/type_checker.ts#L79
125 * @param exceptListener The listener that should remain
126 */
127 private removeAnyAdditionalMessageListeners(exceptListener: NodeJS.MessageListener) {
128 process.listeners('message').forEach((listener) => {
129 if (listener !== exceptListener) {
130 this.log?.debug(
131 "Removing an additional message listener, we don't want eavesdropping on our inter-process communication: %s",
132 listener.toString()
133 );
134 process.removeListener('message', listener);
135 }
136 });
137 }
138
139 /**
140 * During mutation testing, it's to be expected that promise rejections are not handled synchronously anymore (or not at all)
141 * Let's handle those events so future versions of node don't crash
142 * See issue 350: https://github.com/stryker-mutator/stryker-js/issues/350
143 */
144 private handlePromiseRejections() {
145 const unhandledRejections: Array<Promise<unknown>> = [];
146 process.on('unhandledRejection', (reason, promise) => {
147 const unhandledPromiseId = unhandledRejections.push(promise);
148 this.log?.debug(`UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: ${unhandledPromiseId}): ${reason}`);
149 });
150 process.on('rejectionHandled', (promise) => {
151 const unhandledPromiseId = unhandledRejections.indexOf(promise) + 1;
152 this.log?.debug(`PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: ${unhandledPromiseId})`);
153 });
154 }
155}
156
157// Prevent side effects for merely importing the file
158// Only actually start the child worker when it is requested
159// Stryker disable all
160if (fileURLToPath(import.meta.url) === process.argv[1]) {
161 new ChildProcessProxyWorker(createInjector);
162}
163// Stryker restore all