1 | import path from 'path';
|
2 | import { fileURLToPath } from 'url';
|
3 |
|
4 | import { errorToString } from '@stryker-mutator/util';
|
5 | import log4js from 'log4js';
|
6 | import { createInjector } from 'typed-inject';
|
7 | import { commonTokens, PluginContext, Injector } from '@stryker-mutator/api/plugin';
|
8 |
|
9 | import { LogConfigurator } from '../logging/index.js';
|
10 | import { deserialize, serialize } from '../utils/string-utils.js';
|
11 | import { coreTokens, provideLogger, PluginCreator } from '../di/index.js';
|
12 | import { PluginLoader } from '../di/plugin-loader.js';
|
13 |
|
14 | import { CallMessage, ParentMessage, ParentMessageKind, WorkerMessage, WorkerMessageKind, InitMessage } from './message-protocol.js';
|
15 |
|
16 | export interface ChildProcessContext extends PluginContext {
|
17 | [coreTokens.pluginCreator]: PluginCreator;
|
18 | }
|
19 |
|
20 | export class ChildProcessProxyWorker {
|
21 | private log?: log4js.Logger;
|
22 |
|
23 | public realSubject: any;
|
24 |
|
25 | constructor(private readonly injectorFactory: typeof createInjector) {
|
26 |
|
27 | this.handleMessage = this.handleMessage.bind(this);
|
28 |
|
29 |
|
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 |
|
45 | this.handleInit(message);
|
46 | this.removeAnyAdditionalMessageListeners(this.handleMessage);
|
47 | break;
|
48 | case WorkerMessageKind.Call:
|
49 |
|
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 |
|
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 |
|
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 |
|
123 |
|
124 |
|
125 |
|
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 |
|
141 |
|
142 |
|
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 |
|
158 |
|
159 |
|
160 | if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
161 | new ChildProcessProxyWorker(createInjector);
|
162 | }
|
163 |
|