const GcliActor = ActorClassWithSpec(gcliSpec, { initialize: function(conn, targetActor) { Actor.prototype.initialize.call(this, conn); this._commandsChanged = this._commandsChanged.bind(this); this._targetActor = targetActor; // see _getRequisition() this._requisitionPromise = undefined; }, destroy: function() { Actor.prototype.destroy.call(this); // If _getRequisition has not been called, just bail quickly if (this._requisitionPromise == null) { this._commandsChanged = undefined; this._targetActor = undefined; return Promise.resolve(); } return this._getRequisition().then(requisition => { requisition.destroy(); this._system.commands.onCommandsChange.remove(this._commandsChanged); this._system.destroy(); this._system = undefined; this._requisitionPromise = undefined; this._targetActor = undefined; this._commandsChanged = undefined; }); }, /** * Load a module into the requisition */ _testOnlyAddItemsByModule: function(names) { return this._getRequisition().then(requisition => { return requisition.system.addItemsByModule(names); }); }, /** * Unload a module from the requisition */ _testOnlyRemoveItemsByModule: function(names) { return this._getRequisition().then(requisition => { return requisition.system.removeItemsByModule(names); }); }, /** * Retrieve a list of the remotely executable commands * @param customProps Array of strings containing additional properties which, * if specified in the command spec, will be included in the JSON. Normally we * transfer only the properties required for GCLI to function. */ specs: function(customProps) { return this._getRequisition().then(requisition => { return requisition.system.commands.getCommandSpecs(customProps); }); }, /** * Execute a GCLI command * @return a promise of an object with the following properties: * - data: The output of the command * - type: The type of the data to allow selection of a converter * - error: True if the output was considered an error */ execute: function(typed) { return this._getRequisition().then(requisition => { return requisition.updateExec(typed).then(output => output.toJson()); }); }, /** * Get the state of an input string. i.e. requisition.getStateData() */ state: function(typed, start, rank) { return this._getRequisition().then(requisition => { return requisition.update(typed).then(() => { return requisition.getStateData(start, rank); }); }); }, /** * Call type.parse to check validity. Used by the remote type * @return a promise of an object with the following properties: * - status: Of of the following strings: VALID|INCOMPLETE|ERROR * - message: The message to display to the user * - predictions: An array of suggested values for the given parameter */ parseType: function(typed, paramName) { return this._getRequisition().then(requisition => { return requisition.update(typed).then(() => { const assignment = requisition.getAssignment(paramName); return Promise.resolve(assignment.predictions).then(predictions => { return { status: assignment.getStatus().toString(), message: assignment.message, predictions: predictions }; }); }); }); }, /** * Get the incremented/decremented value of some type * @return a promise of a string containing the new argument text */ nudgeType: function(typed, by, paramName) { return this.requisition.update(typed).then(() => { const assignment = this.requisition.getAssignment(paramName); return this.requisition.nudge(assignment, by).then(() => { return assignment.arg == null ? undefined : assignment.arg.text; }); }); }, /** * Perform a lookup on a selection type to get the allowed values */ getSelectionLookup: function(commandName, paramName) { return this._getRequisition().then(requisition => { const command = requisition.system.commands.get(commandName); if (command == null) { throw new Error("No command called '" + commandName + "'"); } let type; command.params.forEach(param => { if (param.name === paramName) { type = param.type; } }); if (type == null) { throw new Error("No parameter called '" + paramName + "' in '" + commandName + "'"); } const reply = type.getLookup(requisition.executionContext); return Promise.resolve(reply).then(lookup => { // lookup returns an array of objects with name/value properties and // the values might not be JSONable, so remove them return lookup.map(info => ({ name: info.name })); }); }); }, /** * Lazy init for a Requisition */ _getRequisition: function() { if (this._targetActor == null) { throw new Error("GcliActor used post-destroy"); } if (this._requisitionPromise != null) { return this._requisitionPromise; } const Requisition = require("gcli/cli").Requisition; const targetActor = this._targetActor; this._system = createSystem({ location: "server" }); this._system.commands.onCommandsChange.add(this._commandsChanged); const gcliInit = require("devtools/shared/gcli/commands/index"); gcliInit.addAllItemsByModule(this._system); // this._requisitionPromise should be created synchronously with the call // to _getRequisition so that destroy can tell whether there is an async // init in progress this._requisitionPromise = this._system.load().then(() => { const environment = { get chromeWindow() { throw new Error("environment.chromeWindow is not available in runAt:server" + " commands"); }, get chromeDocument() { throw new Error("environment.chromeDocument is not available in runAt:server" + " commands"); }, get window() { return targetActor.window; }, get document() { return targetActor.window && targetActor.window.document; } }; return new Requisition(this._system, { environment: environment }); }); return this._requisitionPromise; }, /** * Pass events from requisition.system.commands.onCommandsChange upwards */ _commandsChanged: function() { this.emit("commands-changed"); }, });
const { Cu, CC, components } = require("chrome"); const Services = require("Services"); const { DebuggerServer } = require("devtools/server/main"); const { registerActor, unregisterActor } = require("devtools/server/actors/utils/actor-registry-utils"); const { actorActorSpec, actorRegistrySpec } = require("devtools/shared/specs/actor-registry"); /** * The ActorActor gives you a handle to an actor you've dynamically * registered and allows you to unregister it. */ const ActorActor = protocol.ActorClassWithSpec(actorActorSpec, { initialize: function (conn, options) { protocol.Actor.prototype.initialize.call(this, conn); this.options = options; }, unregister: function () { unregisterActor(this.options); } }); /* * The ActorRegistryActor allows clients to define new actors on the * server. This is particularly useful for addons. */ const ActorRegistryActor = protocol.ActorClassWithSpec(actorRegistrySpec, { initialize: function (conn) { protocol.Actor.prototype.initialize.call(this, conn); },
exports.MemoryActor = protocol.ActorClassWithSpec(memorySpec, { initialize: function (conn, parent, frameCache = new StackFrameCache()) { protocol.Actor.prototype.initialize.call(this, conn); this._onGarbageCollection = this._onGarbageCollection.bind(this); this._onAllocations = this._onAllocations.bind(this); this.bridge = new Memory(parent, frameCache); this.bridge.on("garbage-collection", this._onGarbageCollection); this.bridge.on("allocations", this._onAllocations); }, destroy: function () { this.bridge.off("garbage-collection", this._onGarbageCollection); this.bridge.off("allocations", this._onAllocations); this.bridge.destroy(); protocol.Actor.prototype.destroy.call(this); }, attach: actorBridgeWithSpec("attach"), detach: actorBridgeWithSpec("detach"), getState: actorBridgeWithSpec("getState"), saveHeapSnapshot: function (boundaries) { return this.bridge.saveHeapSnapshot(boundaries); }, takeCensus: actorBridgeWithSpec("takeCensus"), startRecordingAllocations: actorBridgeWithSpec("startRecordingAllocations"), stopRecordingAllocations: actorBridgeWithSpec("stopRecordingAllocations"), getAllocationsSettings: actorBridgeWithSpec("getAllocationsSettings"), getAllocations: actorBridgeWithSpec("getAllocations"), forceGarbageCollection: actorBridgeWithSpec("forceGarbageCollection"), forceCycleCollection: actorBridgeWithSpec("forceCycleCollection"), measure: actorBridgeWithSpec("measure"), residentUnique: actorBridgeWithSpec("residentUnique"), _onGarbageCollection: function (data) { if (this.conn.transport) { events.emit(this, "garbage-collection", data); } }, _onAllocations: function (data) { if (this.conn.transport) { events.emit(this, "allocations", data); } }, });
const NetworkMonitorActor = ActorClassWithSpec(networkMonitorSpec, { /** * NetworkMonitorActor is instanciated from WebConsoleActor.startListeners * Either in the same process, for debugging service worker requests or when debugging * the parent process itself and tracking chrome requests. * Or in another process, for tracking content requests that are actually done in the * parent process. * * @param object filters * Contains an `outerWindowID` attribute when this is used across processes. * Or a `window` attribute when instanciated in the same process. * @param number parentID (optional) * To be removed, specify the ID of the Web console actor. * This is used to fake emitting an event from it to prevent changing RDP * behavior. * @param nsIMessageManager messageManager * This is the manager to use to communicate with the console actor. When both * netmonitor and console actor runs in the same process, this is an instance * of MockMessageManager instead of a real message manager. */ initialize(conn, filters, parentID, messageManager) { Actor.prototype.initialize.call(this, conn); // Map of all NetworkEventActor indexed by channel ID this._netEvents = new Map(); // Map of all NetworkEventActor indexed by URL this._networkEventActorsByURL = new Map(); this.parentID = parentID; this.messageManager = messageManager; // Immediately start watching for new request according to `filters`. // NetworkMonitor will call `onNetworkEvent` method. this.observer = new NetworkObserver(filters, this); this.observer.init(); this.stackTraces = new Set(); this.onStackTraceAvailable = this.onStackTraceAvailable.bind(this); this.onRequestContent = this.onRequestContent.bind(this); this.onSetPreference = this.onSetPreference.bind(this); this.onGetNetworkEventActor = this.onGetNetworkEventActor.bind(this); this.onDestroyMessage = this.onDestroyMessage.bind(this); this.startListening(); }, onDestroyMessage({ data }) { if (data.actorID == this.parentID) { this.destroy(); } }, startListening() { this.messageManager.addMessageListener("debug:request-stack-available", this.onStackTraceAvailable); this.messageManager.addMessageListener("debug:request-content:request", this.onRequestContent); this.messageManager.addMessageListener("debug:netmonitor-preference", this.onSetPreference); this.messageManager.addMessageListener("debug:get-network-event-actor:request", this.onGetNetworkEventActor); this.messageManager.addMessageListener("debug:destroy-network-monitor", this.onDestroyMessage); }, stopListening() { this.messageManager.removeMessageListener("debug:request-stack-available", this.onStackTraceAvailable); this.messageManager.removeMessageListener("debug:request-content:request", this.onRequestContent); this.messageManager.removeMessageListener("debug:netmonitor-preference", this.onSetPreference); this.messageManager.removeMessageListener("debug:get-network-event-actor:request", this.onGetNetworkEventActor); this.messageManager.removeMessageListener("debug:destroy-network-monitor", this.onDestroyMessage); }, destroy() { Actor.prototype.destroy.call(this); if (this.observer) { this.observer.destroy(); this.observer = null; } this.stackTraces.clear(); if (this.messageManager) { this.stopListening(); this.messageManager = null; } }, /** * onBrowserSwap is called by the server when a browser frame swap occurs (typically * switching on/off RDM) and a new message manager should be used. */ onBrowserSwap(mm) { this.stopListening(); this.messageManager = mm; this.stackTraces = new Set(); this.startListening(); }, onStackTraceAvailable(msg) { const { channelId } = msg.data; if (!msg.data.stacktrace) { this.stackTraces.delete(channelId); } else { this.stackTraces.add(channelId); } }, getRequestContentForActor(actor) { const content = actor._response.content; if (actor._discardResponseBody || actor._truncated || !content || !content.size) { // Do not return the stylesheet text if there is no meaningful content or if it's // still loading. Let the caller handle it by doing its own separate request. return null; } if (content.text.type != "longString") { // For short strings, the text is available directly. return { content: content.text, contentType: content.mimeType, }; } // For long strings, look up the actor that holds the full text. const longStringActor = this.conn._getOrCreateActor(content.text.actor); if (!longStringActor) { return null; } return { content: longStringActor.str, contentType: content.mimeType, }; }, onRequestContent(msg) { const { url } = msg.data; const actor = this._networkEventActorsByURL.get(url); // Always reply with a message, but with a null `content` if this instance // did not processed this request const content = actor ? this.getRequestContentForActor(actor) : null; this.messageManager.sendAsyncMessage("debug:request-content:response", { url, content, }); }, onSetPreference({ data }) { if ("saveRequestAndResponseBodies" in data) { this.observer.saveRequestAndResponseBodies = data.saveRequestAndResponseBodies; } if ("throttleData" in data) { this.observer.throttleData = data.throttleData; } }, onGetNetworkEventActor({ data }) { const actor = this.getNetworkEventActor(data.channelId); this.messageManager.sendAsyncMessage("debug:get-network-event-actor:response", { channelId: data.channelId, actor: actor.form() }); }, getNetworkEventActor(channelId) { let actor = this._netEvents.get(channelId); if (actor) { return actor; } actor = new NetworkEventActor(this); this.manage(actor); // map channel to actor so we can associate future events with it this._netEvents.set(channelId, actor); return actor; }, // This method is called by NetworkMonitor instance when a new request is fired onNetworkEvent(event) { const { channelId } = event; const actor = this.getNetworkEventActor(channelId); this._netEvents.set(channelId, actor); event.cause.stacktrace = this.stackTraces.has(channelId); if (event.cause.stacktrace) { this.stackTraces.delete(channelId); } actor.init(event); this._networkEventActorsByURL.set(actor._request.url, actor); const packet = { from: this.parentID, type: "networkEvent", eventActor: actor.form() }; this.conn.send(packet); return actor; }, });
const FlexboxActor = ActorClassWithSpec(flexboxSpec, { /** * @param {LayoutActor} layoutActor * The LayoutActor instance. * @param {DOMNode} containerEl * The flex container element. */ initialize(layoutActor, containerEl) { Actor.prototype.initialize.call(this, layoutActor.conn); this.containerEl = containerEl; this.walker = layoutActor.walker; }, destroy() { Actor.prototype.destroy.call(this); this.containerEl = null; this.walker = null; }, form(detail) { if (detail === "actorid") { return this.actorID; } const form = { actor: this.actorID, }; // If the WalkerActor already knows the container element, then also return its // ActorID so we avoid the client from doing another round trip to get it in many // cases. if (this.walker.hasNode(this.containerEl)) { form.containerNodeActorID = this.walker.getNode(this.containerEl).actorID; } return form; }, });
methods: { getChild: { response: RetVal("child") } } }); // The child actor doesn't provide a form description var ChildActor = protocol.ActorClassWithSpec(childSpec, { initialize(conn) { protocol.Actor.prototype.initialize.call(this, conn); }, form(detail) { return { actor: this.actorID, extra: "extra" }; }, getChild: function () { return this; } }); var ChildFront = protocol.FrontClassWithSpec(childSpec, { initialize(client) { protocol.Front.prototype.initialize.call(this, client); }, form(v, ctx, detail) { this.extra = v.extra;
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { Actor, ActorClassWithSpec } = require("devtools/shared/protocol"); const { actorBridgeWithSpec } = require("devtools/server/actors/common"); const { Framerate } = require("devtools/server/performance/framerate"); const { framerateSpec } = require("devtools/shared/specs/framerate"); /** * An actor wrapper around Framerate. Uses exposed * methods via bridge and provides RDP definitions. * * @see devtools/server/performance/framerate.js for documentation. */ exports.FramerateActor = ActorClassWithSpec(framerateSpec, { initialize: function(conn, targetActor) { Actor.prototype.initialize.call(this, conn); this.bridge = new Framerate(targetActor); }, destroy: function(conn) { Actor.prototype.destroy.call(this, conn); this.bridge.destroy(); }, startRecording: actorBridgeWithSpec("startRecording"), stopRecording: actorBridgeWithSpec("stopRecording"), cancelRecording: actorBridgeWithSpec("cancelRecording"), isRecording: actorBridgeWithSpec("isRecording"), getPendingTicks: actorBridgeWithSpec("getPendingTicks"), });
var WebExtensionInspectedWindowActor = protocol.ActorClassWithSpec( webExtensionInspectedWindowSpec, { /** * Created the WebExtension InspectedWindow actor */ initialize(conn, tabActor) { protocol.Actor.prototype.initialize.call(this, conn); this.tabActor = tabActor; }, destroy(conn) { protocol.Actor.prototype.destroy.call(this, conn); if (this.customizedReload) { this.customizedReload.stop( new Error("WebExtensionInspectedWindowActor destroyed") ); delete this.customizedReload; } if (this._dbg) { this._dbg.enabled = false; delete this._dbg; } }, isSystemPrincipal(window) { const principal = window.document.nodePrincipal; return Services.scriptSecurityManager.isSystemPrincipal(principal); }, get dbg() { if (this._dbg) { return this._dbg; } this._dbg = this.tabActor.makeDebugger(); return this._dbg; }, get window() { return this.tabActor.window; }, get webNavigation() { return this.tabActor.webNavigation; }, /** * Reload the target tab, optionally bypass cache, customize the userAgent and/or * inject a script in targeted document or any of its sub-frame. * * @param {webExtensionCallerInfo} callerInfo * the addonId and the url (the addon base url or the url of the actual caller * filename and lineNumber) used to log useful debugging information in the * produced error logs and eval stack trace. * * @param {webExtensionReloadOptions} options * used to optionally enable the reload customizations. * @param {boolean|undefined} options.ignoreCache * enable/disable the cache bypass headers. * @param {string|undefined} options.userAgent * customize the userAgent during the page reload. * @param {string|undefined} options.injectedScript * evaluate the provided javascript code in the top level and every sub-frame * created during the page reload, before any other script in the page has been * executed. */ reload(callerInfo, {ignoreCache, userAgent, injectedScript}) { if (this.isSystemPrincipal(this.window)) { console.error("Ignored inspectedWindow.reload on system principal target for " + `${callerInfo.url}:${callerInfo.lineNumber}`); return {}; } const delayedReload = () => { // This won't work while the browser is shutting down and we don't really // care. if (Services.startup.shuttingDown) { return; } if (injectedScript || userAgent) { if (this.customizedReload) { // TODO(rpl): check what chrome does, and evaluate if queue the new reload // after the current one has been completed. console.error( "Reload already in progress. Ignored inspectedWindow.reload for " + `${callerInfo.url}:${callerInfo.lineNumber}` ); return; } try { this.customizedReload = new CustomizedReload({ tabActor: this.tabActor, inspectedWindowEval: this.eval.bind(this), callerInfo, injectedScript, userAgent, ignoreCache, }); this.customizedReload.start() .then(() => { delete this.customizedReload; }) .catch(err => { delete this.customizedReload; throw err; }); } catch (err) { // Cancel the customized reload (if any) on exception during the // reload setup. if (this.customizedReload) { this.customizedReload.stop(err); } throw err; } } else { // If there is no custom user agent and/or injected script, then // we can reload the target without subscribing any observer/listener. let reloadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; if (ignoreCache) { reloadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; } this.webNavigation.reload(reloadFlags); } }; // Execute the reload in a dispatched runnable, so that we can // return the reply to the caller before the reload is actually // started. Services.tm.currentThread.dispatch(delayedReload, 0); return {}; }, /** * Evaluate the provided javascript code in a target window (that is always the * tabActor window when called through RDP protocol, or the passed customTargetWindow * when called directly from the CustomizedReload instances). * * @param {webExtensionCallerInfo} callerInfo * the addonId and the url (the addon base url or the url of the actual caller * filename and lineNumber) used to log useful debugging information in the * produced error logs and eval stack trace. * * @param {string} expression * the javascript code to be evaluated in the target window * * @param {webExtensionEvalOptions} evalOptions * used to optionally enable the eval customizations. * NOTE: none of the eval options is currently implemented, they will be already * reported as unsupported by the WebExtensions schema validation wrappers, but * an additional level of error reporting is going to be applied here, so that * if the server and the client have different ideas of which option is supported * the eval call result will contain detailed informations (in the format usually * expected for errors not raised in the evaluated javascript code). * * @param {DOMWindow|undefined} customTargetWindow * Used in the CustomizedReload instances to evaluate the `injectedScript` * javascript code in every sub-frame of the target window during the tab reload. * NOTE: this parameter is not part of the RDP protocol exposed by this actor, when * it is called over the remote debugging protocol the target window is always * `tabActor.window`. */ eval(callerInfo, expression, options, customTargetWindow) { const window = customTargetWindow || this.window; if (Object.keys(options).length > 0) { return { exceptionInfo: { isError: true, code: "E_PROTOCOLERROR", description: "Inspector protocol error: %s", details: [ "The inspectedWindow.eval options are currently not supported", ], }, }; } if (!window) { return { exceptionInfo: { isError: true, code: "E_PROTOCOLERROR", description: "Inspector protocol error: %s", details: [ "The target window is not defined. inspectedWindow.eval not executed.", ], }, }; } if (this.isSystemPrincipal(window)) { // On denied JS evaluation, report it using the same data format // used in the corresponding chrome API method to report issues that are // not exceptions raised in the evaluated javascript code. return { exceptionInfo: { isError: true, code: "E_PROTOCOLERROR", description: "Inspector protocol error: %s", details: [ "This target has a system principal. inspectedWindow.eval denied.", ], }, }; } const dbgWindow = this.dbg.makeGlobalObjectReference(window); let evalCalledFrom = callerInfo.url; if (callerInfo.lineNumber) { evalCalledFrom += `:${callerInfo.lineNumber}`; } // TODO(rpl): add $0 and inspect(...) bindings (Bug 1300590) const result = dbgWindow.executeInGlobalWithBindings(expression, {}, { url: `debugger eval called from ${evalCalledFrom} - eval code`, }); let evalResult; if (result) { if ("return" in result) { evalResult = result.return; } else if ("yield" in result) { evalResult = result.yield; } else if ("throw" in result) { const throwErr = result.throw; // XXXworkers: Calling unsafeDereference() returns an object with no // toString method in workers. See Bug 1215120. const unsafeDereference = throwErr && (typeof throwErr === "object") && throwErr.unsafeDereference(); const message = unsafeDereference && unsafeDereference.toString ? unsafeDereference.toString() : String(throwErr); const stack = unsafeDereference && unsafeDereference.stack ? unsafeDereference.stack : null; return { exceptionInfo: { isException: true, value: `${message}\n\t${stack}`, }, }; } } else { // TODO(rpl): can the result of executeInGlobalWithBinding be null or // undefined? (which means that it is not a return, a yield or a throw). console.error("Unexpected empty inspectedWindow.eval result for", `${callerInfo.url}:${callerInfo.lineNumber}`); } if (evalResult) { try { if (evalResult && typeof evalResult === "object") { evalResult = evalResult.unsafeDereference(); } evalResult = JSON.parse(JSON.stringify(evalResult)); } catch (err) { // The evaluation result cannot be sent over the RDP Protocol, // report it as with the same data format used in the corresponding // chrome API method. return { exceptionInfo: { isError: true, code: "E_PROTOCOLERROR", description: "Inspector protocol error: %s", details: [ String(err), ], }, }; } } return {value: evalResult}; } } );
var AnimationPlayerActor = protocol.ActorClassWithSpec(animationPlayerSpec, { /** * @param {AnimationsActor} The main AnimationsActor instance * @param {AnimationPlayer} The player object returned by getAnimationPlayers */ initialize: function (animationsActor, player) { Actor.prototype.initialize.call(this, animationsActor.conn); this.onAnimationMutation = this.onAnimationMutation.bind(this); this.walker = animationsActor.walker; this.player = player; // Listen to animation mutations on the node to alert the front when the // current animation changes. // If the node is a pseudo-element, then we listen on its parent with // subtree:true (there's no risk of getting too many notifications in // onAnimationMutation since we filter out events that aren't for the // current animation). this.observer = new this.window.MutationObserver(this.onAnimationMutation); if (this.isPseudoElement) { this.observer.observe(this.node.parentElement, {animations: true, subtree: true}); } else { this.observer.observe(this.node, {animations: true}); } }, destroy: function () { // Only try to disconnect the observer if it's not already dead (i.e. if the // container view hasn't navigated since). if (this.observer && !Cu.isDeadWrapper(this.observer)) { this.observer.disconnect(); } this.player = this.observer = this.walker = null; Actor.prototype.destroy.call(this); }, get isPseudoElement() { return !this.player.effect.target.ownerDocument; }, get node() { if (this._node) { return this._node; } let node = this.player.effect.target; if (this.isPseudoElement) { // The target is a CSSPseudoElement object which just has a property that // points to its parent element and a string type (::before or ::after). let treeWalker = this.walker.getDocumentWalker(node.parentElement); while (treeWalker.nextNode()) { let currentNode = treeWalker.currentNode; if ((currentNode.nodeName === "_moz_generated_content_before" && node.type === "::before") || (currentNode.nodeName === "_moz_generated_content_after" && node.type === "::after")) { this._node = currentNode; } } } else { // The target is a DOM node. this._node = node; } return this._node; }, get window() { return this.node.ownerDocument.defaultView; }, /** * Release the actor, when it isn't needed anymore. * Protocol.js uses this release method to call the destroy method. */ release: function () {}, form: function (detail) { if (detail === "actorid") { return this.actorID; } let data = this.getCurrentState(); data.actor = this.actorID; // If we know the WalkerActor, and if the animated node is known by it, then // return its corresponding NodeActor ID too. if (this.walker && this.walker.hasNode(this.node)) { data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID; } return data; }, isCssAnimation: function (player = this.player) { return player instanceof this.window.CSSAnimation; }, isCssTransition: function (player = this.player) { return player instanceof this.window.CSSTransition; }, isScriptAnimation: function (player = this.player) { return player instanceof this.window.Animation && !( player instanceof this.window.CSSAnimation || player instanceof this.window.CSSTransition ); }, getType: function () { if (this.isCssAnimation()) { return ANIMATION_TYPES.CSS_ANIMATION; } else if (this.isCssTransition()) { return ANIMATION_TYPES.CSS_TRANSITION; } else if (this.isScriptAnimation()) { return ANIMATION_TYPES.SCRIPT_ANIMATION; } return ANIMATION_TYPES.UNKNOWN; }, /** * Get the name of this animation. This can be either the animation.id * property if it was set, or the keyframe rule name or the transition * property. * @return {String} */ getName: function () { if (this.player.id) { return this.player.id; } else if (this.isCssAnimation()) { return this.player.animationName; } else if (this.isCssTransition()) { return this.player.transitionProperty; } return ""; }, /** * Get the animation duration from this player, in milliseconds. * @return {Number} */ getDuration: function () { return this.player.effect.getComputedTiming().duration; }, /** * Get the animation delay from this player, in milliseconds. * @return {Number} */ getDelay: function () { return this.player.effect.getComputedTiming().delay; }, /** * Get the animation endDelay from this player, in milliseconds. * @return {Number} */ getEndDelay: function () { return this.player.effect.getComputedTiming().endDelay; }, /** * Get the animation iteration count for this player. That is, how many times * is the animation scheduled to run. * @return {Number} The number of iterations, or null if the animation repeats * infinitely. */ getIterationCount: function () { let iterations = this.player.effect.getComputedTiming().iterations; return iterations === "Infinity" ? null : iterations; }, /** * Get the animation iterationStart from this player, in ratio. * That is offset of starting position of the animation. * @return {Number} */ getIterationStart: function () { return this.player.effect.getComputedTiming().iterationStart; }, getPropertiesCompositorStatus: function () { let properties = this.player.effect.getProperties(); return properties.map(prop => { return { property: prop.property, runningOnCompositor: prop.runningOnCompositor, warning: prop.warning }; }); }, /** * Return the current start of the Animation. * @return {Object} */ getState: function () { // Remember the startTime each time getState is called, it may be useful // when animations get paused. As in, when an animation gets paused, its // startTime goes back to null, but the front-end might still be interested // in knowing what the previous startTime was. So everytime it is set, // remember it and send it along with the newState. if (this.player.startTime) { this.previousStartTime = this.player.startTime; } // Note that if you add a new property to the state object, make sure you // add the corresponding property in the AnimationPlayerFront' initialState // getter. return { type: this.getType(), // startTime is null whenever the animation is paused or waiting to start. startTime: this.player.startTime, previousStartTime: this.previousStartTime, currentTime: this.player.currentTime, playState: this.player.playState, playbackRate: this.player.playbackRate, name: this.getName(), duration: this.getDuration(), delay: this.getDelay(), endDelay: this.getEndDelay(), iterationCount: this.getIterationCount(), iterationStart: this.getIterationStart(), // animation is hitting the fast path or not. Returns false whenever the // animation is paused as it is taken off the compositor then. isRunningOnCompositor: this.getPropertiesCompositorStatus() .some(propState => propState.runningOnCompositor), propertyState: this.getPropertiesCompositorStatus(), // The document timeline's currentTime is being sent along too. This is // not strictly related to the node's animationPlayer, but is useful to // know the current time of the animation with respect to the document's. documentCurrentTime: this.node.ownerDocument.timeline.currentTime }; }, /** * Get the current state of the AnimationPlayer (currentTime, playState, ...). * Note that the initial state is returned as the form of this actor when it * is initialized. * This protocol method only returns a trimed down version of this state in * case some properties haven't changed since last time (since the front can * reconstruct those). If you want the full state, use the getState method. * @return {Object} */ getCurrentState: function () { let newState = this.getState(); // If we've saved a state before, compare and only send what has changed. // It's expected of the front to also save old states to re-construct the // full state when an incomplete one is received. // This is to minimize protocol traffic. let sentState = {}; if (this.currentState) { for (let key in newState) { if (typeof this.currentState[key] === "undefined" || this.currentState[key] !== newState[key]) { sentState[key] = newState[key]; } } } else { sentState = newState; } this.currentState = newState; return sentState; }, /** * Executed when the current animation changes, used to emit the new state * the the front. */ onAnimationMutation: function (mutations) { let isCurrentAnimation = animation => animation === this.player; let hasCurrentAnimation = animations => animations.some(isCurrentAnimation); let hasChanged = false; for (let {removedAnimations, changedAnimations} of mutations) { if (hasCurrentAnimation(removedAnimations)) { // Reset the local copy of the state on removal, since the animation can // be kept on the client and re-added, its state needs to be sent in // full. this.currentState = null; } if (hasCurrentAnimation(changedAnimations)) { // Only consider the state has having changed if any of delay, duration, // iterationcount or iterationStart has changed (for now at least). let newState = this.getState(); let oldState = this.currentState; hasChanged = newState.delay !== oldState.delay || newState.iterationCount !== oldState.iterationCount || newState.iterationStart !== oldState.iterationStart || newState.duration !== oldState.duration || newState.endDelay !== oldState.endDelay; break; } } if (hasChanged) { events.emit(this, "changed", this.getCurrentState()); } }, /** * Pause the player. */ pause: function () { this.player.pause(); return this.player.ready; }, /** * Play the player. * This method only returns when the animation has left its pending state. */ play: function () { this.player.play(); return this.player.ready; }, /** * Simply exposes the player ready promise. * * When an animation is created/paused then played, there's a short time * during which its playState is pending, before being set to running. * * If you either created a new animation using the Web Animations API or * paused/played an existing one, and then want to access the playState, you * might be interested to call this method. * This is especially important for tests. */ ready: function () { return this.player.ready; }, /** * Set the current time of the animation player. */ setCurrentTime: function (currentTime) { this.player.currentTime = currentTime * this.player.playbackRate; }, /** * Set the playback rate of the animation player. */ setPlaybackRate: function (playbackRate) { this.player.playbackRate = playbackRate; }, /** * Get data about the keyframes of this animation player. * @return {Object} Returns a list of frames, each frame containing the list * animated properties as well as the frame's offset. */ getFrames: function () { return this.player.effect.getKeyframes(); }, /** * Get data about the animated properties of this animation player. * @return {Array} Returns a list of animated properties. * Each property contains a list of values and their offsets */ getProperties: function () { return this.player.effect.getProperties().map(property => { return {name: property.property, values: property.values}; }); } });
methods: { simpleReturn: { response: { value: RetVal() }, }, }, }); var RootActor = protocol.ActorClassWithSpec(rootSpec, { typeName: "root", initialize: function(conn) { protocol.Actor.prototype.initialize.call(this, conn); // Root actor owns itself. this.manage(this); this.actorID = "root"; this.sequence = 0; }, sayHello: simpleHello, simpleReturn: function() { return this.sequence++; }, }); class RootFront extends protocol.FrontClassWithSpec(rootSpec) { constructor(client) { super(client); this.actorID = "root"; // Root owns itself. this.manage(this); }
* The response object which holds the startedListeners array. */ startListeners: function ACAOnStartListeners(request) { let startedListeners = []; while (request.listeners.length > 0) { let listener = request.listeners.shift(); switch (listener) { case "ConsoleAPI": if (!this.consoleAPIListener) { this.consoleAPIListener = new ConsoleAPIListener(null, this, { addonId: this.addon.id }); this.consoleAPIListener.init(); } startedListeners.push(listener); break; } } return { startedListeners: startedListeners, nativeConsoleAPI: true, traits: this.traits, }; }, }); exports.AddonConsoleActor = ActorClassWithSpec(webconsoleSpec, addonConsolePrototype); // TODO: remove once protocol.js can handle inheritance. Bug #1450960 exports.AddonConsoleActor.prototype.typeName = "addonConsole";
var PerformanceActor = ActorClassWithSpec(performanceSpec, { traits: { features: { withMarkers: true, withTicks: true, withMemory: true, withFrames: true, withGCEvents: true, withDocLoadingEvents: true, withAllocations: true, }, }, initialize: function (conn, tabActor) { Actor.prototype.initialize.call(this, conn); this._onRecorderEvent = this._onRecorderEvent.bind(this); this.bridge = new PerformanceRecorder(conn, tabActor); events.on(this.bridge, "*", this._onRecorderEvent); }, destroy: function () { events.off(this.bridge, "*", this._onRecorderEvent); this.bridge.destroy(); Actor.prototype.destroy.call(this); }, connect: function (config) { this.bridge.connect({ systemClient: config.systemClient }); return { traits: this.traits }; }, canCurrentlyRecord: function () { return this.bridge.canCurrentlyRecord(); }, startRecording: Task.async(function* (options = {}) { if (!this.bridge.canCurrentlyRecord().success) { return null; } let normalizedOptions = normalizePerformanceFeatures(options, this.traits.features); let recording = yield this.bridge.startRecording(normalizedOptions); this.manage(recording); return recording; }), stopRecording: actorBridgeWithSpec("stopRecording"), isRecording: actorBridgeWithSpec("isRecording"), getRecordings: actorBridgeWithSpec("getRecordings"), getConfiguration: actorBridgeWithSpec("getConfiguration"), setProfilerStatusInterval: actorBridgeWithSpec("setProfilerStatusInterval"), /** * Filter which events get piped to the front. */ _onRecorderEvent: function (eventName, ...data) { // If this is a recording state change, call // a method on the related PerformanceRecordingActor so it can // update its internal state. if (RECORDING_STATE_CHANGE_EVENTS.has(eventName)) { let recording = data[0]; let extraData = data[1]; recording._setState(eventName, extraData); } if (PIPE_TO_FRONT_EVENTS.has(eventName)) { events.emit(this, eventName, ...data); } }, });
var RootActor = protocol.ActorClassWithSpec(rootSpec, { initialize: function (conn) { protocol.Actor.prototype.initialize.call(this, conn); // Root actor owns itself. this.manage(this); this.actorID = "root"; }, sayHello: simpleHello, simpleReturn: function () { return 1; }, promiseReturn: function () { return promise.resolve(1); }, simpleArgs: function (a, b) { return { firstResponse: a + 1, secondResponse: b + 1 }; }, nestedArgs: function (a, b, c) { return { a: a, b: b, c: c }; }, optionArgs: function (options) { return { option1: options.option1, option2: options.option2 }; }, optionalArgs: function (a, b = 200) { return b; }, arrayArgs: function (a) { return a; }, nestedArrayArgs: function (a) { return a; }, /** * Test that the 'type' part of the request packet works * correctly when the type isn't the same as the method name */ renamedEcho: function (a) { if (this.conn.currentPacket.type != "echo") { return "goodbye"; } return a; }, testOneWay: function (a) { // Emit to show that we got this message, because there won't be a response. events.emit(this, "oneway", a); }, emitFalsyOptions: function () { events.emit(this, "falsyOptions", { zero: 0, farce: false }); } });
var MediaRuleActor = protocol.ActorClassWithSpec(mediaRuleSpec, { get window() { return this.parentActor.window; }, get document() { return this.window.document; }, get matches() { return this.mql ? this.mql.matches : null; }, initialize: function(mediaRule, parentActor) { protocol.Actor.prototype.initialize.call(this, null); this.rawRule = mediaRule; this.parentActor = parentActor; this.conn = this.parentActor.conn; this._matchesChange = this._matchesChange.bind(this); this.line = InspectorUtils.getRuleLine(mediaRule); this.column = InspectorUtils.getRuleColumn(mediaRule); try { this.mql = this.window.matchMedia(mediaRule.media.mediaText); } catch (e) { // Ignored } if (this.mql) { this.mql.addListener(this._matchesChange); } }, destroy: function() { if (this.mql) { this.mql.removeListener(this._matchesChange); } protocol.Actor.prototype.destroy.call(this); }, form: function(detail) { if (detail === "actorid") { return this.actorID; } const form = { actor: this.actorID, // actorID is set when this is added to a pool mediaText: this.rawRule.media.mediaText, conditionText: this.rawRule.conditionText, matches: this.matches, line: this.line, column: this.column, parentStyleSheet: this.parentActor.actorID }; return form; }, _matchesChange: function() { this.emit("matches-change", this.matches); } });
const SourceActor = ActorClassWithSpec(sourceSpec, { typeName: "source", initialize: function({ source, thread, originalUrl, generatedSource, isInlineSource, contentType }) { this._threadActor = thread; this._originalUrl = originalUrl; this._source = source; this._generatedSource = generatedSource; this._contentType = contentType; this._isInlineSource = isInlineSource; this.onSource = this.onSource.bind(this); this._invertSourceMap = this._invertSourceMap.bind(this); this._encodeAndSetSourceMapURL = this._encodeAndSetSourceMapURL.bind(this); this._getSourceText = this._getSourceText.bind(this); this._mapSourceToAddon(); if (this.threadActor.sources.isPrettyPrinted(this.url)) { this._init = this.prettyPrint( this.threadActor.sources.prettyPrintIndent(this.url) ).catch(error => { DevToolsUtils.reportException("SourceActor", error); }); } else { this._init = null; } }, get isSourceMapped() { return !!(!this.isInlineSource && ( this._originalURL || this._generatedSource || this.threadActor.sources.isPrettyPrinted(this.url) )); }, get isInlineSource() { return this._isInlineSource; }, get threadActor() { return this._threadActor; }, get sources() { return this._threadActor.sources; }, get dbg() { return this.threadActor.dbg; }, get source() { return this._source; }, get generatedSource() { return this._generatedSource; }, get breakpointActorMap() { return this.threadActor.breakpointActorMap; }, get url() { if (this.source) { return getSourceURL(this.source, this.threadActor._parent.window); } return this._originalUrl; }, get addonID() { return this._addonID; }, get addonPath() { return this._addonPath; }, get prettyPrintWorker() { return this.threadActor.prettyPrintWorker; }, get isCacheEnabled() { if (this.threadActor._parent._getCacheDisabled) { return !this.threadActor._parent._getCacheDisabled(); } return true; }, form: function() { const source = this.source || this.generatedSource; // This might not have a source or a generatedSource because we // treat HTML pages with inline scripts as a special SourceActor // that doesn't have either let introductionUrl = null; if (source && source.introductionScript) { introductionUrl = source.introductionScript.source.url; } return { actor: this.actorID, generatedUrl: this.generatedSource ? this.generatedSource.url : null, url: this.url ? this.url.split(" -> ").pop() : null, addonID: this._addonID, addonPath: this._addonPath, isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url), isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url), isSourceMapped: this.isSourceMapped, sourceMapURL: source ? source.sourceMapURL : null, introductionUrl: introductionUrl ? introductionUrl.split(" -> ").pop() : null, introductionType: source ? source.introductionType : null, }; }, destroy: function() { if (this.registeredPool && this.registeredPool.sourceActors) { delete this.registeredPool.sourceActors[this.actorID]; } }, _mapSourceToAddon: function() { let nsuri; try { nsuri = Services.io.newURI(this.url.split(" -> ").pop()); } catch (e) { // We can't do anything with an invalid URI return; } const localURI = resolveURIToLocalPath(nsuri); if (!localURI) { return; } const id = mapURIToAddonID(localURI); if (!id) { return; } this._addonID = id; if (localURI instanceof Ci.nsIJARURI) { // The path in the add-on is easy for jar: uris this._addonPath = localURI.JAREntry; } else if (localURI instanceof Ci.nsIFileURL) { // For file: uris walk up to find the last directory that is part of the // add-on const target = localURI.file; let path = target.leafName; // We can assume that the directory containing the source file is part // of the add-on let root = target.parent; let file = root.parent; while (file && mapURIToAddonID(Services.io.newFileURI(file))) { path = root.leafName + "/" + path; root = file; file = file.parent; } if (!file) { const error = new Error("Could not find the root of the add-on for " + this.url); DevToolsUtils.reportException("SourceActor.prototype._mapSourceToAddon", error); return; } this._addonPath = path; } }, _reportLoadSourceError: function(error, map = null) { try { DevToolsUtils.reportException("SourceActor", error); JSON.stringify(this.form(), null, 4).split(/\n/g) .forEach(line => console.error("\t", line)); if (!map) { return; } console.error("\t", "source map's sourceRoot =", map.sourceRoot); console.error("\t", "source map's sources ="); map.sources.forEach(s => { const hasSourceContent = map.sourceContentFor(s, true); console.error("\t\t", s, "\t", hasSourceContent ? "has source content" : "no source content"); }); console.error("\t", "source map's sourcesContent ="); map.sourcesContent.forEach(c => { if (c.length > 80) { c = c.slice(0, 77) + "..."; } c = c.replace(/\n/g, "\\n"); console.error("\t\t", c); }); } catch (e) { // ignore } }, _getSourceText: function() { const toResolvedContent = t => ({ content: t, contentType: this._contentType, }); const isWasm = this.source && this.source.introductionType === "wasm"; const genSource = this.generatedSource || this.source; return this.threadActor.sources.fetchSourceMap(genSource).then(map => { if (map) { try { const sourceContent = map.sourceContentFor(this.url); if (sourceContent) { return toResolvedContent(sourceContent); } } catch (error) { this._reportLoadSourceError(error, map); throw error; } } if (isWasm && this.dbg.allowWasmBinarySource) { const wasm = this.source.binary; const buffer = wasm.buffer; assert( wasm.byteOffset === 0 && wasm.byteLength === buffer.byteLength, "Typed array from wasm source binary must cover entire buffer" ); return toResolvedContent(buffer); } // If we are replaying then we can only use source saved during the // original recording. If we try to fetch it now it may have changed or // may no longer exist. if (this.dbg.replaying) { assert(!this._contentType); return this.dbg.replayingContent(this.url); } // Use `source.text` if it exists, is not the "no source" string, and // the content type of the source is JavaScript or it is synthesized // wasm. It will be "no source" if the Debugger API wasn't able to load // the source because sources were discarded // (javascript.options.discardSystemSource == true). Re-fetch non-JS // sources to get the contentType from the headers. if (this.source && this.source.text !== "[no source]" && this._contentType && (this._contentType.includes("javascript") || this._contentType === "text/wasm")) { return toResolvedContent(this.source.text); } // Only load the HTML page source from cache (which exists when // there are inline sources). Otherwise, we can't trust the // cache because we are most likely here because we are // fetching the original text for sourcemapped code, and the // page hasn't requested it before (if it has, it was a // previous debugging session). // Additionally, we should only try the cache if it is currently enabled // for the document. Without this check, the cache may return stale data // that doesn't match the document shown in the browser. const loadFromCache = this.isInlineSource && this.isCacheEnabled; // Fetch the sources with the same principal as the original document const win = this.threadActor._parent.window; let principal, cacheKey; // On xpcshell, we don't have a window but a Sandbox if (!isWorker && win instanceof Ci.nsIDOMWindow) { const docShell = win.docShell; const channel = docShell.currentDocumentChannel; principal = channel.loadInfo.loadingPrincipal; // Retrieve the cacheKey in order to load POST requests from cache // Note that chrome:// URLs don't support this interface. if (loadFromCache && docShell.currentDocumentChannel instanceof Ci.nsICacheInfoChannel) { cacheKey = docShell.currentDocumentChannel.cacheKey; } } const sourceFetched = fetch(this.url, { principal, cacheKey, loadFromCache, }); // Record the contentType we just learned during fetching return sourceFetched .then(result => { this._contentType = result.contentType; return result; }, error => { this._reportLoadSourceError(error, map); throw error; }); }); }, /** * Get all executable lines from the current source * @return Array - Executable lines of the current script **/ getExecutableLines: function() { function sortLines(lines) { // Converting the Set into an array lines = [...lines]; lines.sort((a, b) => { return a - b; }); return lines; } if (this.generatedSource) { return this.threadActor.sources.getSourceMap(this.generatedSource).then(sm => { const lines = new Set(); // Position of executable lines in the generated source const offsets = this.getExecutableOffsets(this.generatedSource, false); for (const offset of offsets) { const {line, source: sourceUrl} = sm.originalPositionFor({ line: offset.lineNumber, column: offset.columnNumber, }); if (sourceUrl === this.url) { lines.add(line); } } return sortLines(lines); }); } const lines = this.getExecutableOffsets(this.source, true); return sortLines(lines); }, /** * Extract all executable offsets from the given script * @param String url - extract offsets of the script with this url * @param Boolean onlyLine - will return only the line number * @return Set - Executable offsets/lines of the script **/ getExecutableOffsets: function(source, onlyLine) { const offsets = new Set(); for (const s of this.dbg.findScripts({ source })) { for (const offset of s.getAllColumnOffsets()) { offsets.add(onlyLine ? offset.lineNumber : offset); } } return offsets; }, /** * Handler for the "source" packet. */ onSource: function() { return Promise.resolve(this._init) .then(this._getSourceText) .then(({ content, contentType }) => { if (typeof content === "object" && content && content.constructor && content.constructor.name === "ArrayBuffer") { return { source: arrayBufferGrip(content, this.threadActor.threadLifetimePool), contentType, }; } return { source: createValueGrip(content, this.threadActor.threadLifetimePool, this.threadActor.objectGrip), contentType: contentType, }; }) .catch(error => { reportError(error, "Got an exception during SA_onSource: "); throw new Error("Could not load the source for " + this.url + ".\n" + DevToolsUtils.safeErrorString(error)); }); }, /** * Handler for the "prettyPrint" packet. */ prettyPrint: function(indent) { this.threadActor.sources.prettyPrint(this.url, indent); return this._getSourceText() .then(this._sendToPrettyPrintWorker(indent)) .then(this._invertSourceMap) .then(this._encodeAndSetSourceMapURL) .then(() => { // We need to reset `_init` now because we have already done the work of // pretty printing, and don't want onSource to wait forever for // initialization to complete. this._init = null; }) .then(this.onSource) .catch(error => { this.disablePrettyPrint(); throw new Error(DevToolsUtils.safeErrorString(error)); }); }, /** * Return a function that sends a request to the pretty print worker, waits on * the worker's response, and then returns the pretty printed code. * * @param Number indent * The number of spaces to indent by the code by, when we send the * request to the pretty print worker. * @returns Function * Returns a function which takes an AST, and returns a promise that * is resolved with `{ code, mappings }` where `code` is the pretty * printed code, and `mappings` is an array of source mappings. */ _sendToPrettyPrintWorker: function(indent) { return ({ content }) => { return this.prettyPrintWorker.performTask("pretty-print", { url: this.url, indent, source: content, }); }; }, /** * Invert a source map. So if a source map maps from a to b, return a new * source map from b to a. We need to do this because the source map we get * from _generatePrettyCodeAndMap goes the opposite way we want it to for * debugging. * * Note that the source map is modified in place. */ _invertSourceMap: function({ code, mappings }) { const generator = new SourceMapGenerator({ file: this.url }); return DevToolsUtils.yieldingEach(mappings._array, m => { const mapping = { generated: { line: m.originalLine, column: m.originalColumn, }, }; if (m.source) { mapping.source = m.source; mapping.original = { line: m.generatedLine, column: m.generatedColumn, }; mapping.name = m.name; } generator.addMapping(mapping); }).then(() => { generator.setSourceContent(this.url, code); const consumer = SourceMapConsumer.fromSourceMap(generator); return { code: code, map: consumer, }; }); }, /** * Save the source map back to our thread's ThreadSources object so that * stepping, breakpoints, debugger statements, etc can use it. If we are * pretty printing a source mapped source, we need to compose the existing * source map with our new one. */ _encodeAndSetSourceMapURL: function({ map: sm }) { const source = this.generatedSource || this.source; const sources = this.threadActor.sources; return sources.getSourceMap(source).then(prevMap => { if (prevMap) { // Compose the source maps this._oldSourceMapping = { url: source.sourceMapURL, map: prevMap, }; prevMap = SourceMapGenerator.fromSourceMap(prevMap); prevMap.applySourceMap(sm, this.url); sm = SourceMapConsumer.fromSourceMap(prevMap); } const actorSources = this.threadActor.sources; actorSources.clearSourceMapCache(source.sourceMapURL); actorSources.setSourceMapHard(source, null, sm); }); }, /** * Handler for the "disablePrettyPrint" packet. */ disablePrettyPrint: function() { const source = this.generatedSource || this.source; const sources = this.threadActor.sources; sources.clearSourceMapCache(source.sourceMapURL, { hard: true }); if (this._oldSourceMapping) { sources.setSourceMapHard(source, this._oldSourceMapping.url, this._oldSourceMapping.map); this._oldSourceMapping = null; } this.threadActor.sources.disablePrettyPrint(this.url); return this.onSource(); }, /** * Handler for the "blackbox" packet. */ blackbox: function() { this.threadActor.sources.blackBox(this.url); if (this.threadActor.state == "paused" && this.threadActor.youngestFrame && this.threadActor.youngestFrame.script.url == this.url) { return true; } return false; }, /** * Handler for the "unblackbox" packet. */ unblackbox: function() { this.threadActor.sources.unblackBox(this.url); }, /** * Handler for the "setPausePoints" packet. * * @param Array pausePoints * A dictionary of pausePoint objects * * type PausePoints = { * line: { * column: { break?: boolean, step?: boolean } * } * } */ setPausePoints: function(pausePoints) { const uncompressed = {}; const points = { 0: {}, 1: { break: true }, 2: { step: true }, 3: { break: true, step: true }, }; for (const line in pausePoints) { uncompressed[line] = {}; for (const col in pausePoints[line]) { uncompressed[line][col] = points[pausePoints[line][col]]; } } this.pausePoints = uncompressed; }, /** * Handle a request to set a breakpoint. * * @param Number line * Line to break on. * @param Number column * Column to break on. * @param String condition * A condition which must be true for breakpoint to be hit. * @param Boolean noSliding * If true, disables breakpoint sliding. * * @returns Promise * A promise that resolves to a JSON object representing the * response. */ setBreakpoint: function(line, column, condition, noSliding) { if (this.threadActor.state !== "paused") { const errorObject = { error: "wrongState", message: "Cannot set breakpoint while debuggee is running.", }; throw errorObject; } const location = new OriginalLocation(this, line, column); return this._getOrCreateBreakpointActor( location, condition, noSliding ).then((actor) => { const response = { actor: actor.actorID, isPending: actor.isPending, }; const actualLocation = actor.originalLocation; if (!actualLocation.equals(location)) { response.actualLocation = actualLocation.toJSON(); } return response; }); }, /** * Get or create a BreakpointActor for the given location in the original * source, and ensure it is set as a breakpoint handler on all scripts that * match the given location. * * @param OriginalLocation originalLocation * An OriginalLocation representing the location of the breakpoint in * the original source. * @param String condition * A string that is evaluated whenever the breakpoint is hit. If the * string evaluates to false, the breakpoint is ignored. * @param Boolean noSliding * If true, disables breakpoint sliding. * * @returns BreakpointActor * A BreakpointActor representing the breakpoint. */ _getOrCreateBreakpointActor: function(originalLocation, condition, noSliding) { let actor = this.breakpointActorMap.getActor(originalLocation); if (!actor) { actor = new BreakpointActor(this.threadActor, originalLocation); this.threadActor.threadLifetimePool.addActor(actor); this.breakpointActorMap.setActor(originalLocation, actor); } actor.condition = condition; return this._setBreakpoint(actor, noSliding); }, /* * Ensure the given BreakpointActor is set as a breakpoint handler on all * scripts that match its location in the original source. * * If there are no scripts that match the location of the BreakpointActor, * we slide its location to the next closest line (for line breakpoints) or * column (for column breakpoint) that does. * * If breakpoint sliding fails, then either there are no scripts that contain * any code for the given location, or they were all garbage collected before * the debugger started running. We cannot distinguish between these two * cases, so we insert the BreakpointActor in the BreakpointActorMap as * a pending breakpoint. Whenever a new script is introduced, this method is * called again for each pending breakpoint. * * @param BreakpointActor actor * The BreakpointActor to be set as a breakpoint handler. * @param Boolean noSliding * If true, disables breakpoint sliding. * * @returns A Promise that resolves to the given BreakpointActor. */ _setBreakpoint: function(actor, noSliding) { const { originalLocation } = actor; const { originalLine, originalSourceActor } = originalLocation; if (!this.isSourceMapped) { const generatedLocation = GeneratedLocation.fromOriginalLocation(originalLocation); const isWasm = this.source && this.source.introductionType === "wasm"; if (!this._setBreakpointAtGeneratedLocation(actor, generatedLocation) && !noSliding && !isWasm) { const query = { line: originalLine }; // For most cases, we have a real source to query for. The // only time we don't is for HTML pages. In that case we want // to query for scripts in an HTML page based on its URL, as // there could be several sources within an HTML page. if (this.source) { query.source = this.source; } else { query.url = this.url; } const scripts = this.dbg.findScripts(query); // Never do breakpoint sliding for column breakpoints. // Additionally, never do breakpoint sliding if no scripts // exist on this line. // // Sliding can go horribly wrong if we always try to find the // next line with valid entry points in the entire file. // Scripts may be completely GCed and we never knew they // existed, so we end up sliding through whole functions to // the user's bewilderment. // // We can slide reliably if any scripts exist, however, due // to how scripts are kept alive. A parent Debugger.Script // keeps all of its children alive, so as long as we have a // valid script, we can slide through it and know we won't // slide through any of its child scripts. Additionally, if a // script gets GCed, that means that all parents scripts are // GCed as well, and no scripts will exist on those lines // anymore. We will never slide through a GCed script. if (originalLocation.originalColumn || scripts.length === 0) { return Promise.resolve(actor); } // Find the script that spans the largest amount of code to // determine the bounds for sliding. const largestScript = scripts.reduce((largestScr, script) => { if (script.lineCount > largestScr.lineCount) { return script; } return largestScr; }); const maxLine = largestScript.startLine + largestScript.lineCount - 1; let actualLine = originalLine; for (; actualLine <= maxLine; actualLine++) { const loc = new GeneratedLocation(this, actualLine); if (this._setBreakpointAtGeneratedLocation(actor, loc)) { break; } } // The above loop should never complete. We only did breakpoint sliding // because we found scripts on the line we started from, // which means there must be valid entry points somewhere // within those scripts. if (actualLine > maxLine) { return Promise.reject({ error: "noCodeAtLineColumn", message: "Could not find any entry points to set a breakpoint on, " + "even though I was told a script existed on the line I started " + "the search with.", }); } // Update the actor to use the new location (reusing a // previous breakpoint if it already exists on that line). const actualLocation = new OriginalLocation(originalSourceActor, actualLine); const existingActor = this.breakpointActorMap.getActor(actualLocation); this.breakpointActorMap.deleteActor(originalLocation); if (existingActor) { actor.delete(); actor = existingActor; } else { actor.originalLocation = actualLocation; this.breakpointActorMap.setActor(actualLocation, actor); } } return Promise.resolve(actor); } return this.sources.getAllGeneratedLocations(originalLocation) .then((generatedLocations) => { this._setBreakpointAtAllGeneratedLocations( actor, generatedLocations ); return actor; }); }, _setBreakpointAtAllGeneratedLocations: function(actor, generatedLocations) { let success = false; for (const generatedLocation of generatedLocations) { if (this._setBreakpointAtGeneratedLocation( actor, generatedLocation )) { success = true; } } return success; }, /* * Ensure the given BreakpointActor is set as breakpoint handler on all * scripts that match the given location in the generated source. * * @param BreakpointActor actor * The BreakpointActor to be set as a breakpoint handler. * @param GeneratedLocation generatedLocation * A GeneratedLocation representing the location in the generated * source for which the given BreakpointActor is to be set as a * breakpoint handler. * * @returns A Boolean that is true if the BreakpointActor was set as a * breakpoint handler on at least one script, and false otherwise. */ _setBreakpointAtGeneratedLocation: function(actor, generatedLocation) { const { generatedSourceActor, generatedLine, generatedColumn, generatedLastColumn, } = generatedLocation; // Find all scripts that match the given source actor and line // number. const query = { line: generatedLine }; if (generatedSourceActor.source) { query.source = generatedSourceActor.source; } else { query.url = generatedSourceActor.url; } let scripts = this.dbg.findScripts(query); scripts = scripts.filter((script) => !actor.hasScript(script)); // Find all entry points that correspond to the given location. const entryPoints = []; if (generatedColumn === undefined) { // This is a line breakpoint, so we are interested in all offsets // that correspond to the given line number. for (const script of scripts) { const offsets = script.getLineOffsets(generatedLine); if (offsets.length > 0) { entryPoints.push({ script, offsets }); } } } else { // Compute columnToOffsetMaps for each script so that we can // find matching entrypoints for the column breakpoint. const columnToOffsetMaps = scripts.map(script => [ script, script.getAllColumnOffsets() .filter(({ lineNumber }) => lineNumber === generatedLine), ] ); // This is a column breakpoint, so we are interested in all column // offsets that correspond to the given line *and* column number. for (const [script, columnToOffsetMap] of columnToOffsetMaps) { for (const { columnNumber: column, offset } of columnToOffsetMap) { if (column >= generatedColumn && column <= generatedLastColumn) { entryPoints.push({ script, offsets: [offset] }); } } } // If we don't find any matching entrypoints, // then we should see if the breakpoint comes before or after the column offsets. if (entryPoints.length === 0) { for (const [script, columnToOffsetMap] of columnToOffsetMaps) { if (columnToOffsetMap.length > 0) { const firstColumnOffset = columnToOffsetMap[0]; const lastColumnOffset = columnToOffsetMap[columnToOffsetMap.length - 1]; if (generatedColumn < firstColumnOffset.columnNumber) { entryPoints.push({ script, offsets: [firstColumnOffset.offset] }); } if (generatedColumn > lastColumnOffset.columnNumber) { entryPoints.push({ script, offsets: [lastColumnOffset.offset] }); } } } } } if (entryPoints.length === 0) { return false; } setBreakpointAtEntryPoints(actor, entryPoints); return true; }, });
exports.DirectorRegistryActor = protocol.ActorClassWithSpec(directorRegistrySpec, { /* init & destroy methods */ initialize: function (conn, parentActor) { protocol.Actor.prototype.initialize.call(this, conn); this.maybeSetupChildProcess(conn); }, destroy: function (conn) { protocol.Actor.prototype.destroy.call(this, conn); this.finalize(); }, finalize: function () { // nothing to cleanup }, maybeSetupChildProcess(conn) { // skip child setup if this actor module is not running in a child process if (!DebuggerServer.isInChildProcess) { return; } const { sendSyncMessage } = conn.parentMessageManager; conn.setupInParent({ module: "devtools/server/actors/director-registry", setupParent: "setupParentProcess" }); DirectorRegistry.install = notImplemented.bind(null, "install"); DirectorRegistry.uninstall = notImplemented.bind(null, "uninstall"); DirectorRegistry.clear = notImplemented.bind(null, "clear"); DirectorRegistry.get = callParentProcess.bind(null, "get"); DirectorRegistry.list = callParentProcess.bind(null, "list"); /* child process helpers */ function notImplemented(method) { console.error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD, method); throw Error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD); } function callParentProcess(method, ...args) { let reply = sendSyncMessage("debug:director-registry-request", { method: method, args: args }); if (reply.length === 0) { console.error(ERR_DIRECTOR_CHILD_NO_REPLY); throw Error(ERR_DIRECTOR_CHILD_NO_REPLY); } else if (reply.length > 1) { console.error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES); throw Error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES); } return reply[0]; } }, /** * Install a new director-script definition. * * @param String id * The director-script definition identifier. * @param String scriptCode * The director-script javascript source. * @param Object scriptOptions * The director-script option object. */ install: function (id, { scriptCode, scriptOptions }) { // TODO: add more checks on id format? if (!id || id.length === 0) { throw Error("director-script id is mandatory"); } if (!scriptCode) { throw Error("director-script scriptCode is mandatory"); } return DirectorRegistry.install(id, { scriptId: id, scriptCode: scriptCode, scriptOptions: scriptOptions }); }, /** * Uninstall a director-script definition. * * @param String id * The identifier of the director-script definition to be removed */ uninstall: function (id) { return DirectorRegistry.uninstall(id); }, /** * Retrieves the list of installed director-scripts. */ list: function () { return DirectorRegistry.list(); } });
}); var RootActor = protocol.ActorClassWithSpec(rootSpec, { initialize: function (conn) { rootActor = this; protocol.Actor.prototype.initialize.call(this, conn); // Root actor owns itself. this.manage(this); this.actorID = "root"; }, sayHello: simpleHello, shortString: function () { return new LongStringActor(this.conn, SHORT_STR); }, longString: function () { return new LongStringActor(this.conn, LONG_STR); }, emitShortString: function () { events.emit(this, "string-event", new LongStringActor(this.conn, SHORT_STR)); }, emitLongString: function () { events.emit(this, "string-event", new LongStringActor(this.conn, LONG_STR)); }, }); var RootFront = protocol.FrontClassWithSpec(rootSpec, {
var AnimationPlayerActor = protocol.ActorClassWithSpec(animationPlayerSpec, { /** * @param {AnimationsActor} The main AnimationsActor instance * @param {AnimationPlayer} The player object returned by getAnimationPlayers */ initialize: function (animationsActor, player) { Actor.prototype.initialize.call(this, animationsActor.conn); this.onAnimationMutation = this.onAnimationMutation.bind(this); this.walker = animationsActor.walker; this.player = player; // Listen to animation mutations on the node to alert the front when the // current animation changes. // If the node is a pseudo-element, then we listen on its parent with // subtree:true (there's no risk of getting too many notifications in // onAnimationMutation since we filter out events that aren't for the // current animation). this.observer = new this.window.MutationObserver(this.onAnimationMutation); if (this.isPseudoElement) { this.observer.observe(this.node.parentElement, {animations: true, subtree: true}); } else { this.observer.observe(this.node, {animations: true}); } }, destroy: function () { // Only try to disconnect the observer if it's not already dead (i.e. if the // container view hasn't navigated since). if (this.observer && !Cu.isDeadWrapper(this.observer)) { this.observer.disconnect(); } this.player = this.observer = this.walker = null; Actor.prototype.destroy.call(this); }, get isPseudoElement() { return !this.player.effect.target.ownerDocument; }, get node() { if (this._node) { return this._node; } let node = this.player.effect.target; if (this.isPseudoElement) { // The target is a CSSPseudoElement object which just has a property that // points to its parent element and a string type (::before or ::after). let treeWalker = this.walker.getDocumentWalker(node.parentElement); while (treeWalker.nextNode()) { let currentNode = treeWalker.currentNode; if ((currentNode.nodeName === "_moz_generated_content_before" && node.type === "::before") || (currentNode.nodeName === "_moz_generated_content_after" && node.type === "::after")) { this._node = currentNode; } } } else { // The target is a DOM node. this._node = node; } return this._node; }, get window() { return this.node.ownerDocument.defaultView; }, /** * Release the actor, when it isn't needed anymore. * Protocol.js uses this release method to call the destroy method. */ release: function () {}, form: function (detail) { if (detail === "actorid") { return this.actorID; } let data = this.getCurrentState(); data.actor = this.actorID; // If we know the WalkerActor, and if the animated node is known by it, then // return its corresponding NodeActor ID too. if (this.walker && this.walker.hasNode(this.node)) { data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID; } return data; }, isCssAnimation: function (player = this.player) { return player instanceof this.window.CSSAnimation; }, isCssTransition: function (player = this.player) { return player instanceof this.window.CSSTransition; }, isScriptAnimation: function (player = this.player) { return player instanceof this.window.Animation && !( player instanceof this.window.CSSAnimation || player instanceof this.window.CSSTransition ); }, getType: function () { if (this.isCssAnimation()) { return ANIMATION_TYPES.CSS_ANIMATION; } else if (this.isCssTransition()) { return ANIMATION_TYPES.CSS_TRANSITION; } else if (this.isScriptAnimation()) { return ANIMATION_TYPES.SCRIPT_ANIMATION; } return ANIMATION_TYPES.UNKNOWN; }, /** * Get the name of this animation. This can be either the animation.id * property if it was set, or the keyframe rule name or the transition * property. * @return {String} */ getName: function () { if (this.player.id) { return this.player.id; } else if (this.isCssAnimation()) { return this.player.animationName; } else if (this.isCssTransition()) { return this.player.transitionProperty; } return ""; }, /** * Get the animation duration from this player, in milliseconds. * @return {Number} */ getDuration: function () { return this.player.effect.getComputedTiming().duration; }, /** * Get the animation delay from this player, in milliseconds. * @return {Number} */ getDelay: function () { return this.player.effect.getComputedTiming().delay; }, /** * Get the animation endDelay from this player, in milliseconds. * @return {Number} */ getEndDelay: function () { return this.player.effect.getComputedTiming().endDelay; }, /** * Get the animation iteration count for this player. That is, how many times * is the animation scheduled to run. * @return {Number} The number of iterations, or null if the animation repeats * infinitely. */ getIterationCount: function () { let iterations = this.player.effect.getComputedTiming().iterations; return iterations === "Infinity" ? null : iterations; }, /** * Get the animation iterationStart from this player, in ratio. * That is offset of starting position of the animation. * @return {Number} */ getIterationStart: function () { return this.player.effect.getComputedTiming().iterationStart; }, getPropertiesCompositorStatus: function () { let properties = this.player.effect.getProperties(); return properties.map(prop => { return { property: prop.property, runningOnCompositor: prop.runningOnCompositor, warning: prop.warning }; }); }, /** * Return the current start of the Animation. * @return {Object} */ getState: function () { // Remember the startTime each time getState is called, it may be useful // when animations get paused. As in, when an animation gets paused, its // startTime goes back to null, but the front-end might still be interested // in knowing what the previous startTime was. So everytime it is set, // remember it and send it along with the newState. if (this.player.startTime) { this.previousStartTime = this.player.startTime; } // Note that if you add a new property to the state object, make sure you // add the corresponding property in the AnimationPlayerFront' initialState // getter. return { type: this.getType(), // startTime is null whenever the animation is paused or waiting to start. startTime: this.player.startTime, previousStartTime: this.previousStartTime, currentTime: this.player.currentTime, playState: this.player.playState, playbackRate: this.player.playbackRate, name: this.getName(), duration: this.getDuration(), delay: this.getDelay(), endDelay: this.getEndDelay(), iterationCount: this.getIterationCount(), iterationStart: this.getIterationStart(), // animation is hitting the fast path or not. Returns false whenever the // animation is paused as it is taken off the compositor then. isRunningOnCompositor: this.getPropertiesCompositorStatus() .some(propState => propState.runningOnCompositor), propertyState: this.getPropertiesCompositorStatus(), // The document timeline's currentTime is being sent along too. This is // not strictly related to the node's animationPlayer, but is useful to // know the current time of the animation with respect to the document's. documentCurrentTime: this.node.ownerDocument.timeline.currentTime }; }, /** * Get the current state of the AnimationPlayer (currentTime, playState, ...). * Note that the initial state is returned as the form of this actor when it * is initialized. * This protocol method only returns a trimed down version of this state in * case some properties haven't changed since last time (since the front can * reconstruct those). If you want the full state, use the getState method. * @return {Object} */ getCurrentState: function () { let newState = this.getState(); // If we've saved a state before, compare and only send what has changed. // It's expected of the front to also save old states to re-construct the // full state when an incomplete one is received. // This is to minimize protocol traffic. let sentState = {}; if (this.currentState) { for (let key in newState) { if (typeof this.currentState[key] === "undefined" || this.currentState[key] !== newState[key]) { sentState[key] = newState[key]; } } } else { sentState = newState; } this.currentState = newState; return sentState; }, /** * Executed when the current animation changes, used to emit the new state * the the front. */ onAnimationMutation: function (mutations) { let isCurrentAnimation = animation => animation === this.player; let hasCurrentAnimation = animations => animations.some(isCurrentAnimation); let hasChanged = false; for (let {removedAnimations, changedAnimations} of mutations) { if (hasCurrentAnimation(removedAnimations)) { // Reset the local copy of the state on removal, since the animation can // be kept on the client and re-added, its state needs to be sent in // full. this.currentState = null; } if (hasCurrentAnimation(changedAnimations)) { // Only consider the state has having changed if any of delay, duration, // iterationcount or iterationStart has changed (for now at least). let newState = this.getState(); let oldState = this.currentState; hasChanged = newState.delay !== oldState.delay || newState.iterationCount !== oldState.iterationCount || newState.iterationStart !== oldState.iterationStart || newState.duration !== oldState.duration || newState.endDelay !== oldState.endDelay; break; } } if (hasChanged) { events.emit(this, "changed", this.getCurrentState()); } }, /** * Pause the player. */ pause: function () { this.player.pause(); return this.player.ready; }, /** * Play the player. * This method only returns when the animation has left its pending state. */ play: function () { this.player.play(); return this.player.ready; }, /** * Simply exposes the player ready promise. * * When an animation is created/paused then played, there's a short time * during which its playState is pending, before being set to running. * * If you either created a new animation using the Web Animations API or * paused/played an existing one, and then want to access the playState, you * might be interested to call this method. * This is especially important for tests. */ ready: function () { return this.player.ready; }, /** * Set the current time of the animation player. */ setCurrentTime: function (currentTime) { this.player.currentTime = currentTime * this.player.playbackRate; }, /** * Set the playback rate of the animation player. */ setPlaybackRate: function (playbackRate) { this.player.playbackRate = playbackRate; }, /** * Get data about the keyframes of this animation player. * @return {Object} Returns a list of frames, each frame containing the list * animated properties as well as the frame's offset. */ getFrames: function () { return this.player.effect.getKeyframes(); }, /** * Get data about the animated properties of this animation player. * @return {Array} Returns a list of animated properties. * Each property contains a list of values and their offsets */ getProperties: function () { return this.player.effect.getProperties().map(property => { return {name: property.property, values: property.values}; }); } });
const BreakpointActor = ActorClassWithSpec(breakpointSpec, { /** * Create a Breakpoint actor. * * @param ThreadActor threadActor * The parent thread actor that contains this breakpoint. * @param GeneratedLocation generatedLocation * The generated location of the breakpoint. */ initialize: function(threadActor, generatedLocation) { // The set of Debugger.Script instances that this breakpoint has been set // upon. this.scripts = new Set(); this.threadActor = threadActor; this.generatedLocation = generatedLocation; this.condition = null; this.isPending = true; }, destroy: function() { this.removeScripts(); }, hasScript: function(script) { return this.scripts.has(script); }, /** * Called when this same breakpoint is added to another Debugger.Script * instance. * * @param script Debugger.Script * The new source script on which the breakpoint has been set. */ addScript: function(script) { this.scripts.add(script); this.isPending = false; }, /** * Remove the breakpoints from associated scripts and clear the script cache. */ removeScripts: function() { for (const script of this.scripts) { script.clearBreakpoint(this); } this.scripts.clear(); }, /** * Check if this breakpoint has a condition that doesn't error and * evaluates to true in frame. * * @param frame Debugger.Frame * The frame to evaluate the condition in * @returns Object * - result: boolean|undefined * True when the conditional breakpoint should trigger a pause, * false otherwise. If the condition evaluation failed/killed, * `result` will be `undefined`. * - message: string * If the condition throws, this is the thrown message. */ checkCondition: function(frame) { const completion = frame.eval(this.condition); if (completion) { if (completion.throw) { // The evaluation failed and threw let message = "Unknown exception"; try { if (completion.throw.getOwnPropertyDescriptor) { message = completion.throw.getOwnPropertyDescriptor("message") .value; } else if (completion.toString) { message = completion.toString(); } } catch (ex) { // ignore } return { result: true, message: message, }; } else if (completion.yield) { assert(false, "Shouldn't ever get yield completions from an eval"); } else { return { result: !!completion.return }; } } // The evaluation was killed (possibly by the slow script dialog) return { result: undefined }; }, /** * A function that the engine calls when a breakpoint has been hit. * * @param frame Debugger.Frame * The stack frame that contained the breakpoint. */ hit: function(frame) { // Don't pause if we are currently stepping (in or over) or the frame is // black-boxed. const { generatedSourceActor, generatedLine, generatedColumn, } = this.threadActor.sources.getFrameLocation(frame); const url = generatedSourceActor.url; if (this.threadActor.sources.isBlackBoxed(url) || this.threadActor.skipBreakpoints || frame.onStep) { return undefined; } // If we're trying to pop this frame, and we see a breakpoint at // the spot at which popping started, ignore it. See bug 970469. const locationAtFinish = frame.onPop && frame.onPop.generatedLocation; if (locationAtFinish && locationAtFinish.generatedLine === generatedLine && locationAtFinish.generatedColumn === generatedColumn) { return undefined; } const reason = {}; if (this.threadActor._hiddenBreakpoints.has(this.actorID)) { reason.type = "pauseOnDOMEvents"; } else if (!this.condition) { reason.type = "breakpoint"; // TODO: add the rest of the breakpoints on that line (bug 676602). reason.actors = [ this.actorID ]; } else { const { result, message } = this.checkCondition(frame); if (result) { if (!message) { reason.type = "breakpoint"; } else { reason.type = "breakpointConditionThrown"; reason.message = message; } reason.actors = [ this.actorID ]; } else { return undefined; } } return this.threadActor._pauseAndRespond(frame, reason); }, /** * Handle a protocol request to remove this breakpoint. */ delete: function() { // Remove from the breakpoint store. if (this.generatedLocation) { this.threadActor.breakpointActorMap.deleteActor(this.generatedLocation); } this.threadActor.threadLifetimePool.removeActor(this); // Remove the actual breakpoint from the associated scripts. this.removeScripts(); }, });
var AudioNodeActor = exports.AudioNodeActor = protocol.ActorClassWithSpec(audionodeSpec, { form: function(detail) { if (detail === "actorid") { return this.actorID; } return { // actorID is set when this is added to a pool actor: this.actorID, type: this.type, source: this.source, bypassable: this.bypassable, }; }, /** * Create the Audio Node actor. * * @param DebuggerServerConnection conn * The server connection. * @param AudioNode node * The AudioNode that was created. */ initialize: function(conn, node) { protocol.Actor.prototype.initialize.call(this, conn); // Store ChromeOnly property `id` to identify AudioNode, // rather than storing a strong reference, and store a weak // ref to underlying node for controlling. this.nativeID = node.id; this.node = Cu.getWeakReference(node); // Stores the AutomationTimelines for this node's AudioParams. this.automation = {}; try { this.type = getConstructorName(node); } catch (e) { this.type = ""; } this.source = !!AUDIO_NODE_DEFINITION[this.type].source; this.bypassable = !AUDIO_NODE_DEFINITION[this.type].unbypassable; // Create automation timelines for all AudioParams Object.keys(AUDIO_NODE_DEFINITION[this.type].properties || {}) .filter(isAudioParam.bind(null, node)) .forEach(paramName => { this.automation[paramName] = new AutomationTimeline(node[paramName].defaultValue); }); }, /** * Returns the string name of the audio type. * * DEPRECATED: Use `audionode.type` instead, left here for legacy reasons. */ getType: function() { return this.type; }, /** * Returns a boolean indicating if the AudioNode has been "bypassed", * via `AudioNodeActor#bypass` method. * * @return Boolean */ isBypassed: function() { const node = this.node.get(); if (node === null) { return false; } // Cast to boolean incase `passThrough` is undefined, // like for AudioDestinationNode return !!node.passThrough; }, /** * Takes a boolean, either enabling or disabling the "passThrough" option * on an AudioNode. If a node is bypassed, an effects processing node (like gain, * biquad), will allow the audio stream to pass through the node, unaffected. * Returns the bypass state of the node. * * @param Boolean enable * Whether the bypass value should be set on or off. * @return Boolean */ bypass: function(enable) { const node = this.node.get(); if (node === null) { return undefined; } if (this.bypassable) { node.passThrough = enable; } return this.isBypassed(); }, /** * Changes a param on the audio node. Responds with either `undefined` * on success, or a description of the error upon param set failure. * * @param String param * Name of the AudioParam to change. * @param String value * Value to change AudioParam to. */ setParam: function(param, value) { const node = this.node.get(); if (node === null) { return CollectedAudioNodeError(); } try { if (isAudioParam(node, param)) { node[param].value = value; this.automation[param].setValue(value); } else { node[param] = value; } return undefined; } catch (e) { return constructError(e); } }, /** * Gets a param on the audio node. * * @param String param * Name of the AudioParam to fetch. */ getParam: function(param) { const node = this.node.get(); if (node === null) { return CollectedAudioNodeError(); } // Check to see if it's an AudioParam -- if so, // return the `value` property of the parameter. const value = isAudioParam(node, param) ? node[param].value : node[param]; // Return the grip form of the value; at this time, // there shouldn't be any non-primitives at the moment, other than // AudioBuffer or Float32Array references and the like, // so this just formats the value to be displayed in the VariablesView, // without using real grips and managing via actor pools. const grip = createValueGrip(value, null, createObjectGrip); return grip; }, /** * Get an object containing key-value pairs of additional attributes * to be consumed by a front end, like if a property should be read only, * or is a special type (Float32Array, Buffer, etc.) * * @param String param * Name of the AudioParam whose flags are desired. */ getParamFlags: function(param) { return ((AUDIO_NODE_DEFINITION[this.type] || {}).properties || {})[param]; }, /** * Get an array of objects each containing a `param` and `value` property, * corresponding to a property name and current value of the audio node. */ getParams: function(param) { const props = Object.keys(AUDIO_NODE_DEFINITION[this.type].properties || {}); return props.map(prop => ({ param: prop, value: this.getParam(prop), flags: this.getParamFlags(prop) })); }, /** * Connects this audionode to an AudioParam via `node.connect(param)`. */ connectParam: function(destActor, paramName, output) { const srcNode = this.node.get(); const destNode = destActor.node.get(); if (srcNode === null || destNode === null) { return CollectedAudioNodeError(); } try { // Connect via the unwrapped node, so we can call the // patched method that fires the webaudio actor's `connect-param` event. // Connect directly to the wrapped `destNode`, otherwise // the patched method thinks this is a new node and won't be // able to find it in `_nativeToActorID`. XPCNativeWrapper.unwrap(srcNode).connect(destNode[paramName], output); } catch (e) { return constructError(e); } return undefined; }, /** * Connects this audionode to another via `node.connect(dest)`. */ connectNode: function(destActor, output, input) { const srcNode = this.node.get(); const destNode = destActor.node.get(); if (srcNode === null || destNode === null) { return CollectedAudioNodeError(); } try { // Connect via the unwrapped node, so we can call the // patched method that fires the webaudio actor's `connect-node` event. // Connect directly to the wrapped `destNode`, otherwise // the patched method thinks this is a new node and won't be // able to find it in `_nativeToActorID`. XPCNativeWrapper.unwrap(srcNode).connect(destNode, output, input); } catch (e) { return constructError(e); } return undefined; }, /** * Disconnects this audionode from all connections via `node.disconnect()`. */ disconnect: function(destActor, output) { const node = this.node.get(); if (node === null) { return CollectedAudioNodeError(); } try { // Disconnect via the unwrapped node, so we can call the // patched method that fires the webaudio actor's `disconnect` event. XPCNativeWrapper.unwrap(node).disconnect(output); } catch (e) { return constructError(e); } return undefined; }, getAutomationData: function(paramName) { const timeline = this.automation[paramName]; if (!timeline) { return null; } const values = []; let i = 0; if (!timeline.events.length) { return { events: timeline.events, values }; } const firstEvent = timeline.events[0]; const lastEvent = timeline.events[timeline.events.length - 1]; // `setValueCurveAtTime` will have a duration value -- other // events will have duration of `0`. const timeDelta = (lastEvent.time + lastEvent.duration) - firstEvent.time; const scale = timeDelta / AUTOMATION_GRANULARITY; for (; i < AUTOMATION_GRANULARITY; i++) { const delta = firstEvent.time + (i * scale); const value = timeline.getValueAtTime(delta); values.push({ delta, value }); } // If the last event is setTargetAtTime, the automation // doesn't actually begin until the event's time, and exponentially // approaches the target value. In this case, we add more values // until we're "close enough" to the target. if (lastEvent.type === "setTargetAtTime") { for (; i < AUTOMATION_GRANULARITY_MAX; i++) { const delta = firstEvent.time + (++i * scale); const value = timeline.getValueAtTime(delta); values.push({ delta, value }); } } return { events: timeline.events, values }; }, /** * Called via WebAudioActor, registers an automation event * for the AudioParam called. * * @param String paramName * Name of the AudioParam. * @param String eventName * Name of the automation event called. * @param Array args * Arguments passed into the automation call. */ addAutomationEvent: function(paramName, eventName, args = []) { const node = this.node.get(); const timeline = this.automation[paramName]; if (node === null) { return CollectedAudioNodeError(); } if (!timeline || !node[paramName][eventName]) { return InvalidCommandError(); } try { // Using the unwrapped node and parameter, the corresponding // WebAudioActor event will be fired, subsequently calling // `_recordAutomationEvent`. Some finesse is required to handle // the cast of TypedArray arguments over the protocol, which is // taken care of below. The event will cast the argument back // into an array to be broadcasted from WebAudioActor, but the // double-casting will only occur when starting from `addAutomationEvent`, // which is only used in tests. const param = XPCNativeWrapper.unwrap(node[paramName]); const contentGlobal = Cu.getGlobalForObject(param); const contentArgs = Cu.cloneInto(args, contentGlobal); // If calling `setValueCurveAtTime`, the first argument // is a Float32Array, which won't be able to be serialized // over the protocol. Cast a normal array to a Float32Array here. if (eventName === "setValueCurveAtTime") { // Create a Float32Array from the content, seeding with an array // from the same scope. const curve = new contentGlobal.Float32Array(contentArgs[0]); contentArgs[0] = curve; } // Apply the args back from the content scope, which is necessary // due to the method wrapping changing in bug 1130901 to be exported // directly to the content scope. param[eventName].apply(param, contentArgs); } catch (e) { return constructError(e); } return undefined; }, /** * Registers the automation event in the AudioNodeActor's * internal timeline. Called when setting automation via * `addAutomationEvent`, or from the WebAudioActor's listening * to the event firing via content. * * @param String paramName * Name of the AudioParam. * @param String eventName * Name of the automation event called. * @param Array args * Arguments passed into the automation call. */ _recordAutomationEvent: function(paramName, eventName, args) { const timeline = this.automation[paramName]; timeline[eventName].apply(timeline, args); } });
var AnimationPlayerActor = protocol.ActorClassWithSpec(animationPlayerSpec, { /** * @param {AnimationsActor} The main AnimationsActor instance * @param {AnimationPlayer} The player object returned by getAnimationPlayers * @param {Number} Time which animation created */ initialize: function(animationsActor, player, createdTime) { Actor.prototype.initialize.call(this, animationsActor.conn); this.onAnimationMutation = this.onAnimationMutation.bind(this); this.walker = animationsActor.walker; this.player = player; // Listen to animation mutations on the node to alert the front when the // current animation changes. // If the node is a pseudo-element, then we listen on its parent with // subtree:true (there's no risk of getting too many notifications in // onAnimationMutation since we filter out events that aren't for the // current animation). this.observer = new this.window.MutationObserver(this.onAnimationMutation); if (this.isPseudoElement) { this.observer.observe(this.node.parentElement, {animations: true, subtree: true}); } else { this.observer.observe(this.node, {animations: true}); } this.createdTime = createdTime; }, destroy: function() { // Only try to disconnect the observer if it's not already dead (i.e. if the // container view hasn't navigated since). if (this.observer && !Cu.isDeadWrapper(this.observer)) { this.observer.disconnect(); } this.player = this.observer = this.walker = null; Actor.prototype.destroy.call(this); }, get isPseudoElement() { return !this.player.effect.target.ownerDocument; }, get node() { if (!this.isPseudoElement) { return this.player.effect.target; } const pseudo = this.player.effect.target; const treeWalker = this.walker.getDocumentWalker(pseudo.parentElement); return pseudo.type === "::before" ? treeWalker.firstChild() : treeWalker.lastChild(); }, get document() { return this.node.ownerDocument; }, get window() { return this.document.defaultView; }, /** * Release the actor, when it isn't needed anymore. * Protocol.js uses this release method to call the destroy method. */ release: function() {}, form: function(detail) { if (detail === "actorid") { return this.actorID; } const data = this.getCurrentState(); data.actor = this.actorID; // If we know the WalkerActor, and if the animated node is known by it, then // return its corresponding NodeActor ID too. if (this.walker && this.walker.hasNode(this.node)) { data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID; } return data; }, isCssAnimation: function(player = this.player) { return player instanceof this.window.CSSAnimation; }, isCssTransition: function(player = this.player) { return player instanceof this.window.CSSTransition; }, isScriptAnimation: function(player = this.player) { return player instanceof this.window.Animation && !( player instanceof this.window.CSSAnimation || player instanceof this.window.CSSTransition ); }, getType: function() { if (this.isCssAnimation()) { return ANIMATION_TYPES.CSS_ANIMATION; } else if (this.isCssTransition()) { return ANIMATION_TYPES.CSS_TRANSITION; } else if (this.isScriptAnimation()) { return ANIMATION_TYPES.SCRIPT_ANIMATION; } return ANIMATION_TYPES.UNKNOWN; }, /** * Get the name of this animation. This can be either the animation.id * property if it was set, or the keyframe rule name or the transition * property. * @return {String} */ getName: function() { if (this.player.id) { return this.player.id; } else if (this.isCssAnimation()) { return this.player.animationName; } else if (this.isCssTransition()) { return this.player.transitionProperty; } return ""; }, /** * Get the animation duration from this player, in milliseconds. * @return {Number} */ getDuration: function() { return this.player.effect.getComputedTiming().duration; }, /** * Get the animation delay from this player, in milliseconds. * @return {Number} */ getDelay: function() { return this.player.effect.getComputedTiming().delay; }, /** * Get the animation endDelay from this player, in milliseconds. * @return {Number} */ getEndDelay: function() { return this.player.effect.getComputedTiming().endDelay; }, /** * Get the animation iteration count for this player. That is, how many times * is the animation scheduled to run. * @return {Number} The number of iterations, or null if the animation repeats * infinitely. */ getIterationCount: function() { const iterations = this.player.effect.getComputedTiming().iterations; return iterations === Infinity ? null : iterations; }, /** * Get the animation iterationStart from this player, in ratio. * That is offset of starting position of the animation. * @return {Number} */ getIterationStart: function() { return this.player.effect.getComputedTiming().iterationStart; }, /** * Get the animation easing from this player. * @return {String} */ getEasing: function() { return this.player.effect.getComputedTiming().easing; }, /** * Get the animation fill mode from this player. * @return {String} */ getFill: function() { return this.player.effect.getComputedTiming().fill; }, /** * Get the animation direction from this player. * @return {String} */ getDirection: function() { return this.player.effect.getComputedTiming().direction; }, /** * Get animation-timing-function from animated element if CSS Animations. * @return {String} */ getAnimationTimingFunction: function() { if (!this.isCssAnimation()) { return null; } let pseudo = null; let target = this.player.effect.target; if (target.type) { // Animated element is a pseudo element. pseudo = target.type; target = target.parentElement; } return this.window.getComputedStyle(target, pseudo).animationTimingFunction; }, getPropertiesCompositorStatus: function() { const properties = this.player.effect.getProperties(); return properties.map(prop => { return { property: prop.property, runningOnCompositor: prop.runningOnCompositor, warning: prop.warning }; }); }, /** * Return the current start of the Animation. * @return {Object} */ getState: function() { // Remember the startTime each time getState is called, it may be useful // when animations get paused. As in, when an animation gets paused, its // startTime goes back to null, but the front-end might still be interested // in knowing what the previous startTime was. So everytime it is set, // remember it and send it along with the newState. if (this.player.startTime) { this.previousStartTime = this.player.startTime; } // Note that if you add a new property to the state object, make sure you // add the corresponding property in the AnimationPlayerFront' initialState // getter. return { type: this.getType(), // startTime is null whenever the animation is paused or waiting to start. startTime: this.player.startTime, previousStartTime: this.previousStartTime, currentTime: this.player.currentTime, playState: this.player.playState, playbackRate: this.player.playbackRate, name: this.getName(), duration: this.getDuration(), delay: this.getDelay(), endDelay: this.getEndDelay(), iterationCount: this.getIterationCount(), iterationStart: this.getIterationStart(), fill: this.getFill(), easing: this.getEasing(), direction: this.getDirection(), animationTimingFunction: this.getAnimationTimingFunction(), // animation is hitting the fast path or not. Returns false whenever the // animation is paused as it is taken off the compositor then. isRunningOnCompositor: this.getPropertiesCompositorStatus() .some(propState => propState.runningOnCompositor), propertyState: this.getPropertiesCompositorStatus(), // The document timeline's currentTime is being sent along too. This is // not strictly related to the node's animationPlayer, but is useful to // know the current time of the animation with respect to the document's. documentCurrentTime: this.node.ownerDocument.timeline.currentTime, // The time which this animation created. createdTime: this.createdTime, }; }, /** * Get the current state of the AnimationPlayer (currentTime, playState, ...). * Note that the initial state is returned as the form of this actor when it * is initialized. * This protocol method only returns a trimed down version of this state in * case some properties haven't changed since last time (since the front can * reconstruct those). If you want the full state, use the getState method. * @return {Object} */ getCurrentState: function() { const newState = this.getState(); // If we've saved a state before, compare and only send what has changed. // It's expected of the front to also save old states to re-construct the // full state when an incomplete one is received. // This is to minimize protocol traffic. let sentState = {}; if (this.currentState) { for (const key in newState) { if (typeof this.currentState[key] === "undefined" || this.currentState[key] !== newState[key]) { sentState[key] = newState[key]; } } } else { sentState = newState; } this.currentState = newState; return sentState; }, /** * Executed when the current animation changes, used to emit the new state * the the front. */ onAnimationMutation: function(mutations) { const isCurrentAnimation = animation => animation === this.player; const hasCurrentAnimation = animations => animations.some(isCurrentAnimation); let hasChanged = false; for (const {removedAnimations, changedAnimations} of mutations) { if (hasCurrentAnimation(removedAnimations)) { // Reset the local copy of the state on removal, since the animation can // be kept on the client and re-added, its state needs to be sent in // full. this.currentState = null; } if (hasCurrentAnimation(changedAnimations)) { // Only consider the state has having changed if any of delay, duration, // iterationCount, iterationStart, or playbackRate has changed (for now // at least). const newState = this.getState(); const oldState = this.currentState; hasChanged = newState.delay !== oldState.delay || newState.iterationCount !== oldState.iterationCount || newState.iterationStart !== oldState.iterationStart || newState.duration !== oldState.duration || newState.endDelay !== oldState.endDelay || newState.playbackRate !== oldState.playbackRate; break; } } if (hasChanged) { this.emit("changed", this.getCurrentState()); } }, /** * Pause the player. */ pause: function() { this.player.pause(); return this.player.ready; }, /** * Play the player. * This method only returns when the animation has left its pending state. */ play: function() { this.player.play(); return this.player.ready; }, /** * Simply exposes the player ready promise. * * When an animation is created/paused then played, there's a short time * during which its playState is pending, before being set to running. * * If you either created a new animation using the Web Animations API or * paused/played an existing one, and then want to access the playState, you * might be interested to call this method. * This is especially important for tests. */ ready: function() { return this.player.ready; }, /** * Set the current time of the animation player. */ setCurrentTime: function(currentTime) { // The spec is that the progress of animation is changed // if the time of setCurrentTime is during the endDelay. // We should prevent the time // to make the same animation behavior as the original. // Likewise, in case the time is less than 0. const timing = this.player.effect.getComputedTiming(); if (timing.delay < 0) { currentTime += timing.delay; } if (currentTime < 0) { currentTime = 0; } else if (currentTime * this.player.playbackRate > timing.endTime) { currentTime = timing.endTime; } this.player.currentTime = currentTime * this.player.playbackRate; }, /** * Set the playback rate of the animation player. */ setPlaybackRate: function(playbackRate) { this.player.updatePlaybackRate(playbackRate); return this.player.ready; }, /** * Get data about the keyframes of this animation player. * @return {Object} Returns a list of frames, each frame containing the list * animated properties as well as the frame's offset. */ getFrames: function() { return this.player.effect.getKeyframes(); }, /** * Get data about the animated properties of this animation player. * @return {Array} Returns a list of animated properties. * Each property contains a list of values, their offsets and distances. */ getProperties: function() { const properties = this.player.effect.getProperties().map(property => { return {name: property.property, values: property.values}; }); const DOMWindowUtils = this.window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); // Fill missing keyframe with computed value. for (const property of properties) { let underlyingValue = null; // Check only 0% and 100% keyframes. [0, property.values.length - 1].forEach(index => { const values = property.values[index]; if (values.value !== undefined) { return; } if (!underlyingValue) { let pseudo = null; let target = this.player.effect.target; if (target.type) { // This target is a pseudo element. pseudo = target.type; target = target.parentElement; } const value = DOMWindowUtils.getUnanimatedComputedStyle(target, pseudo, property.name, DOMWindowUtils.FLUSH_NONE); const animationType = getAnimationTypeForLonghand(property.name); underlyingValue = animationType === "float" ? parseFloat(value, 10) : value; } values.value = underlyingValue; }); } // Calculate the distance. for (const property of properties) { const propertyName = property.name; const maxObject = { distance: -1 }; for (let i = 0; i < property.values.length - 1; i++) { const value1 = property.values[i].value; for (let j = i + 1; j < property.values.length; j++) { const value2 = property.values[j].value; const distance = this.getDistance(this.node, propertyName, value1, value2, DOMWindowUtils); if (maxObject.distance >= distance) { continue; } maxObject.distance = distance; maxObject.value1 = value1; maxObject.value2 = value2; } } if (maxObject.distance === 0) { // Distance is zero means that no values change or can't calculate the distance. // In this case, we use the keyframe offset as the distance. property.values.reduce((previous, current) => { // If the current value is same as previous value, use previous distance. current.distance = current.value === previous.value ? previous.distance : current.offset; return current; }, property.values[0]); continue; } const baseValue = maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2; for (const values of property.values) { const value = values.value; const distance = this.getDistance(this.node, propertyName, baseValue, value, DOMWindowUtils); values.distance = distance / maxObject.distance; } } return properties; }, /** * Get the animation types for a given list of CSS property names. * @param {Array} propertyNames - CSS property names (e.g. background-color) * @return {Object} Returns animation types (e.g. {"background-color": "rgb(0, 0, 0)"}. */ getAnimationTypes: function(propertyNames) { const animationTypes = {}; for (const propertyName of propertyNames) { animationTypes[propertyName] = getAnimationTypeForLonghand(propertyName); } return animationTypes; }, /** * Returns the distance of between value1, value2. * @param {Object} target - dom element * @param {String} propertyName - e.g. transform * @param {String} value1 - e.g. translate(0px) * @param {String} value2 - e.g. translate(10px) * @param {Object} DOMWindowUtils * @param {float} distance */ getDistance: function(target, propertyName, value1, value2, DOMWindowUtils) { if (value1 === value2) { return 0; } try { const distance = DOMWindowUtils.computeAnimationDistance(target, propertyName, value1, value2); return distance; } catch (e) { // We can't compute the distance such the 'discrete' animation, // 'auto' keyword and so on. return 0; } } });
var RootActor = protocol.ActorClassWithSpec(rootSpec, { initialize: function(conn) { protocol.Actor.prototype.initialize.call(this, conn); // Root actor owns itself. this.manage(this); this.actorID = "root"; this.sequence = 0; }, sayHello: simpleHello, simpleReturn: function() { return this.sequence++; }, promiseReturn: function(toWait) { // Guarantee that this resolves after simpleReturn returns. const deferred = defer(); const sequence = this.sequence++; // Wait until the number of requests specified by toWait have // happened, to test queuing. const check = () => { if ((this.sequence - sequence) < toWait) { executeSoon(check); return; } deferred.resolve(sequence); }; executeSoon(check); return deferred.promise; }, simpleThrow: function() { throw new Error(this.sequence++); }, promiseThrow: function() { // Guarantee that this resolves after simpleReturn returns. const deferred = defer(); let sequence = this.sequence++; // This should be enough to force a failure if the code is broken. do_timeout(150, () => { deferred.reject(sequence++); }); return deferred.promise; }, });
let WorkerActor = protocol.ActorClassWithSpec(workerSpec, { initialize: function (conn, dbg) { protocol.Actor.prototype.initialize.call(this, conn); this._dbg = dbg; this._attached = false; this._threadActor = null; this._transport = null; this.manage(this); }, form: function (detail) { if (detail === "actorid") { return this.actorID; } let form = { actor: this.actorID, consoleActor: this._consoleActor, url: this._dbg.url, type: this._dbg.type }; if (this._dbg.type === Ci.nsIWorkerDebugger.TYPE_SERVICE) { let registration = this._getServiceWorkerRegistrationInfo(); form.scope = registration.scope; } return form; }, attach: function () { if (this._dbg.isClosed) { return { error: "closed" }; } if (!this._attached) { // Automatically disable their internal timeout that shut them down // Should be refactored by having actors specific to service workers if (this._dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE) { let worker = this._getServiceWorkerInfo(); if (worker) { worker.attachDebugger(); } } this._dbg.addListener(this); this._attached = true; } return { type: "attached", url: this._dbg.url }; }, detach: function () { if (!this._attached) { return { error: "wrongState" }; } this._detach(); return { type: "detached" }; }, connect: function (options) { if (!this._attached) { return { error: "wrongState" }; } if (this._threadActor !== null) { return { type: "connected", threadActor: this._threadActor }; } return DebuggerServer.connectToWorker( this.conn, this._dbg, this.actorID, options ).then(({ threadActor, transport, consoleActor }) => { this._threadActor = threadActor; this._transport = transport; this._consoleActor = consoleActor; return { type: "connected", threadActor: this._threadActor, consoleActor: this._consoleActor }; }, (error) => { return { error: error.toString() }; }); }, push: function () { if (this._dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) { return { error: "wrongType" }; } let registration = this._getServiceWorkerRegistrationInfo(); let originAttributes = ChromeUtils.originAttributesToSuffix( this._dbg.principal.originAttributes); swm.sendPushEvent(originAttributes, registration.scope); return { type: "pushed" }; }, onClose: function () { if (this._attached) { this._detach(); } this.conn.sendActorEvent(this.actorID, "close"); }, onError: function (filename, lineno, message) { reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n"); }, _getServiceWorkerRegistrationInfo() { return swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url); }, _getServiceWorkerInfo: function () { let registration = this._getServiceWorkerRegistrationInfo(); return registration.getWorkerByID(this._dbg.serviceWorkerID); }, _detach: function () { if (this._threadActor !== null) { this._transport.close(); this._transport = null; this._threadActor = null; } // If the worker is already destroyed, nsIWorkerDebugger.type throws // (_dbg.closed appears to be false when it throws) let type; try { type = this._dbg.type; } catch (e) {} if (type == Ci.nsIWorkerDebugger.TYPE_SERVICE) { let worker = this._getServiceWorkerInfo(); if (worker) { worker.detachDebugger(); } } this._dbg.removeListener(this); this._attached = false; } });
const FrameActor = ActorClassWithSpec(frameSpec, { /** * Creates the Frame actor. * * @param frame Debugger.Frame * The debuggee frame. * @param threadActor ThreadActor * The parent thread actor for this frame. */ initialize: function(frame, threadActor) { this.frame = frame; this.threadActor = threadActor; }, /** * A pool that contains frame-lifetime objects, like the environment. */ _frameLifetimePool: null, get frameLifetimePool() { if (!this._frameLifetimePool) { this._frameLifetimePool = new ActorPool(this.conn); this.conn.addActorPool(this._frameLifetimePool); } return this._frameLifetimePool; }, /** * Finalization handler that is called when the actor is being evicted from * the pool. */ destroy: function() { this.conn.removeActorPool(this._frameLifetimePool); this._frameLifetimePool = null; }, getEnvironment: function() { if (!this.frame.environment) { return {}; } const envActor = this.threadActor.createEnvironmentActor( this.frame.environment, this.frameLifetimePool ); return envActor.form(); }, /** * Returns a frame form for use in a protocol message. */ form: function() { const threadActor = this.threadActor; const form = { actor: this.actorID, type: this.frame.type }; if (this.frame.type === "call") { form.callee = createValueGrip(this.frame.callee, threadActor._pausePool, threadActor.objectGrip); } // NOTE: ignoreFrameEnvironment lets the client explicitly avoid // populating form environments on pause. if ( !this.threadActor._options.ignoreFrameEnvironment && this.frame.environment ) { form.environment = this.getEnvironment(); } if (this.frame.type != "wasmcall") { form.this = createValueGrip(this.frame.this, threadActor._pausePool, threadActor.objectGrip); } form.arguments = this._args(); if (this.frame.script) { const generatedLocation = this.threadActor.sources.getFrameLocation(this.frame); form.where = { source: generatedLocation.generatedSourceActor.form(), line: generatedLocation.generatedLine, column: generatedLocation.generatedColumn }; } if (!this.frame.older) { form.oldest = true; } return form; }, _args: function() { if (!this.frame.arguments) { return []; } return this.frame.arguments.map(arg => createValueGrip(arg, this.threadActor._pausePool, this.threadActor.objectGrip)); } });
const EmulationActor = protocol.ActorClassWithSpec(emulationSpec, { initialize(conn, targetActor) { protocol.Actor.prototype.initialize.call(this, conn); this.targetActor = targetActor; this.docShell = targetActor.docShell; this.onWillNavigate = this.onWillNavigate.bind(this); this.onWindowReady = this.onWindowReady.bind(this); this.targetActor.on("will-navigate", this.onWillNavigate); this.targetActor.on("window-ready", this.onWindowReady); }, destroy() { this.stopPrintMediaSimulation(); this.clearDPPXOverride(); this.clearNetworkThrottling(); this.clearTouchEventsOverride(); this.clearMetaViewportOverride(); this.clearUserAgentOverride(); this.targetActor.off("will-navigate", this.onWillNavigate); this.targetActor.off("window-ready", this.onWindowReady); this.targetActor = null; this.docShell = null; this._touchSimulator = null; protocol.Actor.prototype.destroy.call(this); }, /** * Retrieve the console actor for this tab. This allows us to expose network throttling * as part of emulation settings, even though it's internally connected to the network * monitor, which for historical reasons is part of the console actor. */ get _consoleActor() { if (this.targetActor.exited || !this.targetActor.actorID) { return null; } const form = this.targetActor.form(); return this.conn._getOrCreateActor(form.consoleActor); }, get touchSimulator() { if (!this._touchSimulator) { this._touchSimulator = new TouchSimulator(this.targetActor.chromeEventHandler); } return this._touchSimulator; }, onWillNavigate({ isTopLevel }) { // Make sure that print simulation is stopped before navigating to another page. We // need to do this since the browser will cache the last state of the page in its // session history. if (this._printSimulationEnabled && isTopLevel) { this.stopPrintMediaSimulation(true); } }, onWindowReady({ isTopLevel }) { // Since `emulateMedium` only works for the current page, we need to ensure persistent // print simulation for when the user navigates to a new page while its enabled. // To do this, we need to tell the page to begin print simulation before the DOM // content is available to the user: if (this._printSimulationEnabled && isTopLevel) { this.startPrintMediaSimulation(); } }, /* DPPX override */ _previousDPPXOverride: undefined, setDPPXOverride(dppx) { if (this.getDPPXOverride() === dppx) { return false; } if (this._previousDPPXOverride === undefined) { this._previousDPPXOverride = this.getDPPXOverride(); } this.docShell.contentViewer.overrideDPPX = dppx; return true; }, getDPPXOverride() { return this.docShell.contentViewer.overrideDPPX; }, clearDPPXOverride() { if (this._previousDPPXOverride !== undefined) { return this.setDPPXOverride(this._previousDPPXOverride); } return false; }, /* Network Throttling */ _previousNetworkThrottling: undefined, /** * Transform the RDP format into the internal format and then set network throttling. */ setNetworkThrottling({ downloadThroughput, uploadThroughput, latency }) { const throttleData = { latencyMean: latency, latencyMax: latency, downloadBPSMean: downloadThroughput, downloadBPSMax: downloadThroughput, uploadBPSMean: uploadThroughput, uploadBPSMax: uploadThroughput, }; return this._setNetworkThrottling(throttleData); }, _setNetworkThrottling(throttleData) { const current = this._getNetworkThrottling(); // Check if they are both objects or both null let match = throttleData == current; // If both objects, check all entries if (match && current && throttleData) { match = Object.entries(current).every(([ k, v ]) => { return throttleData[k] === v; }); } if (match) { return false; } if (this._previousNetworkThrottling === undefined) { this._previousNetworkThrottling = current; } const consoleActor = this._consoleActor; if (!consoleActor) { return false; } consoleActor.startListeners({ listeners: [ "NetworkActivity" ], }); consoleActor.setPreferences({ preferences: { "NetworkMonitor.throttleData": throttleData, }, }); return true; }, /** * Get network throttling and then transform the internal format into the RDP format. */ getNetworkThrottling() { const throttleData = this._getNetworkThrottling(); if (!throttleData) { return null; } const { downloadBPSMax, uploadBPSMax, latencyMax } = throttleData; return { downloadThroughput: downloadBPSMax, uploadThroughput: uploadBPSMax, latency: latencyMax, }; }, _getNetworkThrottling() { const consoleActor = this._consoleActor; if (!consoleActor) { return null; } const prefs = consoleActor.getPreferences({ preferences: [ "NetworkMonitor.throttleData" ], }); return prefs.preferences["NetworkMonitor.throttleData"] || null; }, clearNetworkThrottling() { if (this._previousNetworkThrottling !== undefined) { return this._setNetworkThrottling(this._previousNetworkThrottling); } return false; }, /* Touch events override */ _previousTouchEventsOverride: undefined, /** * Set the current element picker state. * * True means the element picker is currently active and we should not be emulating * touch events. * False means the element picker is not active and it is ok to emulate touch events. * * This actor method is meant to be called by the DevTools front-end. The reason for * this is the following: * RDM is the only current consumer of the touch simulator. RDM instantiates this actor * on its own, whether or not the Toolbox is opened. That means it does so in its own * Debugger Server instance. * When the Toolbox is running, it uses a different DebuggerServer. Therefore, it is not * possible for the touch simulator to know whether the picker is active or not. This * state has to be sent by the client code of the Toolbox to this actor. * If a future use case arises where we want to use the touch simulator from the Toolbox * too, then we could add code in here to detect the picker mode as described in * https://bugzilla.mozilla.org/show_bug.cgi?id=1409085#c3 * @param {Boolean} state */ setElementPickerState(state) { this.touchSimulator.setElementPickerState(state); }, setTouchEventsOverride(flag) { if (this.getTouchEventsOverride() == flag) { return false; } if (this._previousTouchEventsOverride === undefined) { this._previousTouchEventsOverride = this.getTouchEventsOverride(); } // Start or stop the touch simulator depending on the override flag if (flag == Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED) { this.touchSimulator.start(); } else { this.touchSimulator.stop(); } this.docShell.touchEventsOverride = flag; return true; }, getTouchEventsOverride() { return this.docShell.touchEventsOverride; }, clearTouchEventsOverride() { if (this._previousTouchEventsOverride !== undefined) { return this.setTouchEventsOverride(this._previousTouchEventsOverride); } return false; }, /* Meta viewport override */ _previousMetaViewportOverride: undefined, setMetaViewportOverride(flag) { if (this.getMetaViewportOverride() == flag) { return false; } if (this._previousMetaViewportOverride === undefined) { this._previousMetaViewportOverride = this.getMetaViewportOverride(); } this.docShell.metaViewportOverride = flag; return true; }, getMetaViewportOverride() { return this.docShell.metaViewportOverride; }, clearMetaViewportOverride() { if (this._previousMetaViewportOverride !== undefined) { return this.setMetaViewportOverride(this._previousMetaViewportOverride); } return false; }, /* User agent override */ _previousUserAgentOverride: undefined, setUserAgentOverride(userAgent) { if (this.getUserAgentOverride() == userAgent) { return false; } if (this._previousUserAgentOverride === undefined) { this._previousUserAgentOverride = this.getUserAgentOverride(); } this.docShell.customUserAgent = userAgent; return true; }, getUserAgentOverride() { return this.docShell.customUserAgent; }, clearUserAgentOverride() { if (this._previousUserAgentOverride !== undefined) { return this.setUserAgentOverride(this._previousUserAgentOverride); } return false; }, /* Simulating print media for the page */ _printSimulationEnabled: false, getIsPrintSimulationEnabled() { return this._printSimulationEnabled; }, async startPrintMediaSimulation() { this._printSimulationEnabled = true; this.targetActor.docShell.contentViewer.emulateMedium("print"); }, /** * Stop simulating print media for the current page. * * @param {Boolean} state * Whether or not to set _printSimulationEnabled to false. If true, we want to * stop simulation print media for the current page but NOT set * _printSimulationEnabled to false. We do this specifically for the * "will-navigate" event where we still want to continue simulating print when * navigating to the next page. Defaults to false, meaning we want to completely * stop print simulation. */ async stopPrintMediaSimulation(state = false) { this._printSimulationEnabled = state; this.targetActor.docShell.contentViewer.stopEmulatingMedium(); }, });
let EnvironmentActor = ActorClassWithSpec(environmentSpec, { initialize: function(environment, threadActor) { this.obj = environment; this.threadActor = threadActor; }, /** * When the Environment Actor is destroyed it removes the * Debugger.Environment.actor field so that environment does not * reference a destroyed actor. */ destroy: function() { this.obj.actor = null; }, /** * Return an environment form for use in a protocol message. */ form: function() { let form = { actor: this.actorID }; // What is this environment's type? if (this.obj.type == "declarative") { form.type = this.obj.callee ? "function" : "block"; } else { form.type = this.obj.type; } // Does this environment have a parent? if (this.obj.parent) { form.parent = (this.threadActor .createEnvironmentActor(this.obj.parent, this.registeredPool) .form()); } // Does this environment reflect the properties of an object as variables? if (this.obj.type == "object" || this.obj.type == "with") { form.object = createValueGrip(this.obj.object, this.registeredPool, this.threadActor.objectGrip); } // Is this the environment created for a function call? if (this.obj.callee) { form.function = createValueGrip(this.obj.callee, this.registeredPool, this.threadActor.objectGrip); } // Shall we list this environment's bindings? if (this.obj.type == "declarative") { form.bindings = this.bindings(); } return form; }, /** * Handle a protocol request to change the value of a variable bound in this * lexical environment. * * @param string name * The name of the variable to be changed. * @param any value * The value to be assigned. */ assign: function(name, value) { // TODO: enable the commented-out part when getVariableDescriptor lands // (bug 725815). /* let desc = this.obj.getVariableDescriptor(name); if (!desc.writable) { return { error: "immutableBinding", message: "Changing the value of an immutable binding is not " + "allowed" }; }*/ try { this.obj.setVariable(name, value); } catch (e) { if (e instanceof Debugger.DebuggeeWouldRun) { const errorObject = { error: "threadWouldRun", message: "Assigning a value would cause the debuggee to run" }; throw errorObject; } else { throw e; } } return { from: this.actorID }; }, /** * Handle a protocol request to fully enumerate the bindings introduced by the * lexical environment. */ bindings: function() { let bindings = { arguments: [], variables: {} }; // TODO: this part should be removed in favor of the commented-out part // below when getVariableDescriptor lands (bug 725815). if (typeof this.obj.getVariable != "function") { // if (typeof this.obj.getVariableDescriptor != "function") { return bindings; } let parameterNames; if (this.obj.callee) { parameterNames = this.obj.callee.parameterNames; } else { parameterNames = []; } for (let name of parameterNames) { let arg = {}; let value = this.obj.getVariable(name); // TODO: this part should be removed in favor of the commented-out part // below when getVariableDescriptor lands (bug 725815). let desc = { value: value, configurable: false, writable: !(value && value.optimizedOut), enumerable: true }; // let desc = this.obj.getVariableDescriptor(name); let descForm = { enumerable: true, configurable: desc.configurable }; if ("value" in desc) { descForm.value = createValueGrip(desc.value, this.registeredPool, this.threadActor.objectGrip); descForm.writable = desc.writable; } else { descForm.get = createValueGrip(desc.get, this.registeredPool, this.threadActor.objectGrip); descForm.set = createValueGrip(desc.set, this.registeredPool, this.threadActor.objectGrip); } arg[name] = descForm; bindings.arguments.push(arg); } for (let name of this.obj.names()) { if (bindings.arguments.some(function exists(element) { return !!element[name]; })) { continue; } let value = this.obj.getVariable(name); // TODO: this part should be removed in favor of the commented-out part // below when getVariableDescriptor lands. let desc = { value: value, configurable: false, writable: !(value && (value.optimizedOut || value.uninitialized || value.missingArguments)), enumerable: true }; // let desc = this.obj.getVariableDescriptor(name); let descForm = { enumerable: true, configurable: desc.configurable }; if ("value" in desc) { descForm.value = createValueGrip(desc.value, this.registeredPool, this.threadActor.objectGrip); descForm.writable = desc.writable; } else { descForm.get = createValueGrip(desc.get || undefined, this.registeredPool, this.threadActor.objectGrip); descForm.set = createValueGrip(desc.set || undefined, this.registeredPool, this.threadActor.objectGrip); } bindings.variables[name] = descForm; } return bindings; } });
const NodeActor = protocol.ActorClassWithSpec(nodeSpec, { initialize: function(walker, node) { protocol.Actor.prototype.initialize.call(this, null); this.walker = walker; this.rawNode = node; this._eventParsers = new EventParsers().parsers; // Store the original display type and whether or not the node is displayed to // track changes when reflows occur. this.currentDisplayType = this.displayType; this.wasDisplayed = this.isDisplayed; }, toString: function() { return "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]"; }, /** * Instead of storing a connection object, the NodeActor gets its connection * from its associated walker. */ get conn() { return this.walker.conn; }, isDocumentElement: function() { return this.rawNode.ownerDocument && this.rawNode.ownerDocument.documentElement === this.rawNode; }, destroy: function() { protocol.Actor.prototype.destroy.call(this); if (this.mutationObserver) { if (!Cu.isDeadWrapper(this.mutationObserver)) { this.mutationObserver.disconnect(); } this.mutationObserver = null; } if (this.slotchangeListener) { if (!InspectorActorUtils.isNodeDead(this)) { this.rawNode.removeEventListener("slotchange", this.slotchangeListener); } this.slotchangeListener = null; } this.rawNode = null; this.walker = null; }, // Returns the JSON representation of this object over the wire. form: function(detail) { if (detail === "actorid") { return this.actorID; } const parentNode = this.walker.parentNode(this); const inlineTextChild = this.walker.inlineTextChild(this); const shadowRoot = isShadowRoot(this.rawNode); const hostActor = shadowRoot ? this.walker.getNode(this.rawNode.host) : null; const form = { actor: this.actorID, host: hostActor ? hostActor.actorID : undefined, baseURI: this.rawNode.baseURI, parent: parentNode ? parentNode.actorID : undefined, nodeType: this.rawNode.nodeType, namespaceURI: this.rawNode.namespaceURI, nodeName: this.rawNode.nodeName, nodeValue: this.rawNode.nodeValue, displayName: InspectorActorUtils.getNodeDisplayName(this.rawNode), numChildren: this.numChildren, inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined, displayType: this.displayType, // doctype attributes name: this.rawNode.name, publicId: this.rawNode.publicId, systemId: this.rawNode.systemId, attrs: this.writeAttrs(), customElementLocation: this.getCustomElementLocation(), isBeforePseudoElement: isBeforePseudoElement(this.rawNode), isAfterPseudoElement: isAfterPseudoElement(this.rawNode), isAnonymous: isAnonymous(this.rawNode), isNativeAnonymous: isNativeAnonymous(this.rawNode), isXBLAnonymous: isXBLAnonymous(this.rawNode), isShadowAnonymous: isShadowAnonymous(this.rawNode), isShadowRoot: shadowRoot, shadowRootMode: getShadowRootMode(this.rawNode), isShadowHost: isShadowHost(this.rawNode), isDirectShadowHostChild: isDirectShadowHostChild(this.rawNode), pseudoClassLocks: this.writePseudoClassLocks(), isDisplayed: this.isDisplayed, isInHTMLDocument: this.rawNode.ownerDocument && this.rawNode.ownerDocument.contentType === "text/html", hasEventListeners: this._hasEventListeners, }; if (this.isDocumentElement()) { form.isDocumentElement = true; } return form; }, /** * Watch the given document node for mutations using the DOM observer * API. */ watchDocument: function(doc, callback) { const node = this.rawNode; // Create the observer on the node's actor. The node will make sure // the observer is cleaned up when the actor is released. const observer = new doc.defaultView.MutationObserver(callback); observer.mergeAttributeRecords = true; observer.observe(node, { nativeAnonymousChildList: true, attributes: true, characterData: true, characterDataOldValue: true, childList: true, subtree: true }); this.mutationObserver = observer; }, /** * Watch for all "slotchange" events on the node. */ watchSlotchange: function(callback) { this.slotchangeListener = callback; this.rawNode.addEventListener("slotchange", this.slotchangeListener); }, // Estimate the number of children that the walker will return without making // a call to children() if possible. get numChildren() { // For pseudo elements, childNodes.length returns 1, but the walker // will return 0. if (isBeforePseudoElement(this.rawNode) || isAfterPseudoElement(this.rawNode)) { return 0; } const rawNode = this.rawNode; let numChildren = rawNode.childNodes.length; const hasAnonChildren = rawNode.nodeType === Node.ELEMENT_NODE && rawNode.ownerDocument.getAnonymousNodes(rawNode); const hasContentDocument = rawNode.contentDocument; const hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument(); if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) { // This might be an iframe with virtual children. numChildren = 1; } // Normal counting misses ::before/::after. Also, some anonymous children // may ultimately be skipped, so we have to consult with the walker. if (numChildren === 0 || hasAnonChildren || isShadowHost(this.rawNode) || isShadowAnonymous(this.rawNode)) { numChildren = this.walker.countChildren(this); } return numChildren; }, get computedStyle() { if (!this._computedStyle) { this._computedStyle = CssLogic.getComputedStyle(this.rawNode); } return this._computedStyle; }, /** * Returns the computed display style property value of the node. */ get displayType() { // Consider all non-element nodes as displayed. if (InspectorActorUtils.isNodeDead(this) || this.rawNode.nodeType !== Node.ELEMENT_NODE || isAfterPseudoElement(this.rawNode) || isBeforePseudoElement(this.rawNode)) { return null; } const style = this.computedStyle; if (!style) { return null; } let display = null; try { display = style.display; } catch (e) { // Fails for <scrollbar> elements. } if (SUBGRID_ENABLED && (display === "grid" || display === "inline-grid") && (style.gridTemplateRows === "subgrid" || style.gridTemplateColumns === "subgrid")) { display = "subgrid"; } return display; }, /** * Is the node currently displayed? */ get isDisplayed() { const type = this.displayType; // Consider all non-elements or elements with no display-types to be displayed. if (!type) { return true; } // Otherwise consider elements to be displayed only if their display-types is other // than "none"". return type !== "none"; }, /** * Are there event listeners that are listening on this node? This method * uses all parsers registered via event-parsers.js.registerEventParser() to * check if there are any event listeners. */ get _hasEventListeners() { const parsers = this._eventParsers; for (const [, {hasListeners}] of parsers) { try { if (hasListeners && hasListeners(this.rawNode)) { return true; } } catch (e) { // An object attached to the node looked like a listener but wasn't... // do nothing. } } return false; }, writeAttrs: function() { if (!this.rawNode.attributes) { return undefined; } return [...this.rawNode.attributes].map(attr => { return {namespace: attr.namespace, name: attr.name, value: attr.value }; }); }, writePseudoClassLocks: function() { if (this.rawNode.nodeType !== Node.ELEMENT_NODE) { return undefined; } let ret = undefined; for (const pseudo of PSEUDO_CLASSES) { if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) { ret = ret || []; ret.push(pseudo); } } return ret; }, /** * Gets event listeners and adds their information to the events array. * * @param {Node} node * Node for which we are to get listeners. */ getEventListeners: function(node) { const parsers = this._eventParsers; const dbg = this.parent().targetActor.makeDebugger(); const listenerArray = []; for (const [, {getListeners, normalizeListener}] of parsers) { try { const listeners = getListeners(node); if (!listeners) { continue; } for (const listener of listeners) { if (normalizeListener) { listener.normalizeListener = normalizeListener; } this.processHandlerForEvent(node, listenerArray, dbg, listener); } } catch (e) { // An object attached to the node looked like a listener but wasn't... // do nothing. } } listenerArray.sort((a, b) => { return a.type.localeCompare(b.type); }); return listenerArray; }, /** * Retrieve the script location of the custom element definition for this node, when * relevant. To be linked to a custom element definition */ getCustomElementLocation: function() { // Get a reference to the custom element definition function. const name = this.rawNode.localName; const customElementsRegistry = this.rawNode.ownerGlobal.customElements; const customElement = customElementsRegistry && customElementsRegistry.get(name); if (!customElement) { return undefined; } // Create debugger object for the customElement function. const global = Cu.getGlobalForObject(customElement); const dbg = this.parent().targetActor.makeDebugger(); const globalDO = dbg.addDebuggee(global); const customElementDO = globalDO.makeDebuggeeValue(customElement); // Return undefined if we can't find a script for the custom element definition. if (!customElementDO.script) { return undefined; } return { url: customElementDO.script.url, line: customElementDO.script.startLine, }; }, /** * Process a handler * * @param {Node} node * The node for which we want information. * @param {Array} listenerArray * listenerArray contains all event objects that we have gathered * so far. * @param {Debugger} dbg * JSDebugger instance. * @param {Object} eventInfo * See event-parsers.js.registerEventParser() for a description of the * eventInfo object. * * @return {Array} * An array of objects where a typical object looks like this: * { * type: "click", * handler: function() { doSomething() }, * origin: "http://www.mozilla.com", * searchString: 'onclick="doSomething()"', * tags: tags, * DOM0: true, * capturing: true, * hide: { * DOM0: true * }, * native: false * } */ processHandlerForEvent: function(node, listenerArray, dbg, listener) { const { handler } = listener; const global = Cu.getGlobalForObject(handler); const globalDO = dbg.addDebuggee(global); let listenerDO = globalDO.makeDebuggeeValue(handler); const { normalizeListener } = listener; if (normalizeListener) { listenerDO = normalizeListener(listenerDO, listener); } const { capturing } = listener; let dom0 = false; let functionSource = handler.toString(); const hide = listener.hide || {}; let line = 0; let native = false; const override = listener.override || {}; const tags = listener.tags || ""; const type = listener.type || ""; let url = ""; // If the listener is an object with a 'handleEvent' method, use that. if (listenerDO.class === "Object" || /^XUL\w*Element$/.test(listenerDO.class)) { let desc; while (!desc && listenerDO) { desc = listenerDO.getOwnPropertyDescriptor("handleEvent"); listenerDO = listenerDO.proto; } if (desc && desc.value) { listenerDO = desc.value; } } // If the listener is bound to a different context then we need to switch // to the bound function. if (listenerDO.isBoundFunction) { listenerDO = listenerDO.boundTargetFunction; } const { isArrowFunction, name, script, parameterNames } = listenerDO; if (script) { const scriptSource = script.source.text; // Scripts are provided via script tags. If it wasn't provided by a // script tag it must be a DOM0 event. if (script.source.element) { dom0 = script.source.element.class !== "HTMLScriptElement"; } else { dom0 = false; } line = script.startLine; url = script.url; // Checking for the string "[native code]" is the only way at this point // to check for native code. Even if this provides a false positive then // grabbing the source code a second time is harmless. if (functionSource === "[object Object]" || functionSource === "[object XULElement]" || functionSource.includes("[native code]")) { functionSource = scriptSource.substr(script.sourceStart, script.sourceLength); // At this point the script looks like this: // () { ... } // We prefix this with "function" if it is not a fat arrow function. if (!isArrowFunction) { functionSource = "function " + functionSource; } } } else { // If the listener is a native one (provided by C++ code) then we have no // access to the script. We use the native flag to prevent showing the // debugger button because the script is not available. native = true; } // Fat arrow function text always contains the parameters. Function // parameters are often missing e.g. if Array.sort is used as a handler. // If they are missing we provide the parameters ourselves. if (parameterNames && parameterNames.length > 0) { const prefix = "function " + name + "()"; const paramString = parameterNames.join(", "); if (functionSource.startsWith(prefix)) { functionSource = functionSource.substr(prefix.length); functionSource = `function ${name} (${paramString})${functionSource}`; } } // If the listener is native code we display the filename "[native code]." // This is the official string and should *not* be translated. let origin; if (native) { origin = "[native code]"; } else { origin = url + ((dom0 || line === 0) ? "" : ":" + line); } const eventObj = { type: override.type || type, handler: override.handler || functionSource.trim(), origin: override.origin || origin, tags: override.tags || tags, DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0, capturing: typeof override.capturing !== "undefined" ? override.capturing : capturing, hide: typeof override.hide !== "undefined" ? override.hide : hide, native }; // Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are // generated dynamically from e.g. an onclick="" attribute so the script // doesn't actually exist. if (native || dom0) { eventObj.hide.debugger = true; } listenerArray.push(eventObj); dbg.removeDebuggee(globalDO); }, /** * Returns a LongStringActor with the node's value. */ getNodeValue: function() { return new LongStringActor(this.conn, this.rawNode.nodeValue || ""); }, /** * Set the node's value to a given string. */ setNodeValue: function(value) { this.rawNode.nodeValue = value; }, /** * Get a unique selector string for this node. */ getUniqueSelector: function() { if (Cu.isDeadWrapper(this.rawNode)) { return ""; } return findCssSelector(this.rawNode); }, /** * Get the full CSS path for this node. * * @return {String} A CSS selector with a part for the node and each of its ancestors. */ getCssPath: function() { if (Cu.isDeadWrapper(this.rawNode)) { return ""; } return getCssPath(this.rawNode); }, /** * Get the XPath for this node. * * @return {String} The XPath for finding this node on the page. */ getXPath: function() { if (Cu.isDeadWrapper(this.rawNode)) { return ""; } return getXPath(this.rawNode); }, /** * Scroll the selected node into view. */ scrollIntoView: function() { this.rawNode.scrollIntoView(true); }, /** * Get the node's image data if any (for canvas and img nodes). * Returns an imageData object with the actual data being a LongStringActor * and a size json object. * The image data is transmitted as a base64 encoded png data-uri. * The method rejects if the node isn't an image or if the image is missing * * Accepts a maxDim request parameter to resize images that are larger. This * is important as the resizing occurs server-side so that image-data being * transfered in the longstring back to the client will be that much smaller */ getImageData: function(maxDim) { return InspectorActorUtils.imageToImageData(this.rawNode, maxDim).then(imageData => { return { data: LongStringActor(this.conn, imageData.data), size: imageData.size }; }); }, /** * Get all event listeners that are listening on this node. */ getEventListenerInfo: function() { const node = this.rawNode; if (this.rawNode.nodeName.toLowerCase() === "html") { const winListeners = this.getEventListeners(node.ownerGlobal) || []; const docElementListeners = this.getEventListeners(node) || []; const docListeners = this.getEventListeners(node.parentNode) || []; return [...winListeners, ...docElementListeners, ...docListeners].sort((a, b) => { return a.type.localeCompare(b.type); }); } return this.getEventListeners(node); }, /** * Modify a node's attributes. Passed an array of modifications * similar in format to "attributes" mutations. * { * attributeName: <string> * attributeNamespace: <optional string> * newValue: <optional string> - If null or undefined, the attribute * will be removed. * } * * Returns when the modifications have been made. Mutations will * be queued for any changes made. */ modifyAttributes: function(modifications) { const rawNode = this.rawNode; for (const change of modifications) { if (change.newValue == null) { if (change.attributeNamespace) { rawNode.removeAttributeNS(change.attributeNamespace, change.attributeName); } else { rawNode.removeAttribute(change.attributeName); } } else if (change.attributeNamespace) { rawNode.setAttributeNS(change.attributeNamespace, change.attributeName, change.newValue); } else { rawNode.setAttribute(change.attributeName, change.newValue); } } }, /** * Given the font and fill style, get the image data of a canvas with the * preview text and font. * Returns an imageData object with the actual data being a LongStringActor * and the width of the text as a string. * The image data is transmitted as a base64 encoded png data-uri. */ getFontFamilyDataURL: function(font, fillStyle = "black") { const doc = this.rawNode.ownerDocument; const options = { previewText: FONT_FAMILY_PREVIEW_TEXT, previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE, fillStyle: fillStyle }; const { dataURL, size } = getFontPreviewData(font, doc, options); return { data: LongStringActor(this.conn, dataURL), size: size }; }, /** * Finds the computed background color of the closest parent with a set background * color. * * @return {String} * String with the background color of the form rgba(r, g, b, a). Defaults to * rgba(255, 255, 255, 1) if no background color is found. */ getClosestBackgroundColor: function() { return InspectorActorUtils.getClosestBackgroundColor(this.rawNode); }, /** * Returns an object with the width and height of the node's owner window. * * @return {Object} */ getOwnerGlobalDimensions: function() { const win = this.rawNode.ownerGlobal; return { innerWidth: win.innerWidth, innerHeight: win.innerHeight, }; } });
exports.ReflowActor = protocol.ActorClassWithSpec(reflowSpec, { initialize: function (conn, tabActor) { protocol.Actor.prototype.initialize.call(this, conn); this.tabActor = tabActor; this._onReflow = this._onReflow.bind(this); this.observer = getLayoutChangesObserver(tabActor); this._isStarted = false; }, destroy: function () { this.stop(); releaseLayoutChangesObserver(this.tabActor); this.observer = null; this.tabActor = null; protocol.Actor.prototype.destroy.call(this); }, /** * Start tracking reflows and sending events to clients about them. * This is a oneway method, do not expect a response and it won't return a * promise. */ start: function () { if (!this._isStarted) { this.observer.on("reflows", this._onReflow); this._isStarted = true; } }, /** * Stop tracking reflows and sending events to clients about them. * This is a oneway method, do not expect a response and it won't return a * promise. */ stop: function () { if (this._isStarted) { this.observer.off("reflows", this._onReflow); this._isStarted = false; } }, _onReflow: function (event, reflows) { if (this._isStarted) { events.emit(this, "reflows", reflows); } } });
var FunctionCallActor = protocol.ActorClassWithSpec(functionCallSpec, { /** * Creates the function call actor. * * @param DebuggerServerConnection conn * The server connection. * @param DOMWindow window * The content window. * @param string global * The name of the global object owning this function, like * "CanvasRenderingContext2D" or "WebGLRenderingContext". * @param object caller * The object owning the function when it was called. * For example, in `foo.bar()`, the caller is `foo`. * @param number type * Either METHOD_FUNCTION, METHOD_GETTER or METHOD_SETTER. * @param string name * The called function's name. * @param array stack * The called function's stack, as a list of { name, file, line } objects. * @param number timestamp * The performance.now() timestamp when the function was called. * @param array args * The called function's arguments. * @param any result * The value returned by the function call. * @param boolean holdWeak * Determines whether or not FunctionCallActor stores a weak reference * to the underlying objects. */ initialize: function ( conn, [window, global, caller, type, name, stack, timestamp, args, result], holdWeak ) { protocol.Actor.prototype.initialize.call(this, conn); this.details = { global: global, type: type, name: name, stack: stack, timestamp: timestamp }; // Store a weak reference to all objects so we don't // prevent natural GC if `holdWeak` was passed into // setup as truthy. if (holdWeak) { let weakRefs = { window: Cu.getWeakReference(window), caller: Cu.getWeakReference(caller), args: Cu.getWeakReference(args), result: Cu.getWeakReference(result), }; Object.defineProperties(this.details, { window: { get: () => weakRefs.window.get() }, caller: { get: () => weakRefs.caller.get() }, args: { get: () => weakRefs.args.get() }, result: { get: () => weakRefs.result.get() }, }); } else { // Otherwise, hold strong references to the objects. this.details.window = window; this.details.caller = caller; this.details.args = args; this.details.result = result; } // The caller, args and results are string names for now. It would // certainly be nicer if they were Object actors. Make this smarter, so // that the frontend can inspect each argument, be it object or primitive. // Bug 978960. this.details.previews = { caller: this._generateStringPreview(caller), args: this._generateArgsPreview(args), result: this._generateStringPreview(result) }; }, /** * Customize the marshalling of this actor to provide some generic information * directly on the Front instance. */ form: function () { return { actor: this.actorID, type: this.details.type, name: this.details.name, file: this.details.stack[0].file, line: this.details.stack[0].line, timestamp: this.details.timestamp, callerPreview: this.details.previews.caller, argsPreview: this.details.previews.args, resultPreview: this.details.previews.result }; }, /** * Gets more information about this function call, which is not necessarily * available on the Front instance. */ getDetails: function () { let { type, name, stack, timestamp } = this.details; // Since not all calls on the stack have corresponding owner files (e.g. // callbacks of a requestAnimationFrame etc.), there's no benefit in // returning them, as the user can't jump to the Debugger from them. for (let i = stack.length - 1; ;) { if (stack[i].file) { break; } stack.pop(); i--; } // XXX: Use grips for objects and serialize them properly, in order // to add the function's caller, arguments and return value. Bug 978957. return { type: type, name: name, stack: stack, timestamp: timestamp }; }, /** * Serializes the arguments so that they can be easily be transferred * as a string, but still be useful when displayed in a potential UI. * * @param array args * The source arguments. * @return string * The arguments as a string. */ _generateArgsPreview: function (args) { let { global, name, caller } = this.details; // Get method signature to determine if there are any enums // used in this method. let methodSignatureEnums; let knownGlobal = CallWatcherFront.KNOWN_METHODS[global]; if (knownGlobal) { let knownMethod = knownGlobal[name]; if (knownMethod) { let isOverloaded = typeof knownMethod.enums === "function"; if (isOverloaded) { methodSignatureEnums = methodSignatureEnums(args); } else { methodSignatureEnums = knownMethod.enums; } } } let serializeArgs = () => args.map((arg, i) => { // XXX: Bug 978960. if (arg === undefined) { return "undefined"; } if (arg === null) { return "null"; } if (typeof arg == "function") { return "Function"; } if (typeof arg == "object") { return "Object"; } // If this argument matches the method's signature // and is an enum, change it to its constant name. if (methodSignatureEnums && methodSignatureEnums.has(i)) { return getBitToEnumValue(global, caller, arg); } return arg + ""; }); return serializeArgs().join(", "); }, /** * Serializes the data so that it can be easily be transferred * as a string, but still be useful when displayed in a potential UI. * * @param object data * The source data. * @return string * The arguments as a string. */ _generateStringPreview: function (data) { // XXX: Bug 978960. if (data === undefined) { return "undefined"; } if (data === null) { return "null"; } if (typeof data == "function") { return "Function"; } if (typeof data == "object") { return "Object"; } return data + ""; } });
return global.document.ownerGlobal && this.isExtensionWindowDescendent(global.document.ownerGlobal); } try { // This will fail for non-Sandbox objects, hence the try-catch block. const metadata = Cu.getSandboxMetadata(global); if (metadata) { return metadata.addonID === this.addonId; } } catch (e) { // Unable to retrieve the sandbox metadata. } return false; }; // Handlers for the messages received from the parent actor. webExtensionTargetPrototype._onParentExit = function(msg) { if (msg.json.actor !== this.actorID) { return; } this.exit(); }; exports.WebExtensionTargetActor = ActorClassWithSpec(webExtensionTargetSpec, webExtensionTargetPrototype);