Exemplo n.º 1
0
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");
  },
});
Exemplo n.º 2
0
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);
  },
Exemplo n.º 3
0
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);
    }
  },
});
Exemplo n.º 4
0
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;
  },

});
Exemplo n.º 5
0
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;
Exemplo n.º 7
0
 * 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};
    }
  }
);
Exemplo n.º 9
0
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};
    });
  }
});
Exemplo n.º 10
0
  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);
  }
Exemplo n.º 11
0
   *         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";
Exemplo n.º 12
0
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);
    }
  },
});
Exemplo n.º 13
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";
  },

  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 });
  }
});
Exemplo n.º 14
0
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);
  }
});
Exemplo n.º 15
0
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;
  },
});
Exemplo n.º 16
0
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, {
Exemplo n.º 18
0
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};
    });
  }
});
Exemplo n.º 19
0
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();
  },
});
Exemplo n.º 20
0
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);
  }
});
Exemplo n.º 21
0
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;
    }
  }
});
Exemplo n.º 22
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;
  },
});
Exemplo n.º 23
0
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;
  }
});
Exemplo n.º 24
0
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));
  }
});
Exemplo n.º 25
0
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();
  },
});
Exemplo n.º 26
0
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;
  }
});
Exemplo n.º 27
0
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,
    };
  }
});
Exemplo n.º 28
0
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);
    }
  }
});
Exemplo n.º 29
0
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 + "";
  }
});
Exemplo n.º 30
0
    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);