-
Notifications
You must be signed in to change notification settings - Fork 282
Expand file tree
/
Copy pathforkedAppWorker.ts
More file actions
212 lines (195 loc) · 9.02 KB
/
forkedAppWorker.ts
File metadata and controls
212 lines (195 loc) · 9.02 KB
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as path from "path";
import * as url from "url";
import * as cp from "child_process";
import * as fs from "fs";
import { logger } from "@vscode/debugadapter";
import { ErrorHelper } from "../common/error/errorHelper";
import { InternalErrorCode } from "../common/error/internalErrorCode";
import { getLoggingDirectory } from "../extension/log/LogHelper";
import { generateRandomPortNumber } from "../common/extensionHelper";
import { IDebuggeeWorker, RNAppMessage } from "./appWorker";
import { ScriptImporter } from "./scriptImporter";
function printDebuggingError(error: Error, reason: any) {
const nestedError = ErrorHelper.getNestedError(
error,
InternalErrorCode.DebuggingWontWorkReloadJSAndReconnect,
reason,
);
logger.error(nestedError.message);
}
/** This class will run the RN App logic inside a forked Node process. The framework to run the logic is provided by the file
* debuggerWorker.js (designed to run on a WebWorker). We add a couple of tweaks (mostly to polyfill WebWorker API) to that
* file and load it inside of a process.
* On this side we listen to IPC messages and either respond to them or redirect them to packager via MultipleLifetimeAppWorker's
* instance. We also intercept packager's signal to load the bundle's code and mutate the message with path to file we've downloaded
* to let importScripts function take this file.
*/
export class ForkedAppWorker implements IDebuggeeWorker {
protected scriptImporter: ScriptImporter;
protected debuggeeProcess: cp.ChildProcess | null = null;
/** A promise that we use to make sure that worker has been loaded completely before start sending IPC messages */
protected workerLoaded: Promise<void> | undefined;
private bundleLoaded: Promise<void> | undefined;
private logWriteStream!: fs.WriteStream;
private logDirectory: string | null = null;
constructor(
private packagerAddress: string,
private packagerPort: number,
private sourcesStoragePath: string,
private projectRootPath: string,
private postReplyToApp: (message: any) => void,
private packagerRemoteRoot?: string,
private packagerLocalRoot?: string,
) {
this.scriptImporter = new ScriptImporter(
this.packagerAddress,
this.packagerPort,
this.sourcesStoragePath,
this.packagerRemoteRoot,
this.packagerLocalRoot,
);
}
public stop(): void {
if (this.debuggeeProcess) {
logger.verbose(`About to kill debuggee with pid ${this.debuggeeProcess.pid}`);
this.debuggeeProcess.kill();
this.debuggeeProcess = null;
}
}
public async start(): Promise<number> {
const scriptToRunPath = path.resolve(
this.sourcesStoragePath,
ScriptImporter.DEBUGGER_WORKER_FILENAME,
);
const port = generateRandomPortNumber();
// Note that we set --inspect-brk flag to pause the process on the first line - this is
// required for debug adapter to set the breakpoints BEFORE the debuggee has started.
// The adapter will continue execution once it's done with breakpoints.
// --no-deprecation flag disables deprecation warnings like "[DEP0005] DeprecationWarning: Buffer() is deprecated..." and so on that leads to errors in native app
// https://nodejs.org/dist/latest-v7.x/docs/api/cli.html
const nodeArgs = [`--inspect-brk=${port}`, "--no-deprecation", scriptToRunPath];
// Start child Node process in debugging mode
// Using fork instead of spawn causes breakage of piping between app worker and VS Code debug console, e.g. console.log() in application
// wouldn't work. Please see https://github.com/microsoft/vscode-react-native/issues/758
this.debuggeeProcess = cp
.spawn("node", nodeArgs, {
stdio: ["pipe", "pipe", "pipe", "ipc"],
})
.on("message", (message: any) => {
// 'workerLoaded' is a special message that indicates that worker is done with loading.
// We need to wait for it before doing any IPC because process.send doesn't seems to care
// about whether the message has been received or not and the first messages are often get
// discarded by spawned process
if (message && message.workerLoaded) {
this.workerLoaded = Promise.resolve();
return;
}
this.postReplyToApp(message);
})
.on("error", (error: Error) => {
printDebuggingError(
ErrorHelper.getInternalError(
InternalErrorCode.ReactNativeWorkerProcessThrownAnError,
),
error,
);
});
// If special env variables are defined, then write process outputs to file
this.logDirectory = getLoggingDirectory();
if (this.logDirectory) {
this.logWriteStream = fs.createWriteStream(
path.join(this.logDirectory, "nodeProcessLog.txt"),
);
this.logWriteStream.on("error", err => {
logger.error(
`Error creating log file at path: ${String(this.logDirectory)}. Error: ${String(
err.toString(),
)}\n`,
);
});
this.debuggeeProcess.stdout.pipe(this.logWriteStream);
this.debuggeeProcess.stderr.pipe(this.logWriteStream);
this.debuggeeProcess.on("close", () => {
this.logWriteStream.end();
});
}
// Resolve with port debugger server is listening on
// This will be sent to subscribers of MLAppWorker in "connected" event
logger.verbose(
`Spawned debuggee process with pid ${this.debuggeeProcess.pid} listening to ${port}`,
);
return port;
}
public async postMessage(rnMessage: RNAppMessage): Promise<RNAppMessage> {
// Before sending messages, make sure that the worker is loaded
await new Promise<void>(resolve => {
if (this.workerLoaded) {
resolve();
} else {
const checkWorkerLoaded = setInterval(() => {
if (this.workerLoaded) {
clearInterval(checkWorkerLoaded);
resolve();
}
}, 1000);
}
});
const promise = (async () => {
await this.workerLoaded;
if (rnMessage.method !== "executeApplicationScript") {
// Before sending messages, make sure that the app script executed
await this.bundleLoaded;
return rnMessage;
}
// When packager asks worker to load bundle we download that bundle and
// then set url field to point to that downloaded bundle, so the worker
// will take our modified bundle
if (rnMessage.url) {
const packagerUrl = url.parse(rnMessage.url);
packagerUrl.host = `${this.packagerAddress}:${this.packagerPort}`;
rnMessage = {
...rnMessage,
url: url.format(packagerUrl),
};
logger.verbose(
`Packager requested runtime to load script from ${String(rnMessage.url)}`,
);
const downloadedScript = await this.scriptImporter.downloadAppScript(
<string>rnMessage.url,
this.projectRootPath,
);
this.bundleLoaded = Promise.resolve();
return Object.assign({}, rnMessage, {
url: `${this.pathToFileUrl(downloadedScript.filepath)}`,
});
}
throw ErrorHelper.getInternalError(
InternalErrorCode.RNMessageWithMethodExecuteApplicationScriptDoesntHaveURLProperty,
);
})();
promise.then(
(message: RNAppMessage) => {
if (this.debuggeeProcess) {
this.debuggeeProcess.send({ data: message });
}
},
reason =>
printDebuggingError(
ErrorHelper.getInternalError(
InternalErrorCode.CouldntImportScriptAt,
rnMessage.url,
),
reason,
),
);
return promise;
}
// Simple and reliable implementation for converting file path to file:// URL
// Keep this instead of using url.pathToFileURL() to avoid URL encoding issues
public pathToFileUrl(url: string): string {
const filePrefix = process.platform === "win32" ? "file:///" : "file://";
return filePrefix + url;
}
}