blob: 0e9670b2772b8fa4c6fca30795e86f34a9b9cf51 [file] [log] [blame]
// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
"use strict";
// If true, prints all messages sent and received by inspector.
const printProtocolMessages = false;
// The active wrapper instance.
let activeWrapper = undefined;
// Receiver function called by inspector, delegating to active wrapper.
function receive(message) {
activeWrapper.receiveMessage(message);
}
class DebugWrapper {
constructor() {
// Message dictionary storing {id, message} pairs.
this.receivedMessages = new Map();
// Each message dispatched by the Debug wrapper is assigned a unique number
// using nextMessageId.
this.nextMessageId = 0;
// The listener method called on certain events.
this.listener = undefined;
// Debug events which can occur in the V8 JavaScript engine.
this.DebugEvent = { Break: 1,
Exception: 2,
AfterCompile: 3,
CompileError: 4,
OOM: 5,
};
// The different types of steps.
this.StepAction = { StepOut: 0,
StepNext: 1,
StepIn: 2,
};
// The different types of scripts matching enum ScriptType in objects.h.
this.ScriptType = { Native: 0,
Extension: 1,
Normal: 2,
Wasm: 3,
Inspector: 4,
};
// A copy of the scope types from runtime-debug.cc.
// NOTE: these constants should be backward-compatible, so
// add new ones to the end of this list.
this.ScopeType = { Global: 0,
Local: 1,
With: 2,
Closure: 3,
Catch: 4,
Block: 5,
Script: 6,
Eval: 7,
Module: 8
};
// Types of exceptions that can be broken upon.
this.ExceptionBreak = { Caught : 0,
Uncaught: 1 };
// The different script break point types.
this.ScriptBreakPointType = { ScriptId: 0,
ScriptName: 1,
ScriptRegExp: 2 };
// Store the current script id so we can skip corresponding break events.
this.thisScriptId = %FunctionGetScriptId(receive);
// Stores all set breakpoints.
this.breakpoints = new Set();
// Register as the active wrapper.
assertTrue(activeWrapper === undefined);
activeWrapper = this;
}
enable() { this.sendMessageForMethodChecked("Debugger.enable"); }
disable() { this.sendMessageForMethodChecked("Debugger.disable"); }
setListener(listener) { this.listener = listener; }
stepOver() { this.sendMessageForMethodChecked("Debugger.stepOver"); }
stepInto() { this.sendMessageForMethodChecked("Debugger.stepInto"); }
stepOut() { this.sendMessageForMethodChecked("Debugger.stepOut"); }
setBreakOnException() {
this.sendMessageForMethodChecked(
"Debugger.setPauseOnExceptions", { state : "all" });
}
clearBreakOnException() {
const newState = this.isBreakOnUncaughtException() ? "uncaught" : "none";
this.sendMessageForMethodChecked(
"Debugger.setPauseOnExceptions", { state : newState });
}
isBreakOnException() {
return !!%IsBreakOnException(this.ExceptionBreak.Caught);
};
setBreakOnUncaughtException() {
const newState = this.isBreakOnException() ? "all" : "uncaught";
this.sendMessageForMethodChecked(
"Debugger.setPauseOnExceptions", { state : newState });
}
clearBreakOnUncaughtException() {
const newState = this.isBreakOnException() ? "all" : "none";
this.sendMessageForMethodChecked(
"Debugger.setPauseOnExceptions", { state : newState });
}
isBreakOnUncaughtException() {
return !!%IsBreakOnException(this.ExceptionBreak.Uncaught);
};
clearStepping() { %ClearStepping(); };
// Returns the resulting breakpoint id.
setBreakPoint(func, opt_line, opt_column, opt_condition) {
assertTrue(%IsFunction(func));
assertFalse(%FunctionIsAPIFunction(func));
const scriptid = %FunctionGetScriptId(func);
assertTrue(scriptid != -1);
const offset = %FunctionGetScriptSourcePosition(func);
const loc =
%ScriptLocationFromLine2(scriptid, opt_line, opt_column, offset);
return this.setBreakPointAtLocation(scriptid, loc, opt_condition);
}
setScriptBreakPoint(type, scriptid, opt_line, opt_column, opt_condition) {
// Only sets by script id are supported for now.
assertEquals(this.ScriptBreakPointType.ScriptId, type);
return this.setScriptBreakPointById(scriptid, opt_line, opt_column,
opt_condition);
}
setScriptBreakPointById(scriptid, opt_line, opt_column, opt_condition) {
const loc = %ScriptLocationFromLine2(scriptid, opt_line, opt_column, 0);
return this.setBreakPointAtLocation(scriptid, loc, opt_condition);
}
setBreakPointByScriptIdAndPosition(scriptid, position) {
const loc = %ScriptPositionInfo2(scriptid, position, false);
return this.setBreakPointAtLocation(scriptid, loc, undefined);
}
clearBreakPoint(breakpoint) {
assertTrue(this.breakpoints.has(breakpoint));
const breakid = breakpoint.id;
const {msgid, msg} = this.createMessage(
"Debugger.removeBreakpoint", { breakpointId : breakid });
this.sendMessage(msg);
this.takeReplyChecked(msgid);
this.breakpoints.delete(breakid);
}
clearAllBreakPoints() {
for (let breakpoint of this.breakpoints) {
this.clearBreakPoint(breakpoint);
}
this.breakpoints.clear();
}
showBreakPoints(f) {
if (!%IsFunction(f)) throw new Error("Not passed a Function");
const source = %FunctionGetSourceCode(f);
const offset = %FunctionGetScriptSourcePosition(f);
const locations = %GetBreakLocations(f);
if (!locations) return source;
locations.sort(function(x, y) { return x - y; });
let result = "";
let prev_pos = 0;
let pos;
for (var i = 0; i < locations.length; i++) {
pos = locations[i] - offset;
result += source.slice(prev_pos, pos);
result += "[B" + i + "]";
prev_pos = pos;
}
pos = source.length;
result += source.substring(prev_pos, pos);
return result;
}
debuggerFlags() {
return { breakPointsActive :
{ setValue : (enabled) => this.setBreakPointsActive(enabled) }
};
}
scripts() {
// Collect all scripts in the heap.
return %DebugGetLoadedScripts();
}
// Returns a Script object. If the parameter is a function the return value
// is the script in which the function is defined. If the parameter is a
// string the return value is the script for which the script name has that
// string value. If it is a regexp and there is a unique script whose name
// matches we return that, otherwise undefined.
findScript(func_or_script_name) {
if (%IsFunction(func_or_script_name)) {
return %FunctionGetScript(func_or_script_name);
} else if (%IsRegExp(func_or_script_name)) {
var scripts = this.scripts();
var last_result = null;
var result_count = 0;
for (var i in scripts) {
var script = scripts[i];
if (func_or_script_name.test(script.name)) {
last_result = script;
result_count++;
}
}
// Return the unique script matching the regexp. If there are more
// than one we don't return a value since there is no good way to
// decide which one to return. Returning a "random" one, say the
// first, would introduce nondeterminism (or something close to it)
// because the order is the heap iteration order.
if (result_count == 1) {
return last_result;
} else {
return undefined;
}
} else {
return %GetScript(func_or_script_name);
}
}
// Returns the script source. If the parameter is a function the return value
// is the script source for the script in which the function is defined. If the
// parameter is a string the return value is the script for which the script
// name has that string value.
scriptSource(func_or_script_name) {
return this.findScript(func_or_script_name).source;
};
sourcePosition(f) {
if (!%IsFunction(f)) throw new Error("Not passed a Function");
return %FunctionGetScriptSourcePosition(f);
};
// Returns the character position in a script based on a line number and an
// optional position within that line.
findScriptSourcePosition(script, opt_line, opt_column) {
var location = %ScriptLocationFromLine(script, opt_line, opt_column, 0);
return location ? location.position : null;
};
findFunctionSourceLocation(func, opt_line, opt_column) {
var script = %FunctionGetScript(func);
var script_offset = %FunctionGetScriptSourcePosition(func);
return %ScriptLocationFromLine(script, opt_line, opt_column, script_offset);
}
setBreakPointsActive(enabled) {
const {msgid, msg} = this.createMessage(
"Debugger.setBreakpointsActive", { active : enabled });
this.sendMessage(msg);
this.takeReplyChecked(msgid);
}
generatorScopeCount(gen) {
return %GetGeneratorScopeCount(gen);
}
generatorScope(gen, index) {
// These indexes correspond definitions in debug-scopes.h.
const kScopeDetailsTypeIndex = 0;
const kScopeDetailsObjectIndex = 1;
const details = %GetGeneratorScopeDetails(gen, index);
function scopeObjectProperties() {
const obj = details[kScopeDetailsObjectIndex];
return Object.keys(obj).map((k, v) => v);
}
function setScopeVariableValue(name, value) {
const res = %SetScopeVariableValue(gen, null, null, index, name, value);
if (!res) throw new Error("Failed to set variable value");
}
const scopeObject =
{ value : () => details[kScopeDetailsObjectIndex],
property : (prop) => details[kScopeDetailsObjectIndex][prop],
properties : scopeObjectProperties,
propertyNames : () => Object.keys(details[kScopeDetailsObjectIndex])
.map((key, _) => key),
};
return { scopeType : () => details[kScopeDetailsTypeIndex],
scopeIndex : () => index,
scopeObject : () => scopeObject,
setVariableValue : setScopeVariableValue,
}
}
generatorScopes(gen) {
const count = %GetGeneratorScopeCount(gen);
const scopes = [];
for (let i = 0; i < count; i++) {
scopes.push(this.generatorScope(gen, i));
}
return scopes;
}
get LiveEdit() {
const debugContext = %GetDebugContext();
return debugContext.Debug.LiveEdit;
}
// --- Internal methods. -----------------------------------------------------
getNextMessageId() {
return this.nextMessageId++;
}
createMessage(method, params) {
const id = this.getNextMessageId();
const msg = JSON.stringify({
id: id,
method: method,
params: params,
});
return { msgid : id, msg: msg };
}
receiveMessage(message) {
const parsedMessage = JSON.parse(message);
if (printProtocolMessages) {
print(JSON.stringify(parsedMessage, undefined, 1));
}
if (parsedMessage.id !== undefined) {
this.receivedMessages.set(parsedMessage.id, parsedMessage);
}
this.dispatchMessage(parsedMessage);
}
sendMessage(message) {
if (printProtocolMessages) print(message);
send(message);
}
sendMessageForMethodChecked(method, params) {
const {msgid, msg} = this.createMessage(method, params);
this.sendMessage(msg);
this.takeReplyChecked(msgid);
}
takeReplyChecked(msgid) {
const reply = this.receivedMessages.get(msgid);
assertTrue(reply !== undefined);
this.receivedMessages.delete(msgid);
return reply;
}
setBreakPointAtLocation(scriptid, loc, opt_condition) {
const params = { location :
{ scriptId : scriptid.toString(),
lineNumber : loc.line,
columnNumber : loc.column,
},
condition : opt_condition,
};
const {msgid, msg} = this.createMessage("Debugger.setBreakpoint", params);
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
const result = reply.result;
assertTrue(result !== undefined);
const breakid = result.breakpointId;
assertTrue(breakid !== undefined);
const actualLoc = %ScriptLocationFromLine2(scriptid,
result.actualLocation.lineNumber, result.actualLocation.columnNumber,
0);
const breakpoint = { id : result.breakpointId,
actual_position : actualLoc.position,
}
this.breakpoints.add(breakpoint);
return breakpoint;
}
execStatePrepareStep(action) {
switch(action) {
case this.StepAction.StepOut: this.stepOut(); break;
case this.StepAction.StepNext: this.stepOver(); break;
case this.StepAction.StepIn: this.stepInto(); break;
default: %AbortJS("Unsupported StepAction"); break;
}
}
execStateScopeType(type) {
switch (type) {
case "global": return this.ScopeType.Global;
case "local": return this.ScopeType.Local;
case "with": return this.ScopeType.With;
case "closure": return this.ScopeType.Closure;
case "catch": return this.ScopeType.Catch;
case "block": return this.ScopeType.Block;
case "script": return this.ScopeType.Script;
case "eval": return this.ScopeType.Eval;
case "module": return this.ScopeType.Module;
default: %AbortJS("Unexpected scope type");
}
}
execStateScopeObjectProperty(serialized_scope, prop) {
let found = null;
for (let i = 0; i < serialized_scope.length; i++) {
const elem = serialized_scope[i];
if (elem.name == prop) {
found = elem;
break;
}
}
if (found == null) return { isUndefined : () => true };
const val = { value : () => found.value.value };
// Not undefined in the sense that we did find a property, even though
// the value can be 'undefined'.
return { value : () => val,
isUndefined : () => false,
};
}
// Returns an array of property descriptors of the scope object.
// This is in contrast to the original API, which simply passed object
// mirrors.
execStateScopeObject(obj) {
const serialized_scope = this.getProperties(obj.objectId);
const scope = this.propertiesToObject(serialized_scope);
return { value : () => scope,
property : (prop) =>
this.execStateScopeObjectProperty(serialized_scope, prop),
properties : () => serialized_scope.map(elem => elem.value),
propertyNames : () => serialized_scope.map(elem => elem.name)
};
}
execStateScopeDetails(scope) {
var start_position;
var end_position
const start = scope.startLocation;
const end = scope.endLocation;
if (start) {
start_position = %ScriptLocationFromLine2(
parseInt(start.scriptId), start.lineNumber, start.columnNumber, 0)
.position;
}
if (end) {
end_position = %ScriptLocationFromLine2(
parseInt(end.scriptId), end.lineNumber, end.columnNumber, 0)
.position;
}
return { name : () => scope.name,
startPosition : () => start_position,
endPosition : () => end_position
};
}
setVariableValue(frame, scope_index, name, value) {
const frameid = frame.callFrameId;
const {msgid, msg} = this.createMessage(
"Debugger.setVariableValue",
{ callFrameId : frameid,
scopeNumber : scope_index,
variableName : name,
newValue : { value : value }
});
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
if (reply.error) {
throw new Error("Failed to set variable value");
}
}
execStateScope(frame, scope_index) {
const scope = frame.scopeChain[scope_index];
return { scopeType : () => this.execStateScopeType(scope.type),
scopeIndex : () => scope_index,
frameIndex : () => frame.callFrameId,
scopeObject : () => this.execStateScopeObject(scope.object),
setVariableValue :
(name, value) => this.setVariableValue(frame, scope_index,
name, value),
details : () => this.execStateScopeDetails(scope)
};
}
// Takes a list of properties as produced by getProperties and turns them
// into an object.
propertiesToObject(props) {
const obj = {}
props.forEach((elem) => {
const key = elem.name;
let value;
if (elem.value) {
// Some properties (e.g. with getters/setters) don't have a value.
switch (elem.value.type) {
case "undefined": value = undefined; break;
default: value = elem.value.value; break;
}
}
obj[key] = value;
})
return obj;
}
getProperties(objectId) {
const {msgid, msg} = this.createMessage(
"Runtime.getProperties", { objectId : objectId, ownProperties: true });
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
return reply.result.result;
}
getLocalScopeDetails(frame) {
const scopes = frame.scopeChain;
for (let i = 0; i < scopes.length; i++) {
const scope = scopes[i]
if (scope.type == "local") {
return this.getProperties(scope.object.objectId);
}
}
return undefined;
}
execStateFrameLocalCount(frame) {
const scope_details = this.getLocalScopeDetails(frame);
return scope_details ? scope_details.length : 0;
}
execStateFrameLocalName(frame, index) {
const scope_details = this.getLocalScopeDetails(frame);
if (index < 0 || index >= scope_details.length) return undefined;
return scope_details[index].name;
}
execStateFrameLocalValue(frame, index) {
const scope_details = this.getLocalScopeDetails(frame);
if (index < 0 || index >= scope_details.length) return undefined;
const local = scope_details[index];
let localValue;
switch (local.value.type) {
case "undefined": localValue = undefined; break;
default: localValue = local.value.value; break;
}
return { value : () => localValue };
}
reconstructValue(objectId) {
const {msgid, msg} = this.createMessage(
"Runtime.getProperties", { objectId : objectId, ownProperties: true });
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
return Object(reply.result.internalProperties[0].value.value);
}
reconstructRemoteObject(obj) {
let value = obj.value;
let isUndefined = false;
switch (obj.type) {
case "object": {
switch (obj.subtype) {
case "error": {
const desc = obj.description;
switch (obj.className) {
case "EvalError": throw new EvalError(desc);
case "RangeError": throw new RangeError(desc);
case "ReferenceError": throw new ReferenceError(desc);
case "SyntaxError": throw new SyntaxError(desc);
case "TypeError": throw new TypeError(desc);
case "URIError": throw new URIError(desc);
default: throw new Error(desc);
}
break;
}
case "array": {
const array = [];
const props = this.propertiesToObject(
this.getProperties(obj.objectId));
for (let i = 0; i < props.length; i++) {
array[i] = props[i];
}
value = array;
break;
}
case "null": {
value = null;
break;
}
default: {
switch (obj.className) {
case "global":
value = Function('return this')();
break;
case "Number":
case "String":
case "Boolean":
value = this.reconstructValue(obj.objectId);
break;
default:
value = this.propertiesToObject(
this.getProperties(obj.objectId));
break;
}
break;
}
}
break;
}
case "undefined": {
value = undefined;
isUndefined = true;
break;
}
case "number": {
if (obj.description === "NaN") {
value = NaN;
}
break;
}
case "string":
case "boolean": {
break;
}
default: {
break;
}
}
return { value : () => value,
isUndefined : () => isUndefined,
type : () => obj.type,
className : () => obj.className
};
}
evaluateOnCallFrame(frame, expr, throw_on_side_effect = false) {
const frameid = frame.callFrameId;
const {msgid, msg} = this.createMessage(
"Debugger.evaluateOnCallFrame",
{ callFrameId : frameid,
expression : expr,
throwOnSideEffect : throw_on_side_effect,
});
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
const result = reply.result.result;
return this.reconstructRemoteObject(result);
}
frameReceiver(frame) {
return this.reconstructRemoteObject(frame.this);
}
frameReturnValue(frame) {
return this.reconstructRemoteObject(frame.returnValue);
}
execStateFrameRestart(frame) {
const frameid = frame.callFrameId;
const {msgid, msg} = this.createMessage(
"Debugger.restartFrame", { callFrameId : frameid });
this.sendMessage(msg);
this.takeReplyChecked(msgid);
}
execStateFrame(frame) {
const scriptid = parseInt(frame.location.scriptId);
const line = frame.location.lineNumber;
const column = frame.location.columnNumber;
const loc = %ScriptLocationFromLine2(scriptid, line, column, 0);
const func = { name : () => frame.functionName };
const index = JSON.parse(frame.callFrameId).ordinal;
function allScopes() {
const scopes = [];
for (let i = 0; i < frame.scopeChain.length; i++) {
scopes.push(this.execStateScope(frame, i));
}
return scopes;
}
return { sourceColumn : () => column,
sourceLine : () => line + 1,
sourceLineText : () => loc.sourceText,
sourcePosition : () => loc.position,
evaluate : (expr, throw_on_side_effect) =>
this.evaluateOnCallFrame(frame, expr, throw_on_side_effect),
functionName : () => frame.functionName,
func : () => func,
index : () => index,
localCount : () => this.execStateFrameLocalCount(frame),
localName : (ix) => this.execStateFrameLocalName(frame, ix),
localValue: (ix) => this.execStateFrameLocalValue(frame, ix),
receiver : () => this.frameReceiver(frame),
restart : () => this.execStateFrameRestart(frame),
returnValue : () => this.frameReturnValue(frame),
scopeCount : () => frame.scopeChain.length,
scope : (index) => this.execStateScope(frame, index),
allScopes : allScopes.bind(this)
};
}
execStateEvaluateGlobal(expr) {
const {msgid, msg} = this.createMessage(
"Runtime.evaluate", { expression : expr });
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
const result = reply.result.result;
return this.reconstructRemoteObject(result);
}
eventDataException(params) {
switch (params.data.type) {
case "string": {
return params.data.value;
}
case "object": {
const props = this.getProperties(params.data.objectId);
return this.propertiesToObject(props);
}
default: {
return undefined;
}
}
}
eventDataScriptSource(id) {
const {msgid, msg} = this.createMessage(
"Debugger.getScriptSource", { scriptId : String(id) });
this.sendMessage(msg);
const reply = this.takeReplyChecked(msgid);
return reply.result.scriptSource;
}
eventDataScriptSetSource(id, src) {
const {msgid, msg} = this.createMessage(
"Debugger.setScriptSource", { scriptId : id, scriptSource : src });
this.sendMessage(msg);
this.takeReplyChecked(msgid);
}
eventDataScript(params) {
const id = parseInt(params.scriptId);
const name = params.url ? params.url : undefined;
return { id : () => id,
name : () => name,
source : () => this.eventDataScriptSource(params.scriptId),
setSource : (src) => this.eventDataScriptSetSource(id, src)
};
}
// --- Message handlers. -----------------------------------------------------
dispatchMessage(message) {
const method = message.method;
if (method == "Debugger.paused") {
this.handleDebuggerPaused(message);
} else if (method == "Debugger.scriptParsed") {
this.handleDebuggerScriptParsed(message);
} else if (method == "Debugger.scriptFailedToParse") {
this.handleDebuggerScriptFailedToParse(message);
}
}
handleDebuggerPaused(message) {
const params = message.params;
var debugEvent;
switch (params.reason) {
case "exception":
case "promiseRejection":
debugEvent = this.DebugEvent.Exception;
break;
case "OOM":
debugEvent = this.DebugEvent.OOM;
break;
case "other":
debugEvent = this.DebugEvent.Break;
break;
case "ambiguous":
case "XHR":
case "DOM":
case "EventListener":
case "assert":
case "debugCommand":
assertUnreachable();
default:
assertUnreachable();
}
if (!params.callFrames[0]) return;
// Skip break events in this file.
if (params.callFrames[0].location.scriptId == this.thisScriptId) return;
// TODO(jgruber): Arguments as needed.
let execState = { frames : params.callFrames,
prepareStep : this.execStatePrepareStep.bind(this),
evaluateGlobal :
(expr) => this.execStateEvaluateGlobal(expr),
frame : (index) => this.execStateFrame(
index ? params.callFrames[index]
: params.callFrames[0]),
frameCount : () => params.callFrames.length
};
let eventData = this.execStateFrame(params.callFrames[0]);
if (debugEvent == this.DebugEvent.Exception) {
eventData.uncaught = () => params.data.uncaught;
eventData.exception = () => this.eventDataException(params);
}
this.invokeListener(debugEvent, execState, eventData);
}
handleDebuggerScriptParsed(message) {
const params = message.params;
let eventData = { scriptId : params.scriptId,
script : () => this.eventDataScript(params),
eventType : this.DebugEvent.AfterCompile
}
// TODO(jgruber): Arguments as needed. Still completely missing exec_state,
// and eventData used to contain the script mirror instead of its id.
this.invokeListener(this.DebugEvent.AfterCompile, undefined, eventData,
undefined);
}
handleDebuggerScriptFailedToParse(message) {
const params = message.params;
let eventData = { scriptId : params.scriptId,
script : () => this.eventDataScript(params),
eventType : this.DebugEvent.CompileError
}
// TODO(jgruber): Arguments as needed. Still completely missing exec_state,
// and eventData used to contain the script mirror instead of its id.
this.invokeListener(this.DebugEvent.CompileError, undefined, eventData,
undefined);
}
invokeListener(event, exec_state, event_data, data) {
if (this.listener) {
this.listener(event, exec_state, event_data, data);
}
}
}
// Simulate the debug object generated by --expose-debug-as debug.
var debug = { instance : undefined };
Object.defineProperty(debug, 'Debug', { get: function() {
if (!debug.instance) {
debug.instance = new DebugWrapper();
debug.instance.enable();
}
return debug.instance;
}});
Object.defineProperty(debug, 'ScopeType', { get: function() {
const instance = debug.Debug;
return instance.ScopeType;
}});