microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
74471e03aeec8c50a688655033482e07b2a39112

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

276lines · 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 Q from "q";
5import * as path from "path";
6import * as fs from "fs";
7import stripJsonComments = require("strip-json-comments");
8
9import { Telemetry } from "../common/telemetry";
10import { TelemetryHelper } from "../common/telemetryHelper";
11import { RemoteExtension } from "../common/remoteExtension";
12import { RemoteTelemetryReporter, ReassignableTelemetryReporter } from "../common/telemetryReporters";
13import { ChromeDebugSession, IChromeDebugSessionOpts, ChromeDebugAdapter, logger } from "vscode-chrome-debug-core";
14import { ContinuedEvent, TerminatedEvent, Logger, Response } from "vscode-debugadapter";
15import { DebugProtocol } from "vscode-debugprotocol";
16
17import { MultipleLifetimesAppWorker } from "./appWorker";
18
19import { ReactNativeProjectHelper } from "../common/reactNativeProjectHelper";
20import * as nls from "vscode-nls";
21import { ErrorHelper } from "../common/error/errorHelper";
22import { InternalErrorCode } from "../common/error/internalErrorCode";
23import { getLoggingDirectory } from "../extension/log/LogHelper";
24const localize = nls.loadMessageBundle();
25
26export function makeSession(
27 debugSessionClass: typeof ChromeDebugSession,
28 debugSessionOpts: IChromeDebugSessionOpts,
29 telemetryReporter: ReassignableTelemetryReporter,
30 appName: string, version: string): typeof ChromeDebugSession {
31
32 return class extends debugSessionClass {
33
34 private projectRootPath: string;
35 private remoteExtension: RemoteExtension;
36 private appWorker: MultipleLifetimesAppWorker | null = null;
37
38 constructor(debuggerLinesAndColumnsStartAt1?: boolean, isServer?: boolean) {
39 super(debuggerLinesAndColumnsStartAt1, isServer, debugSessionOpts);
40 }
41
42 // Override ChromeDebugSession's sendEvent to control what we will send to client
43 public sendEvent(event: DebugProtocol.Event): void {
44 // Do not send "terminated" events signaling about session's restart to client as it would cause it
45 // to restart adapter's process, while we want to stay alive and don't want to interrupt connection
46 // to packager.
47
48 if (event.event === "terminated" && event.body && event.body.restart) {
49
50 // Worker has been reloaded and switched to "continue" state
51 // So we have to send "continued" event to client instead of "terminated"
52 // Otherwise client might mistakenly show "stopped" state
53 let continuedEvent: ContinuedEvent = {
54 event: "continued",
55 type: "event",
56 seq: event["seq"], // tslint:disable-line
57 body: { threadId: event.body.threadId },
58 };
59
60 super.sendEvent(continuedEvent);
61 return;
62 }
63
64 super.sendEvent(event);
65 }
66
67 protected dispatchRequest(request: DebugProtocol.Request): void {
68 if (request.command === "disconnect")
69 return this.disconnect(request);
70
71 if (request.command === "attach")
72 return this.attach(request);
73
74 if (request.command === "launch")
75 return this.launch(request);
76
77 return super.dispatchRequest(request);
78 }
79
80 private launch(request: DebugProtocol.Request): void {
81 this.requestSetup(request.arguments)
82 .then(() => {
83 logger.verbose(`Handle launch request: ${JSON.stringify(request.arguments, null , 2)}`);
84 return this.remoteExtension.launch(request);
85 })
86 .then(() => {
87 return this.remoteExtension.getPackagerPort(request.arguments.program);
88 })
89 .then((packagerPort: number) => {
90 this.attachRequest({
91 ...request,
92 arguments: {
93 ...request.arguments,
94 port: packagerPort,
95 },
96 });
97 })
98 .catch(error => {
99 this.bailOut(error.data || error.message);
100 });
101 }
102
103 private attach(request: DebugProtocol.Request): void {
104 this.requestSetup(request.arguments)
105 .then(() => {
106 logger.verbose(`Handle attach request: ${JSON.stringify(request.arguments, null , 2)}`);
107 return this.remoteExtension.getPackagerPort(request.arguments.program);
108 })
109 .then((packagerPort: number) => {
110 this.attachRequest({
111 ...request,
112 arguments: {
113 ...request.arguments,
114 port: request.arguments.port || packagerPort,
115 },
116 });
117 })
118 .catch(error => {
119 this.bailOut(error.data || error.message);
120 });
121 }
122
123 private disconnect(request: DebugProtocol.Request): void {
124 // The client is about to disconnect so first we need to stop app worker
125 if (this.appWorker) {
126 this.appWorker.stop();
127 }
128
129 // Then we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
130 if (request.arguments.platform === "android") {
131 this.remoteExtension.stopMonitoringLogcat()
132 .catch(reason => logger.warn(localize("CouldNotStopMonitoringLogcat", "Couldn't stop monitoring logcat: {0}", reason.message || reason)))
133 .finally(() => super.dispatchRequest(request));
134 } else {
135 super.dispatchRequest(request);
136 }
137 }
138
139 private requestSetup(args: any): Q.Promise<void> {
140 // If special env variables are defined, then write process outputs to file
141 let chromeDebugCoreLogs = getLoggingDirectory();
142 if (chromeDebugCoreLogs) {
143 chromeDebugCoreLogs = path.join(chromeDebugCoreLogs, "ChromeDebugCoreLogs.txt");
144 }
145 let logLevel: string = args.trace;
146 if (logLevel) {
147 logLevel = logLevel.replace(logLevel[0], logLevel[0].toUpperCase());
148 logger.setup(Logger.LogLevel[logLevel], chromeDebugCoreLogs || false);
149 } else {
150 logger.setup(Logger.LogLevel.Log, chromeDebugCoreLogs || false);
151 }
152
153 if (!args.sourceMaps) {
154 args.sourceMaps = true;
155 }
156 const projectRootPath = getProjectRoot(args);
157 return ReactNativeProjectHelper.isReactNativeProject(projectRootPath)
158 .then((result) => {
159 if (!result) {
160 throw ErrorHelper.getInternalError(InternalErrorCode.NotInReactNativeFolderError);
161 }
162 this.projectRootPath = projectRootPath;
163 this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
164
165 // Start to send telemetry
166 telemetryReporter.reassignTo(new RemoteTelemetryReporter(
167 appName, version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
168 return void 0;
169 });
170 }
171
172 /**
173 * Runs logic needed to attach.
174 * Attach should:
175 * - Enable js debugging
176 */
177 // tslint:disable-next-line:member-ordering
178 protected attachRequest(request: DebugProtocol.Request): Q.Promise<void> {
179 const extProps = {
180 platform: {
181 value: request.arguments.platform,
182 isPii: false,
183 },
184 };
185
186 return TelemetryHelper.generate("attach", extProps, (generator) => {
187 return Q({})
188 .then(() => {
189 logger.log(localize("StartingDebuggerAppWorker", "Starting debugger app worker."));
190 // TODO: remove dependency on args.program - "program" property is technically
191 // no more required in launch configuration and could be removed
192 const workspaceRootPath = path.resolve(path.dirname(request.arguments.program), "..");
193 const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
194
195 // If launch is invoked first time, appWorker is undefined, so create it here
196 this.appWorker = new MultipleLifetimesAppWorker(
197 request.arguments,
198 sourcesStoragePath,
199 this.projectRootPath,
200 undefined);
201 this.appWorker.on("connected", (port: number) => {
202 logger.log(localize("DebuggerWorkerLoadedRuntimeOnPort", "Debugger worker loaded runtime on port {0}", port));
203 // Don't mutate original request to avoid side effects
204 let attachArguments = Object.assign({}, request.arguments, {
205 address: "localhost",
206 port,
207 restart: true,
208 request: "attach",
209 remoteRoot: undefined,
210 localRoot: undefined,
211 });
212 // Reinstantiate debug adapter, as the current implementation of ChromeDebugAdapter
213 // doesn't allow us to reattach to another debug target easily. As of now it's easier
214 // to throw previous instance out and create a new one.
215 (this as any)._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
216
217 // Explicity call _debugAdapter.attach() to prevent directly calling dispatchRequest()
218 // yield a response as "attach" even for "launch" request. Because dispatchRequest() will
219 // decide to do a sendResponse() aligning with the request parameter passed in.
220 Q((this as any)._debugAdapter.attach(attachArguments, request.seq))
221 .then((responseBody) => {
222 const response: DebugProtocol.Response = new Response(request);
223 response.body = responseBody;
224 this.sendResponse(response);
225 });
226 });
227
228 return this.appWorker.start();
229 })
230 .catch(error => this.bailOut(error.message));
231 });
232 }
233
234 /**
235 * Logs error to user and finishes the debugging process.
236 */
237 private bailOut(message: string): void {
238 logger.error(localize("CouldNotDebug", "Could not debug. {0}" , message));
239 this.sendEvent(new TerminatedEvent());
240 }
241 };
242}
243
244export function makeAdapter(debugAdapterClass: typeof ChromeDebugAdapter): typeof ChromeDebugAdapter {
245 return class extends debugAdapterClass {
246 public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
247 // We need to overwrite ChromeDebug's _attachMode to let Node2 adapter
248 // to set up breakpoints on initial pause event
249 (this as any)._attachMode = false;
250 return super.doAttach(port, targetUrl, address, timeout);
251 }
252
253 public async terminate(args: DebugProtocol.TerminatedEvent) {
254 return this.disconnect({
255 terminateDebuggee: true,
256 });
257 }
258 };
259}
260
261/**
262 * Parses settings.json file for workspace root property
263 */
264function getProjectRoot(args: any): string {
265 try {
266 let vsCodeRoot = path.resolve(args.program, "../..");
267 let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
268 let settingsContent = fs.readFileSync(settingsPath, "utf8");
269 settingsContent = stripJsonComments(settingsContent);
270 let parsedSettings = JSON.parse(settingsContent);
271 let projectRootPath = parsedSettings["react-native-tools.projectRoot"] || parsedSettings["react-native-tools"].projectRoot;
272 return path.resolve(vsCodeRoot, projectRootPath);
273 } catch (e) {
274 return path.resolve(args.program, "../..");
275 }
276}
277