gecko-dev/devtools/server/actors/replay/debugger.js

1033 строки
32 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-disable spaced-comment, brace-style, indent-legacy */
// When recording/replaying an execution with Web Replay, Devtools server code
// runs in the middleman process instead of the recording/replaying process the
// code is interested in.
//
// This file defines replay objects analogous to those constructed by the
// C++ Debugger (Debugger, Debugger.Object, etc.), which implement similar
// methods and properties to those C++ objects. These replay objects are
// created in the middleman process, and describe things that exist in the
// recording/replaying process, inspecting them via the RecordReplayControl
// interface.
"use strict";
const RecordReplayControl = !isWorker && require("RecordReplayControl");
const Services = require("Services");
///////////////////////////////////////////////////////////////////////////////
// ReplayDebugger
///////////////////////////////////////////////////////////////////////////////
// Possible preferred directions of travel.
const Direction = {
FORWARD: "FORWARD",
BACKWARD: "BACKWARD",
NONE: "NONE",
};
function ReplayDebugger() {
const existing = RecordReplayControl.registerReplayDebugger(this);
if (existing) {
// There is already a ReplayDebugger in existence, use that. There can only
// be one ReplayDebugger in the process.
return existing;
}
// Whether the process is currently paused.
this._paused = false;
// Preferred direction of travel when not explicitly resumed.
this._direction = Direction.NONE;
// All breakpoint positions and handlers installed by this debugger.
this._breakpoints = [];
// All ReplayDebuggerFramees that have been created while paused at the
// current position, indexed by their index (zero is the oldest frame, with
// the index increasing for newer frames). These are invalidated when
// unpausing.
this._frames = [];
// All ReplayDebuggerObjects and ReplayDebuggerEnvironments that have been
// created while paused at the current position, indexed by their id. These
// are invalidated when unpausing.
this._objects = [];
// All ReplayDebuggerScripts and ReplayDebuggerScriptSources that have been
// created, indexed by their id. These stay valid even after unpausing.
this._scripts = [];
this._scriptSources = [];
// How many nested thread-wide paused have been entered.
this._threadPauseCount = 0;
// Flag set if the dispatched _performPause() call can be ignored because the
// server entered a thread-wide pause first.
this._cancelPerformPause = false;
// After we are done pausing, callback describing how to resume.
this._resumeCallback = null;
// Handler called when hitting the beginning/end of the recording, or when
// a time warp target has been reached.
this.replayingOnForcedPause = null;
// Handler called when the child pauses for any reason.
this.replayingOnPositionChange = null;
}
// Frame index used to refer to the newest frame in the child process.
const NewestFrameIndex = -1;
ReplayDebugger.prototype = {
/////////////////////////////////////////////////////////
// General methods
/////////////////////////////////////////////////////////
replaying: true,
canRewind: RecordReplayControl.canRewind,
replayCurrentExecutionPoint() {
return this._sendRequest({ type: "currentExecutionPoint" });
},
replayRecordingEndpoint() {
return this._sendRequest({ type: "recordingEndpoint" });
},
replayIsRecording: RecordReplayControl.childIsRecording,
addDebuggee() {},
removeAllDebuggees() {},
replayingContent(url) {
this._ensurePaused();
return this._sendRequest({ type: "getContent", url });
},
// Send a request object to the child process, and synchronously wait for it
// to respond.
_sendRequest(request) {
assert(this._paused);
const data = RecordReplayControl.sendRequest(request);
dumpv("SendRequest: " +
JSON.stringify(request) + " -> " + JSON.stringify(data));
if (data.exception) {
ThrowError(data.exception);
}
return data;
},
// Send a request that requires the child process to perform actions that
// diverge from the recording. In such cases we want to be interacting with a
// replaying process (if there is one), as recording child processes won't
// provide useful responses to such requests.
_sendRequestAllowDiverge(request) {
assert(this._paused);
RecordReplayControl.maybeSwitchToReplayingChild();
return this._sendRequest(request);
},
// Update graphics according to the current state of the child process. This
// should be done anytime we pause and allow the user to interact with the
// debugger.
_repaint() {
const rv = this._sendRequestAllowDiverge({ type: "repaint" });
if ("width" in rv && "height" in rv) {
RecordReplayControl.hadRepaint(rv.width, rv.height);
} else {
RecordReplayControl.hadRepaintFailure();
}
},
/////////////////////////////////////////////////////////
// Paused/running state
/////////////////////////////////////////////////////////
// Paused State Management
//
// The single ReplayDebugger is exclusively responsible for controlling the
// position of the child process by keeping track of when it pauses and
// sending it commands to resume.
//
// The general goal of controlling this position is to make the child process
// execute at predictable times, similar to how it would execute if the
// debuggee was in the same process as this one (as is the case when not
// replaying), as described below:
//
// - After the child pauses, the it will only resume executing when an event
// loop is running that is *not* associated with the thread actor's nested
// pauses. As long as the thread actor has pushed a pause, the child will
// remain paused.
//
// - After the child resumes, installed breakpoint handlers will only execute
// when an event loop is running (which, because of the above point, cannot
// be associated with a thread actor's nested pause).
replayResumeBackward() { this._resume(/* forward = */ false); },
replayResumeForward() { this._resume(/* forward = */ true); },
_resume(forward) {
this._ensurePaused();
this._setResume(() => {
this._paused = false;
this._direction = forward ? Direction.FORWARD : Direction.BACKWARD;
dumpv("Resuming " + this._direction);
RecordReplayControl.resume(forward);
if (this._paused) {
// If we resume and immediately pause, we are at an endpoint of the
// recording. Force the thread to pause.
this.replayingOnForcedPause(this.getNewestFrame());
}
});
},
replayTimeWarp(target) {
this._ensurePaused();
this._setResume(() => {
this._paused = false;
this._direction = Direction.NONE;
dumpv("Warping " + JSON.stringify(target));
RecordReplayControl.timeWarp(target);
// timeWarp() doesn't return until the child has reached the target of
// the warp, after which we force the thread to pause.
assert(this._paused);
this.replayingOnForcedPause(this.getNewestFrame());
});
},
replayPause() {
this._ensurePaused();
// Cancel any pending resume.
this._resumeCallback = null;
},
_ensurePaused() {
if (!this._paused) {
RecordReplayControl.waitUntilPaused();
assert(this._paused);
}
},
// This hook is called whenever the child has paused, which can happen
// within a RecordReplayControl method (resume, timeWarp, waitUntilPaused) or
// or be delivered via the event loop.
_onPause() {
this._paused = true;
// The position change handler is always called on pause notifications.
if (this.replayingOnPositionChange) {
this.replayingOnPositionChange();
}
// Call _performPause() soon via the event loop to check for breakpoint
// handlers at this point.
this._cancelPerformPause = false;
Services.tm.dispatchToMainThread(this._performPause.bind(this));
},
_performPause() {
// The child paused at some time in the past and any breakpoint handlers
// may still need to be called. If we've entered a thread-wide pause or
// have already told the child to resume, don't call handlers.
if (!this._paused || this._cancelPerformPause || this._resumeCallback) {
return;
}
const point = this.replayCurrentExecutionPoint();
dumpv("PerformPause " + JSON.stringify(point));
if (point.position.kind == "Invalid") {
// We paused at a checkpoint, and there are no handlers to call.
} else {
// Call any handlers for this point, unless one resumes execution.
for (const { handler, position } of this._breakpoints) {
if (RecordReplayControl.positionSubsumes(position, point.position)) {
handler();
assert(!this._threadPauseCount);
if (this._resumeCallback) {
break;
}
}
}
}
// If no handlers entered a thread-wide pause (resetting this._direction)
// or gave an explicit resume, continue traveling in the same direction
// we were going when we paused.
assert(!this._threadPauseCount);
if (!this._resumeCallback) {
switch (this._direction) {
case Direction.FORWARD: this.replayResumeForward(); break;
case Direction.BACKWARD: this.replayResumeBackward(); break;
}
}
},
// This hook is called whenever we switch between recording and replaying
// child processes.
_onSwitchChild() {
// The position change handler listens to changes to the current child.
if (this.replayingOnPositionChange) {
// Children are paused whenever we switch between them.
const paused = this._paused;
this._paused = true;
this.replayingOnPositionChange();
this._paused = paused;
}
},
replayPushThreadPause() {
// The thread has paused so that the user can interact with it. The child
// will stay paused until this thread-wide pause has been popped.
assert(this._paused);
assert(!this._resumeCallback);
if (++this._threadPauseCount == 1) {
// Save checkpoints near the current position in case the user rewinds.
RecordReplayControl.markExplicitPause();
// There is no preferred direction of travel after an explicit pause.
this._direction = Direction.NONE;
// Update graphics according to the current state of the child.
this._repaint();
// If breakpoint handlers for the pause haven't been called yet, don't
// call them at all.
this._cancelPerformPause = true;
}
const point = this.replayCurrentExecutionPoint();
dumpv("PushPause " + JSON.stringify(point));
},
replayPopThreadPause() {
dumpv("PopPause");
// After popping the last thread-wide pause, the child can resume.
if (--this._threadPauseCount == 0 && this._resumeCallback) {
Services.tm.dispatchToMainThread(this._performResume.bind(this));
}
},
_setResume(callback) {
assert(this._paused);
// Overwrite any existing resume direction.
this._resumeCallback = callback;
// The child can resume immediately if there is no thread-wide pause.
if (!this._threadPauseCount) {
Services.tm.dispatchToMainThread(this._performResume.bind(this));
}
},
_performResume() {
assert(this._paused && !this._threadPauseCount);
if (this._resumeCallback && !this._threadPauseCount) {
const callback = this._resumeCallback;
this._invalidateAfterUnpause();
this._resumeCallback = null;
callback();
}
},
// Clear out all data that becomes invalid when the child unpauses.
_invalidateAfterUnpause() {
this._frames.forEach(frame => frame._invalidate());
this._frames.length = 0;
this._objects.forEach(obj => obj._invalidate());
this._objects.length = 0;
},
/////////////////////////////////////////////////////////
// Breakpoint management
/////////////////////////////////////////////////////////
_setBreakpoint(handler, position, data) {
this._ensurePaused();
dumpv("AddBreakpoint " + JSON.stringify(position));
RecordReplayControl.addBreakpoint(position);
this._breakpoints.push({handler, position, data});
},
_clearMatchingBreakpoints(callback) {
this._ensurePaused();
const newBreakpoints = this._breakpoints.filter(bp => !callback(bp));
if (newBreakpoints.length != this._breakpoints.length) {
dumpv("ClearBreakpoints");
RecordReplayControl.clearBreakpoints();
for (const { position } of newBreakpoints) {
dumpv("AddBreakpoint " + JSON.stringify(position));
RecordReplayControl.addBreakpoint(position);
}
}
this._breakpoints = newBreakpoints;
},
_searchBreakpoints(callback) {
for (const breakpoint of this._breakpoints) {
const v = callback(breakpoint);
if (v) {
return v;
}
}
return undefined;
},
// Getter for a breakpoint kind that has no script/offset/frameIndex.
_breakpointKindGetter(kind) {
return this._searchBreakpoints(({position, data}) => {
return (position.kind == kind) ? data : null;
});
},
// Setter for a breakpoint kind that has no script/offset/frameIndex.
_breakpointKindSetter(kind, handler, callback) {
if (handler) {
this._setBreakpoint(callback, { kind }, handler);
} else {
this._clearMatchingBreakpoints(({position}) => position.kind == kind);
}
},
/////////////////////////////////////////////////////////
// Script methods
/////////////////////////////////////////////////////////
_getScript(id) {
if (!id) {
return null;
}
const rv = this._scripts[id];
if (rv) {
return rv;
}
return this._addScript(this._sendRequest({ type: "getScript", id }));
},
_addScript(data) {
if (!this._scripts[data.id]) {
this._scripts[data.id] = new ReplayDebuggerScript(this, data);
}
return this._scripts[data.id];
},
_convertScriptQuery(query) {
// Make a copy of the query, converting properties referring to debugger
// things into their associated ids.
const rv = Object.assign({}, query);
if ("global" in query) {
rv.global = query.global._data.id;
}
if ("source" in query) {
rv.source = query.source._data.id;
}
return rv;
},
findScripts(query) {
const data = this._sendRequest({
type: "findScripts",
query: this._convertScriptQuery(query),
});
return data.map(script => this._addScript(script));
},
findAllConsoleMessages() {
this._ensurePaused();
const messages = this._sendRequest({ type: "findConsoleMessages" });
return messages.map(this._convertConsoleMessage.bind(this));
},
/////////////////////////////////////////////////////////
// ScriptSource methods
/////////////////////////////////////////////////////////
_getSource(id) {
const source = this._scriptSources[id];
if (source) {
return source;
}
return this._addSource(this._sendRequest({ type: "getSource", id }));
},
_addSource(data) {
if (!this._scriptSources[data.id]) {
this._scriptSources[data.id] = new ReplayDebuggerScriptSource(this, data);
}
return this._scriptSources[data.id];
},
findSources() {
this._ensurePaused();
const data = this._sendRequest({ type: "findSources" });
return data.map(source => this._addSource(source));
},
/////////////////////////////////////////////////////////
// Object methods
/////////////////////////////////////////////////////////
// Objects which |forConsole| is set are objects that were logged in console
// messages, and had their properties recorded so that they can be inspected
// without switching to a replaying child.
_getObject(id, forConsole) {
if (id && !this._objects[id]) {
const data = this._sendRequest({ type: "getObject", id });
switch (data.kind) {
case "Object":
this._objects[id] = new ReplayDebuggerObject(this, data, forConsole);
break;
case "Environment":
this._objects[id] = new ReplayDebuggerEnvironment(this, data);
break;
default:
ThrowError("Unknown object kind");
}
}
const rv = this._objects[id];
if (forConsole) {
rv._forConsole = true;
}
return rv;
},
_convertValue(value, forConsole) {
if (isNonNullObject(value)) {
if (value.object) {
return this._getObject(value.object, forConsole);
} else if (value.special == "undefined") {
return undefined;
} else if (value.special == "NaN") {
return NaN;
} else if (value.special == "Infinity") {
return Infinity;
} else if (value.special == "-Infinity") {
return -Infinity;
}
}
return value;
},
_convertCompletionValue(value) {
if ("return" in value) {
return { return: this._convertValue(value.return) };
}
if ("throw" in value) {
return { throw: this._convertValue(value.throw) };
}
ThrowError("Unexpected completion value");
return null; // For eslint
},
/////////////////////////////////////////////////////////
// Frame methods
/////////////////////////////////////////////////////////
_getFrame(index) {
if (index == NewestFrameIndex) {
if (this._frames.length) {
return this._frames[this._frames.length - 1];
}
} else {
assert(index < this._frames.length);
if (this._frames[index]) {
return this._frames[index];
}
}
const data = this._sendRequest({ type: "getFrame", index });
if (index == NewestFrameIndex) {
if ("index" in data) {
index = data.index;
} else {
// There are no frames on the stack.
return null;
}
}
this._frames[index] = new ReplayDebuggerFrame(this, data);
return this._frames[index];
},
getNewestFrame() {
return this._getFrame(NewestFrameIndex);
},
/////////////////////////////////////////////////////////
// Console Message methods
/////////////////////////////////////////////////////////
_convertConsoleMessage(message) {
// Console API message arguments need conversion to debuggee values, but
// other contents of the message can be left alone.
if (message.messageType == "ConsoleAPI" && message.arguments) {
for (let i = 0; i < message.arguments.length; i++) {
message.arguments[i] = this._convertValue(message.arguments[i],
/* forConsole = */ true);
}
}
return message;
},
/////////////////////////////////////////////////////////
// Handlers
/////////////////////////////////////////////////////////
_getNewScript() {
return this._addScript(this._sendRequest({ type: "getNewScript" }));
},
get onNewScript() { return this._breakpointKindGetter("NewScript"); },
set onNewScript(handler) {
this._breakpointKindSetter("NewScript", handler,
() => handler.call(this, this._getNewScript()));
},
get onEnterFrame() { return this._breakpointKindGetter("EnterFrame"); },
set onEnterFrame(handler) {
this._breakpointKindSetter("EnterFrame", handler,
() => { handler.call(this, this.getNewestFrame()); });
},
get replayingOnPopFrame() {
return this._searchBreakpoints(({position, data}) => {
return (position.kind == "OnPop" && !position.script) ? data : null;
});
},
set replayingOnPopFrame(handler) {
if (handler) {
this._setBreakpoint(() => { handler.call(this, this.getNewestFrame()); },
{ kind: "OnPop" }, handler);
} else {
this._clearMatchingBreakpoints(({position}) => {
return position.kind == "OnPop" && !position.script;
});
}
},
getNewConsoleMessage() {
const message = this._sendRequest({ type: "getNewConsoleMessage" });
return this._convertConsoleMessage(message);
},
get onConsoleMessage() {
return this._breakpointKindGetter("ConsoleMessage");
},
set onConsoleMessage(handler) {
this._breakpointKindSetter("ConsoleMessage", handler,
() => handler.call(this, this.getNewConsoleMessage()));
},
clearAllBreakpoints: NYI,
}; // ReplayDebugger.prototype
///////////////////////////////////////////////////////////////////////////////
// ReplayDebuggerScript
///////////////////////////////////////////////////////////////////////////////
function ReplayDebuggerScript(dbg, data) {
this._dbg = dbg;
this._data = data;
}
ReplayDebuggerScript.prototype = {
get displayName() { return this._data.displayName; },
get url() { return this._data.url; },
get startLine() { return this._data.startLine; },
get lineCount() { return this._data.lineCount; },
get source() { return this._dbg._getSource(this._data.sourceId); },
get sourceStart() { return this._data.sourceStart; },
get sourceLength() { return this._data.sourceLength; },
_forward(type, value) {
return this._dbg._sendRequest({ type, id: this._data.id, value });
},
getLineOffsets(line) { return this._forward("getLineOffsets", line); },
getOffsetLocation(pc) { return this._forward("getOffsetLocation", pc); },
getSuccessorOffsets(pc) { return this._forward("getSuccessorOffsets", pc); },
getPredecessorOffsets(pc) { return this._forward("getPredecessorOffsets", pc); },
setBreakpoint(offset, handler) {
this._dbg._setBreakpoint(() => { handler.hit(this._dbg.getNewestFrame()); },
{ kind: "Break", script: this._data.id, offset },
handler);
},
clearBreakpoint(handler) {
this._dbg._clearMatchingBreakpoints(({position, data}) => {
return position.script == this._data.id && handler == data;
});
},
get isGeneratorFunction() { NYI(); },
get isAsyncFunction() { NYI(); },
get format() { NYI(); },
getChildScripts: NYI,
getAllOffsets: NYI,
getAllColumnOffsets: NYI,
getBreakpoints: NYI,
clearAllBreakpoints: NYI,
isInCatchScope: NYI,
};
///////////////////////////////////////////////////////////////////////////////
// ReplayDebuggerScriptSource
///////////////////////////////////////////////////////////////////////////////
function ReplayDebuggerScriptSource(dbg, data) {
this._dbg = dbg;
this._data = data;
}
ReplayDebuggerScriptSource.prototype = {
get text() { return this._data.text; },
get url() { return this._data.url; },
get displayURL() { return this._data.displayURL; },
get elementAttributeName() { return this._data.elementAttributeName; },
get introductionOffset() { return this._data.introductionOffset; },
get introductionType() { return this._data.introductionType; },
get sourceMapURL() { return this._data.sourceMapURL; },
get element() { return null; },
get introductionScript() {
return this._dbg._getScript(this._data.introductionScript);
},
get binary() { NYI(); },
};
///////////////////////////////////////////////////////////////////////////////
// ReplayDebuggerFrame
///////////////////////////////////////////////////////////////////////////////
function ReplayDebuggerFrame(dbg, data) {
this._dbg = dbg;
this._data = data;
if (this._data.arguments) {
this._data.arguments =
this._data.arguments.map(a => this._dbg._convertValue(a));
}
}
ReplayDebuggerFrame.prototype = {
_invalidate() {
this._data = null;
},
get type() { return this._data.type; },
get callee() { return this._dbg._getObject(this._data.callee); },
get environment() { return this._dbg._getObject(this._data.environment); },
get generator() { return this._data.generator; },
get constructing() { return this._data.constructing; },
get this() { return this._dbg._convertValue(this._data.this); },
get script() { return this._dbg._getScript(this._data.script); },
get offset() { return this._data.offset; },
get arguments() { return this._data.arguments; },
get live() { return true; },
eval(text, options) {
const rv = this._dbg._sendRequestAllowDiverge({
type: "frameEvaluate",
index: this._data.index,
text,
options,
});
return this._dbg._convertCompletionValue(rv);
},
_positionMatches(position, kind) {
return position.kind == kind
&& position.script == this._data.script
&& position.frameIndex == this._data.index;
},
get onStep() {
return this._dbg._searchBreakpoints(({position, data}) => {
return this._positionMatches(position, "OnStep") ? data : null;
});
},
set onStep(handler) {
if (handler) {
// Use setReplayingOnStep instead.
NotAllowed();
}
this._clearOnStepBreakpoints();
},
_clearOnStepBreakpoints() {
this._dbg._clearMatchingBreakpoints(
({position}) => this._positionMatches(position, "OnStep")
);
},
setReplayingOnStep(handler, offsets) {
this._clearOnStepBreakpoints();
offsets.forEach(offset => {
this._dbg._setBreakpoint(
() => { handler.call(this._dbg.getNewestFrame()); },
{ kind: "OnStep",
script: this._data.script,
offset,
frameIndex: this._data.index },
handler);
});
},
get onPop() {
return this._dbg._searchBreakpoints(({position, data}) => {
return this._positionMatches(position, "OnPop") ? data : null;
});
},
set onPop(handler) {
if (handler) {
this._dbg._setBreakpoint(() => {
const result = this._dbg._sendRequest({ type: "popFrameResult" });
handler.call(this._dbg.getNewestFrame(),
this._dbg._convertCompletionValue(result));
},
{ kind: "OnPop", script: this._data.script, frameIndex: this._data.index },
handler);
} else {
this._dbg._clearMatchingBreakpoints(
({position}) => this._positionMatches(position, "OnPop")
);
}
},
get older() {
if (this._data.index == 0) {
// This is the oldest frame.
return null;
}
return this._dbg._getFrame(this._data.index - 1);
},
get implementation() { NYI(); },
evalWithBindings: NYI,
};
///////////////////////////////////////////////////////////////////////////////
// ReplayDebuggerObject
///////////////////////////////////////////////////////////////////////////////
function ReplayDebuggerObject(dbg, data, forConsole) {
this._dbg = dbg;
this._data = data;
this._forConsole = forConsole;
this._properties = null;
}
ReplayDebuggerObject.prototype = {
_invalidate() {
this._data = null;
this._properties = null;
},
get callable() { return this._data.callable; },
get isBoundFunction() { return this._data.isBoundFunction; },
get isArrowFunction() { return this._data.isArrowFunction; },
get isGeneratorFunction() { return this._data.isGeneratorFunction; },
get isAsyncFunction() { return this._data.isAsyncFunction; },
get class() { return this._data.class; },
get name() { return this._data.name; },
get displayName() { return this._data.displayName; },
get parameterNames() { return this._data.parameterNames; },
get script() { return this._dbg._getScript(this._data.script); },
get environment() { return this._dbg._getObject(this._data.environment); },
get boundTargetFunction() { return this.isBoundFunction ? NYI() : undefined; },
get boundThis() { return this.isBoundFunction ? NYI() : undefined; },
get boundArguments() { return this.isBoundFunction ? NYI() : undefined; },
get global() { return this._dbg._getObject(this._data.global); },
get isProxy() { return this._data.isProxy; },
isExtensible() { return this._data.isExtensible; },
isSealed() { return this._data.isSealed; },
isFrozen() { return this._data.isFrozen; },
unwrap() { return this.isProxy ? NYI() : this; },
get proto() {
// Don't allow inspection of the prototypes of objects logged to the
// console. This is a hack that prevents the object inspector from crawling
// the object's prototype chain.
return this._forConsole ? null : this._dbg._getObject(this._data.proto);
},
unsafeDereference() {
// Direct access to the referent is not currently available.
return null;
},
getOwnPropertyNames() {
this._ensureProperties();
return Object.keys(this._properties);
},
getOwnPropertySymbols() {
// Symbol properties are not handled yet.
return [];
},
getOwnPropertyDescriptor(name) {
this._ensureProperties();
const desc = this._properties[name];
return desc ? this._convertPropertyDescriptor(desc) : null;
},
_ensureProperties() {
if (!this._properties) {
const id = this._data.id;
const properties = this._forConsole
? this._dbg._sendRequest({ type: "getObjectPropertiesForConsole", id })
: this._dbg._sendRequestAllowDiverge({ type: "getObjectProperties", id });
this._properties = {};
properties.forEach(({name, desc}) => { this._properties[name] = desc; });
}
},
_convertPropertyDescriptor(desc) {
const rv = Object.assign({}, desc);
if ("value" in desc) {
rv.value = this._dbg._convertValue(desc.value);
}
if ("get" in desc) {
rv.get = this._dbg._getObject(desc.get);
}
if ("set" in desc) {
rv.set = this._dbg._getObject(desc.set);
}
return rv;
},
get allocationSite() { NYI(); },
get errorMessageName() { NYI(); },
get errorNotes() { NYI(); },
get errorLineNumber() { NYI(); },
get errorColumnNumber() { NYI(); },
get proxyTarget() { NYI(); },
get proxyHandler() { NYI(); },
get isPromise() { NYI(); },
call: NYI,
apply: NYI,
asEnvironment: NYI,
executeInGlobal: NYI,
executeInGlobalWithBindings: NYI,
makeDebuggeeValue: NotAllowed,
preventExtensions: NotAllowed,
seal: NotAllowed,
freeze: NotAllowed,
defineProperty: NotAllowed,
defineProperties: NotAllowed,
deleteProperty: NotAllowed,
forceLexicalInitializationByName: NotAllowed,
};
///////////////////////////////////////////////////////////////////////////////
// ReplayDebuggerEnvironment
///////////////////////////////////////////////////////////////////////////////
function ReplayDebuggerEnvironment(dbg, data) {
this._dbg = dbg;
this._data = data;
this._names = null;
}
ReplayDebuggerEnvironment.prototype = {
_invalidate() {
this._data = null;
this._names = null;
},
get type() { return this._data.type; },
get parent() { return this._dbg._getObject(this._data.parent); },
get object() { return this._dbg._getObject(this._data.object); },
get callee() { return this._dbg._getObject(this._data.callee); },
get optimizedOut() { return this._data.optimizedOut; },
_ensureNames() {
if (!this._names) {
const names = this._dbg._sendRequestAllowDiverge({
type: "getEnvironmentNames",
id: this._data.id,
});
this._names = {};
names.forEach(({ name, value }) => {
this._names[name] = this._dbg._convertValue(value);
});
}
},
names() {
this._ensureNames();
return Object.keys(this._names);
},
getVariable(name) {
this._ensureNames();
return this._names[name];
},
get inspectable() {
// All ReplayDebugger environments are inspectable, as all compartments in
// the replayed process are considered to be debuggees.
return true;
},
find: NYI,
setVariable: NotAllowed,
};
///////////////////////////////////////////////////////////////////////////////
// Utilities
///////////////////////////////////////////////////////////////////////////////
function dumpv(str) {
//dump("[ReplayDebugger] " + str + "\n");
}
function NYI() {
ThrowError("Not yet implemented");
}
function NotAllowed() {
ThrowError("Not allowed");
}
function ThrowError(msg)
{
const error = new Error(msg);
dump("ReplayDebugger Server Error: " + msg + " Stack: " + error.stack + "\n");
throw error;
}
function assert(v) {
if (!v) {
ThrowError("Assertion Failed!");
}
}
function isNonNullObject(obj) {
return obj && (typeof obj == "object" || typeof obj == "function");
}
module.exports = ReplayDebugger;