microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1c2424f4d9af42a0cefcacb0fcb9670b5aa292cd

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/forkedAppWorker.ts

217lines · 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, DownloadedScript } 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>;
38 private bundleLoaded: Promise<void>;
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 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 Promise.resolve(port);
137 }
138
139 public postMessage(rnMessage: RNAppMessage): Promise<RNAppMessage> {
140 return new Promise(resolve => {
141 if (this.workerLoaded) {
142 resolve();
143 } else {
144 const checkWorkerLoaded = setInterval(() => {
145 if (this.workerLoaded) {
146 clearInterval(checkWorkerLoaded);
147 resolve();
148 }
149 }, 1000);
150 }
151 }).then(() => {
152 // Before sending messages, make sure that the worker is loaded
153 const promise = this.workerLoaded.then(() => {
154 if (rnMessage.method !== "executeApplicationScript") {
155 // Before sending messages, make sure that the app script executed
156 if (this.bundleLoaded) {
157 return this.bundleLoaded.then(() => {
158 return rnMessage;
159 });
160 } else {
161 return rnMessage;
162 }
163 } else {
164 // When packager asks worker to load bundle we download that bundle and
165 // then set url field to point to that downloaded bundle, so the worker
166 // will take our modified bundle
167 if (rnMessage.url) {
168 const packagerUrl = url.parse(rnMessage.url);
169 packagerUrl.host = `${this.packagerAddress}:${this.packagerPort}`;
170 rnMessage = {
171 ...rnMessage,
172 url: url.format(packagerUrl),
173 };
174 logger.verbose(
175 `Packager requested runtime to load script from ${rnMessage.url}`,
176 );
177 return this.scriptImporter
178 .downloadAppScript(<string>rnMessage.url, this.projectRootPath)
179 .then((downloadedScript: DownloadedScript) => {
180 this.bundleLoaded = Promise.resolve();
181 return Object.assign({}, rnMessage, {
182 url: `${this.pathToFileUrl(downloadedScript.filepath)}`,
183 });
184 });
185 } else {
186 throw ErrorHelper.getInternalError(
187 InternalErrorCode.RNMessageWithMethodExecuteApplicationScriptDoesntHaveURLProperty,
188 );
189 }
190 }
191 });
192 promise.then(
193 (message: RNAppMessage) => {
194 if (this.debuggeeProcess) {
195 this.debuggeeProcess.send({ data: message });
196 }
197 },
198 reason =>
199 printDebuggingError(
200 ErrorHelper.getInternalError(
201 InternalErrorCode.CouldntImportScriptAt,
202 rnMessage.url,
203 ),
204 reason,
205 ),
206 );
207
208 return promise;
209 });
210 }
211
212 // TODO: Replace by url.pathToFileURL method when Node 10 LTS become deprecated
213 public pathToFileUrl(url: string): string {
214 const filePrefix = process.platform === "win32" ? "file:///" : "file://";
215 return filePrefix + url;
216 }
217}
218