microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
4cd259621ddfbd348fade892a2f3ee87fd1924c5

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/debugger/forkedAppWorker.ts

213lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import * as path from "path";
5import * as url from "url";
6import * as cp from "child_process";
7import * as fs from "fs";
8import { ScriptImporter } from "./scriptImporter";
9import { logger } from "vscode-debugadapter";
10import { ErrorHelper } from "../common/error/errorHelper";
11import { IDebuggeeWorker, RNAppMessage } from "./appWorker";
12import { InternalErrorCode } from "../common/error/internalErrorCode";
13import { getLoggingDirectory } from "../extension/log/LogHelper";
14import { generateRandomPortNumber } from "../common/extensionHelper";
15
16function printDebuggingError(error: Error, reason: any) {
17 const nestedError = ErrorHelper.getNestedError(
18 error,
19 InternalErrorCode.DebuggingWontWorkReloadJSAndReconnect,
20 reason,
21 );
22
23 logger.error(nestedError.message);
24}
25
26/** This class will run the RN App logic inside a forked Node process. The framework to run the logic is provided by the file
27 * debuggerWorker.js (designed to run on a WebWorker). We add a couple of tweaks (mostly to polyfill WebWorker API) to that
28 * file and load it inside of a process.
29 * On this side we listen to IPC messages and either respond to them or redirect them to packager via MultipleLifetimeAppWorker's
30 * instance. We also intercept packager's signal to load the bundle's code and mutate the message with path to file we've downloaded
31 * to let importScripts function take this file.
32 */
33export class ForkedAppWorker implements IDebuggeeWorker {
34 protected scriptImporter: ScriptImporter;
35 protected debuggeeProcess: cp.ChildProcess | null = null;
36 /** A promise that we use to make sure that worker has been loaded completely before start sending IPC messages */
37 protected workerLoaded: Promise<void> | undefined;
38 private bundleLoaded: Promise<void> | undefined;
39 private logWriteStream: fs.WriteStream;
40 private logDirectory: string | null;
41
42 constructor(
43 private packagerAddress: string,
44 private packagerPort: number,
45 private sourcesStoragePath: string,
46 private projectRootPath: string,
47 private postReplyToApp: (message: any) => void,
48 private packagerRemoteRoot?: string,
49 private packagerLocalRoot?: string,
50 ) {
51 this.scriptImporter = new ScriptImporter(
52 this.packagerAddress,
53 this.packagerPort,
54 this.sourcesStoragePath,
55 this.packagerRemoteRoot,
56 this.packagerLocalRoot,
57 );
58 }
59
60 public stop(): void {
61 if (this.debuggeeProcess) {
62 logger.verbose(`About to kill debuggee with pid ${this.debuggeeProcess.pid}`);
63 this.debuggeeProcess.kill();
64 this.debuggeeProcess = null;
65 }
66 }
67
68 public async start(): Promise<number> {
69 let scriptToRunPath = path.resolve(
70 this.sourcesStoragePath,
71 ScriptImporter.DEBUGGER_WORKER_FILENAME,
72 );
73 const port = generateRandomPortNumber();
74
75 // Note that we set --inspect-brk flag to pause the process on the first line - this is
76 // required for debug adapter to set the breakpoints BEFORE the debuggee has started.
77 // The adapter will continue execution once it's done with breakpoints.
78 // --no-deprecation flag disables deprecation warnings like "[DEP0005] DeprecationWarning: Buffer() is deprecated..." and so on that leads to errors in native app
79 // https://nodejs.org/dist/latest-v7.x/docs/api/cli.html
80 const nodeArgs = [`--inspect-brk=${port}`, "--no-deprecation", scriptToRunPath];
81 // Start child Node process in debugging mode
82 // Using fork instead of spawn causes breakage of piping between app worker and VS Code debug console, e.g. console.log() in application
83 // wouldn't work. Please see https://github.com/microsoft/vscode-react-native/issues/758
84 this.debuggeeProcess = cp
85 .spawn("node", nodeArgs, {
86 stdio: ["pipe", "pipe", "pipe", "ipc"],
87 })
88 .on("message", (message: any) => {
89 // 'workerLoaded' is a special message that indicates that worker is done with loading.
90 // We need to wait for it before doing any IPC because process.send doesn't seems to care
91 // about whether the message has been received or not and the first messages are often get
92 // discarded by spawned process
93 if (message && message.workerLoaded) {
94 this.workerLoaded = Promise.resolve();
95 return;
96 }
97
98 this.postReplyToApp(message);
99 })
100 .on("error", (error: Error) => {
101 printDebuggingError(
102 ErrorHelper.getInternalError(
103 InternalErrorCode.ReactNativeWorkerProcessThrownAnError,
104 ),
105 error,
106 );
107 });
108
109 // If special env variables are defined, then write process outputs to file
110 this.logDirectory = getLoggingDirectory();
111
112 if (this.logDirectory) {
113 this.logWriteStream = fs.createWriteStream(
114 path.join(this.logDirectory, "nodeProcessLog.txt"),
115 );
116 this.logWriteStream.on("error", err => {
117 logger.error(
118 `Error creating log file at path: ${
119 this.logDirectory
120 }. Error: ${err.toString()}\n`,
121 );
122 });
123 this.debuggeeProcess.stdout.pipe(this.logWriteStream);
124 this.debuggeeProcess.stderr.pipe(this.logWriteStream);
125 this.debuggeeProcess.on("close", () => {
126 this.logWriteStream.end();
127 });
128 }
129
130 // Resolve with port debugger server is listening on
131 // This will be sent to subscribers of MLAppWorker in "connected" event
132 logger.verbose(
133 `Spawned debuggee process with pid ${this.debuggeeProcess.pid} listening to ${port}`,
134 );
135
136 return port;
137 }
138
139 public async postMessage(rnMessage: RNAppMessage): Promise<RNAppMessage> {
140 // Before sending messages, make sure that the worker is loaded
141 await new Promise(resolve => {
142 if (this.workerLoaded) {
143 resolve();
144 } else {
145 const checkWorkerLoaded = setInterval(() => {
146 if (this.workerLoaded) {
147 clearInterval(checkWorkerLoaded);
148 resolve();
149 }
150 }, 1000);
151 }
152 });
153
154 const promise = (async () => {
155 await this.workerLoaded;
156
157 if (rnMessage.method !== "executeApplicationScript") {
158 // Before sending messages, make sure that the app script executed
159 await this.bundleLoaded;
160 return rnMessage;
161 } else {
162 // When packager asks worker to load bundle we download that bundle and
163 // then set url field to point to that downloaded bundle, so the worker
164 // will take our modified bundle
165 if (rnMessage.url) {
166 const packagerUrl = url.parse(rnMessage.url);
167 packagerUrl.host = `${this.packagerAddress}:${this.packagerPort}`;
168 rnMessage = {
169 ...rnMessage,
170 url: url.format(packagerUrl),
171 };
172 logger.verbose(
173 `Packager requested runtime to load script from ${rnMessage.url}`,
174 );
175 const downloadedScript = await this.scriptImporter.downloadAppScript(
176 <string>rnMessage.url,
177 this.projectRootPath,
178 );
179 this.bundleLoaded = Promise.resolve();
180 return Object.assign({}, rnMessage, {
181 url: `${this.pathToFileUrl(downloadedScript.filepath)}`,
182 });
183 } else {
184 throw ErrorHelper.getInternalError(
185 InternalErrorCode.RNMessageWithMethodExecuteApplicationScriptDoesntHaveURLProperty,
186 );
187 }
188 }
189 })();
190 promise.then(
191 (message: RNAppMessage) => {
192 if (this.debuggeeProcess) {
193 this.debuggeeProcess.send({ data: message });
194 }
195 },
196 reason =>
197 printDebuggingError(
198 ErrorHelper.getInternalError(
199 InternalErrorCode.CouldntImportScriptAt,
200 rnMessage.url,
201 ),
202 reason,
203 ),
204 );
205 return promise;
206 }
207
208 // TODO: Replace by url.pathToFileURL method when Node 10 LTS become deprecated
209 public pathToFileUrl(url: string): string {
210 const filePrefix = process.platform === "win32" ? "file:///" : "file://";
211 return filePrefix + url;
212 }
213}
214