microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e866487435c097b8faba759339189dbe5705eea9

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/forkedAppWorker.ts

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