author | Brian Hackett <bhackett1024@gmail.com> |
Sun, 22 Jul 2018 11:59:42 +0000 | |
changeset 469498 | dd58259f140ade2a5f38b41978c5e8f0bdb4ca28 |
parent 469497 | 1fede6c8a76a69c35aa93b234e551c1937a1f891 |
child 469499 | a4a06f53d689f3dedfca8eaf23f417ec54696927 |
push id | unknown |
push user | unknown |
push date | unknown |
reviewers | jimb |
bugs | 1470795 |
milestone | 63.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/devtools/server/actors/moz.build +++ b/devtools/server/actors/moz.build @@ -6,16 +6,17 @@ DIRS += [ 'addon', 'canvas', 'emulation', 'highlighters', 'inspector', 'object', + 'replay', 'targets', 'utils', 'webconsole', 'worker', ] DevToolsModules( 'accessibility-parent.js',
new file mode 100644 --- /dev/null +++ b/devtools/server/actors/replay/debugger.js @@ -0,0 +1,673 @@ +/* -*- 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 = require("RecordReplayControl"); + +/////////////////////////////////////////////////////////////////////////////// +// ReplayDebugger +/////////////////////////////////////////////////////////////////////////////// + +function ReplayDebugger() { + RecordReplayControl.registerReplayDebugger(this); + + // All breakpoints (per BreakpointPosition) 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 = []; +} + +// 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, + replayResumeBackward() { RecordReplayControl.resume(/* forward = */ false); }, + replayResumeForward() { RecordReplayControl.resume(/* forward = */ true); }, + replayPause: RecordReplayControl.pause, + + addDebuggee() {}, + removeAllDebuggees() {}, + + replayingContent(url) { + return this._sendRequest({ type: "getContent", url }); + }, + + _sendRequest(request) { + const data = RecordReplayControl.sendRequest(request); + //dump("SendRequest: " + + // JSON.stringify(request) + " -> " + JSON.stringify(data) + "\n"); + if (data.exception) { + ThrowError(data.exception); + } + return data; + }, + + _setBreakpoint(handler, position, data) { + const id = RecordReplayControl.setBreakpoint(handler, position); + this._breakpoints.push({id, position, data}); + }, + + _clearMatchingBreakpoints(callback) { + this._breakpoints = this._breakpoints.filter(breakpoint => { + if (callback(breakpoint)) { + RecordReplayControl.clearBreakpoint(breakpoint.id); + return false; + } + return true; + }); + }, + + _searchBreakpoints(callback) { + for (const breakpoint of this._breakpoints) { + const v = callback(breakpoint); + if (v) { + return v; + } + } + return undefined; + }, + + // This is called on all ReplayDebuggers whenever the child process is about + // to unpause. Clear out all data that is invalidated as a result. + invalidateAfterUnpause() { + this._frames.forEach(frame => { + if (frame) { + frame._invalidate(); + } + }); + this._frames.length = 0; + + this._objects.forEach(obj => obj._invalidate()); + this._objects.length = 0; + }, + + ///////////////////////////////////////////////////////// + // 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]; + }, + + findScripts() { + // Note: Debugger's findScripts() method takes a query argument, which + // we ignore here. + const data = this._sendRequest({ type: "findScripts" }); + return data.map(script => this._addScript(script)); + }, + + ///////////////////////////////////////////////////////// + // ScriptSource methods + ///////////////////////////////////////////////////////// + + _getSource(id) { + if (!this._scriptSources[id]) { + const data = this._sendRequest({ type: "getSource", id }); + this._scriptSources[id] = new ReplayDebuggerScriptSource(this, data); + } + return this._scriptSources[id]; + }, + + ///////////////////////////////////////////////////////// + // Object methods + ///////////////////////////////////////////////////////// + + _getObject(id) { + if (id && !this._objects[id]) { + const data = this._sendRequest({ type: "getObject", id }); + switch (data.kind) { + case "Object": + this._objects[id] = new ReplayDebuggerObject(this, data); + break; + case "Environment": + this._objects[id] = new ReplayDebuggerEnvironment(this, data); + break; + default: + ThrowError("Unknown object kind"); + } + } + return this._objects[id]; + }, + + _convertValue(value) { + if (value && typeof value == "object") { + if (value.object) { + return this._getObject(value.object); + } 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; + } + + // Fill in the older frames. + while (index >= this._frames.length) { + this._frames.push(null); + } + } + + this._frames[index] = new ReplayDebuggerFrame(this, data); + return this._frames[index]; + }, + + getNewestFrame() { + return this._getFrame(NewestFrameIndex); + }, + + get onNewScript() { + return this._searchBreakpoints(({position, data}) => { + return position.kind == "NewScript" ? data : null; + }); + }, + + set onNewScript(handler) { + if (handler) { + this._setBreakpoint(() => { + const script = this._sendRequest({ type: "getNewScript" }); + const debugScript = this._addScript(script); + handler.call(this, debugScript); + }, { kind: "NewScript" }, handler); + } else { + this._clearMatchingBreakpoints(({position}) => position.kind == "NewScript"); + } + }, + + get onEnterFrame() { + return this._searchBreakpoints(({position, data}) => { + return position.kind == "EnterFrame" ? data : null; + }); + }, + + set onEnterFrame(handler) { + if (handler) { + this._setBreakpoint(() => handler.call(this, this.getNewestFrame()), + { kind: "EnterFrame" }, handler); + } else { + this._clearMatchingBreakpoints(({position}) => position.kind == "EnterFrame"); + } + }, + + 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 == "EnterFrame" && !position.script; + }); + } + }, + + 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(this._dbg._convertValue.bind(this._dbg)); + } +} + +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._sendRequest({ 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"); + }); + }, + + 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) { + this._dbg = dbg; + this._data = data; + 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 proto() { return this._dbg._getObject(this._data.proto); }, + 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; }, + + 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(); + return this._properties[name]; + }, + + _ensureProperties() { + if (!this._properties) { + const properties = this._dbg._sendRequest({ + type: "getObjectProperties", + id: this._data.id + }); + this._properties = {}; + properties.forEach(({name, desc}) => { + if ("value" in desc) { + desc.value = this._dbg._convertValue(desc.value); + } + if ("get" in desc) { + desc.get = this._dbg._getObject(desc.get); + } + if ("set" in desc) { + desc.set = this._dbg._getObject(desc.set); + } + this._properties[name] = desc; + }); + } + }, + + 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: NYI, + + 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._sendRequest({ 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 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) { + throw new Error("Assertion Failed!"); + } +} + +module.exports = ReplayDebugger;
new file mode 100644 --- /dev/null +++ b/devtools/server/actors/replay/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'debugger.js', + 'replay.js', +)
new file mode 100644 --- /dev/null +++ b/devtools/server/actors/replay/replay.js @@ -0,0 +1,598 @@ +/* -*- 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 */ + +// This file defines the logic that runs in the record/replay devtools sandbox. +// This code is loaded into all recording/replaying processes, and responds to +// requests and other instructions from the middleman via the exported symbols +// defined at the end of this file. +// +// Like all other JavaScript in the recording/replaying process, this code's +// state is included in memory snapshots and reset when checkpoints are +// restored. In the process of handling the middleman's requests, however, its +// state may vary between recording and replaying, or between different +// replays. As a result, we have to be very careful about performing operations +// that might interact with the recording --- any time we enter the debuggee +// and evaluate code or perform other operations. +// The RecordReplayControl.maybeDivergeFromRecording function should be used at +// any point where such interactions might occur. +// eslint-disable spaced-comment + +"use strict"; + +const CC = Components.Constructor; + +// Create a sandbox with the resources we need. require() doesn't work here. +const sandbox = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")()); +Cu.evalInSandbox( + "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" + + "addDebuggerToGlobal(this);", + sandbox +); +const Debugger = sandbox.Debugger; +const RecordReplayControl = sandbox.RecordReplayControl; + +const dbg = new Debugger(); + +// We are interested in debugging all globals in the process. +dbg.onNewGlobalObject = function(global) { + dbg.addDebuggee(global); +}; + +/////////////////////////////////////////////////////////////////////////////// +// Utilities +/////////////////////////////////////////////////////////////////////////////// + +function assert(v) { + if (!v) { + RecordReplayControl.dump("Assertion Failed: " + (new Error()).stack + "\n"); + throw new Error("Assertion Failed!"); + } +} + +// Bidirectional map between objects and IDs. +function IdMap() { + this._idToObject = [ undefined ]; + this._objectToId = new Map(); +} + +IdMap.prototype = { + add(object) { + assert(object && !this._objectToId.has(object)); + const id = this._idToObject.length; + this._idToObject.push(object); + this._objectToId.set(object, id); + return id; + }, + + getId(object) { + const id = this._objectToId.get(object); + return (id === undefined) ? 0 : id; + }, + + getObject(id) { + return this._idToObject[id]; + }, + + forEach(callback) { + for (let i = 1; i < this._idToObject.length; i++) { + callback(i, this._idToObject[i]); + } + }, + + lastId() { + return this._idToObject.length - 1; + }, +}; + +function countScriptFrames() { + let count = 0; + let frame = dbg.getNewestFrame(); + while (frame) { + if (considerScript(frame.script)) { + count++; + } + frame = frame.older; + } + return count; +} + +function scriptFrameForIndex(index) { + let indexFromTop = countScriptFrames() - 1 - index; + let frame = dbg.getNewestFrame(); + while (true) { + if (considerScript(frame.script)) { + if (indexFromTop-- == 0) { + break; + } + } + frame = frame.older; + } + return frame; +} + +/////////////////////////////////////////////////////////////////////////////// +// Persistent State +/////////////////////////////////////////////////////////////////////////////// + +// Association between Debugger.Scripts and their IDs. The indices that this +// table assigns to scripts are stable across the entire recording, even though +// this table (like all JS state) is included in snapshots, rolled back when +// rewinding, and so forth. In debuggee time, this table only grows (there is +// no way to remove entries). Scripts created for debugger activity (e.g. eval) +// are ignored, and off thread compilation is disabled, so this table acquires +// the same scripts in the same order as we roll back and run forward in the +// recording. +const gScripts = new IdMap(); + +function addScript(script) { + gScripts.add(script); + script.getChildScripts().forEach(addScript); +} + +// Association between Debugger.ScriptSources and their IDs. As for gScripts, +// the indices assigned to a script source are consistent across all replays +// and rewinding. +const gScriptSources = new IdMap(); + +function addScriptSource(source) { + gScriptSources.add(source); +} + +function considerScript(script) { + return script.url + && !script.url.startsWith("resource:") + && !script.url.startsWith("chrome:"); +} + +dbg.onNewScript = function(script) { + if (RecordReplayControl.areThreadEventsDisallowed()) { + // This script is part of an eval on behalf of the debugger. + return; + } + + if (!considerScript(script)) { + return; + } + + addScript(script); + addScriptSource(script.source); + + // Each onNewScript call advances the progress counter, to preserve the + // ProgressCounter invariant when onNewScript is called multiple times + // without executing any scripts. + RecordReplayControl.advanceProgressCounter(); + + if (gHasNewScriptHandler) { + RecordReplayControl.positionHit({ kind: "NewScript" }); + } + + // Check in case any handlers we need to install are on the scripts just + // created. + installPendingHandlers(); +}; + +/////////////////////////////////////////////////////////////////////////////// +// Position Handler State +/////////////////////////////////////////////////////////////////////////////// + +// Whether there is a position handler for NewScript. +let gHasNewScriptHandler = false; + +// Whether there is a position handler for EnterFrame. +let gHasEnterFrameHandler = false; + +// Handlers we tried to install but couldn't due to a script not existing. +// Breakpoints requested by the middleman --- which are preserved when +// restoring earlier checkpoints --- identify target scripts by their stable ID +// in gScripts. This array holds the breakpoints for scripts whose IDs we know +// but which have not been created yet. +const gPendingPcHandlers = []; + +// Script/offset pairs where we have installed a breakpoint handler. We have to +// avoid installing duplicate handlers here because they will both be called. +const gInstalledPcHandlers = []; + +// Callbacks to test whether a frame should have an OnPop handler. +const gOnPopFilters = []; + +// eslint-disable-next-line no-unused-vars +function ClearPositionHandlers() { + dbg.clearAllBreakpoints(); + dbg.onEnterFrame = undefined; + + gHasNewScriptHandler = false; + gHasEnterFrameHandler = false; + gPendingPcHandlers.length = 0; + gInstalledPcHandlers.length = 0; + gOnPopFilters.length = 0; +} + +function installPendingHandlers() { + const pending = gPendingPcHandlers.map(position => position); + gPendingPcHandlers.length = 0; + + pending.forEach(EnsurePositionHandler); +} + +// The completion state of any frame that is being popped. +let gPopFrameResult = null; + +function onPopFrame(completion) { + gPopFrameResult = completion; + RecordReplayControl.positionHit({ + kind: "OnPop", + script: gScripts.getId(this.script), + frameIndex: countScriptFrames() - 1, + }); + gPopFrameResult = null; +} + +function onEnterFrame(frame) { + if (gHasEnterFrameHandler) { + RecordReplayControl.positionHit({ kind: "EnterFrame" }); + } + + if (considerScript(frame.script)) { + gOnPopFilters.forEach(filter => { + if (filter(frame)) { + frame.onPop = onPopFrame; + } + }); + } +} + +function addOnPopFilter(filter) { + let frame = dbg.getNewestFrame(); + while (frame) { + if (considerScript(frame.script) && filter(frame)) { + frame.onPop = onPopFrame; + } + frame = frame.older; + } + + gOnPopFilters.push(filter); + dbg.onEnterFrame = onEnterFrame; +} + +function EnsurePositionHandler(position) { + switch (position.kind) { + case "Break": + case "OnStep": + let debugScript; + if (position.script) { + debugScript = gScripts.getObject(position.script); + if (!debugScript) { + // The script referred to in this position does not exist yet, so we + // can't install a handler for it. Add a pending handler so that we + // can install the handler once the script is created. + gPendingPcHandlers.push(position); + return; + } + } + + const match = function({script, offset}) { + return script == position.script && offset == position.offset; + }; + if (gInstalledPcHandlers.some(match)) { + return; + } + gInstalledPcHandlers.push({ script: position.script, offset: position.offset }); + + debugScript.setBreakpoint(position.offset, { + hit() { + RecordReplayControl.positionHit({ + kind: "OnStep", + script: position.script, + offset: position.offset, + frameIndex: countScriptFrames() - 1, + }); + } + }); + break; + case "OnPop": + if (position.script) { + addOnPopFilter(frame => gScripts.getId(frame.script) == position.script); + } else { + addOnPopFilter(frame => true); + } + break; + case "EnterFrame": + gHasEnterFrameHandler = true; + dbg.onEnterFrame = onEnterFrame; + break; + case "NewScript": + gHasNewScriptHandler = true; + break; + } +} + +// eslint-disable-next-line no-unused-vars +function GetEntryPosition(position) { + if (position.kind == "Break" || position.kind == "OnStep") { + const script = gScripts.getObject(position.script); + if (script) { + return { + kind: "Break", + script: position.script, + offset: script.mainOffset, + }; + } + } + return null; +} + +/////////////////////////////////////////////////////////////////////////////// +// Paused State +/////////////////////////////////////////////////////////////////////////////// + +let gPausedObjects = new IdMap(); + +function getObjectId(obj) { + const id = gPausedObjects.getId(obj); + if (!id && obj) { + assert((obj instanceof Debugger.Object) || + (obj instanceof Debugger.Environment)); + return gPausedObjects.add(obj); + } + return id; +} + +function convertValue(value) { + if (value instanceof Debugger.Object) { + return { object: getObjectId(value) }; + } + if (value === undefined) { + return { special: "undefined" }; + } + if (value !== value) { // eslint-disable-line no-self-compare + return { special: "NaN" }; + } + if (value == Infinity) { + return { special: "Infinity" }; + } + if (value == -Infinity) { + return { special: "-Infinity" }; + } + return value; +} + +function convertCompletionValue(value) { + if ("return" in value) { + return { return: convertValue(value.return) }; + } + if ("throw" in value) { + return { throw: convertValue(value.throw) }; + } + throw new Error("Unexpected completion value"); +} + +// eslint-disable-next-line no-unused-vars +function ClearPausedState() { + gPausedObjects = new IdMap(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Handler Helpers +/////////////////////////////////////////////////////////////////////////////// + +function getScriptData(id) { + const script = gScripts.getObject(id); + return { + id, + sourceId: gScriptSources.getId(script.source), + startLine: script.startLine, + lineCount: script.lineCount, + sourceStart: script.sourceStart, + sourceLength: script.sourceLength, + displayName: script.displayName, + url: script.url, + }; +} + +function forwardToScript(name) { + return request => gScripts.getObject(request.id)[name](request.value); +} + +/////////////////////////////////////////////////////////////////////////////// +// Handlers +/////////////////////////////////////////////////////////////////////////////// + +const gRequestHandlers = { + + findScripts(request) { + const rv = []; + gScripts.forEach((id) => { + rv.push(getScriptData(id)); + }); + return rv; + }, + + getScript(request) { + return getScriptData(request.id); + }, + + getNewScript(request) { + return getScriptData(gScripts.lastId()); + }, + + getContent(request) { + return RecordReplayControl.getContent(request.url); + }, + + getSource(request) { + const source = gScriptSources.getObject(request.id); + const introductionScript = gScripts.getId(source.introductionScript); + return { + id: request.id, + text: source.text, + url: source.url, + displayURL: source.displayURL, + elementAttributeName: source.elementAttributeName, + introductionScript, + introductionOffset: introductionScript ? source.introductionOffset : undefined, + introductionType: source.introductionType, + sourceMapURL: source.sourceMapURL, + }; + }, + + getObject(request) { + const object = gPausedObjects.getObject(request.id); + if (object instanceof Debugger.Object) { + return { + id: request.id, + kind: "Object", + callable: object.callable, + isBoundFunction: object.isBoundFunction, + isArrowFunction: object.isArrowFunction, + isGeneratorFunction: object.isGeneratorFunction, + isAsyncFunction: object.isAsyncFunction, + proto: getObjectId(object.proto), + class: object.class, + name: object.name, + displayName: object.displayName, + parameterNames: object.parameterNames, + script: gScripts.getId(object.script), + environment: getObjectId(object.environment), + global: getObjectId(object.global), + isProxy: object.isProxy, + isExtensible: object.isExtensible(), + isSealed: object.isSealed(), + isFrozen: object.isFrozen(), + }; + } + if (object instanceof Debugger.Environment) { + return { + id: request.id, + kind: "Environment", + type: object.type, + parent: getObjectId(object.parent), + object: object.type == "declarative" ? 0 : getObjectId(object.object), + callee: getObjectId(object.callee), + optimizedOut: object.optimizedOut, + }; + } + throw new Error("Unknown object kind"); + }, + + getObjectProperties(request) { + if (!RecordReplayControl.maybeDivergeFromRecording()) { + return [{ + name: "Unknown properties", + desc: { + value: "Recording divergence in getObjectProperties", + enumerable: true + }, + }]; + } + + const object = gPausedObjects.getObject(request.id); + const names = object.getOwnPropertyNames(); + + return names.map(name => { + const desc = object.getOwnPropertyDescriptor(name); + if ("value" in desc) { + desc.value = convertValue(desc.value); + } + if ("get" in desc) { + desc.get = getObjectId(desc.get); + } + if ("set" in desc) { + desc.set = getObjectId(desc.set); + } + return { name, desc }; + }); + }, + + getEnvironmentNames(request) { + if (!RecordReplayControl.maybeDivergeFromRecording()) { + return [{name: "Unknown names", + value: "Recording divergence in getEnvironmentNames" }]; + } + + const env = gPausedObjects.getObject(request.id); + const names = env.names(); + + return names.map(name => { + return { name, value: convertValue(env.getVariable(name)) }; + }); + }, + + getFrame(request) { + if (request.index == -1 /* NewestFrameIndex */) { + const numFrames = countScriptFrames(); + if (!numFrames) { + // Return an empty object when there are no frames. + return {}; + } + request.index = numFrames - 1; + } + + const frame = scriptFrameForIndex(request.index); + + let _arguments = null; + if (frame.arguments) { + _arguments = []; + for (let i = 0; i < frame.arguments.length; i++) { + _arguments.push(convertValue(frame.arguments[i])); + } + } + + return { + index: request.index, + type: frame.type, + callee: getObjectId(frame.callee), + environment: getObjectId(frame.environment), + generator: frame.generator, + constructing: frame.constructing, + this: convertValue(frame.this), + script: gScripts.getId(frame.script), + offset: frame.offset, + arguments: _arguments, + }; + }, + + getLineOffsets: forwardToScript("getLineOffsets"), + getOffsetLocation: forwardToScript("getOffsetLocation"), + getSuccessorOffsets: forwardToScript("getSuccessorOffsets"), + getPredecessorOffsets: forwardToScript("getPredecessorOffsets"), + + frameEvaluate(request) { + if (!RecordReplayControl.maybeDivergeFromRecording()) { + return { throw: "Recording divergence in frameEvaluate" }; + } + + const frame = scriptFrameForIndex(request.index); + const rv = frame.eval(request.text, request.options); + return convertCompletionValue(rv); + }, + + popFrameResult(request) { + return gPopFrameResult ? convertCompletionValue(gPopFrameResult) : {}; + }, +}; + +// eslint-disable-next-line no-unused-vars +function ProcessRequest(request) { + try { + if (gRequestHandlers[request.type]) { + return gRequestHandlers[request.type](request); + } + return { exception: "No handler for " + request.type }; + } catch (e) { + RecordReplayControl.dump("ReplayDebugger Record/Replay Error: " + e + "\n"); + return { exception: "" + e }; + } +} + +// eslint-disable-next-line no-unused-vars +var EXPORTED_SYMBOLS = [ + "EnsurePositionHandler", + "ClearPositionHandlers", + "ClearPausedState", + "ProcessRequest", + "GetEntryPosition", +];