Prepack Debugger+Nuclide integration (#2116)

Summary:
Release Notes: Fixed inconsistencies in debugger+Nuclide launch process (handshake ordering). Also implemented stopping on compiler diagnostics while using the debugger in Nuclide.

Demo Video: https://www.dropbox.com/s/4swkc186fjw74ol/nuclide-integration-demo.mov?dl=0

Summary of Handshake Process with Nuclide: https://fb.quip.com/0uQCAtkuPAHF
Closes https://github.com/facebook/prepack/pull/2116

Differential Revision: D8456201

Pulled By: caiismyname

fbshipit-source-id: 77277cfa0d595ef227336e8a377977911899e2c0
This commit is contained in:
David Cai 2018-06-15 13:42:49 -07:00 committed by Facebook Github Bot
parent f2219fd33c
commit 03b7fd37ff
6 changed files with 88 additions and 47 deletions

View File

@ -34,7 +34,7 @@ export default function(
// Presence of debugChannel indicates we wish to use debugger.
if (debugChannel) {
invariant(debugChannel.debuggerIsAttached(), "Debugger intends to be used but is not attached.");
invariant(opts.debuggerConfigArgs !== undefined, "Debugger intends to be used but does not have launch arguments.");
invariant(opts.debuggerConfigArgs !== undefined, "Debugger intends to be used but does not have config arguments.");
r.debuggerInstance = new DebugServer(debugChannel, r, opts.debuggerConfigArgs);
}

View File

@ -36,7 +36,7 @@ class PrepackDebugSession extends DebugSession {
this.setDebuggerColumnsStartAt1(true);
}
_clientID: void | string;
_adapterChannel: AdapterChannel;
_adapterChannel: void | AdapterChannel;
_generateDebugFilePath(direction: "in" | "out") {
let time = Date.now();
@ -50,17 +50,21 @@ class PrepackDebugSession extends DebugSession {
}
_registerMessageCallbacks() {
this._adapterChannel.registerChannelEvent(DebugMessage.STOPPED_RESPONSE, (response: DebuggerResponse) => {
this._ensureAdapterChannelCreated("registerMessageCallbacks");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
// Create local copy to ensure external functions don't modify the adapterChannel, satisfy flow.
let localCopyAdapterChannel = this._adapterChannel;
localCopyAdapterChannel.registerChannelEvent(DebugMessage.STOPPED_RESPONSE, (response: DebuggerResponse) => {
let result = response.result;
invariant(result.kind === "stopped");
let message = `${result.reason}: ${result.filePath} ${result.line}:${result.column}`;
// Append message if there exists one (for PP errors)
// Append message if there exists one (for Prepack errors)
if (result.message !== undefined) {
message += `. ${result.message}`;
}
this.sendEvent(new StoppedEvent(message, DebuggerConstants.PREPACK_THREAD_ID));
});
this._adapterChannel.registerChannelEvent(DebugMessage.STEPINTO_RESPONSE, (response: DebuggerResponse) => {
localCopyAdapterChannel.registerChannelEvent(DebugMessage.STEPINTO_RESPONSE, (response: DebuggerResponse) => {
let result = response.result;
invariant(result.kind === "stepInto");
this.sendEvent(
@ -78,10 +82,6 @@ class PrepackDebugSession extends DebugSession {
*/
// Override
initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
// Let the UI know that we can start accepting breakpoint requests.
// The UI will end the configuration sequence by calling 'configurationDone' request.
this.sendEvent(new InitializedEvent());
this._clientID = args.clientID;
response.body = response.body || {};
response.body.supportsConfigurationDoneRequest = true;
@ -97,6 +97,8 @@ class PrepackDebugSession extends DebugSession {
): void {
// initial handshake with UI is complete
if (this._clientID !== DebuggerConstants.CLI_CLIENTID) {
this._ensureAdapterChannelCreated("configurationDoneRequest");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
// for all ui except the CLI, autosend the first run request
this._adapterChannel.run(DebuggerConstants.DEFAULT_REQUEST_ID, (runResponse: DebuggerResponse) => {});
}
@ -107,8 +109,9 @@ class PrepackDebugSession extends DebugSession {
launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void {
let inFilePath = this._generateDebugFilePath("in");
let outFilePath = this._generateDebugFilePath("out");
// set up the communication channel
this._adapterChannel = new AdapterChannel(inFilePath, outFilePath);
// Set up the communication channel to the debugger.
let adapterChannel = new AdapterChannel(inFilePath, outFilePath);
this._adapterChannel = adapterChannel;
this._registerMessageCallbacks();
let launchArgs: PrepackLaunchArguments = {
kind: "launch",
@ -126,9 +129,16 @@ class PrepackDebugSession extends DebugSession {
process.exit();
},
};
this._adapterChannel.launch(response.request_seq, launchArgs, (dbgResponse: DebuggerResponse) => {
adapterChannel.launch(response.request_seq, launchArgs, (dbgResponse: DebuggerResponse) => {
this.sendResponse(response);
});
// Important: InitializedEvent indicates to the protocol that further requests (e.g. breakpoints, execution control)
// are ready to be received. Prepack debugger is not ready to receive these requests until the Adapter Channel
// has been created and Prepack has been launched. Thus, the InitializedEvent is sent after Prepack launch and
// the creation of the Adapter Channel.
this.sendEvent(new InitializedEvent());
}
/**
@ -137,6 +147,8 @@ class PrepackDebugSession extends DebugSession {
// Override
continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void {
// send a Run request to Prepack and try to send the next request
this._ensureAdapterChannelCreated("continueRequest");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
this._adapterChannel.run(response.request_seq, (dbgResponse: DebuggerResponse) => {
this.sendResponse(response);
});
@ -165,6 +177,9 @@ class PrepackDebugSession extends DebugSession {
};
breakpointInfos.push(breakpointInfo);
}
this._ensureAdapterChannelCreated("setBreakPointsRequest");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
this._adapterChannel.setBreakpoints(response.request_seq, breakpointInfos, (dbgResponse: DebuggerResponse) => {
let result = dbgResponse.result;
invariant(result.kind === "breakpoint-add");
@ -190,6 +205,8 @@ class PrepackDebugSession extends DebugSession {
// Override
stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
this._ensureAdapterChannelCreated("stackTraceRequest");
invariant(this._adapterChannel !== undefined);
this._adapterChannel.getStackFrames(response.request_seq, (dbgResponse: DebuggerResponse) => {
let result = dbgResponse.result;
invariant(result.kind === "stackframe");
@ -230,6 +247,8 @@ class PrepackDebugSession extends DebugSession {
// Override
scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
this._ensureAdapterChannelCreated("scopesRequest");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
this._adapterChannel.getScopes(response.request_seq, args.frameId, (dbgResponse: DebuggerResponse) => {
let result = dbgResponse.result;
invariant(result.kind === "scopes");
@ -252,6 +271,8 @@ class PrepackDebugSession extends DebugSession {
// Override
variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void {
this._ensureAdapterChannelCreated("variablesRequest");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
this._adapterChannel.getVariables(
response.request_seq,
args.variablesReference,
@ -278,6 +299,8 @@ class PrepackDebugSession extends DebugSession {
// Override
stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments): void {
this._ensureAdapterChannelCreated("stepInRequest");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
this._adapterChannel.stepInto(response.request_seq, (dbgResponse: DebuggerResponse) => {
this.sendResponse(response);
});
@ -285,6 +308,8 @@ class PrepackDebugSession extends DebugSession {
// Override
nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void {
this._ensureAdapterChannelCreated("nextRequest");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
this._adapterChannel.stepOver(response.request_seq, (dbgResponse: DebuggerResponse) => {
this.sendResponse(response);
});
@ -292,6 +317,8 @@ class PrepackDebugSession extends DebugSession {
// Override
stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments): void {
this._ensureAdapterChannelCreated("stepOutRequest");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
this._adapterChannel.stepOut(response.request_seq, (dbgResponse: DebuggerResponse) => {
this.sendResponse(response);
});
@ -299,6 +326,8 @@ class PrepackDebugSession extends DebugSession {
// Override
evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void {
this._ensureAdapterChannelCreated("evaluateRequest");
invariant(this._adapterChannel !== undefined, "Adapter Channel used before it was created, in debugger.");
this._adapterChannel.evaluate(
response.request_seq,
args.frameId,
@ -315,6 +344,15 @@ class PrepackDebugSession extends DebugSession {
}
);
}
_ensureAdapterChannelCreated(callingRequest: string) {
// All responses that involve the Adapter Channel should only be invoked
// after the channel has been created. If this ordering is perturbed,
// there was likely a change in the protocol implementation by Nuclide.
if (this._adapterChannel !== undefined) {
throw Error(`Adapter Channel in Debugger is being used before it has been created. Caused by ${callingRequest}.`);
}
}
}
DebugSession.run(PrepackDebugSession);

View File

@ -524,8 +524,6 @@ export class UISession {
clientID: DebuggerConstants.CLI_CLIENTID,
// a unique name for each adapter
adapterID: "Prepack-Debugger-Adapter",
linesStartAt1: true,
columnsStartAt1: true,
supportsVariableType: true,
supportsVariablePaging: false,
supportsRunInTerminalRequest: false,

View File

@ -56,7 +56,9 @@ export class BreakpointManager {
breakpointMap = new PerFileBreakpointMap(bp.filePath);
this._breakpointMaps.set(bp.filePath, breakpointMap);
}
breakpointMap.addBreakpoint(bp.line, bp.column);
// Nuclide doesn't support column debugging, so set every breakpoint
// to column 0 for consistency.
breakpointMap.addBreakpoint(bp.line, 0);
}
getBreakpoint(filePath: string, lineNum: number, columnNum: number = 0): void | Breakpoint {

View File

@ -10,7 +10,8 @@
/* @flow strict-local */
import { BreakpointManager } from "./BreakpointManager.js";
import type { BabelNode, BabelNodeSourceLocation } from "babel-types";
import { BabelNode } from "babel-types";
import type { BabelNodeSourceLocation } from "babel-types";
import invariant from "../common/invariant.js";
import type { DebugChannel } from "./channel/DebugChannel.js";
import { DebugMessage } from "./../common/channel/DebugMessage.js";
@ -50,8 +51,8 @@ export class DebugServer {
this._variableManager = new VariableManager(realm);
this._stepManager = new SteppingManager(this._realm, /* default discard old steppers */ false);
this._stopEventManager = new StopEventManager();
this.waitForRun(undefined);
this._diagnosticSeverity = configArgs.diagnosticSeverity || "FatalError";
this.waitForRun(undefined);
}
// the collection of breakpoints
_breakpointManager: BreakpointManager;
@ -69,12 +70,12 @@ export class DebugServer {
/* ast: the current ast node we are stopped on
/* reason: the reason the debuggee is stopping
*/
waitForRun(ast: void | BabelNode) {
waitForRun(loc: void | BabelNodeSourceLocation) {
let keepRunning = false;
let request;
while (!keepRunning) {
request = this._channel.readIn();
keepRunning = this.processDebuggerCommand(request, ast);
keepRunning = this.processDebuggerCommand(request, loc);
}
}
@ -88,14 +89,15 @@ export class DebugServer {
if (reason) {
invariant(ast.loc && ast.loc.source);
this._channel.sendStoppedResponse(reason, ast.loc.source, ast.loc.start.line, ast.loc.start.column);
this.waitForRun(ast);
invariant(ast.loc && ast.loc !== null);
this.waitForRun(ast.loc);
}
}
}
// Process a command from a debugger. Returns whether Prepack should unblock
// if it is blocked
processDebuggerCommand(request: DebuggerRequest, ast: void | BabelNode) {
processDebuggerCommand(request: DebuggerRequest, loc: void | BabelNodeSourceLocation) {
let requestID = request.id;
let command = request.command;
let args = request.arguments;
@ -126,7 +128,7 @@ export class DebugServer {
return true;
case DebugMessage.STACKFRAMES_COMMAND:
invariant(args.kind === "stackframe");
this.processStackframesCommand(requestID, args, ast);
this.processStackframesCommand(requestID, args, loc);
break;
case DebugMessage.SCOPES_COMMAND:
invariant(args.kind === "scopes");
@ -137,18 +139,18 @@ export class DebugServer {
this.processVariablesCommand(requestID, args);
break;
case DebugMessage.STEPINTO_COMMAND:
invariant(ast !== undefined);
this._stepManager.processStepCommand("in", ast);
invariant(loc !== undefined);
this._stepManager.processStepCommand("in", loc);
this._onDebuggeeResume();
return true;
case DebugMessage.STEPOVER_COMMAND:
invariant(ast !== undefined);
this._stepManager.processStepCommand("over", ast);
invariant(loc !== undefined);
this._stepManager.processStepCommand("over", loc);
this._onDebuggeeResume();
return true;
case DebugMessage.STEPOUT_COMMAND:
invariant(ast !== undefined);
this._stepManager.processStepCommand("out", ast);
invariant(loc !== undefined);
this._stepManager.processStepCommand("out", loc);
this._onDebuggeeResume();
return true;
case DebugMessage.EVALUATE_COMMAND:
@ -161,9 +163,9 @@ export class DebugServer {
return false;
}
processStackframesCommand(requestID: number, args: StackframeArguments, ast: void | BabelNode) {
processStackframesCommand(requestID: number, args: StackframeArguments, astLoc: void | BabelNodeSourceLocation) {
let frameInfos: Array<Stackframe> = [];
let loc = this._getFrameLocation(ast ? ast.loc : null);
let loc = this._getFrameLocation(astLoc ? astLoc : null);
let fileName = loc.fileName;
let line = loc.line;
let column = loc.column;
@ -303,6 +305,7 @@ export class DebugServer {
*/
handlePrepackError(diagnostic: CompilerDiagnostic) {
invariant(diagnostic.location && diagnostic.location.source);
// The following constructs the message and stop-instruction that is sent to the UI to actually stop the execution.
let location = diagnostic.location;
let message = `${diagnostic.severity} ${diagnostic.errorCode}: ${diagnostic.message}`;
this._channel.sendStoppedResponse(
@ -312,10 +315,10 @@ export class DebugServer {
location.start.column,
message
);
// No ast parameter b/c you cannot stepInto/Over a line that's causing an exception
this.waitForRun();
}
// The AST Node's location is needed to satisfy the subsequent stackTrace request.
this.waitForRun(location);
}
// Return whether the debugger should stop on a CompilerDiagnostic of a given severity.
shouldStopForSeverity(severity: Severity): boolean {
switch (this._diagnosticSeverity) {

View File

@ -9,7 +9,7 @@
/* @flow strict-local */
import { BabelNode } from "babel-types";
import { BabelNodeSourceLocation } from "babel-types";
import invariant from "./../common/invariant.js";
import { Stepper, StepIntoStepper, StepOverStepper, StepOutStepper } from "./Stepper.js";
import type { Realm } from "./../../realm.js";
@ -26,45 +26,45 @@ export class SteppingManager {
_keepOldSteppers: boolean;
_steppers: Array<Stepper>;
processStepCommand(kind: "in" | "over" | "out", currentNode: BabelNode) {
processStepCommand(kind: "in" | "over" | "out", currentNodeLocation: BabelNodeSourceLocation) {
if (kind === "in") {
this._processStepIn(currentNode);
this._processStepIn(currentNodeLocation);
} else if (kind === "over") {
this._processStepOver(currentNode);
this._processStepOver(currentNodeLocation);
} else if (kind === "out") {
this._processStepOut(currentNode);
this._processStepOut(currentNodeLocation);
} else {
invariant(false, `Invalid step type: ${kind}`);
}
}
_processStepIn(ast: BabelNode) {
invariant(ast.loc && ast.loc.source);
_processStepIn(loc: BabelNodeSourceLocation) {
invariant(loc && loc.source);
if (!this._keepOldSteppers) {
this._steppers = [];
}
this._steppers.push(
new StepIntoStepper(ast.loc.source, ast.loc.start.line, ast.loc.start.column, this._realm.contextStack.length)
new StepIntoStepper(loc.source, loc.start.line, loc.start.column, this._realm.contextStack.length)
);
}
_processStepOver(ast: BabelNode) {
invariant(ast.loc && ast.loc.source);
_processStepOver(loc: BabelNodeSourceLocation) {
invariant(loc && loc.source);
if (!this._keepOldSteppers) {
this._steppers = [];
}
this._steppers.push(
new StepOverStepper(ast.loc.source, ast.loc.start.line, ast.loc.start.column, this._realm.contextStack.length)
new StepOverStepper(loc.source, loc.start.line, loc.start.column, this._realm.contextStack.length)
);
}
_processStepOut(ast: BabelNode) {
invariant(ast.loc && ast.loc.source);
_processStepOut(loc: BabelNodeSourceLocation) {
invariant(loc && loc.source);
if (!this._keepOldSteppers) {
this._steppers = [];
}
this._steppers.push(
new StepOutStepper(ast.loc.source, ast.loc.start.line, ast.loc.start.column, this._realm.contextStack.length)
new StepOutStepper(loc.source, loc.start.line, loc.start.column, this._realm.contextStack.length)
);
}