Set up adapter communication channel with Prepack

Summary:
Release note: none
Issue: #907

- Set up AdapterChannel object to read and write to Prepack
- Changed Prepack DebugChannel to read and write in the same format
- Implement init, stopped, terminated events
- Implement set breakpoints end to end

The adapter will hold a queue to store requests to Prepack that came from the UI. When Prepack is ready to receive a request, the adapter will send a request from the queue through this AdapterChannel.
Closes https://github.com/facebook/prepack/pull/1091

Differential Revision: D6174872

Pulled By: JWZ2018

fbshipit-source-id: 63c0ebf32bd76d7c6214c5a294b38e07c6be51f7
This commit is contained in:
Wuhan Zhou 2017-10-27 12:28:35 -07:00 committed by Facebook Github Bot
parent 3ab3d3c133
commit f7025fc2ed
17 changed files with 700 additions and 158 deletions

View File

@ -9,6 +9,8 @@
/* @flow */
// Generated using flowgen from vscode-debugadapter npm package and modified
import DebugProtocol from 'vscode-debugprotocol'
import * as ee from 'events';
declare module 'vscode-debugadapter' {
@ -28,9 +30,31 @@ declare module 'vscode-debugadapter' {
event: string;
constructor(event: string, body?: any): this
}
declare export class StoppedEvent mixins Event, DebugProtocol.StoppedEvent {
body: {
reason: string,
threadId: number
};
constructor(reason: string, threadId: number, exception_text?: string): this
}
declare export class InitializedEvent mixins Event, DebugProtocol.InitializedEvent {
constructor(): this
}
declare export class TerminatedEvent mixins Event, DebugProtocol.TerminatedEvent {
/** Protocol: A debug adapter may set 'restart' to true (or to an arbitrary object) to request that the front end restarts the session.
The value is not interpreted by the client and passed unmodified as an attribute '__restart' to the launchRequest.
*/
constructor(restart?: any): this
}
declare export class OutputEvent mixins Event, DebugProtocol.OutputEvent {
body: {
category: string,
output: string,
// Protocol: this can be any optional data to report
data?: any
};
constructor(output: string, category?: string, data?: any): this
}
declare export class ProtocolServer mixins ee.EventEmitter {
constructor(): this;
start(inStream: ee.EventEmitter.ReadableStream, outStream: ee.EventEmitter.WritableStream): void;

View File

@ -1,66 +0,0 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
import path from "path";
import type { DebuggerOptions } from "./options";
export class DebugChannel {
constructor(fs: any, dbgOptions: DebuggerOptions) {
this.inFilePath = path.join(__dirname, "../", dbgOptions.inFilePath);
this.outFilePath = path.join(__dirname, "../", dbgOptions.outFilePath);
this.fs = fs;
this.requestReceived = false;
}
inFilePath: string;
outFilePath: string;
fs: any;
requestReceived: boolean;
/*
/* Only called in the beginning to check if a debugger is attached
*/
debuggerIsAttached(): boolean {
let line = this.readIn();
if (line === "Debugger Attached\n") {
this.writeOut("Ready\n");
return true;
}
return false;
}
/* Reads in a request from the debug adapter
/* The caller is responsible for sending a response with the appropriate
/* contents at the right time.
/* For now, it returns the request as a string. It will be made to return a
/* Request object based on the protocol
*/
readIn(): string {
let contents = "";
while (contents.length === 0) {
contents = this.fs.readFileSync(this.inFilePath, "utf8");
}
//clear the file
this.fs.writeFileSync(this.inFilePath, "");
this.requestReceived = true;
return contents;
}
/* Write out a response to the debug adapter
/* For now, it writes the response as a string. It will be made to return
/* a Response object based on the protocol
*/
writeOut(contents: string): void {
//Prepack only writes back to the debug adapter in response to a request
if (this.requestReceived) {
this.fs.writeFileSync(this.outFilePath, contents);
this.requestReceived = false;
}
}
}

View File

@ -18,7 +18,7 @@ import * as partialEvaluators from "./partial-evaluators/index.js";
import { NewGlobalEnvironment } from "./methods/index.js";
import { ObjectValue } from "./values/index.js";
import { DebugServer } from "./debugger/Debugger.js";
import { DebugChannel } from "./DebugChannel.js";
import type { DebugChannel } from "./debugger/channel/DebugChannel.js";
import simplifyAndRefineAbstractValue from "./utils/simplifier.js";
export default function(opts: RealmOptions = {}, debugChannel: void | DebugChannel = undefined): Realm {

View File

@ -11,9 +11,12 @@
import type { BabelNode } from "babel-types";
import { BreakpointCollection } from "./BreakpointCollection.js";
import { Breakpoint } from "./Breakpoint.js";
import type { BreakpointCommandArguments } from "./types.js";
import invariant from "../invariant.js";
import { DebugChannel } from "../DebugChannel.js";
import type { DebugChannel } from "./channel/DebugChannel.js";
import { DebugMessage } from "./channel/DebugMessage.js";
import { DebuggerError } from "./DebuggerError.js";
export class DebugServer {
constructor(channel: DebugChannel) {
@ -21,9 +24,7 @@ export class DebugServer {
this.previousExecutedLine = 0;
this.previousExecutedCol = 0;
this.channel = channel;
this.waitForRun(function(line) {
return line === "Run";
});
this.waitForRun();
}
// the collection of breakpoints
breakpoints: BreakpointCollection;
@ -37,30 +38,18 @@ export class DebugServer {
/* runCondition: a function that determines whether the adapter has told
/* Prepack to continue running
*/
waitForRun(runCondition: string => boolean) {
let blocking = true;
let line = "";
while (blocking) {
line = this.channel.readIn().toString();
if (runCondition(line)) {
//The adapter gave the command to continue running
//The caller (or someone else later) needs to send a response
//to the adapter
//We cannot pass in the response too because it may not be ready
//immediately after Prepack unblocks
blocking = false;
} else {
//The adapter gave another command so Prepack still blocks
//but can read in other commands and respond to them
this.executeCommand(line);
}
waitForRun() {
let keepRunning = false;
let message = "";
while (!keepRunning) {
message = this.channel.readIn().toString();
keepRunning = this.processDebuggerCommand(message);
}
}
// Checking if the debugger needs to take any action on reaching this ast node
checkForActions(ast: BabelNode) {
this.checkForBreakpoint(ast);
// last step: set the current location as the previously executed line
if (ast.loc && ast.loc.source !== null) {
this.previousExecutedFile = ast.loc.source;
@ -69,7 +58,8 @@ export class DebugServer {
}
}
proceedBreakpoint(filePath: string, lineNum: number, colNum: number): boolean {
// Try to find a breakpoint at the given location and check if we should stop on it
findStoppableBreakpoint(filePath: string, lineNum: number, colNum: number): null | Breakpoint {
let breakpoint = this.breakpoints.getBreakpoint(filePath, lineNum, colNum);
if (breakpoint && breakpoint.enabled) {
// checking if this is the same file and line we stopped at last time
@ -78,16 +68,24 @@ export class DebugServer {
// breakpoint consecutively (e.g. the statement is in a loop), some other
// ast node (e.g. block, loop) must have been checked in between so
// previousExecutedFile and previousExecutedLine will have changed
if (
filePath === this.previousExecutedFile &&
lineNum === this.previousExecutedLine &&
colNum === this.previousExecutedCol
) {
return false;
if (breakpoint.column !== 0) {
// this is a column breakpoint
if (
filePath === this.previousExecutedFile &&
lineNum === this.previousExecutedLine &&
colNum === this.previousExecutedCol
) {
return null;
}
} else {
// this is a line breakpoint
if (filePath === this.previousExecutedFile && lineNum === this.previousExecutedLine) {
return null;
}
}
return true;
return breakpoint;
}
return false;
return null;
}
checkForBreakpoint(ast: BabelNode) {
@ -97,56 +95,89 @@ export class DebugServer {
if (filePath === null) return;
let lineNum = location.start.line;
let colNum = location.start.column;
// Check whether there is a breakpoint we need to stop on here
if (!this.proceedBreakpoint(filePath, lineNum, colNum)) return;
let breakpoint = this.findStoppableBreakpoint(filePath, lineNum, colNum);
if (breakpoint === null) return;
// Tell the adapter that Prepack has stopped on this breakpoint
this.channel.writeOut(`breakpoint stopped ${lineNum}:${colNum}`);
this.channel.writeOut(
`${DebugMessage.BREAKPOINT_STOPPED_RESPONSE} ${breakpoint.filePath} ${breakpoint.line}:${breakpoint.column}`
);
// Wait for the adapter to tell us to run again
this.waitForRun(function(line) {
return line === "proceed";
});
this.waitForRun();
}
}
executeCommand(command: string) {
// Process a command from a debugger. Returns whether Prepack should unblock
// if it is blocked
processDebuggerCommand(command: string) {
if (command.length === 0) {
return;
}
let parts = command.split(" ");
if (parts[0] === "breakpoint") {
this.executeBreakpointCommand(this._parseBreakpointArguments(parts));
let prefix = parts[0];
switch (prefix) {
case DebugMessage.BREAKPOINT_ADD_COMMAND:
let addArgs = this._parseBreakpointArguments(parts);
this.breakpoints.addBreakpoint(addArgs.filePath, addArgs.lineNum, addArgs.columnNum);
this._sendBreakpointAcknowledge(
DebugMessage.BREAKPOINT_ADD_ACKNOWLEDGE,
addArgs.filePath,
addArgs.lineNum,
addArgs.columnNum
);
break;
case DebugMessage.BREAKPOINT_REMOVE_COMMAND:
let removeArgs = this._parseBreakpointArguments(parts);
this.breakpoints.removeBreakpoint(removeArgs.filePath, removeArgs.lineNum, removeArgs.columnNum);
this._sendBreakpointAcknowledge(
DebugMessage.BREAKPOINT_REMOVE_ACKNOWLEDGE,
removeArgs.filePath,
removeArgs.lineNum,
removeArgs.columnNum
);
break;
case DebugMessage.BREAKPOINT_ENABLE_COMMAND:
let enableArgs = this._parseBreakpointArguments(parts);
this.breakpoints.enableBreakpoint(enableArgs.filePath, enableArgs.lineNum, enableArgs.columnNum);
this._sendBreakpointAcknowledge(
DebugMessage.BREAKPOINT_ENABLE_ACKNOWLEDGE,
enableArgs.filePath,
enableArgs.lineNum,
enableArgs.columnNum
);
break;
case DebugMessage.BREAKPOINT_DISABLE_COMMAND:
let disableArgs = this._parseBreakpointArguments(parts);
this.breakpoints.disableBreakpoint(disableArgs.filePath, disableArgs.lineNum, disableArgs.columnNum);
this._sendBreakpointAcknowledge(
DebugMessage.BREAKPOINT_DISABLE_ACKNOWLEDGE,
disableArgs.filePath,
disableArgs.lineNum,
disableArgs.columnNum
);
break;
case DebugMessage.PREPACK_RUN_COMMAND:
return true;
default:
throw new DebuggerError("Invalid command", "Invalid command from adapter: " + prefix);
}
return false;
}
executeBreakpointCommand(args: BreakpointCommandArguments) {
if (args.kind === "add") {
this.breakpoints.addBreakpoint(args.filePath, args.lineNum, args.columnNum);
this.channel.writeOut(`added breakpoint ${args.filePath} ${args.lineNum} ${args.columnNum}`);
} else if (args.kind === "remove") {
this.breakpoints.removeBreakpoint(args.filePath, args.lineNum, args.columnNum);
this.channel.writeOut(`removed breakpoint ${args.filePath} ${args.lineNum} ${args.columnNum}`);
} else if (args.kind === "enable") {
this.breakpoints.enableBreakpoint(args.filePath, args.lineNum, args.columnNum);
this.channel.writeOut(`enabled breakpoint ${args.filePath} ${args.lineNum} ${args.columnNum}`);
} else if (args.kind === "disable") {
this.breakpoints.disableBreakpoint(args.filePath, args.lineNum, args.columnNum);
this.channel.writeOut(`disabled breakpoint ${args.filePath} ${args.lineNum} ${args.columnNum}`);
}
_sendBreakpointAcknowledge(responsePrefix: string, filePath: string, line: number, column: number) {
this.channel.writeOut(`${responsePrefix} ${filePath} ${line} ${column}`);
}
_parseBreakpointArguments(parts: Array<string>): BreakpointCommandArguments {
invariant(parts[0] === "breakpoint");
let kind = parts[1];
let filePath = parts[2];
let kind = parts[0];
let filePath = parts[1];
let lineNum = parseInt(parts[3], 10);
let lineNum = parseInt(parts[2], 10);
invariant(!isNaN(lineNum));
let columnNum = 0;
if (parts.length === 5) {
columnNum = parseInt(parts[4], 10);
if (parts.length === 4) {
columnNum = parseInt(parts[3], 10);
invariant(!isNaN(columnNum));
}
@ -162,6 +193,6 @@ export class DebugServer {
shutdown() {
//let the adapter know Prepack is done running
this.channel.writeOut("Finished");
this.channel.writeOut(DebugMessage.PREPACK_FINISH_RESPONSE);
}
}

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
// More error types will be added as needed
export type DebuggerErrorType = "Invalid command" | "Invalid response";
export class DebuggerError {
constructor(errorType: DebuggerErrorType, message: string) {
this.errorType = errorType;
this.message = message;
}
errorType: DebuggerErrorType;
message: string;
}

View File

@ -35,6 +35,11 @@ export class PerFileBreakpointMap {
let key = this._getKey(line, column);
if (key in this.breakpoints) {
return this.breakpoints[key];
} else {
key = this._getKey(line, 0);
if (key in this.breakpoints) {
return this.breakpoints[key];
}
}
} else {
let key = this._getKey(line, 0);

View File

@ -9,10 +9,21 @@
/* @flow */
import { DebugSession, LoggingDebugSession, InitializedEvent } from "vscode-debugadapter";
import {
DebugSession,
LoggingDebugSession,
InitializedEvent,
OutputEvent,
TerminatedEvent,
StoppedEvent,
} from "vscode-debugadapter";
import * as DebugProtocol from "vscode-debugprotocol";
import child_process from "child_process";
import Queue from "queue-fifo";
import { AdapterChannel } from "./../channel/AdapterChannel.js";
import type { DebuggerOptions } from "./../../options.js";
import { getDebuggerOptions } from "./../../prepack-options.js";
import { DebugMessage } from "./../channel/DebugMessage.js";
/* An implementation of an debugger adapter adhering to the VSCode Debug protocol
* The adapter is responsible for communication between the UI and Prepack
@ -26,29 +37,54 @@ class PrepackDebugSession extends LoggingDebugSession {
super("prepack");
this.setDebuggerLinesStartAt1(true);
this.setDebuggerColumnsStartAt1(true);
this._prepackWaiting = false;
this._readCLIParameters();
this._startPrepack();
}
_prepackCommand: string;
_inFilePath: string;
_outFilePath: string;
_prepackProcess: child_process.ChildProcess;
_messageQueue: Queue;
_adapterChannel: AdapterChannel;
_debuggerOptions: DebuggerOptions;
_prepackWaiting: boolean;
_readCLIParameters() {
let args = Array.from(process.argv);
args.splice(0, 2);
let inFilePath;
let outFilePath;
while (args.length > 0) {
let arg = args.shift();
if (arg.startsWith("--")) {
arg = arg.slice(2);
if (arg === "prepack") {
this._prepackCommand = args.shift();
} else if (arg === "inFilePath") {
inFilePath = args.shift();
} else if (arg === "outFilePath") {
outFilePath = args.shift();
}
} else {
console.error("Unknown parameter: " + arg);
process.exit(1);
}
}
if (!inFilePath || inFilePath.length === 0) {
console.error("No debugger input file given");
process.exit(1);
}
if (!outFilePath || outFilePath.length === 0) {
console.error("No debugger output file given");
process.exit(1);
}
this._debuggerOptions = getDebuggerOptions({
debugInFilePath: inFilePath,
debugOutFilePath: outFilePath,
});
}
// Start Prepack in a child process
@ -57,13 +93,26 @@ class PrepackDebugSession extends LoggingDebugSession {
console.error("No command given to start Prepack in adapter");
process.exit(1);
}
//set up message queue
// set up message queue
this._messageQueue = new Queue();
// set up the communication channel
this._adapterChannel = new AdapterChannel(this._debuggerOptions);
this._adapterChannel.writeOut(DebugMessage.DEBUGGER_ATTACHED);
this._adapterChannel.listenOnFile(this._handleFileReadError.bind(this), this._processPrepackMessage.bind(this));
this._prepackProcess = child_process.spawn("node", this._prepackCommand.split(" "));
let prepackArgs = this._prepackCommand.split(" ");
// Note: here the input file for the adapter is the output file for Prepack, and vice versa.
prepackArgs = prepackArgs.concat([
"--debugInFilePath",
this._debuggerOptions.outFilePath,
"--debugOutFilePath",
this._debuggerOptions.inFilePath,
]);
this._prepackProcess = child_process.spawn("node", prepackArgs);
process.on("exit", () => {
this._prepackProcess.kill();
this._adapterChannel.clean();
process.exit();
});
@ -71,6 +120,58 @@ class PrepackDebugSession extends LoggingDebugSession {
this._prepackProcess.kill();
process.exit();
});
this._prepackProcess.stdout.on("data", (data: Buffer) => {
let outputEvent = new OutputEvent(data.toString(), "stdout");
this.sendEvent(outputEvent);
});
this._prepackProcess.on("exit", () => {
this.sendEvent(new TerminatedEvent());
process.exit();
});
}
_processPrepackMessage(message: string) {
let parts = message.split(" ");
let prefix = parts[0];
if (prefix === DebugMessage.PREPACK_READY_RESPONSE) {
this._prepackWaiting = true;
// the second argument is the threadID required by the protocol, since
// Prepack only has one thread, this argument will be ignored
this.sendEvent(new StoppedEvent("entry", 1));
this._trySendNextRequest();
} else if (prefix === DebugMessage.BREAKPOINT_ADD_ACKNOWLEDGE) {
// Prepack acknowledged adding a breakpoint
this._prepackWaiting = true;
this._trySendNextRequest();
} else if (prefix === DebugMessage.BREAKPOINT_STOPPED_RESPONSE) {
// Prepack stopped on a breakpoint
this._prepackWaiting = true;
// the second argument is the threadID required by the protocol, since
// Prepack only has one thread, this argument will be ignored
this.sendEvent(new StoppedEvent("breakpoint " + parts.slice(2).join(" "), 1));
this._trySendNextRequest();
}
}
// Error handler for errors in files from the adapter channel
_handleFileReadError(err: ?ErrnoError) {
console.error(err);
process.exit(1);
}
// Check to see if the next request to Prepack can be sent and send it if so
_trySendNextRequest(): boolean {
// check to see if Prepack is ready to accept another request
if (!this._prepackWaiting) return false;
// check that there is a message to send
if (this._messageQueue.isEmpty()) return false;
let request = this._messageQueue.dequeue();
this._adapterChannel.listenOnFile(this._handleFileReadError.bind(this), this._processPrepackMessage.bind(this));
this._adapterChannel.writeOut(request);
this._prepackWaiting = false;
return true;
}
/**
@ -87,6 +188,35 @@ class PrepackDebugSession extends LoggingDebugSession {
// Respond back to the UI with the configurations. Will add more configurations gradually as needed.
this.sendResponse(response);
}
/**
* Request Prepack to continue running when it is stopped
*/
continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void {
// queue a Run request to Prepack and try to send the next request in the queue
this._messageQueue.enqueue(DebugMessage.PREPACK_RUN_COMMAND);
this._trySendNextRequest();
this.sendResponse(response);
}
setBreakPointsRequest(
response: DebugProtocol.SetBreakpointsResponse,
args: DebugProtocol.SetBreakpointsArguments
): void {
if (!args.source.path || !args.breakpoints) return;
let filePath = args.source.path;
for (const breakpoint of args.breakpoints) {
let line = breakpoint.line;
let column = 0;
if (breakpoint.column) {
column = breakpoint.column;
}
this._messageQueue.enqueue(`${DebugMessage.BREAKPOINT_ADD_COMMAND} ${filePath} ${line} ${column}`);
}
this._trySendNextRequest();
this.sendResponse(response);
}
}
DebugSession.run(PrepackDebugSession);

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
import type { DebuggerOptions } from "./../../options.js";
import { FileIOWrapper } from "./FileIOWrapper.js";
//Channel used by the debug adapter to communicate with Prepack
export class AdapterChannel {
constructor(dbgOptions: DebuggerOptions) {
this._ioWrapper = new FileIOWrapper(true, dbgOptions.inFilePath, dbgOptions.outFilePath);
}
_ioWrapper: FileIOWrapper;
writeOut(contents: string) {
this._ioWrapper.writeOutSync(contents);
}
listenOnFile(errorHandler: (err: ?ErrnoError) => void, messageProcessor: (message: string) => void) {
this._ioWrapper.readIn(errorHandler, messageProcessor);
}
clean() {
this._ioWrapper.clearInFile();
this._ioWrapper.clearOutFile();
}
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
import invariant from "./../../invariant.js";
import { FileIOWrapper } from "./FileIOWrapper.js";
import { DebugMessage } from "./DebugMessage.js";
//Channel used by the DebugServer in Prepack to communicate with the debug adapter
export class DebugChannel {
constructor(ioWrapper: FileIOWrapper) {
this._requestReceived = false;
this._ioWrapper = ioWrapper;
}
_requestReceived: boolean;
_ioWrapper: FileIOWrapper;
/*
/* Only called in the beginning to check if a debugger is attached
*/
debuggerIsAttached(): boolean {
let message = this._ioWrapper.readInSyncOnce();
if (message === DebugMessage.DEBUGGER_ATTACHED) {
this._requestReceived = true;
this._ioWrapper.clearInFile();
this.writeOut(DebugMessage.PREPACK_READY_RESPONSE);
return true;
}
return false;
}
/* Reads in a request from the debug adapter
/* The caller is responsible for sending a response with the appropriate
/* contents at the right time.
*/
readIn(): string {
let message = this._ioWrapper.readInSync();
this._requestReceived = true;
return message;
}
// Write out a response to the debug adapter
writeOut(contents: string): void {
//Prepack only writes back to the debug adapter in response to a request
invariant(this._requestReceived);
this._ioWrapper.writeOutSync(contents);
this._requestReceived = false;
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
//A collection of messages used between Prepack and the debug adapter
export class DebugMessage {
/* Messages from adapter to Prepack */
// Tell Prepack a debugger is present
static DEBUGGER_ATTACHED: string = "DebuggerAttached";
// Command Prepack to keep running
static PREPACK_RUN_COMMAND: string = "PrepackRun";
// Command to set a breakpoint
static BREAKPOINT_ADD_COMMAND: string = "Breakpoint-add-command";
// Command to remove a breakpoint
static BREAKPOINT_REMOVE_COMMAND: string = "Breakpoint-remove-command";
// Command to enable a breakpoint
static BREAKPOINT_ENABLE_COMMAND: string = "Breakpoint-enable-command";
// Command to disable a breakpoint
static BREAKPOINT_DISABLE_COMMAND: string = "Breakpoint-disable-command";
/* Messages from Prepack to adapter */
// Respond to the adapter that Prepack is ready
static PREPACK_READY_RESPONSE: string = "PrepackReady";
// Respond to the adapter that Prepack is finished
static PREPACK_FINISH_RESPONSE: string = "PrepackFinish";
// Respond to the adapter that Prepack has stopped on a breakpoint
static BREAKPOINT_STOPPED_RESPONSE: string = "Breakpoint-stopped";
// Acknowledgement for setting a breakpoint
static BREAKPOINT_ADD_ACKNOWLEDGE: string = "Breakpoint-add-acknowledge";
// Acknowledgement for removing a breakpoint
static BREAKPOINT_REMOVE_ACKNOWLEDGE: string = "Breakpoint-remove-acknowledge";
// Acknowledgement for enabling a breakpoint
static BREAKPOINT_ENABLE_ACKNOWLEDGE: string = "Breakpoint-enable-acknowledge";
// Acknoledgement for disabling a breakpoint
static BREAKPOINT_DISABLE_ACKNOWLEDGE: string = "Breakpoint-disable-acknowledge";
}

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
import fs from "fs";
import path from "path";
import { MessagePackager } from "./MessagePackager.js";
import invariant from "../../invariant.js";
export class FileIOWrapper {
constructor(isAdapter: boolean, inFilePath: string, outFilePath: string) {
// the paths are expected to be relative to Prepack top level directory
this._inFilePath = path.join(__dirname, "../../../", inFilePath);
this._outFilePath = path.join(__dirname, "../../../", outFilePath);
this._packager = new MessagePackager(isAdapter);
this._isAdapter = isAdapter;
}
_inFilePath: string;
_outFilePath: string;
_packager: MessagePackager;
_isAdapter: boolean;
// Read in a message from the input asynchronously
readIn(errorHandler: (err: ?ErrnoError) => void, messageProcessor: (message: string) => void) {
fs.readFile(this._inFilePath, { encoding: "utf8" }, (err: ?ErrnoError, contents: string) => {
if (err) {
errorHandler(err);
return;
}
let message = this._packager.unpackage(contents);
if (message === null) {
this.readIn(errorHandler, messageProcessor);
return;
}
//clear the file
fs.writeFileSync(this._inFilePath, "");
//process the message
messageProcessor(message);
});
}
// Read in a message from the input synchronously
readInSync(): string {
let message: null | string = null;
while (true) {
let contents = fs.readFileSync(this._inFilePath, "utf8");
message = this._packager.unpackage(contents);
if (message === null) continue;
break;
}
// loop should not break when message is still null
invariant(message !== null);
//clear the file
fs.writeFileSync(this._inFilePath, "");
return message;
}
// Read in a message from the input synchronously only once
readInSyncOnce(): null | string {
let contents = fs.readFileSync(this._inFilePath, "utf8");
let message = this._packager.unpackage(contents);
return message;
}
// Write out a message to the output synchronously
writeOutSync(contents: string) {
fs.writeFileSync(this._outFilePath, this._packager.package(contents));
}
clearInFile() {
fs.writeFileSync(this._inFilePath, "");
}
clearOutFile() {
fs.writeFileSync(this._outFilePath, "");
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
import invariant from "../../invariant.js";
const LENGTH_SEPARATOR = "--";
// Package a message sent or unpackage a message received
export class MessagePackager {
constructor(isAdapter: boolean) {
this._isAdapter = isAdapter;
}
_isAdapter: boolean;
// package a message to be sent
package(contents: string): string {
// format: <length>--<contents>
return contents.length + LENGTH_SEPARATOR + contents;
}
// unpackage a message received, verify it, and return it
// returns null if no message or the message is only partially read
// errors if the message violates the format
unpackage(contents: string): null | string {
// format: <length>--<contents>
let separatorIndex = contents.indexOf(LENGTH_SEPARATOR);
// if the separator is not written in yet --> partial read
if (separatorIndex === -1) {
return null;
}
let messageLength = parseInt(contents.slice(0, separatorIndex), 10);
// if the part before the separator is not a valid length, it is a
// violation of protocol
invariant(!isNaN(messageLength));
let startIndex = separatorIndex + LENGTH_SEPARATOR.length;
let endIndex = startIndex + messageLength;
// there should only be one message in the contents at a time
invariant(contents.length <= startIndex + messageLength);
// if we didn't read the whole message yet --> partial read
if (contents.length < endIndex) {
return null;
}
let message = contents.slice(startIndex, endIndex);
return message;
}
}

View File

@ -22,18 +22,25 @@ const TWO_CRLF = "\r\n\r\n";
* sends the commands to the adapter and process any responses
*/
export class UISession {
constructor(proc: Process, adapterPath: string, prepackCommand: string) {
constructor(proc: Process, adapterPath: string, prepackCommand: string, inFilePath: string, outFilePath: string) {
this._proc = proc;
this._adapterPath = adapterPath;
this._prepackCommand = prepackCommand;
this._inFilePath = inFilePath;
this._outFilePath = outFilePath;
this._sequenceNum = 1;
this._invalidCount = 0;
this._dataHandler = new DataHandler();
this._prepackWaiting = false;
}
// the parent (i.e. ui) process
_proc: Process;
//path to the debug adapter
_adapterPath: string;
// path to debugger input file
_inFilePath: string;
// path to debugger output file
_outFilePath: string;
// the child (i.e. adapter) process
_adapterProcess: child_process.ChildProcess;
@ -47,9 +54,18 @@ export class UISession {
_prepackCommand: string;
// handler for any received messages
_dataHandler: DataHandler;
_prepackWaiting: boolean;
_startAdapter() {
let adapterArgs = [this._adapterPath, "--prepack", this._prepackCommand];
let adapterArgs = [
this._adapterPath,
"--prepack",
this._prepackCommand,
"--inFilePath",
this._inFilePath,
"--outFilePath",
this._outFilePath,
];
this._adapterProcess = child_process.spawn("node", adapterArgs);
this._proc.on("exit", () => {
this.shutdown();
@ -57,16 +73,15 @@ export class UISession {
this._proc.on("SIGINT", () => {
this.shutdown();
});
this._adapterProcess.on("exit", () => {
this.shutdown();
});
this._adapterProcess.stdout.on("data", (data: Buffer) => {
//handle the received data
this._dataHandler.handleData(data, this._processMessage.bind(this));
//ask the user for the next command
this._reader.question("(dbg) ", (input: string) => {
this._dispatch(input);
});
if (this._prepackWaiting) {
this._reader.question("(dbg) ", (input: string) => {
this._dispatch(input);
});
}
});
this._adapterProcess.stderr.on("data", (data: Buffer) => {
console.error(data.toString());
@ -90,8 +105,21 @@ export class UISession {
}
_processEvent(event: DebugProtocol.Event) {
// to be implemented
console.log(event);
if (event.event === "output") {
this._uiOutput("Prepack output:\n" + event.body.output);
} else if (event.event === "terminated") {
this._uiOutput("Prepack exited! Shutting down...");
this.shutdown();
} else if (event.event === "stopped") {
this._prepackWaiting = true;
if (event.body) {
if (event.body.reason === "entry") {
this._uiOutput("Prepack is ready");
} else if (event.body.reason.startsWith("breakpoint")) {
this._uiOutput("Prepack stopped on: " + event.body.reason);
}
}
}
}
_processResponse(response: DebugProtocol.Response) {
@ -132,6 +160,30 @@ export class UISession {
let configDoneArgs: DebugProtocol.ConfigurationDoneArguments = {};
this._sendConfigDoneRequest(configDoneArgs);
break;
case "run":
// format: run
if (parts.length !== 1) return false;
let continueArgs: DebugProtocol.ContinueArguments = {
// Prepack will only have 1 thread, this argument will be ignored
threadId: 1,
};
this._sendContinueRequest(continueArgs);
break;
case "breakpoint":
// format: breakpoint add <filePath> <line> ?<column>
if (parts.length !== 4 && parts.length !== 5) return false;
if (parts[1] === "add") {
let filePath = parts[2];
let line = parseInt(parts[3], 10);
if (isNaN(line)) return false;
let column = 0;
if (parts.length === 5) {
column = parseInt(parts[4], 10);
if (isNaN(column)) return false;
}
this._sendBreakpointRequest(filePath, line, column);
}
break;
default:
// invalid command
return false;
@ -186,6 +238,41 @@ export class UISession {
this._packageAndSend(json);
}
// tell the adapter to continue running Prepack
_sendContinueRequest(args: DebugProtocol.ContinueArguments) {
let message = {
type: "request",
seq: this._sequenceNum,
command: "continue",
arguments: args,
};
let json = JSON.stringify(message);
this._packageAndSend(json);
this._prepackWaiting = false;
}
_sendBreakpointRequest(filePath: string, line: number, column: number = 0) {
let source: DebugProtocol.Source = {
path: filePath,
};
let breakpoint: DebugProtocol.SourceBreakpoint = {
line: line,
column: column,
};
let args: DebugProtocol.SetBreakpointsArguments = {
source: source,
breakpoints: [breakpoint],
};
let message = {
type: "request",
seq: this._sequenceNum,
command: "setBreakpoints",
arguments: args,
};
let json = JSON.stringify(message);
this._packageAndSend(json);
}
// write out a message to the adapter on stdout
_packageAndSend(message: string) {
// format: Content-Length: <length> separator <message>
@ -196,15 +283,15 @@ export class UISession {
this._sequenceNum++;
}
_uiOutput(message: string) {
console.log(message);
}
serve() {
this._uiOutput("Debugger is starting up Prepack...");
// Set up the adapter connection
this._startAdapter();
this._reader = readline.createInterface({ input: this._proc.stdin, output: this._proc.stdout });
// Start taking in commands and execute them
this._reader.question("(dbg) ", (input: string) => {
this._dispatch(input);
});
}
shutdown() {

View File

@ -11,13 +11,33 @@
import { UISession } from "./UISession.js";
type DebuggerCLIArguments = {
adapterPath: string,
prepackCommand: string,
inFilePath: string,
outFilePath: string,
};
/* The entry point to start up the debugger CLI
* Reads in command line arguments and starts up a UISession
*/
function run(process, console) {
let args = readCLIArguments(process, console);
let session = new UISession(process, args.adapterPath, args.prepackCommand, args.inFilePath, args.outFilePath);
try {
session.serve();
} catch (e) {
console.error(e);
session.shutdown();
}
}
function readCLIArguments(process, console): DebuggerCLIArguments {
let adapterPath = "";
let prepackCommand = "";
let inFilePath = "";
let outFilePath = "";
let args = Array.from(process.argv);
args.splice(0, 2);
@ -33,11 +53,23 @@ function run(process, console) {
adapterPath = args.shift();
} else if (arg === "prepack") {
prepackCommand = args.shift();
} else if (arg === "inFilePath") {
inFilePath = args.shift();
} else if (arg === "outFilePath") {
outFilePath = args.shift();
} else {
console.error("Unknown argument: " + arg);
process.exit(1);
}
}
if (inFilePath === 0) {
console.error("No input file path provided!");
process.exit(1);
}
if (outFilePath === 0) {
console.error("No output file path provided!");
process.exit(1);
}
if (adapterPath.length === 0) {
console.error("No path to the debug adapter provided!");
process.exit(1);
@ -46,13 +78,12 @@ function run(process, console) {
console.error("No command given to start Prepack");
process.exit(1);
}
let session = new UISession(process, adapterPath, prepackCommand);
try {
session.serve();
} catch (e) {
console.error(e);
session.shutdown();
}
let result: DebuggerCLIArguments = {
adapterPath: adapterPath,
prepackCommand: prepackCommand,
inFilePath: inFilePath,
outFilePath: outFilePath,
};
return result;
}
run(process, console);

View File

@ -140,6 +140,12 @@ function run(
let line = args.shift();
additionalFunctions = line.split(",");
break;
case "debugInFilePath":
debugInFilePath = args.shift();
break;
case "debugOutFilePath":
debugOutFilePath = args.shift();
break;
case "help":
console.log(
"Usage: prepack.js [ -- | input.js ] [ --out output.js ] [ --compatibility jsc ] [ --mathRandomSeed seedvalue ] [ --srcmapIn inputMap ] [ --srcmapOut outputMap ] [ --maxStackDepth depthValue ] [ --timeout seconds ] [ --additionalFunctions fnc1,fnc2,... ]" +

View File

@ -18,7 +18,8 @@ import { getDebuggerOptions } from "./prepack-options";
import { prepackNodeCLI, prepackNodeCLISync } from "./prepack-node-environment.js";
import { prepackSources } from "./prepack-standalone.js";
import { type SourceMap } from "./types.js";
import { DebugChannel } from "./DebugChannel.js";
import { DebugChannel } from "./debugger/channel/DebugChannel.js";
import { FileIOWrapper } from "./debugger/channel/FileIOWrapper.js";
import fs from "fs";
@ -113,7 +114,8 @@ export function prepackFileSync(filenames: Array<string>, options: PrepackOption
//flag to hide the debugger for now
if (options.enableDebugger && options.debugInFilePath && options.debugOutFilePath) {
let debugOptions = getDebuggerOptions(options);
debugChannel = new DebugChannel(fs, debugOptions);
let ioWrapper = new FileIOWrapper(false, debugOptions.inFilePath, debugOptions.outFilePath);
debugChannel = new DebugChannel(ioWrapper);
}
return prepackSources(sourceFiles, options, debugChannel);
}

View File

@ -24,7 +24,7 @@ import type { PrepackOptions } from "./prepack-options";
import { defaultOptions } from "./options";
import type { BabelNodeFile, BabelNodeProgram } from "babel-types";
import invariant from "./invariant.js";
import { DebugChannel } from "./DebugChannel.js";
import type { DebugChannel } from "./debugger/channel/DebugChannel.js";
// IMPORTANT: This function is now deprecated and will go away in a future release.
// Please use FatalError instead.