microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
23b5fa80b6aac0ade9330c2324a4df525f04abc6

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/debugger/nodeDebugWrapper.ts

286lines · 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
151 if (!args.sourceMaps) {
152 args.sourceMaps = true;
153 }
154 const projectRootPath = getProjectRoot(args);
155 return ReactNativeProjectHelper.isReactNativeProject(projectRootPath)
156 .then((result) => {
157 if (!result) {
158 throw ErrorHelper.getInternalError(InternalErrorCode.NotInReactNativeFolderError);
159 }
160 this.projectRootPath = projectRootPath;
161 this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
162
163 // Start to send telemetry
164 telemetryReporter.reassignTo(new RemoteTelemetryReporter(
165 appName, version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
166
167 if (args.program) {
168 // TODO: Remove this warning when program property will be completely removed
169 logger.warn(localize("ProgramPropertyDeprecationWarning", "Launched debug configuration contains 'program' property which is deprecated and will be removed soon. Please replace it with: \"cwd\": \"${workspaceFolder}\""));
170 const useProgramEvent = TelemetryHelper.createTelemetryEvent("useProgramProperty");
171 Telemetry.send(useProgramEvent);
172 }
173 if (args.cwd) {
174 // To match count of 'cwd' users with 'program' users. TODO: Remove when program property will be removed
175 const useCwdEvent = TelemetryHelper.createTelemetryEvent("useCwdProperty");
176 Telemetry.send(useCwdEvent);
177 }
178 return void 0;
179 });
180 }
181
182 /**
183 * Runs logic needed to attach.
184 * Attach should:
185 * - Enable js debugging
186 */
187 // tslint:disable-next-line:member-ordering
188 protected attachRequest(request: DebugProtocol.Request): Q.Promise<void> {
189 const extProps = {
190 platform: {
191 value: request.arguments.platform,
192 isPii: false,
193 },
194 };
195
196 return TelemetryHelper.generate("attach", extProps, (generator) => {
197 return Q({})
198 .then(() => {
199 logger.log(localize("StartingDebuggerAppWorker", "Starting debugger app worker."));
200 // TODO: remove dependency on args.program - "program" property is technically
201 // no more required in launch configuration and could be removed
202 const workspaceRootPath = request.arguments.cwd ? path.resolve(request.arguments.cwd) : path.resolve(path.dirname(request.arguments.program), "..");
203 const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
204
205 // If launch is invoked first time, appWorker is undefined, so create it here
206 this.appWorker = new MultipleLifetimesAppWorker(
207 request.arguments,
208 sourcesStoragePath,
209 this.projectRootPath,
210 undefined);
211 this.appWorker.on("connected", (port: number) => {
212 logger.log(localize("DebuggerWorkerLoadedRuntimeOnPort", "Debugger worker loaded runtime on port {0}", port));
213 // Don't mutate original request to avoid side effects
214 let attachArguments = Object.assign({}, request.arguments, {
215 address: "localhost",
216 port,
217 restart: true,
218 request: "attach",
219 remoteRoot: undefined,
220 localRoot: undefined,
221 });
222 // Reinstantiate debug adapter, as the current implementation of ChromeDebugAdapter
223 // doesn't allow us to reattach to another debug target easily. As of now it's easier
224 // to throw previous instance out and create a new one.
225 (this as any)._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
226
227 // Explicity call _debugAdapter.attach() to prevent directly calling dispatchRequest()
228 // yield a response as "attach" even for "launch" request. Because dispatchRequest() will
229 // decide to do a sendResponse() aligning with the request parameter passed in.
230 Q((this as any)._debugAdapter.attach(attachArguments, request.seq))
231 .then((responseBody) => {
232 const response: DebugProtocol.Response = new Response(request);
233 response.body = responseBody;
234 this.sendResponse(response);
235 });
236 });
237
238 return this.appWorker.start();
239 })
240 .catch(error => this.bailOut(error.message));
241 });
242 }
243
244 /**
245 * Logs error to user and finishes the debugging process.
246 */
247 private bailOut(message: string): void {
248 logger.error(localize("CouldNotDebug", "Could not debug. {0}" , message));
249 this.sendEvent(new TerminatedEvent());
250 }
251 };
252}
253
254export function makeAdapter(debugAdapterClass: typeof ChromeDebugAdapter): typeof ChromeDebugAdapter {
255 return class extends debugAdapterClass {
256 public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
257 // We need to overwrite ChromeDebug's _attachMode to let Node2 adapter
258 // to set up breakpoints on initial pause event
259 (this as any)._attachMode = false;
260 return super.doAttach(port, targetUrl, address, timeout);
261 }
262
263 public async terminate(args: DebugProtocol.TerminatedEvent) {
264 return this.disconnect({
265 terminateDebuggee: true,
266 });
267 }
268 };
269}
270
271/**
272 * Parses settings.json file for workspace root property
273 */
274function getProjectRoot(args: any): string {
275 try {
276 let vsCodeRoot = args.cwd ? path.resolve(args.cwd) : path.resolve(args.program, "../..");
277 let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
278 let settingsContent = fs.readFileSync(settingsPath, "utf8");
279 settingsContent = stripJsonComments(settingsContent);
280 let parsedSettings = JSON.parse(settingsContent);
281 let projectRootPath = parsedSettings["react-native-tools.projectRoot"] || parsedSettings["react-native-tools"].projectRoot;
282 return path.resolve(vsCodeRoot, projectRootPath);
283 } catch (e) {
284 return args.cwd ? path.resolve(args.cwd) : path.resolve(args.program, "../..");
285 }
286}
287