microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
0.10.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

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