зеркало из https://github.com/mozilla/gecko-dev.git
1033 строки
32 KiB
JavaScript
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;
|