// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as Q from "q";
import * as path from "path";
import * as fs from "fs";
import stripJsonComments = require("strip-json-comments");
import { Telemetry } from "../common/telemetry";
import { TelemetryHelper } from "../common/telemetryHelper";
import { RemoteExtension } from "../common/remoteExtension";
import { PlatformResolver } from "./platformResolver";
import { IRunOptions } from "../common/launchArgs";
import { TargetPlatformHelper } from "../common/targetPlatformHelper";
import { ExtensionTelemetryReporter, ReassignableTelemetryReporter } from "../common/telemetryReporters";
import { NodeDebugAdapterLogger } from "../common/log/loggers";
import { Log } from "../common/log/log";
import { LogLevel } from "../common/log/logHelper";
import { GeneralMobilePlatform } from "../common/generalMobilePlatform";
import { MultipleLifetimesAppWorker } from "./appWorker";
export function makeSession(
debugSessionClass: typeof ChromeDebuggerCorePackage.ChromeDebugSession,
debugSessionOpts: ChromeDebuggerCorePackage.IChromeDebugSessionOpts,
debugAdapterPackage: typeof VSCodeDebugAdapterPackage,
telemetryReporter: ReassignableTelemetryReporter,
appName: string, version: string): typeof ChromeDebuggerCorePackage.ChromeDebugSession {
return class extends debugSessionClass {
private projectRootPath: string;
private remoteExtension: RemoteExtension;
private mobilePlatformOptions: IRunOptions;
private appWorker: MultipleLifetimesAppWorker | null = null;
constructor(debuggerLinesAndColumnsStartAt1?: boolean, isServer?: boolean) {
super(debuggerLinesAndColumnsStartAt1, isServer, debugSessionOpts);
}
// Override ChromeDebugSession's sendEvent to control what we will send to client
public sendEvent(event: VSCodeDebugAdapterPackage.Event): void {
// Do not send "terminated" events signaling about session's restart to client as it would cause it
// to restart adapter's process, while we want to stay alive and don't want to interrupt connection
// to packager.
if (event.event === "terminated" && event.body && event.body.restart) {
// Worker has been reloaded and switched to "continue" state
// So we have to send "continued" event to client instead of "terminated"
// Otherwise client might mistakenly show "stopped" state
let continuedEvent: VSCodeDebugAdapterPackage.ContinuedEvent = {
event: "continued",
type: "event",
seq: event["seq"], // tslint:disable-line
body: { threadId: event.body.threadId },
};
super.sendEvent(continuedEvent);
return;
}
super.sendEvent(event);
}
protected dispatchRequest(request: VSCodeDebugAdapterPackage.Request): void {
if (request.command === "disconnect")
return this.disconnect(request);
if (request.command === "attach")
return this.attach(request);
if (request.command === "launch")
return this.launch(request);
return super.dispatchRequest(request);
}
private launch(request: VSCodeDebugAdapterPackage.Request): void {
this.requestSetup(request.arguments)
.then(() => {
// We add the parameter if it's defined (adapter crashes otherwise)
if (!isNullOrUndefined(request.arguments.logCatArguments)) {
this.mobilePlatformOptions.logCatArguments = [parseLogCatArguments(request.arguments.logCatArguments)];
}
if (!isNullOrUndefined(request.arguments.variant)) {
this.mobilePlatformOptions.variant = request.arguments.variant;
}
if (!isNullOrUndefined(request.arguments.scheme)) {
this.mobilePlatformOptions.scheme = request.arguments.scheme;
}
TelemetryHelper.generate("launch", (generator) => {
return this.remoteExtension.getPackagerPort()
.then((packagerPort: number) => {
this.mobilePlatformOptions.packagerPort = packagerPort;
const mobilePlatform = new PlatformResolver()
.resolveMobilePlatform(request.arguments.platform, this.mobilePlatformOptions);
generator.step("checkPlatformCompatibility");
TargetPlatformHelper.checkTargetPlatformSupport(this.mobilePlatformOptions.platform);
generator.step("startPackager");
return mobilePlatform.startPackager()
.then(() => {
// We've seen that if we don't prewarm the bundle cache, the app fails on the first attempt to connect to the debugger logic
// and the user needs to Reload JS manually. We prewarm it to prevent that issue
generator.step("prewarmBundleCache");
Log.logMessage("Prewarming bundle cache. This may take a while ...");
return mobilePlatform.prewarmBundleCache();
})
.then(() => {
generator.step("mobilePlatform.runApp");
Log.logMessage("Building and running application.");
return mobilePlatform.runApp();
})
.then(() => {
return this.attachRequest(request, packagerPort, mobilePlatform);
});
})
.catch(error => this.bailOut(error.message));
});
});
}
private attach(request: VSCodeDebugAdapterPackage.Request): void {
this.requestSetup(request.arguments)
.then(() => {
this.remoteExtension.getPackagerPort()
.then((packagerPort: number) => this.attachRequest(request, packagerPort));
});
}
private disconnect(request: VSCodeDebugAdapterPackage.Request): void {
// The client is about to disconnect so first we need to stop app worker
if (this.appWorker) {
this.appWorker.stop();
}
// Then we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
if (this.mobilePlatformOptions.platform === "android") {
this.remoteExtension.stopMonitoringLogcat()
.catch(reason => Log.logError(`WARNING: Couldn't stop monitoring logcat: ${reason.message || reason}\n`))
.finally(() => super.dispatchRequest(request));
} else {
super.dispatchRequest(request);
}
}
private requestSetup(args: any): Q.Promise<void> {
this.projectRootPath = getProjectRoot(args);
this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
this.mobilePlatformOptions = {
projectRoot: this.projectRootPath,
platform: args.platform,
targetType: args.targetType || "simulator",
};
// Start to send telemetry
telemetryReporter.reassignTo(new ExtensionTelemetryReporter(
appName, version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
Log.SetGlobalLogger(new NodeDebugAdapterLogger(debugAdapterPackage, this));
if (!args.runArguments) {
return this.remoteExtension.getRunArgs(args.platform, args.targetType || "simulator")
.then(runArgs => {
this.mobilePlatformOptions.runArguments = runArgs;
});
}
return Q.resolve(void 0);
}
/**
* Runs logic needed to attach.
* Attach should:
* - Enable js debugging
*/
private attachRequest(
request: VSCodeDebugAdapterPackage.Request,
packagerPort: number,
mobilePlatform?: GeneralMobilePlatform): Q.Promise<void> {
return TelemetryHelper.generate("attach", (generator) => {
return Q({})
.then(() => {
generator.step("mobilePlatform.enableJSDebuggingMode");
if (mobilePlatform) {
return mobilePlatform.enableJSDebuggingMode();
} else {
Log.logMessage("Debugger ready. Enable remote debugging in app.");
return void 0;
}
})
.then(() => {
Log.logMessage("Starting debugger app worker.");
// TODO: remove dependency on args.program - "program" property is technically
// no more required in launch configuration and could be removed
const workspaceRootPath = path.resolve(path.dirname(request.arguments.program), "..");
const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
// If launch is invoked first time, appWorker is undefined, so create it here
this.appWorker = new MultipleLifetimesAppWorker(packagerPort, sourcesStoragePath);
this.appWorker.on("connected", (port: number) => {
Log.logMessage("Debugger worker loaded runtime on port " + port);
// Don't mutate original request to avoid side effects
let attachArguments = Object.assign({}, request.arguments, { port, restart: true, request: "attach" });
let attachRequest = Object.assign({}, request, { command: "attach", arguments: attachArguments });
// Reinstantiate debug adapter, as the current implementation of ChromeDebugAdapter
// doesn't allow us to reattach to another debug target easily. As of now it's easier
// to throw previous instance out and create a new one.
this._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
super.dispatchRequest(attachRequest);
});
return this.appWorker.start();
})
.catch(error => this.bailOut(error.message));
});
}
/**
* Logs error to user and finishes the debugging process.
*/
private bailOut(message: string): void {
Log.logError(`Could not debug. ${message}`);
this.sendEvent(new debugAdapterPackage.TerminatedEvent());
}
};
}
export function makeAdapter(debugAdapterClass: typeof Node2DebugAdapterPackage.Node2DebugAdapter): typeof Node2DebugAdapterPackage.Node2DebugAdapter {
return class extends debugAdapterClass {
public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
// We need to overwrite ChromeDebug's _attachMode to let Node2 adapter
// to set up breakpoints on initial pause event
this._attachMode = false;
return super.doAttach(port, targetUrl, address, timeout);
}
public setBreakpoints(args: any, requestSeq: number, ids?: number[]): Promise<Node2DebugAdapterPackage.ISetBreakpointsResponseBody> {
// We need to overwrite ChromeDebug's setBreakpoints to get rid unhandled rejections
// when breakpoints are being set up unsuccessfully
return super.setBreakpoints(args, requestSeq, ids).catch((err) => {
Log.logInternalMessage(LogLevel.Error, err.message);
return {
breakpoints: [],
};
});
}
};
}
/**
* Parses log cat arguments to a string
*/
function parseLogCatArguments(userProvidedLogCatArguments: any): string {
return Array.isArray(userProvidedLogCatArguments)
? userProvidedLogCatArguments.join(" ") // If it's an array, we join the arguments
: userProvidedLogCatArguments; // If not, we leave it as-is
}
/**
* Helper method to know if a value is either null or undefined
*/
function isNullOrUndefined(value: any): boolean {
return typeof value === "undefined" || value === null;
}
/**
* Parses settings.json file for workspace root property
*/
function getProjectRoot(args: any): string {
try {
let vsCodeRoot = path.resolve(args.program, "../..");
let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
let settingsContent = fs.readFileSync(settingsPath, "utf8");
settingsContent = stripJsonComments(settingsContent);
let parsedSettings = JSON.parse(settingsContent);
let projectRootPath = parsedSettings["react-native-tools"].projectRoot;
return path.resolve(vsCodeRoot, projectRootPath);
} catch (e) {
return path.resolve(args.program, "../..");
}
}microsoft/vscode-react-native
Publicmirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable
src/debugger/nodeDebugWrapper.ts
288lines · modepreview