Пример #1
0
var PageStyleActor = protocol.ActorClass({
  typeName: "pagestyle",

  /**
   * Create a PageStyleActor.
   *
   * @param inspector
   *    The InspectorActor that owns this PageStyleActor.
   *
   * @constructor
   */
  initialize: function(inspector) {
    protocol.Actor.prototype.initialize.call(this, null);
    this.inspector = inspector;
    if (!this.inspector.walker) {
      throw Error("The inspector's WalkerActor must be created before " +
                   "creating a PageStyleActor.");
    }
    this.walker = inspector.walker;
    this.cssLogic = new CssLogic();

    // Stores the association of DOM objects -> actors
    this.refMap = new Map();

    this.onFrameUnload = this.onFrameUnload.bind(this);
    events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
  },

  destroy: function () {
    if (!this.walker) {
      return;
    }
    protocol.Actor.prototype.destroy.call(this);
    events.off(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
    this.inspector = null;
    this.walker = null;
    this.refMap = null;
    this.cssLogic = null;
    this._styleElement = null;
  },

  get conn() {
    return this.inspector.conn;
  },

  form: function(detail) {
    if (detail === "actorid") {
      return this.actorID;
    }

    return {
      actor: this.actorID,
      traits: {
        // Whether the actor has had bug 1103993 fixed, which means that the
        // getApplied method calls cssLogic.highlight(node) to recreate the
        // style cache. Clients requesting getApplied from actors that have not
        // been fixed must make sure cssLogic.highlight(node) was called before.
        getAppliedCreatesStyleCache: true
      }
    };
  },

  /**
   * Return or create a StyleRuleActor for the given item.
   * @param item Either a CSSStyleRule or a DOM element.
   */
  _styleRef: function(item) {
    if (this.refMap.has(item)) {
      return this.refMap.get(item);
    }
    let actor = StyleRuleActor(this, item);
    this.manage(actor);
    this.refMap.set(item, actor);

    return actor;
  },

  /**
   * Return or create a StyleSheetActor for the given nsIDOMCSSStyleSheet.
   * @param  {DOMStyleSheet} sheet
   *         The style sheet to create an actor for.
   * @return {StyleSheetActor}
   *         The actor for this style sheet
   */
  _sheetRef: function(sheet) {
    let tabActor = this.inspector.tabActor;
    let actor = tabActor.createStyleSheetActor(sheet);
    return actor;
  },

  /**
   * Get the computed style for a node.
   *
   * @param NodeActor node
   * @param object options
   *   `filter`: A string filter that affects the "matched" handling.
   *     'user': Include properties from user style sheets.
   *     'ua': Include properties from user and user-agent sheets.
   *     Default value is 'ua'
   *   `markMatched`: true if you want the 'matched' property to be added
   *     when a computed property has been modified by a style included
   *     by `filter`.
   *   `onlyMatched`: true if unmatched properties shouldn't be included.
   *
   * @returns a JSON blob with the following form:
   *   {
   *     "property-name": {
   *       value: "property-value",
   *       priority: "!important" <optional>
   *       matched: <true if there are matched selectors for this value>
   *     },
   *     ...
   *   }
   */
  getComputed: method(function(node, options) {
    let ret = Object.create(null);

    this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA;
    this.cssLogic.highlight(node.rawNode);
    let computed = this.cssLogic.computedStyle || [];

    Array.prototype.forEach.call(computed, name => {
      ret[name] = {
        value: computed.getPropertyValue(name),
        priority: computed.getPropertyPriority(name) || undefined
      };
    });

    if (options.markMatched || options.onlyMatched) {
      let matched = this.cssLogic.hasMatchedSelectors(Object.keys(ret));
      for (let key in ret) {
        if (matched[key]) {
          ret[key].matched = options.markMatched ? true : undefined;
        } else if (options.onlyMatched) {
          delete ret[key];
        }
      }
    }

    return ret;
  }, {
    request: {
      node: Arg(0, "domnode"),
      markMatched: Option(1, "boolean"),
      onlyMatched: Option(1, "boolean"),
      filter: Option(1, "string"),
    },
    response: {
      computed: RetVal("json")
    }
  }),

  /**
   * Get all the fonts from a page.
   *
   * @param object options
   *   `includePreviews`: Whether to also return image previews of the fonts.
   *   `previewText`: The text to display in the previews.
   *   `previewFontSize`: The font size of the text in the previews.
   *
   * @returns object
   *   object with 'fontFaces', a list of fonts that apply to this node.
   */
  getAllUsedFontFaces: method(function(options) {
    let windows = this.inspector.tabActor.windows;
    let fontsList = [];
    for (let win of windows) {
      fontsList = [...fontsList,
                   ...this.getUsedFontFaces(win.document.body, options)];
    }
    return fontsList;
  }, {
    request: {
      includePreviews: Option(0, "boolean"),
      previewText: Option(0, "string"),
      previewFontSize: Option(0, "string"),
      previewFillStyle: Option(0, "string")
    },
    response: {
      fontFaces: RetVal("array:fontface")
    }
  }),

  /**
   * Get the font faces used in an element.
   *
   * @param NodeActor node / actual DOM node
   *    The node to get fonts from.
   * @param object options
   *   `includePreviews`: Whether to also return image previews of the fonts.
   *   `previewText`: The text to display in the previews.
   *   `previewFontSize`: The font size of the text in the previews.
   *
   * @returns object
   *   object with 'fontFaces', a list of fonts that apply to this node.
   */
  getUsedFontFaces: method(function(node, options) {
    // node.rawNode is defined for NodeActor objects
    let actualNode = node.rawNode || node;
    let contentDocument = actualNode.ownerDocument;
    // We don't get fonts for a node, but for a range
    let rng = contentDocument.createRange();
    rng.selectNodeContents(actualNode);
    let fonts = DOMUtils.getUsedFontFaces(rng);
    let fontsArray = [];

    for (let i = 0; i < fonts.length; i++) {
      let font = fonts.item(i);
      let fontFace = {
        name: font.name,
        CSSFamilyName: font.CSSFamilyName,
        srcIndex: font.srcIndex,
        URI: font.URI,
        format: font.format,
        localName: font.localName,
        metadata: font.metadata
      };

      // If this font comes from a @font-face rule
      if (font.rule) {
        let styleActor = StyleRuleActor(this, font.rule);
        this.manage(styleActor);
        fontFace.rule = styleActor;
        fontFace.ruleText = font.rule.cssText;
      }

      // Get the weight and style of this font for the preview and sort order
      let weight = NORMAL_FONT_WEIGHT, style = "";
      if (font.rule) {
        weight = font.rule.style.getPropertyValue("font-weight")
                 || NORMAL_FONT_WEIGHT;
        if (weight == "bold") {
          weight = BOLD_FONT_WEIGHT;
        } else if (weight == "normal") {
          weight = NORMAL_FONT_WEIGHT;
        }
        style = font.rule.style.getPropertyValue("font-style") || "";
      }
      fontFace.weight = weight;
      fontFace.style = style;

      if (options.includePreviews) {
        let opts = {
          previewText: options.previewText,
          previewFontSize: options.previewFontSize,
          fontStyle: weight + " " + style,
          fillStyle: options.previewFillStyle
        };
        let { dataURL, size } = getFontPreviewData(font.CSSFamilyName,
                                                   contentDocument, opts);
        fontFace.preview = {
          data: LongStringActor(this.conn, dataURL),
          size: size
        };
      }
      fontsArray.push(fontFace);
    }

    // @font-face fonts at the top, then alphabetically, then by weight
    fontsArray.sort(function(a, b) {
      return a.weight > b.weight ? 1 : -1;
    });
    fontsArray.sort(function(a, b) {
      if (a.CSSFamilyName == b.CSSFamilyName) {
        return 0;
      }
      return a.CSSFamilyName > b.CSSFamilyName ? 1 : -1;
    });
    fontsArray.sort(function(a, b) {
      if ((a.rule && b.rule) || (!a.rule && !b.rule)) {
        return 0;
      }
      return !a.rule && b.rule ? 1 : -1;
    });

    return fontsArray;
  }, {
    request: {
      node: Arg(0, "domnode"),
      includePreviews: Option(1, "boolean"),
      previewText: Option(1, "string"),
      previewFontSize: Option(1, "string"),
      previewFillStyle: Option(1, "string")
    },
    response: {
      fontFaces: RetVal("array:fontface")
    }
  }),

  /**
   * Get a list of selectors that match a given property for a node.
   *
   * @param NodeActor node
   * @param string property
   * @param object options
   *   `filter`: A string filter that affects the "matched" handling.
   *     'user': Include properties from user style sheets.
   *     'ua': Include properties from user and user-agent sheets.
   *     Default value is 'ua'
   *
   * @returns a JSON object with the following form:
   *   {
   *     // An ordered list of rules that apply
   *     matched: [{
   *       rule: <rule actorid>,
   *       sourceText: <string>, // The source of the selector, relative
   *                             // to the node in question.
   *       selector: <string>, // the selector ID that matched
   *       value: <string>, // the value of the property
   *       status: <int>,
   *         // The status of the match - high numbers are better placed
   *         // to provide styling information:
   *         // 3: Best match, was used.
   *         // 2: Matched, but was overridden.
   *         // 1: Rule from a parent matched.
   *         // 0: Unmatched (never returned in this API)
   *     }, ...],
   *
   *     // The full form of any domrule referenced.
   *     rules: [ <domrule>, ... ], // The full form of any domrule referenced
   *
   *     // The full form of any sheets referenced.
   *     sheets: [ <domsheet>, ... ]
   *  }
   */
  getMatchedSelectors: method(function(node, property, options) {
    this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA;
    this.cssLogic.highlight(node.rawNode);

    let rules = new Set();
    let sheets = new Set();

    let matched = [];
    let propInfo = this.cssLogic.getPropertyInfo(property);
    for (let selectorInfo of propInfo.matchedSelectors) {
      let cssRule = selectorInfo.selector.cssRule;
      let domRule = cssRule.sourceElement || cssRule.domRule;

      let rule = this._styleRef(domRule);
      rules.add(rule);

      matched.push({
        rule: rule,
        sourceText: this.getSelectorSource(selectorInfo, node.rawNode),
        selector: selectorInfo.selector.text,
        name: selectorInfo.property,
        value: selectorInfo.value,
        status: selectorInfo.status
      });
    }

    this.expandSets(rules, sheets);

    return {
      matched: matched,
      rules: [...rules],
      sheets: [...sheets]
    };
  }, {
    request: {
      node: Arg(0, "domnode"),
      property: Arg(1, "string"),
      filter: Option(2, "string")
    },
    response: RetVal(types.addDictType("matchedselectorresponse", {
      rules: "array:domstylerule",
      sheets: "array:stylesheet",
      matched: "array:matchedselector"
    }))
  }),

  // Get a selector source for a CssSelectorInfo relative to a given
  // node.
  getSelectorSource: function(selectorInfo, relativeTo) {
    let result = selectorInfo.selector.text;
    if (selectorInfo.elementStyle) {
      let source = selectorInfo.sourceElement;
      if (source === relativeTo) {
        result = "this";
      } else {
        result = CssLogic.getShortName(source);
      }
      result += ".style";
    }
    return result;
  },

  /**
   * Get the set of styles that apply to a given node.
   * @param NodeActor node
   * @param object options
   *   `filter`: A string filter that affects the "matched" handling.
   *     'user': Include properties from user style sheets.
   *     'ua': Include properties from user and user-agent sheets.
   *     Default value is 'ua'
   *   `inherited`: Include styles inherited from parent nodes.
   *   `matchedSelectors`: Include an array of specific selectors that
   *     caused this rule to match its node.
   */
  getApplied: method(function(node, options) {
    if (!node) {
      return {entries: [], rules: [], sheets: []};
    }

    this.cssLogic.highlight(node.rawNode);
    let entries = [];
    entries = entries.concat(this._getAllElementRules(node, undefined, options));
    return this.getAppliedProps(node, entries, options);
  }, {
    request: {
      node: Arg(0, "domnode"),
      inherited: Option(1, "boolean"),
      matchedSelectors: Option(1, "boolean"),
      filter: Option(1, "string")
    },
    response: RetVal("appliedStylesReturn")
  }),

  _hasInheritedProps: function(style) {
    return Array.prototype.some.call(style, prop => {
      return DOMUtils.isInheritedProperty(prop);
    });
  },

  /**
   * Helper function for getApplied, gets all the rules from a given
   * element. See getApplied for documentation on parameters.
   * @param NodeActor node
   * @param bool inherited
   * @param object options

   * @return Array The rules for a given element. Each item in the
   *               array has the following signature:
   *                - rule RuleActor
   *                - isSystem Boolean
   *                - inherited Boolean
   *                - pseudoElement String
   */
  _getAllElementRules: function(node, inherited, options) {
    let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(node.rawNode);
    let rules = [];

    if (!bindingElement || !bindingElement.style) {
      return rules;
    }

    let elementStyle = this._styleRef(bindingElement);
    let showElementStyles = !inherited && !pseudo;
    let showInheritedStyles = inherited &&
                              this._hasInheritedProps(bindingElement.style);

    let rule = {
      rule: elementStyle,
      pseudoElement: null,
      isSystem: false,
      inherited: false
    };

    // First any inline styles
    if (showElementStyles) {
      rules.push(rule);
    }

    // Now any inherited styles
    if (showInheritedStyles) {
      rule.inherited = inherited;
      rules.push(rule);
    }

    // Add normal rules.  Typically this is passing in the node passed into the
    // function, unless if that node was ::before/::after.  In which case,
    // it will pass in the parentNode along with "::before"/"::after".
    this._getElementRules(bindingElement, pseudo, inherited, options).forEach(rule => {
      // The only case when there would be a pseudo here is ::before/::after,
      // and in this case we want to tell the view that it belongs to the
      // element (which is a _moz_generated_content native anonymous element).
      rule.pseudoElement = null;
      rules.push(rule);
    });

    // Now any pseudos (except for ::before / ::after, which was handled as
    // a 'normal rule' above.
    if (showElementStyles) {
      for (let pseudo of PSEUDO_ELEMENTS_TO_READ) {
        this._getElementRules(bindingElement, pseudo, inherited, options).forEach(rule => {
          rules.push(rule);
        });
      }
    }

    return rules;
  },

  /**
   * Helper function for _getAllElementRules, returns the rules from a given
   * element. See getApplied for documentation on parameters.
   * @param DOMNode node
   * @param string pseudo
   * @param DOMNode inherited
   * @param object options
   *
   * @returns Array
   */
  _getElementRules: function(node, pseudo, inherited, options) {
    let domRules = DOMUtils.getCSSStyleRules(node, pseudo);
    if (!domRules) {
      return [];
    }

    let rules = [];

    // getCSSStyleRules returns ordered from least-specific to
    // most-specific.
    for (let i = domRules.Count() - 1; i >= 0; i--) {
      let domRule = domRules.GetElementAt(i);

      let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet);

      if (isSystem && options.filter != CssLogic.FILTER.UA) {
        continue;
      }

      if (inherited) {
        // Don't include inherited rules if none of its properties
        // are inheritable.
        let hasInherited = [...domRule.style].some(
          prop => DOMUtils.isInheritedProperty(prop)
        );
        if (!hasInherited) {
          continue;
        }
      }

      let ruleActor = this._styleRef(domRule);
      rules.push({
        rule: ruleActor,
        inherited: inherited,
        isSystem: isSystem,
        pseudoElement: pseudo
      });
    }
    return rules;
  },

  /**
   * Helper function for getApplied that fetches a set of style properties that
   * apply to the given node and associated rules
   * @param NodeActor node
   * @param object options
   *   `filter`: A string filter that affects the "matched" handling.
   *     'user': Include properties from user style sheets.
   *     'ua': Include properties from user and user-agent sheets.
   *     Default value is 'ua'
   *   `inherited`: Include styles inherited from parent nodes.
   *   `matchedSeletors`: Include an array of specific selectors that
   *     caused this rule to match its node.
   * @param array entries
   *   List of appliedstyle objects that lists the rules that apply to the
   *   node. If adding a new rule to the stylesheet, only the new rule entry
   *   is provided and only the style properties that apply to the new
   *   rule is fetched.
   * @returns Object containing the list of rule entries, rule actors and
   *   stylesheet actors that applies to the given node and its associated
   *   rules.
   */
  getAppliedProps: function(node, entries, options) {
    if (options.inherited) {
      let parent = this.walker.parentNode(node);
      while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) {
        entries = entries.concat(this._getAllElementRules(parent, parent, options));
        parent = this.walker.parentNode(parent);
      }
    }

    if (options.matchedSelectors) {
      for (let entry of entries) {
        if (entry.rule.type === ELEMENT_STYLE) {
          continue;
        }

        let domRule = entry.rule.rawRule;
        let selectors = CssLogic.getSelectors(domRule);
        let element = entry.inherited ? entry.inherited.rawNode : node.rawNode;

        let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(element);
        entry.matchedSelectors = [];
        for (let i = 0; i < selectors.length; i++) {
          if (DOMUtils.selectorMatchesElement(bindingElement, domRule, i, pseudo)) {
            entry.matchedSelectors.push(selectors[i]);
          }
        }
      }
    }

    // Add all the keyframes rule associated with the element
    let computedStyle = this.cssLogic.computedStyle;
    if (computedStyle) {
      let animationNames = computedStyle.animationName.split(",");
      animationNames = animationNames.map(name => name.trim());

      if (animationNames) {
        // Traverse through all the available keyframes rule and add
        // the keyframes rule that matches the computed animation name
        for (let keyframesRule of this.cssLogic.keyframesRules) {
          if (animationNames.indexOf(keyframesRule.name) > -1) {
            for (let rule of keyframesRule.cssRules) {
              entries.push({
                rule: this._styleRef(rule),
                keyframes: this._styleRef(keyframesRule)
              });
            }
          }
        }
      }
    }

    let rules = new Set();
    let sheets = new Set();
    entries.forEach(entry => rules.add(entry.rule));
    this.expandSets(rules, sheets);

    return {
      entries: entries,
      rules: [...rules],
      sheets: [...sheets]
    };
  },

  /**
   * Expand Sets of rules and sheets to include all parent rules and sheets.
   */
  expandSets: function(ruleSet, sheetSet) {
    // Sets include new items in their iteration
    for (let rule of ruleSet) {
      if (rule.rawRule.parentRule) {
        let parent = this._styleRef(rule.rawRule.parentRule);
        if (!ruleSet.has(parent)) {
          ruleSet.add(parent);
        }
      }
      if (rule.rawRule.parentStyleSheet) {
        let parent = this._sheetRef(rule.rawRule.parentStyleSheet);
        if (!sheetSet.has(parent)) {
          sheetSet.add(parent);
        }
      }
    }

    for (let sheet of sheetSet) {
      if (sheet.rawSheet.parentStyleSheet) {
        let parent = this._sheetRef(sheet.rawSheet.parentStyleSheet);
        if (!sheetSet.has(parent)) {
          sheetSet.add(parent);
        }
      }
    }
  },

  /**
   * Get layout-related information about a node.
   * This method returns an object with properties giving information about
   * the node's margin, border, padding and content region sizes, as well
   * as information about the type of box, its position, z-index, etc...
   * @param {NodeActor} node
   * @param {Object} options The only available option is autoMargins.
   * If set to true, the element's margins will receive an extra check to see
   * whether they are set to "auto" (knowing that the computed-style in this
   * case would return "0px").
   * The returned object will contain an extra property (autoMargins) listing
   * all margins that are set to auto, e.g. {top: "auto", left: "auto"}.
   * @return {Object}
   */
  getLayout: method(function(node, options) {
    this.cssLogic.highlight(node.rawNode);

    let layout = {};

    // First, we update the first part of the layout view, with
    // the size of the element.

    let clientRect = node.rawNode.getBoundingClientRect();
    layout.width = parseFloat(clientRect.width.toPrecision(6));
    layout.height = parseFloat(clientRect.height.toPrecision(6));

    // We compute and update the values of margins & co.
    let style = CssLogic.getComputedStyle(node.rawNode);
    for (let prop of [
      "position",
      "margin-top",
      "margin-right",
      "margin-bottom",
      "margin-left",
      "padding-top",
      "padding-right",
      "padding-bottom",
      "padding-left",
      "border-top-width",
      "border-right-width",
      "border-bottom-width",
      "border-left-width",
      "z-index",
      "box-sizing",
      "display"
    ]) {
      layout[prop] = style.getPropertyValue(prop);
    }

    if (options.autoMargins) {
      layout.autoMargins = this.processMargins(this.cssLogic);
    }

    for (let i in this.map) {
      let property = this.map[i].property;
      this.map[i].value = parseFloat(style.getPropertyValue(property));
    }

    return layout;
  }, {
    request: {
      node: Arg(0, "domnode"),
      autoMargins: Option(1, "boolean")
    },
    response: RetVal("json")
  }),

  /**
   * Find 'auto' margin properties.
   */
  processMargins: function(cssLogic) {
    let margins = {};

    for (let prop of ["top", "bottom", "left", "right"]) {
      let info = cssLogic.getPropertyInfo("margin-" + prop);
      let selectors = info.matchedSelectors;
      if (selectors && selectors.length > 0 && selectors[0].value == "auto") {
        margins[prop] = "auto";
      }
    }

    return margins;
  },

  /**
   * On page navigation, tidy up remaining objects.
   */
  onFrameUnload: function() {
    this._styleElement = null;
  },

  /**
   * Helper function to addNewRule to construct a new style tag in the document.
   * @returns DOMElement of the style tag
   */
  get styleElement() {
    if (!this._styleElement) {
      let document = this.inspector.window.document;
      let style = document.createElementNS(XHTML_NS, "style");
      style.setAttribute("type", "text/css");
      document.documentElement.appendChild(style);
      this._styleElement = style;
    }

    return this._styleElement;
  },

  /**
   * Helper function for adding a new rule and getting its applied style
   * properties
   * @param NodeActor node
   * @param CSSStyleRUle rule
   * @returns Object containing its applied style properties
   */
  getNewAppliedProps: function(node, rule) {
    let ruleActor = this._styleRef(rule);
    return this.getAppliedProps(node, [{ rule: ruleActor }],
      { matchedSelectors: true });
  },

  /**
   * Adds a new rule, and returns the new StyleRuleActor.
   * @param NodeActor node
   * @param [string] pseudoClasses The list of pseudo classes to append to the
   * new selector.
   * @returns StyleRuleActor of the new rule
   */
  addNewRule: method(function(node, pseudoClasses) {
    let style = this.styleElement;
    let sheet = style.sheet;
    let cssRules = sheet.cssRules;
    let rawNode = node.rawNode;

    let selector;
    if (rawNode.id) {
      selector = "#" + CSS.escape(rawNode.id);
    } else if (rawNode.className) {
      selector = "." + [...rawNode.classList].map(c => CSS.escape(c)).join(".");
    } else {
      selector = rawNode.tagName.toLowerCase();
    }

    if (pseudoClasses && pseudoClasses.length > 0) {
      selector += pseudoClasses.join("");
    }

    let index = sheet.insertRule(selector + " {}", cssRules.length);
    return this.getNewAppliedProps(node, cssRules.item(index));
  }, {
    request: {
      node: Arg(0, "domnode"),
      pseudoClasses: Arg(1, "nullable:array:string")
    },
    response: RetVal("appliedStylesReturn")
  }),
});
Пример #2
0
/**
 * The ActorActor gives you a handle to an actor you've dynamically
 * registered and allows you to unregister it.
 */
const ActorActor = protocol.ActorClass({
  typeName: "actorActor",

  initialize: function (conn, options) {
    protocol.Actor.prototype.initialize.call(this, conn);

    this.options = options;
  },

  unregister: method(function () {
    if (this.options.tab) {
      DebuggerServer.removeTabActor(this.options);
    }

    if (this.options.global) {
      DebuggerServer.removeGlobalActor(this.options);
    }
  }, {
    request: {},
    response: {}
  })
});

const ActorActorFront = protocol.FrontClass(ActorActor, {
  initialize: function (client, form) {
    protocol.Front.prototype.initialize.call(this, client, form);
  }
Пример #3
0
const { registerActor, unregisterActor } = require("devtools/server/actors/utils/actor-registry-utils");

loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");

/**
 * The ActorActor gives you a handle to an actor you've dynamically
 * registered and allows you to unregister it.
 */
const ActorActor = protocol.ActorClass({
  typeName: "actorActor",

  initialize: function (conn, options) {
    protocol.Actor.prototype.initialize.call(this, conn);

    this.options = options;
  },

  unregister: method(function () {
    unregisterActor(this.options);
  }, {
    request: {},
    response: {}
  })
});

const ActorActorFront = protocol.FrontClass(ActorActor, {
  initialize: function (client, form) {
    protocol.Front.prototype.initialize.call(this, client, form);
  }
});

exports.ActorActorFront = ActorActorFront;
Пример #4
0
let PreferenceActor = protocol.ActorClass({
  typeName: "preference",

  getBoolPref: method(function(name) {
    return Services.prefs.getBoolPref(name);
  }, {
    request: { value: Arg(0) },
    response: { value: RetVal("boolean") }
  }),

  getCharPref: method(function(name) {
    return Services.prefs.getCharPref(name);
  }, {
    request: { value: Arg(0) },
    response: { value: RetVal("string") }
  }),

  getIntPref: method(function(name) {
    return Services.prefs.getIntPref(name);
  }, {
    request: { value: Arg(0) },
    response: { value: RetVal("number") }
  }),

  getAllPrefs: method(function() {
    let prefs = {};
    Services.prefs.getChildList("").forEach(function(name, index) {
      // append all key/value pairs into a huge json object.
      try {
        let value;
        switch (Services.prefs.getPrefType(name)) {
          case Ci.nsIPrefBranch.PREF_STRING:
            value = Services.prefs.getCharPref(name);
            break;
          case Ci.nsIPrefBranch.PREF_INT:
            value = Services.prefs.getIntPref(name);
            break;
          case Ci.nsIPrefBranch.PREF_BOOL:
            value = Services.prefs.getBoolPref(name);
            break;
          default:
        }
        prefs[name] = {
          value: value,
          hasUserValue: Services.prefs.prefHasUserValue(name)
        };
      } catch (e) {
        // pref exists but has no user or default value
      }
    });
    return prefs;
  }, {
    request: {},
    response: { value: RetVal("json") }
  }),

  setBoolPref: method(function(name, value) {
    Services.prefs.setBoolPref(name, value);
    Services.prefs.savePrefFile(null);
  }, {
    request: { name: Arg(0), value: Arg(1) },
    response: {}
  }),

  setCharPref: method(function(name, value) {
    Services.prefs.setCharPref(name, value);
    Services.prefs.savePrefFile(null);
  }, {
    request: { name: Arg(0), value: Arg(1) },
    response: {}
  }),

  setIntPref: method(function(name, value) {
    Services.prefs.setIntPref(name, value);
    Services.prefs.savePrefFile(null);
  }, {
    request: { name: Arg(0), value: Arg(1) },
    response: {}
  }),

  clearUserPref: method(function(name) {
    Services.prefs.clearUserPref(name);
    Services.prefs.savePrefFile(null);
  }, {
    request: { name: Arg(0) },
    response: {}
  }),
});
Пример #5
0
let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
  typeName: "audionode",

  /**
   * 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);
    this.node = unwrap(node);
    try {
      this.type = getConstructorName(this.node);
    } catch (e) {
      this.type = "";
    }
  },

  /**
   * Returns the name of the audio type.
   * Examples: "OscillatorNode", "MediaElementAudioSourceNode"
   */
  getType: method(function () {
    return this.type;
  }, {
    response: { type: RetVal("string") }
  }),

  /**
   * Returns a boolean indicating if the node is a source node,
   * like BufferSourceNode, MediaElementAudioSourceNode, OscillatorNode, etc.
   */
  isSource: method(function () {
    return !!~this.type.indexOf("Source") || this.type === "OscillatorNode";
  }, {
    response: { source: RetVal("boolean") }
  }),

  /**
   * 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: method(function (param, value) {
    try {
      if (isAudioParam(this.node, param))
        this.node[param].value = value;
      else
        this.node[param] = value;
      return undefined;
    } catch (e) {
      return constructError(e);
    }
  }, {
    request: {
      param: Arg(0, "string"),
      value: Arg(1, "nullable:primitive")
    },
    response: { error: RetVal("nullable:json") }
  }),

  /**
   * Gets a param on the audio node.
   *
   * @param String param
   *        Name of the AudioParam to fetch.
   */
  getParam: method(function (param) {
    // Check to see if it's an AudioParam -- if so,
    // return the `value` property of the parameter.
    let value = isAudioParam(this.node, param) ? this.node[param].value : this.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.
    let grip;
    try {
      grip = ThreadActor.prototype.createValueGrip(value);
    }
    catch (e) {
      grip = createObjectGrip(value);
    }
    return grip;
  }, {
    request: {
      param: Arg(0, "string")
    },
    response: { text: RetVal("nullable:primitive") }
  }),

  /**
   * 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: method(function (param) {
    return (NODE_PROPERTIES[this.type] || {})[param];
  }, {
    request: { param: Arg(0, "string") },
    response: { flags: RetVal("nullable:primitive") }
  }),

  /**
   * 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: method(function (param) {
    let props = Object.keys(NODE_PROPERTIES[this.type]);
    return props.map(prop =>
      ({ param: prop, value: this.getParam(prop), flags: this.getParamFlags(prop) }));
  }, {
    response: { params: RetVal("json") }
  })
});
Пример #6
0
let EventLoopLagActor = protocol.ActorClass({

  typeName: "eventLoopLag",

  _observerAdded: false,

  events: {
    "event-loop-lag" : {
      type: "event-loop-lag",
      time: Arg(0, "number") // duration of the lag in milliseconds.
    }
  },

  /**
   * Start tracking the event loop lags.
   */
  start: method(function() {
    if (!this._observerAdded) {
      Services.obs.addObserver(this, 'event-loop-lag', false);
      this._observerAdded = true;
    }
    return Services.appShell.startEventLoopLagTracking();
  }, {
    request: {},
    response: {success: RetVal("number")}
  }),

  /**
   * Stop tracking the event loop lags.
   */
  stop: method(function() {
    if (this._observerAdded) {
      Services.obs.removeObserver(this, 'event-loop-lag');
      this._observerAdded = false;
    }
    Services.appShell.stopEventLoopLagTracking();
  }, {request: {},response: {}}),

  destroy: function() {
    this.stop();
    protocol.Actor.prototype.destroy.call(this);
  },

  // nsIObserver

  observe: function (subject, topic, data) {
    if (topic == "event-loop-lag") {
      // Forward event loop lag event
      events.emit(this, "event-loop-lag", data);
    }
  },

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
});
Пример #7
0
let StyleEditorActor = exports.StyleEditorActor = protocol.ActorClass({
  typeName: "styleeditor",

  /**
   * The window we work with, taken from the parent actor.
   */
  get window() {
    return this.parentActor.window;
  },

  /**
   * The current content document of the window we work with.
   */
  get document() {
    return this.window.document;
  },

  events: {
    "document-load" : {
      type: "documentLoad",
      styleSheets: Arg(0, "array:old-stylesheet")
    }
  },

  form: function()
  {
    return { actor: this.actorID };
  },

  initialize: function (conn, tabActor) {
    protocol.Actor.prototype.initialize.call(this, null);

    this.parentActor = tabActor;

    // keep a map of sheets-to-actors so we don't create two actors for one sheet
    this._sheets = new Map();
  },

  /**
   * Destroy the current StyleEditorActor instance.
   */
  destroy: function()
  {
    this._sheets.clear();
  },

  /**
   * Called by client when target navigates to a new document.
   * Adds load listeners to document.
   */
  newDocument: method(function() {
    // delete previous document's actors
    this._clearStyleSheetActors();

    // Note: listening for load won't be necessary once
    // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed
    if (this.document.readyState == "complete") {
      this._onDocumentLoaded();
    }
    else {
      this.window.addEventListener("load", this._onDocumentLoaded, false);
    }
    return {};
  }),

  /**
   * Event handler for document loaded event. Add actor for each stylesheet
   * and send an event notifying of the load
   */
  _onDocumentLoaded: function(event) {
    if (event) {
      this.window.removeEventListener("load", this._onDocumentLoaded, false);
    }

    let documents = [this.document];
    var forms = [];
    for (let doc of documents) {
      let sheetForms = this._addStyleSheets(doc.styleSheets);
      forms = forms.concat(sheetForms);
      // Recursively handle style sheets of the documents in iframes.
      for (let iframe of doc.getElementsByTagName("iframe")) {
        documents.push(iframe.contentDocument);
      }
    }

    events.emit(this, "document-load", forms);
  },

  /**
   * Add all the stylesheets to the map and create an actor for each one
   * if not already created. Send event that there are new stylesheets.
   *
   * @param {[DOMStyleSheet]} styleSheets
   *        Stylesheets to add
   * @return {[object]}
   *         Array of actors for each StyleSheetActor created
   */
  _addStyleSheets: function(styleSheets)
  {
    let sheets = [];
    for (let i = 0; i < styleSheets.length; i++) {
      let styleSheet = styleSheets[i];
      sheets.push(styleSheet);

      // Get all sheets, including imported ones
      let imports = this._getImported(styleSheet);
      sheets = sheets.concat(imports);
    }
    let actors = sheets.map(this._createStyleSheetActor.bind(this));

    return actors;
  },

  /**
   * Create a new actor for a style sheet, if it hasn't already been created.
   *
   * @param  {DOMStyleSheet} styleSheet
   *         The style sheet to create an actor for.
   * @return {StyleSheetActor}
   *         The actor for this style sheet
   */
  _createStyleSheetActor: function(styleSheet)
  {
    if (this._sheets.has(styleSheet)) {
      return this._sheets.get(styleSheet);
    }
    let actor = new OldStyleSheetActor(styleSheet, this);

    this.manage(actor);
    this._sheets.set(styleSheet, actor);

    return actor;
  },

  /**
   * Get all the stylesheets @imported from a stylesheet.
   *
   * @param  {DOMStyleSheet} styleSheet
   *         Style sheet to search
   * @return {array}
   *         All the imported stylesheets
   */
  _getImported: function(styleSheet) {
   let imported = [];

   for (let i = 0; i < styleSheet.cssRules.length; i++) {
      let rule = styleSheet.cssRules[i];
      if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
        // Associated styleSheet may be null if it has already been seen due to
        // duplicate @imports for the same URL.
        if (!rule.styleSheet) {
          continue;
        }
        imported.push(rule.styleSheet);

        // recurse imports in this stylesheet as well
        imported = imported.concat(this._getImported(rule.styleSheet));
      }
      else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
        // @import rules must precede all others except @charset
        break;
      }
    }
    return imported;
  },

  /**
   * Clear all the current stylesheet actors in map.
   */
  _clearStyleSheetActors: function() {
    for (let actor in this._sheets) {
      this.unmanage(this._sheets[actor]);
    }
    this._sheets.clear();
  },

  /**
   * Create a new style sheet in the document with the given text.
   * Return an actor for it.
   *
   * @param  {object} request
   *         Debugging protocol request object, with 'text property'
   * @return {object}
   *         Object with 'styelSheet' property for form on new actor.
   */
  newStyleSheet: method(function(text) {
    let parent = this.document.documentElement;
    let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
    style.setAttribute("type", "text/css");

    if (text) {
      style.appendChild(this.document.createTextNode(text));
    }
    parent.appendChild(style);

    let actor = this._createStyleSheetActor(style.sheet);
    return actor;
  }, {
    request: { text: Arg(0, "string") },
    response: { styleSheet: RetVal("old-stylesheet") }
  })
});
Пример #8
0
var EventsFormActor = ActorClass({
  typeName: "eventsFormActor",

  initialize: function() {
    Actor.prototype.initialize.apply(this, arguments);
  },

  attach: method(function() {
    Events.on(NodeActor, "form", this.onNodeActorForm);
  }, {
    request: {},
    response: {}
  }),

  detach: method(function() {
    Events.off(NodeActor, "form", this.onNodeActorForm);
  }, {
    request: {},
    response: {}
  }),

  onNodeActorForm: function(event) {
    let nodeActor = event.target;
    if (nodeActor.rawNode.id == "container") {
      let form = event.data;
      form.setFormProperty("test-property", "test-value");
    }
  }
});
Пример #9
0
let FrameSnapshotActor = protocol.ActorClass({
  typeName: "frame-snapshot",

  /**
   * Creates the frame snapshot call actor.
   *
   * @param DebuggerServerConnection conn
   *        The server connection.
   * @param HTMLCanvasElement canvas
   *        A reference to the content canvas.
   * @param array calls
   *        An array of "function-call" actor instances.
   * @param object screenshot
   *        A single "snapshot-image" type instance.
   */
  initialize: function(conn, { canvas, calls, screenshot }) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this._contentCanvas = canvas;
    this._functionCalls = calls;
    this._animationFrameEndScreenshot = screenshot;
  },

  /**
   * Gets as much data about this snapshot without computing anything costly.
   */
  getOverview: method(function() {
    return {
      calls: this._functionCalls,
      thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e),
      screenshot: this._animationFrameEndScreenshot
    };
  }, {
    response: { overview: RetVal("snapshot-overview") }
  }),

  /**
   * Gets a screenshot of the canvas's contents after the specified
   * function was called.
   */
  generateScreenshotFor: method(function(functionCall) {
    let caller = functionCall.details.caller;
    let global = functionCall.meta.global;

    let canvas = this._contentCanvas;
    let calls = this._functionCalls;
    let index = calls.indexOf(functionCall);

    // To get a screenshot, replay all the steps necessary to render the frame,
    // by invoking the context calls up to and including the specified one.
    // This will be done in a custom framebuffer in case of a WebGL context.
    let replayData = ContextUtils.replayAnimationFrame({
      contextType: global,
      canvas: canvas,
      calls: calls,
      first: 0,
      last: index
    });

    let { replayContext, replayContextScaling, lastDrawCallIndex, doCleanup } = replayData;
    let [left, top, width, height] = replayData.replayViewport;
    let screenshot;

    // Depending on the canvas' context, generating a screenshot is done
    // in different ways.
    if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
      screenshot = ContextUtils.getPixelsForWebGL(replayContext, left, top, width, height);
      screenshot.flipped = true;
    } else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
      screenshot = ContextUtils.getPixelsFor2D(replayContext, left, top, width, height);
      screenshot.flipped = false;
    }

    // In case of the WebGL context, we also need to reset the framebuffer
    // binding to the original value, after generating the screenshot.
    doCleanup();

    screenshot.scaling = replayContextScaling;
    screenshot.index = lastDrawCallIndex;
    return screenshot;
  }, {
    request: { call: Arg(0, "function-call") },
    response: { screenshot: RetVal("snapshot-image") }
  })
});
Пример #10
0
var TimelineActor = exports.TimelineActor = protocol.ActorClass({
  typeName: "timeline",

  events: {
    /**
     * The "markers" events emitted every DEFAULT_TIMELINE_DATA_PULL_TIMEOUT ms
     * at most, when profile markers are found. The timestamps on each marker
     * are relative to when recording was started.
     */
    "markers" : {
      type: "markers",
      markers: Arg(0, "json"),
      endTime: Arg(1, "number")
    },

    /**
     * The "memory" events emitted in tandem with "markers", if this was enabled
     * when the recording started. The `delta` timestamp on this measurement is
     * relative to when recording was started.
     */
    "memory" : {
      type: "memory",
      delta: Arg(0, "number"),
      measurement: Arg(1, "json")
    },

    /**
     * The "ticks" events (from the refresh driver) emitted in tandem with
     * "markers", if this was enabled when the recording started. All ticks
     * are timestamps with a zero epoch.
     */
    "ticks" : {
      type: "ticks",
      delta: Arg(0, "number"),
      timestamps: Arg(1, "array-of-numbers-as-strings")
    },

    /**
     * The "frames" events emitted in tandem with "markers", containing
     * JS stack frames. The `delta` timestamp on this frames packet is
     * relative to when recording was started.
     */
    "frames" : {
      type: "frames",
      delta: Arg(0, "number"),
      frames: Arg(1, "json")
    }
  },

  /**
   * Initializes this actor with the provided connection and tab actor.
   */
  initialize: function (conn, tabActor) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this.tabActor = tabActor;
    this.bridge = new Timeline(tabActor);

    this._onTimelineEvent = this._onTimelineEvent.bind(this);
    events.on(this.bridge, "*", this._onTimelineEvent);
  },

  /**
   * The timeline actor is the first (and last) in its hierarchy to use protocol.js
   * so it doesn't have a parent protocol actor that takes care of its lifetime.
   * So it needs a disconnect method to cleanup.
   */
  disconnect: function() {
    this.destroy();
  },

  /**
   * Destroys this actor, stopping recording first.
   */
  destroy: function () {
    events.off(this.bridge, "*", this._onTimelineEvent);
    this.bridge.destroy();
    this.bridge = null;
    this.tabActor = null;
    protocol.Actor.prototype.destroy.call(this);
  },

  /**
   * Propagate events from the Timeline module over
   * RDP if the event is defined here.
   */
  _onTimelineEvent: function (eventName, ...args) {
    if (this.events[eventName]) {
      events.emit(this, eventName, ...args);
    }
  },

  isRecording: actorBridge("isRecording", {
    request: {},
    response: {
      value: RetVal("boolean")
    }
  }),

  start: actorBridge("start", {
    request: {
      withMemory: Option(0, "boolean"),
      withTicks: Option(0, "boolean")
    },
    response: {
      value: RetVal("number")
    }
  }),

  stop: actorBridge("stop", {
    response: {
      // Set as possibly nullable due to the end time possibly being
      // undefined during destruction
      value: RetVal("nullable:number")
    }
  }),
});
Пример #11
0
var PerformanceActor = exports.PerformanceActor = protocol.ActorClass({
  typeName: "performance",

  traits: {
    features: {
      withMarkers: true,
      withTicks: true,
      withMemory: true,
      withFrames: true,
      withGCEvents: true,
      withDocLoadingEvents: true,
      withAllocations: true,
      withJITOptimizations: true,
    },
  },

  /**
   * The set of events the PerformanceActor emits over RDP.
   */
  events: {
    "recording-started": {
      recording: Arg(0, "performance-recording"),
    },
    "recording-stopping": {
      recording: Arg(0, "performance-recording"),
    },
    "recording-stopped": {
      recording: Arg(0, "performance-recording"),
      data: Arg(1, "json"),
    },
    "profiler-status": {
      data: Arg(0, "json"),
    },
    "console-profile-start": {},
    "timeline-data": {
      name: Arg(0, "string"),
      data: Arg(1, "json"),
      recordings: Arg(2, "array:performance-recording"),
    },
  },

  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);
  },

  /**
   * `disconnect` method required to call destroy, since this
   * actor is not managed by a parent actor.
   */
  disconnect: function() {
    this.destroy();
  },

  destroy: function () {
    events.off(this.bridge, "*", this._onRecorderEvent);
    this.bridge.destroy();
    protocol.Actor.prototype.destroy.call(this);
  },

  connect: method(function (config) {
    this.bridge.connect({ systemClient: config.systemClient });
    return { traits: this.traits };
  }, {
    request: { options: Arg(0, "nullable:json") },
    response: RetVal("json")
  }),

  canCurrentlyRecord: method(function() {
    return this.bridge.canCurrentlyRecord();
  }, {
    response: { value: RetVal("json") }
  }),

  startRecording: method(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;
  }), {
    request: {
      options: Arg(0, "nullable:json"),
    },
    response: {
      recording: RetVal("nullable:performance-recording")
    }
  }),

  stopRecording: actorBridge("stopRecording", {
    request: {
      options: Arg(0, "performance-recording"),
    },
    response: {
      recording: RetVal("performance-recording")
    }
  }),

  isRecording: actorBridge("isRecording", {
    response: { isRecording: RetVal("boolean") }
  }),

  getRecordings: actorBridge("getRecordings", {
    response: { recordings: RetVal("array:performance-recording") }
  }),

  getConfiguration: actorBridge("getConfiguration", {
    response: { config: RetVal("json") }
  }),

  setProfilerStatusInterval: actorBridge("setProfilerStatusInterval", {
    request: { interval: Arg(0, "number") },
    response: { oneway: true }
  }),

  /**
   * 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);
    }
  },
});
Пример #12
0
var ChromiumRootActor = protocol.ActorClass({
  typeName: "chromium_root",

  initialize: function(conn, url) {
    this.actorID = "root";
    Actor.prototype.initialize.call(this, conn);
    this.tabActors = new Map();
  },

  sayHello: function() {
    this.conn.send({
      from: this.actorID,
      applicationType: "browser",
      // There's work to do here.
      traits: {
        sources: false,
        editOuterHTML: true,
        highlightable: true,
        urlToImageDataResolver: true,
        networkMonitor: false,
        storageInspector: false,
        storageInspectorReadOnly: false,
        conditionalBreakpoints: false,
        addNewRule: true,
        noBlackBoxing: true,
        noPrettyPrinting: true
      }
    });
  },

  listTabs: asyncMethod(function*() {
    let jsonTabs = yield requestTabs(this.conn.url + "/json");

    let response = {
      tabs: []
    };

    for (let json of jsonTabs) {
      // json.webSocketDebuggerUrl disappears if some client is already
      // connected to that page. To ensure we still show all tabs in the list,
      // we don't filter on this, but it's possible attaching to such a tab will
      // fail if it was some client other than us that did so.
      response.tabs.push(this.tabActorFor(json));
      if (!("selected" in response) && json.type == "page") {
        response.selected = response.tabs.length - 1;
      }
    }

    if (!("selected" in response)) {
      response.selected = 0;
    }

    return response;
  }, {
    request: {},
    response: RetVal("chromium_tablist")
  }),

  protocolDescription: method(function() {
    return protocol.dumpProtocolSpec();
  }, {
    request: {},
    response: RetVal("json")
  }),

  echo: method(function(str) {
    return str;
  }, {
    request: {
      string: Arg(0, "string")
    },
    response: {
      string: RetVal("string")
    }
  }),

  destroy: function() {
    for (let actor of this.tabActors.values()) {
      actor.destroy();
    }
  },

  tabActorFor: function(json) {
    // Safari on IOS doesn't give its tabs ids. Let's use
    // its socket url as a unique ID.
    let uuid = json.id || json.webSocketDebuggerUrl;

    if (this.tabActors.has(uuid)) {
      return this.tabActors.get(uuid);
    }

    let actor = ChromiumTabActor(this, json);
    this.tabActors.set(uuid, actor);
    return actor;
  }
});
Пример #13
0
  return {
    from: "root",
    applicationType: "xpcshell-tests",
    traits: [],
  }
}

var RootActor = protocol.ActorClass({
  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: method(function() {
    return this.sequence++;
  }, {
    response: { value: RetVal() },
  })
});

var RootFront = protocol.FrontClass(RootActor, {
  initialize: function(client) {
    this.actorID = "root";
    protocol.Front.prototype.initialize.call(this, client);
    // Root owns itself.
    this.manage(this);
Пример #14
0
var RootActor = protocol.ActorClass({
  typeName: "root",

  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: method(function() {
    return new LongStringActor(this.conn, SHORT_STR);
  }, {
    response: { value: RetVal("longstring") },
  }),

  longString: method(function() {
    return new LongStringActor(this.conn, LONG_STR);
  }, {
    response: { value: RetVal("longstring") },
  }),

  emitShortString: method(function() {
    events.emit(this, "string-event", new LongStringActor(this.conn, SHORT_STR));
  }, {
    oneway: true,
  }),

  emitLongString: method(function() {
    events.emit(this, "string-event", new LongStringActor(this.conn, LONG_STR));
  }, {
    oneway: true,
  }),

  events: {
    "string-event": {
      str: Arg(0, "longstring")
    }
  }
});
Пример #15
0
const GcliActor = ActorClass({
  typeName: "gcli",

  events: {
    "commands-changed" : {
      type: "commandsChanged"
    }
  },

  initialize: function(conn, tabActor) {
    Actor.prototype.initialize.call(this, conn);

    this._commandsChanged = this._commandsChanged.bind(this);

    this._tabActor = tabActor;
    this._requisitionPromise = undefined; // see _getRequisition()
  },

  disconnect: function() {
    return this.destroy();
  },

  destroy: function() {
    Actor.prototype.destroy.call(this);

    // If _getRequisition has not been called, just bail quickly
    if (this._requisitionPromise == null) {
      this._commandsChanged = undefined;
      this._tabActor = 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._tabActor = undefined;

      this._commandsChanged = undefined;
    });
  },

  /**
   * Load a module into the requisition
   */
  _testOnly_addItemsByModule: method(function(names) {
    return this._getRequisition().then(requisition => {
      return requisition.system.addItemsByModule(names);
    });
  }, {
    request: {
      customProps: Arg(0, "array:string")
    }
  }),

  /**
   * Unload a module from the requisition
   */
  _testOnly_removeItemsByModule: method(function(names) {
    return this._getRequisition().then(requisition => {
      return requisition.system.removeItemsByModule(names);
    });
  }, {
    request: {
      customProps: Arg(0, "array:string")
    }
  }),

  /**
   * 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: method(function(customProps) {
    return this._getRequisition().then(requisition => {
      return requisition.system.commands.getCommandSpecs(customProps);
    });
  }, {
    request: {
      customProps: Arg(0, "nullable:array:string")
    },
    response: {
      value: RetVal("array:json")
    }
  }),

  /**
   * 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: method(function(typed) {
    return this._getRequisition().then(requisition => {
      return requisition.updateExec(typed).then(output => output.toJson());
    });
  }, {
    request: {
      typed: Arg(0, "string") // The command string
    },
    response: RetVal("json")
  }),

  /**
   * Get the state of an input string. i.e. requisition.getStateData()
   */
  state: method(function(typed, start, rank) {
    return this._getRequisition().then(requisition => {
      return requisition.update(typed).then(() => {
        return requisition.getStateData(start, rank);
      });
    });
  }, {
    request: {
      typed: Arg(0, "string"), // The command string
      start: Arg(1, "number"), // Cursor start position
      rank: Arg(2, "number") // The prediction offset (# times UP/DOWN pressed)
    },
    response: RetVal("json")
  }),

  /**
   * 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: method(function(typed, paramName) {
    return this._getRequisition().then(requisition => {
      return requisition.update(typed).then(() => {
        let assignment = requisition.getAssignment(paramName);
        return Promise.resolve(assignment.predictions).then(predictions => {
          return {
            status: assignment.getStatus().toString(),
            message: assignment.message,
            predictions: predictions
          };
        });
      });
    });
  }, {
    request: {
      typed: Arg(0, "string"), // The command string
      paramName: Arg(1, "string") // The name of the parameter to parse
    },
    response: RetVal("json")
  }),

  /**
   * Get the incremented/decremented value of some type
   * @return a promise of a string containing the new argument text
   */
  nudgeType: method(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;
      });
    });
  }, {
    request: {
      typed: Arg(0, "string"),    // The command string
      by: Arg(1, "number"),       // +1/-1 for increment / decrement
      paramName: Arg(2, "string") // The name of the parameter to parse
    },
    response: RetVal("string")
  }),

  /**
   * Perform a lookup on a selection type to get the allowed values
   */
  getSelectionLookup: method(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 }));
      });
    });
  }, {
    request: {
      commandName: Arg(0, "string"), // The command containing the parameter in question
      paramName: Arg(1, "string"),   // The name of the parameter
    },
    response: RetVal("json")
  }),

  /**
   * Lazy init for a Requisition
   */
  _getRequisition: function() {
    if (this._tabActor == null) {
      throw new Error('GcliActor used post-destroy');
    }

    if (this._requisitionPromise != null) {
      return this._requisitionPromise;
    }

    const Requisition = require("gcli/cli").Requisition;
    const tabActor = this._tabActor;

    this._system = createSystem({ location: "server" });
    this._system.commands.onCommandsChange.add(this._commandsChanged);

    const gcliInit = require("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 tabActor.window;
        },

        get document() {
          return tabActor.window.document;
        }
      };

      return new Requisition(this._system, { environment: environment });
    });

    return this._requisitionPromise;
  },

  /**
   * Pass events from requisition.system.commands.onCommandsChange upwards
   */
  _commandsChanged: function() {
    events.emit(this, "commands-changed");
  },
});
Пример #16
0
let DeviceActor = protocol.ActorClass({
  typeName: "device",

  _desc: null,

  _getAppIniString : function(section, key) {
    let inifile = Services.dirsvc.get("GreD", Ci.nsIFile);
    inifile.append("application.ini");

    if (!inifile.exists()) {
      inifile = Services.dirsvc.get("CurProcD", Ci.nsIFile);
      inifile.append("application.ini");
    }

    if (!inifile.exists()) {
      return undefined;
    }

    let iniParser = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService(Ci.nsIINIParserFactory).createINIParser(inifile);
    try {
      return iniParser.getString(section, key);
    } catch (e) {
      return undefined;
    }
  },

  _getSetting: function(name) {
    let deferred = promise.defer();

    if ("@mozilla.org/settingsService;1" in Cc) {
      let settingsService = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
      let req = settingsService.createLock().get(name, {
        handle: (name, value) => deferred.resolve(value),
        handleError: (error) => deferred.reject(error),
      });
    } else {
      deferred.reject(new Error("No settings service"));
    }
    return deferred.promise;
  },

  getDescription: method(function() {
    // Most of this code is inspired from Nightly Tester Tools:
    // https://wiki.mozilla.org/Auto-tools/Projects/NightlyTesterTools

    let appInfo = Services.appinfo;
    let win = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
    let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);

    desc = {
      appid: appInfo.ID,
      apptype: APP_MAP[appInfo.ID],
      vendor: appInfo.vendor,
      name: appInfo.name,
      version: appInfo.version,
      appbuildid: appInfo.appBuildID,
      platformbuildid: appInfo.platformBuildID,
      platformversion: appInfo.platformVersion,
      geckobuildid: appInfo.platformBuildID,
      geckoversion: appInfo.platformVersion,
      changeset: this._getAppIniString("App", "SourceStamp"),
      useragent: win.navigator.userAgent,
      locale: Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global"),
      os: null,
      hardware: "unknown",
      processor: appInfo.XPCOMABI.split("-")[0],
      compiler: appInfo.XPCOMABI.split("-")[1],
      dpi: utils.displayDPI,
      brandName: null,
      channel: null,
      profile: null,
      width: win.screen.width,
      height: win.screen.height
    };

    // Profile
    let profd = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
    let profservice = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
    var profiles = profservice.profiles;
    while (profiles.hasMoreElements()) {
      let profile = profiles.getNext().QueryInterface(Ci.nsIToolkitProfile);
      if (profile.rootDir.path == profd.path) {
        desc.profile = profile.name;
        break;
      }
    }

    if (!desc.profile) {
      desc.profile = profd.leafName;
    }

    // Channel
    try {
      desc.channel = Services.prefs.getCharPref('app.update.channel');
    } catch(e) {}

    if (desc.apptype == "b2g") {
      // B2G specific
      desc.os = "B2G";

      return this._getSetting('deviceinfo.hardware')
      .then(value => desc.hardware = value)
      .then(() => this._getSetting('deviceinfo.os'))
      .then(value => desc.version = value)
      .then(() => desc);
    }

    // Not B2G
    desc.os = appInfo.OS;
    let bundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
    if (bundle) {
      desc.brandName = bundle.GetStringFromName("brandFullName");
    }

    return desc;

  }, {request: {},response: { value: RetVal("json")}}),

  getWallpaper: method(function() {
    let deferred = promise.defer();
    this._getSetting("wallpaper.image").then((blob) => {
      let FileReader = CC("@mozilla.org/files/filereader;1");
      let reader = new FileReader();
      let conn = this.conn;
      reader.addEventListener("load", function() {
        let str = new LongStringActor(conn, reader.result);
        deferred.resolve(str);
      });
      reader.addEventListener("error", function() {
        deferred.reject(reader.error);
      });
      reader.readAsDataURL(blob);
    });
    return deferred.promise;
  }, {request: {},response: { value: RetVal("longstring")}}),

  screenshotToDataURL: method(function() {
    let window = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
    let canvas = window.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
    let width = window.innerWidth;
    let height = window.innerHeight;
    canvas.setAttribute('width', width);
    canvas.setAttribute('height', height);
    let context = canvas.getContext('2d');
    let flags =
          context.DRAWWINDOW_DRAW_CARET |
          context.DRAWWINDOW_DRAW_VIEW |
          context.DRAWWINDOW_USE_WIDGET_LAYERS;
    context.drawWindow(window, 0, 0, width, height, 'rgb(255,255,255)', flags);
    let dataURL = canvas.toDataURL('image/png')
    return new LongStringActor(this.conn, dataURL);
  }, {request: {},response: { value: RetVal("longstring")}}),

  getRawPermissionsTable: method(function() {
    return {
      rawPermissionsTable: PermissionsTable,
      UNKNOWN_ACTION: Ci.nsIPermissionManager.UNKNOWN_ACTION,
      ALLOW_ACTION: Ci.nsIPermissionManager.ALLOW_ACTION,
      DENY_ACTION: Ci.nsIPermissionManager.DENY_ACTION,
      PROMPT_ACTION: Ci.nsIPermissionManager.PROMPT_ACTION
    };
  }, {request: {},response: { value: RetVal("json")}})
});
Пример #17
0
var HighlighterActor = exports.HighlighterActor = protocol.ActorClass({
  typeName: "highlighter",

  initialize: function(inspector, autohide) {
    protocol.Actor.prototype.initialize.call(this, null);

    this._autohide = autohide;
    this._inspector = inspector;
    this._walker = this._inspector.walker;
    this._tabActor = this._inspector.tabActor;
    this._highlighterEnv = new HighlighterEnvironment();
    this._highlighterEnv.initFromTabActor(this._tabActor);

    this._highlighterReady = this._highlighterReady.bind(this);
    this._highlighterHidden = this._highlighterHidden.bind(this);
    this._onNavigate = this._onNavigate.bind(this);

    this._createHighlighter();

    // Listen to navigation events to switch from the BoxModelHighlighter to the
    // SimpleOutlineHighlighter, and back, if the top level window changes.
    events.on(this._tabActor, "navigate", this._onNavigate);
  },

  get conn() {
    return this._inspector && this._inspector.conn;
  },

  form: function() {
    return {
      actor: this.actorID,
      traits: {
        autoHideOnDestroy: true
      }
    };
  },

  _createHighlighter: function() {
    this._isPreviousWindowXUL = isXUL(this._tabActor.window);

    if (!this._isPreviousWindowXUL) {
      this._highlighter = new BoxModelHighlighter(this._highlighterEnv,
                                                  this._inspector);
      this._highlighter.on("ready", this._highlighterReady);
      this._highlighter.on("hide", this._highlighterHidden);
    } else {
      this._highlighter = new SimpleOutlineHighlighter(this._highlighterEnv);
    }
  },

  _destroyHighlighter: function() {
    if (this._highlighter) {
      if (!this._isPreviousWindowXUL) {
        this._highlighter.off("ready", this._highlighterReady);
        this._highlighter.off("hide", this._highlighterHidden);
      }
      this._highlighter.destroy();
      this._highlighter = null;
    }
  },

  _onNavigate: function({isTopLevel}) {
    // Skip navigation events for non top-level windows, or if the document
    // doesn't exist anymore.
    if (!isTopLevel || !this._tabActor.window.document.documentElement) {
      return;
    }

    // Only rebuild the highlighter if the window type changed.
    if (isXUL(this._tabActor.window) !== this._isPreviousWindowXUL) {
      this._destroyHighlighter();
      this._createHighlighter();
    }
  },

  destroy: function() {
    protocol.Actor.prototype.destroy.call(this);

    this.hideBoxModel();
    this._destroyHighlighter();
    events.off(this._tabActor, "navigate", this._onNavigate);

    this._highlighterEnv.destroy();
    this._highlighterEnv = null;

    this._autohide = null;
    this._inspector = null;
    this._walker = null;
    this._tabActor = null;
  },

  /**
   * Display the box model highlighting on a given NodeActor.
   * There is only one instance of the box model highlighter, so calling this
   * method several times won't display several highlighters, it will just move
   * the highlighter instance to these nodes.
   *
   * @param NodeActor The node to be highlighted
   * @param Options See the request part for existing options. Note that not
   * all options may be supported by all types of highlighters.
   */
  showBoxModel: method(function(node, options = {}) {
    if (node && isNodeValid(node.rawNode)) {
      this._highlighter.show(node.rawNode, options);
    } else {
      this._highlighter.hide();
    }
  }, {
    request: {
      node: Arg(0, "domnode"),
      region: Option(1),
      hideInfoBar: Option(1),
      hideGuides: Option(1),
      showOnly: Option(1),
      onlyRegionArea: Option(1)
    }
  }),

  /**
   * Hide the box model highlighting if it was shown before
   */
  hideBoxModel: method(function() {
    this._highlighter.hide();
  }, {
    request: {}
  }),

  /**
   * Returns `true` if the event was dispatched from a window included in
   * the current highlighter environment; or if the highlighter environment has
   * chrome privileges
   *
   * The method is specifically useful on B2G, where we do not want that events
   * from app or main process are processed if we're inspecting the content.
   *
   * @param {Event} event
   *          The event to allow
   * @return {Boolean}
   */
  _isEventAllowed: function({view}) {
    let { window } = this._highlighterEnv;

    return window instanceof Ci.nsIDOMChromeWindow ||
          isWindowIncluded(window, view);
  },

  /**
   * Pick a node on click, and highlight hovered nodes in the process.
   *
   * This method doesn't respond anything interesting, however, it starts
   * mousemove, and click listeners on the content document to fire
   * events and let connected clients know when nodes are hovered over or
   * clicked.
   *
   * Once a node is picked, events will cease, and listeners will be removed.
   */
  _isPicking: false,
  _hoveredNode: null,
  _currentNode: null,

  pick: method(function() {
    if (this._isPicking) {
      return null;
    }
    this._isPicking = true;

    this._preventContentEvent = event => {
      event.stopPropagation();
      event.preventDefault();
    };

    this._onPick = event => {
      this._preventContentEvent(event);

      if (!this._isEventAllowed(event)) {
        return;
      }

      this._stopPickerListeners();
      this._isPicking = false;
      if (this._autohide) {
        this._tabActor.window.setTimeout(() => {
          this._highlighter.hide();
        }, HIGHLIGHTER_PICKED_TIMER);
      }
      if (!this._currentNode) {
        this._currentNode = this._findAndAttachElement(event);
      }
      events.emit(this._walker, "picker-node-picked", this._currentNode);
    };

    this._onHovered = event => {
      this._preventContentEvent(event);

      if (!this._isEventAllowed(event)) {
        return;
      }

      this._currentNode = this._findAndAttachElement(event);
      if (this._hoveredNode !== this._currentNode.node) {
        this._highlighter.show(this._currentNode.node.rawNode);
        events.emit(this._walker, "picker-node-hovered", this._currentNode);
        this._hoveredNode = this._currentNode.node;
      }
    };

    this._onKey = event => {
      if (!this._currentNode || !this._isPicking) {
        return;
      }

      this._preventContentEvent(event);

      if (!this._isEventAllowed(event)) {
        return;
      }

      let currentNode = this._currentNode.node.rawNode;

      /**
       * KEY: Action/scope
       * LEFT_KEY: wider or parent
       * RIGHT_KEY: narrower or child
       * ENTER/CARRIAGE_RETURN: Picks currentNode
       * ESC: Cancels picker, picks currentNode
       */
      switch (event.keyCode) {
        // Wider.
        case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
          if (!currentNode.parentElement) {
            return;
          }
          currentNode = currentNode.parentElement;
          break;

        // Narrower.
        case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
          if (!currentNode.children.length) {
            return;
          }

          // Set firstElementChild by default
          let child = currentNode.firstElementChild;
          // If currentNode is parent of hoveredNode, then
          // previously selected childNode is set
          let hoveredNode = this._hoveredNode.rawNode;
          for (let sibling of currentNode.children) {
            if (sibling.contains(hoveredNode) || sibling === hoveredNode) {
              child = sibling;
            }
          }

          currentNode = child;
          break;

        // Select the element.
        case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
          this._onPick(event);
          return;

        // Cancel pick mode.
        case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
          this.cancelPick();
          events.emit(this._walker, "picker-node-canceled");
          return;

        default: return;
      }

      // Store currently attached element
      this._currentNode = this._walker.attachElement(currentNode);
      this._highlighter.show(this._currentNode.node.rawNode);
      events.emit(this._walker, "picker-node-hovered", this._currentNode);
    };

    this._startPickerListeners();

    return null;
  }),

  _findAndAttachElement: function(event) {
    // originalTarget allows access to the "real" element before any retargeting
    // is applied, such as in the case of XBL anonymous elements.  See also
    // https://developer.mozilla.org/docs/XBL/XBL_1.0_Reference/Anonymous_Content#Event_Flow_and_Targeting
    let node = event.originalTarget || event.target;
    return this._walker.attachElement(node);
  },

  _startPickerListeners: function() {
    let target = this._highlighterEnv.pageListenerTarget;
    target.addEventListener("mousemove", this._onHovered, true);
    target.addEventListener("click", this._onPick, true);
    target.addEventListener("mousedown", this._preventContentEvent, true);
    target.addEventListener("mouseup", this._preventContentEvent, true);
    target.addEventListener("dblclick", this._preventContentEvent, true);
    target.addEventListener("keydown", this._onKey, true);
    target.addEventListener("keyup", this._preventContentEvent, true);
  },

  _stopPickerListeners: function() {
    let target = this._highlighterEnv.pageListenerTarget;
    target.removeEventListener("mousemove", this._onHovered, true);
    target.removeEventListener("click", this._onPick, true);
    target.removeEventListener("mousedown", this._preventContentEvent, true);
    target.removeEventListener("mouseup", this._preventContentEvent, true);
    target.removeEventListener("dblclick", this._preventContentEvent, true);
    target.removeEventListener("keydown", this._onKey, true);
    target.removeEventListener("keyup", this._preventContentEvent, true);
  },

  _highlighterReady: function() {
    events.emit(this._inspector.walker, "highlighter-ready");
  },

  _highlighterHidden: function() {
    events.emit(this._inspector.walker, "highlighter-hide");
  },

  cancelPick: method(function() {
    if (this._isPicking) {
      this._highlighter.hide();
      this._stopPickerListeners();
      this._isPicking = false;
      this._hoveredNode = null;
    }
  })
});
Пример #18
0
let ReflowActor = protocol.ActorClass({
  typeName: "reflow",

  events: {
    /**
     * The reflows event is emitted when reflows have been detected. The event
     * is sent with an array of reflows that occured. Each item has the
     * following properties:
     * - start {Number}
     * - end {Number}
     * - isInterruptible {Boolean}
     */
    "reflows" : {
      type: "reflows",
      reflows: Arg(0, "array:json")
    }
  },

  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;
  },

  /**
   * The reflow actor is the first (and last) in its hierarchy to use protocol.js
   * so it doesn't have a parent protocol actor that takes care of its lifetime.
   * So it needs a disconnect method to cleanup.
   */
  disconnect: function() {
    this.destroy();
  },

  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: method(function() {
    if (!this._isStarted) {
      this.observer.on("reflows", this._onReflow);
      this._isStarted = true;
    }
  }, {oneway: 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: method(function() {
    if (this._isStarted) {
      this.observer.off("reflows", this._onReflow);
      this._isStarted = false;
    }
  }, {oneway: true}),

  _onReflow: function(event, reflows) {
    if (this._isStarted) {
      events.emit(this, "reflows", reflows);
    }
  }
});
Пример #19
0
let UsageReportActor = protocol.ActorClass({
  typeName: "usageReport",

  initialize: function(conn, tabActor) {
    protocol.Actor.prototype.initialize.call(this, conn);

    this._tabActor = tabActor;
    this._running = false;

    this._onTabLoad = this._onTabLoad.bind(this);
    this._onChange = this._onChange.bind(this);
  },

  destroy: function() {
    this._tabActor = undefined;

    delete this._onTabLoad;
    delete this._onChange;

    protocol.Actor.prototype.destroy.call(this);
  },

  /**
   * Begin recording usage data
   */
  start: method(function() {
    if (this._running) {
      throw new Error(l10n.lookup("csscoverageRunningError"));
    }

    this._visitedPages = new Set();
    this._knownRules = new Map();
    this._running = true;
    this._tooManyUnused = false;

    this._tabActor.browser.addEventListener("load", this._onTabLoad, true);

    this._observeMutations(this._tabActor.window.document);

    this._populateKnownRules(this._tabActor.window.document);
    this._updateUsage(this._tabActor.window.document, false);
  }),

  /**
   * Cease recording usage data
   */
  stop: method(function() {
    if (!this._running) {
      throw new Error(l10n.lookup("csscoverageNotRunningError"));
    }

    this._tabActor.browser.removeEventListener("load", this._onTabLoad, true);
    this._running = false;
  }),

  /**
   * Start/stop recording usage data depending on what we're currently doing.
   */
  toggle: method(function() {
    return this._running ?
        this.stop().then(() => false) :
        this.start().then(() => true);
  }, {
    response: RetVal("boolean"),
  }),

  /**
   * Running start() quickly followed by stop() does a bunch of unnecessary
   * work, so this cuts all that out
   */
  oneshot: method(function() {
    if (this._running) {
      throw new Error(l10n.lookup("csscoverageRunningError"));
    }

    this._visitedPages = new Set();
    this._knownRules = new Map();

    this._populateKnownRules(this._tabActor.window.document);
    this._updateUsage(this._tabActor.window.document, false);
  }),

  /**
   * Called from the tab "load" event
   */
  _onTabLoad: function(ev) {
    let document = ev.target;
    this._populateKnownRules(document);
    this._updateUsage(document, true);

    this._observeMutations(document);
  },

  /**
   * Setup a MutationObserver on the current document
   */
  _observeMutations: function(document) {
    let MutationObserver = document.defaultView.MutationObserver;
    let observer = new MutationObserver(mutations => {
      // It's possible that one of the mutations in this list adds a 'use' of
      // a CSS rule, and another takes it away. See Bug 1010189
      this._onChange(document);
    });

    observer.observe(document, {
      attributes: true,
      childList: true,
      characterData: false,
      subtree: true
    });
  },

  /**
   * Event handler for whenever we think the page has changed in a way that
   * means the CSS usage might have changed.
   */
  _onChange: function(document) {
    // Ignore changes pre 'load'
    if (!this._visitedPages.has(getURL(document))) {
      return;
    }
    this._updateUsage(document, false);
  },

  /**
   * Called whenever we think the list of stylesheets might have changed so
   * we can update the list of rules that we should be checking
   */
  _populateKnownRules: function(document) {
    let url = getURL(document);
    this._visitedPages.add(url);
    // Go through all the rules in the current sheets adding them to knownRules
    // if needed and adding the current url to the list of pages they're on
    for (let rule of getAllSelectorRules(document)) {
      let ruleId = ruleToId(rule);
      let ruleData = this._knownRules.get(ruleId);
      if (ruleData == null) {
        ruleData = {
           selectorText: rule.selectorText,
           cssText: rule.cssText,
           test: getTestSelector(rule.selectorText),
           isUsed: false,
           presentOn: new Set(),
           preLoadOn: new Set(),
           isError: false
        };
        this._knownRules.set(ruleId, ruleData);
      }

      ruleData.presentOn.add(url);
    }
  },

  /**
   * Update knownRules with usage information from the current page
   */
  _updateUsage: function(document, isLoad) {
    let qsaCount = 0;

    // Update this._data with matches to say 'used at load time' by sheet X
    let url = getURL(document);

    for (let [ , ruleData ] of this._knownRules) {
      // If it broke before, don't try again selectors don't change
      if (ruleData.isError) {
        continue;
      }

      // If it's used somewhere already, don't bother checking again unless
      // this is a load event in which case we need to add preLoadOn
      if (!isLoad && ruleData.isUsed) {
        continue;
      }

      // Ignore rules that are not present on this page
      if (!ruleData.presentOn.has(url)) {
        continue;
      }

      qsaCount++;
      if (qsaCount > MAX_UNUSED_RULES) {
        console.error("Too many unused rules on " + url + " ");
        this._tooManyUnused = true;
        continue;
      }

      try {
        let match = document.querySelector(ruleData.test);
        if (match != null) {
          ruleData.isUsed = true;
          if (isLoad) {
            ruleData.preLoadOn.add(url);
          }
        }
      }
      catch (ex) {
        ruleData.isError = true;
      }
    }
  },

  /**
   * Returns a JSONable structure designed to help marking up the style editor,
   * which describes the CSS selector usage.
   * Example:
   *   [
   *     {
   *       selectorText: "p#content",
   *       usage: "unused|used",
   *       start: { line: 3, column: 0 },
   *     },
   *     ...
   *   ]
   */
  createEditorReport: method(function(url) {
    if (this._knownRules == null) {
      return { reports: [] };
    }

    let reports = [];
    for (let [ruleId, ruleData] of this._knownRules) {
      let { url: ruleUrl, line, column } = deconstructRuleId(ruleId);
      if (ruleUrl !== url || ruleData.isUsed) {
        continue;
      }

      let ruleReport = {
        selectorText: ruleData.selectorText,
        start: { line: line, column: column }
      };

      if (ruleData.end) {
        ruleReport.end = ruleData.end;
      }

      reports.push(ruleReport);
    }

    return { reports: reports };
  }, {
    request: { url: Arg(0, "string") },
    response: { reports: RetVal("array:json") }
  }),

  /**
   * Returns a JSONable structure designed for the page report which shows
   * the recommended changes to a page.
   *
   * "preload" means that a rule is used before the load event happens, which
   * means that the page could by optimized by placing it in a <style> element
   * at the top of the page, moving the <link> elements to the bottom.
   *
   * Example:
   *   {
   *     preload: [
   *       {
   *         url: "http://example.org/page1.html",
   *         shortUrl: "page1.html",
   *         rules: [
   *           {
   *             url: "http://example.org/style1.css",
   *             shortUrl: "style1.css",
   *             start: { line: 3, column: 4 },
   *             selectorText: "p#content",
   *             formattedCssText: "p#content {\n  color: red;\n }\n"
   *          },
   *          ...
   *         ]
   *       }
   *     ],
   *     unused: [
   *       {
   *         url: "http://example.org/style1.css",
   *         shortUrl: "style1.css",
   *         rules: [ ... ]
   *       }
   *     ]
   *   }
   */
  createPageReport: method(function() {
    if (this._running) {
      throw new Error(l10n.lookup("csscoverageRunningError"));
    }

    if (this._visitedPages == null) {
      throw new Error(l10n.lookup("csscoverageNotRunError"));
    }

    // Helper function to create a JSONable data structure representing a rule
    const ruleToRuleReport = function(rule, ruleData) {
      return {
        url: rule.url,
        shortUrl: rule.url.split("/").slice(-1)[0],
        start: { line: rule.line, column: rule.column },
        selectorText: ruleData.selectorText,
        formattedCssText: CssLogic.prettifyCSS(ruleData.cssText)
      };
    }

    // A count of each type of rule for the bar chart
    let summary = { used: 0, unused: 0, preload: 0 };

    // Create the set of the unused rules
    let unusedMap = new Map();
    for (let [ruleId, ruleData] of this._knownRules) {
      let rule = deconstructRuleId(ruleId);
      let rules = unusedMap.get(rule.url)
      if (rules == null) {
        rules = [];
        unusedMap.set(rule.url, rules);
      }
      if (!ruleData.isUsed) {
        let ruleReport = ruleToRuleReport(rule, ruleData);
        rules.push(ruleReport);
      }
      else {
        summary.unused++;
      }
    }
    let unused = [];
    for (let [url, rules] of unusedMap) {
      unused.push({
        url: url,
        shortUrl: url.split("/").slice(-1),
        rules: rules
      });
    }

    // Create the set of rules that could be pre-loaded
    let preload = [];
    for (let url of this._visitedPages) {
      let page = {
        url: url,
        shortUrl: url.split("/").slice(-1),
        rules: []
      };

      for (let [ruleId, ruleData] of this._knownRules) {
        if (ruleData.preLoadOn.has(url)) {
          let rule = deconstructRuleId(ruleId);
          let ruleReport = ruleToRuleReport(rule, ruleData);
          page.rules.push(ruleReport);
          summary.preload++;
        }
        else {
          summary.used++;
        }
      }

      if (page.rules.length > 0) {
        preload.push(page);
      }
    }

    return {
      summary: summary,
      preload: preload,
      unused: unused
    };
  }, {
    response: RetVal("json")
  }),

  /**
   * For testing only. Is css coverage running.
   */
  _testOnly_isRunning: method(function() {
    return this._running;
  }, {
    response: { value: RetVal("boolean") }
  }),

  /**
   * For testing only. What pages did we visit.
   */
  _testOnly_visitedPages: method(function() {
    return [...this._visitedPages];
  }, {
    response: { value: RetVal("array:string") }
  }),
});
Пример #20
0
var SettingsActor = exports.SettingsActor = protocol.ActorClass({
  typeName: "settings",

  _getSettingsService: function() {
    let win = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
    return win.navigator.mozSettings;
  },

  getSetting: method(function(name) {
    let deferred = promise.defer();
    let lock = this._getSettingsService().createLock();
    let req = lock.get(name);
    req.onsuccess = function() {
      deferred.resolve(req.result[name]);
    };
    req.onerror = function() {
      deferred.reject(req.error);
    };
    return deferred.promise;
  }, {
    request: { value: Arg(0) },
    response: { value: RetVal("json") }
  }),

  setSetting: method(function(name, value) {
    let deferred = promise.defer();
    let data = {};
    data[name] = value;
    let lock = this._getSettingsService().createLock();
    let req = lock.set(data);
    req.onsuccess = function() {
      deferred.resolve(true);
    };
    req.onerror = function() {
      deferred.reject(req.error);
    };
    return deferred.promise;
  }, {
    request: { name: Arg(0), value: Arg(1) },
    response: {}
  }),

  _hasUserSetting: function(name, value) {
    if (typeof value === "object") {
      return JSON.stringify(defaultSettings[name]) !== JSON.stringify(value);
    }
    return (defaultSettings[name] !== value);
  },

  getAllSettings: method(function() {
    loadSettingsFile();
    let settings = {};
    let self = this;

    let deferred = promise.defer();
    let lock = this._getSettingsService().createLock();
    let req = lock.get("*");

    req.onsuccess = function() {
      for (var name in req.result) {
        settings[name] = {
          value: req.result[name],
          hasUserValue: self._hasUserSetting(name, req.result[name])
        };
      }
      deferred.resolve(settings);
    };
    req.onfailure = function() {
      deferred.reject(req.error);
    };

    return deferred.promise;
  }, {
    request: {},
    response: { value: RetVal("json") }
  }),

  clearUserSetting: method(function(name) {
    loadSettingsFile();
    try {
      this.setSetting(name, defaultSettings[name]);
    } catch (e) {
      console.log(e);
    }
  }, {
    request: { name: Arg(0) },
    response: {}
  })
});
Пример #21
0
let PromisesActor = protocol.ActorClass({
  typeName: "promises",

  events: {
    // Event emitted for new promises allocated in debuggee and bufferred by
    // sending the list of promise objects in a batch.
    "new-promises": {
      type: "new-promises",
      data: Arg(0, "array:ObjectActor"),
    },
    // Event emitted for promise settlements.
    "promises-settled": {
      type: "promises-settled",
      data: Arg(0, "array:ObjectActor")
    }
  },

  /**
   * @param conn DebuggerServerConnection.
   * @param parent TabActor|RootActor
   */
  initialize: function(conn, parent) {
    protocol.Actor.prototype.initialize.call(this, conn);

    this.conn = conn;
    this.parent = parent;
    this.state = "detached";
    this._dbg = null;
    this._gripDepth = 0;
    this._navigationLifetimePool = null;
    this._newPromises = null;
    this._promisesSettled = null;

    this.objectGrip = this.objectGrip.bind(this);
    this._makePromiseEventHandler = this._makePromiseEventHandler.bind(this);
    this._onWindowReady = this._onWindowReady.bind(this);
  },

  destroy: function() {
    protocol.Actor.prototype.destroy.call(this, this.conn);

    if (this.state === "attached") {
      this.detach();
    }
  },

  get dbg() {
    if (!this._dbg) {
      this._dbg = this.parent.makeDebugger();
    }
    return this._dbg;
  },

  /**
   * Attach to the PromisesActor.
   */
  attach: method(expectState("detached", function() {
    this.dbg.addDebuggees();

    this._navigationLifetimePool = this._createActorPool();
    this.conn.addActorPool(this._navigationLifetimePool);

    this._newPromises = [];
    this._promisesSettled = [];

    events.on(this.parent, "window-ready", this._onWindowReady);

    this.state = "attached";
  }, `attaching to the PromisesActor`), {
    request: {},
    response: {}
  }),

  /**
   * Detach from the PromisesActor upon Debugger closing.
   */
  detach: method(expectState("attached", function() {
    this.dbg.removeAllDebuggees();
    this.dbg.enabled = false;
    this._dbg = null;
    this._newPromises = null;
    this._promisesSettled = null;

    if (this._navigationLifetimePool) {
      this.conn.removeActorPool(this._navigationLifetimePool);
      this._navigationLifetimePool = null;
    }

    events.off(this.parent, "window-ready", this._onWindowReady);

    this.state = "detached";
  }, `detaching from the PromisesActor`), {
    request: {},
    response: {}
  }),

  _createActorPool: function() {
    let pool = new ActorPool(this.conn);
    pool.objectActors = new WeakMap();
    return pool;
  },

  /**
   * Create an ObjectActor for the given Promise object.
   *
   * @param object promise
   *        The promise object
   * @return object
   *        An ObjectActor object that wraps the given Promise object
   */
  _createObjectActorForPromise: function(promise) {
    if (this._navigationLifetimePool.objectActors.has(promise)) {
      return this._navigationLifetimePool.objectActors.get(promise);
    }

    let actor = new ObjectActor(promise, {
      getGripDepth: () => this._gripDepth,
      incrementGripDepth: () => this._gripDepth++,
      decrementGripDepth: () => this._gripDepth--,
      createValueGrip: v =>
        createValueGrip(v, this._navigationLifetimePool, this.objectGrip),
      sources: () => DevToolsUtils.reportException("PromisesActor",
        Error("sources not yet implemented")),
      createEnvironmentActor: () => DevToolsUtils.reportException(
        "PromisesActor", Error("createEnvironmentActor not yet implemented"))
    });

    this._navigationLifetimePool.addActor(actor);
    this._navigationLifetimePool.objectActors.set(promise, actor);

    return actor;
  },

  /**
   * Get a grip for the given Promise object.
   *
   * @param object value
   *        The Promise object
   * @return object
   *        The grip for the given Promise object
   */
  objectGrip: function(value) {
    return this._createObjectActorForPromise(value).grip();
  },

  /**
   * Get a list of ObjectActors for all live Promise Objects.
   */
  listPromises: method(function() {
    let promises = this.dbg.findObjects({ class: "Promise" });

    this.dbg.onNewPromise = this._makePromiseEventHandler(this._newPromises,
      "new-promises");
    this.dbg.onPromiseSettled = this._makePromiseEventHandler(
      this._promisesSettled, "promises-settled");

    return promises.map(p => this._createObjectActorForPromise(p));
  }, {
    request: {
    },
    response: {
      promises: RetVal("array:ObjectActor")
    }
  }),

  /**
   * Creates an event handler for onNewPromise that will add the new
   * Promise ObjectActor to the array and schedule it to be emitted as a
   * batch for the provided event.
   *
   * @param array array
   *        The list of Promise ObjectActors to emit
   * @param string eventName
   *        The event name
   */
  _makePromiseEventHandler: function(array, eventName) {
    return promise => {
      let actor = this._createObjectActorForPromise(promise)
      let needsScheduling = array.length == 0;

      array.push(actor);

      if (needsScheduling) {
        DevToolsUtils.executeSoon(() => {
          events.emit(this, eventName, array.splice(0, array.length));
        });
      }
    }
  },

  _onWindowReady: expectState("attached", function({ isTopLevel }) {
    if (!isTopLevel) {
      return;
    }

    this._navigationLifetimePool.cleanup();
    this.dbg.removeAllDebuggees();
    this.dbg.addDebuggees();
  })
});
Пример #22
0
var PerformanceRecordingActor = exports.PerformanceRecordingActor = protocol.ActorClass(merge({
  typeName: "performance-recording",

  form: function(detail) {
    if (detail === "actorid") {
      return this.actorID;
    }

    let form = {
      actor: this.actorID,  // actorID is set when this is added to a pool
      configuration: this._configuration,
      startingBufferStatus: this._startingBufferStatus,
      console: this._console,
      label: this._label,
      startTime: this._startTime,
      localStartTime: this._localStartTime,
      recording: this._recording,
      completed: this._completed,
      duration: this._duration,
    };

    // Only send profiler data once it exists and it has
    // not yet been sent
    if (this._profile && !this._sentFinalizedData) {
      form.finalizedData = true;
      form.profile = this.getProfile();
      form.systemHost = this.getHostSystemInfo();
      form.systemClient = this.getClientSystemInfo();
      this._sentFinalizedData = true;
    }

    return form;
  },

  /**
   * @param {object} conn
   * @param {object} options
   *        A hash of features that this recording is utilizing.
   * @param {object} meta
   *        A hash of temporary metadata for a recording that is recording
   *        (as opposed to an imported recording).
   */
  initialize: function (conn, options, meta) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this._configuration = {
      withMarkers: options.withMarkers || false,
      withTicks: options.withTicks || false,
      withMemory: options.withMemory || false,
      withAllocations: options.withAllocations || false,
      allocationsSampleProbability: options.allocationsSampleProbability || 0,
      allocationsMaxLogLength: options.allocationsMaxLogLength || 0,
      bufferSize: options.bufferSize || 0,
      sampleFrequency: options.sampleFrequency || 1
    };

    this._console = !!options.console;
    this._label = options.label || "";

    if (meta) {
      // Store the start time roughly with Date.now() so when we
      // are checking the duration during a recording, we can get close
      // to the approximate duration to render elements without
      // making a real request
      this._localStartTime = Date.now();

      this._startTime = meta.startTime;
      this._startingBufferStatus = {
        position: meta.position,
        totalSize: meta.totalSize,
        generation: meta.generation
      };

      this._recording = true;
      this._markers = [];
      this._frames = [];
      this._memory = [];
      this._ticks = [];
      this._allocations = { sites: [], timestamps: [], frames: [], sizes: [] };

      this._systemHost = meta.systemHost || {};
      this._systemClient = meta.systemClient || {};
    }
  },

  destroy: function() {
    protocol.Actor.prototype.destroy.call(this);
  },

  /**
   * Internal utility called by the PerformanceActor and PerformanceFront on state changes
   * to update the internal state of the PerformanceRecording.
   *
   * @param {string} state
   * @param {object} extraData
   */
  _setState: function (state, extraData) {
    switch (state) {
      case "recording-started": {
        this._recording = true;
        break;
      }
      case "recording-stopping": {
        this._recording = false;
        break;
      }
      case "recording-stopped": {
        this._profile = extraData.profile;
        this._duration = extraData.duration;

        // We filter out all samples that fall out of current profile's range
        // since the profiler is continuously running. Because of this, sample
        // times are not guaranteed to have a zero epoch, so offset the
        // timestamps.
        RecordingUtils.offsetSampleTimes(this._profile, this._startTime);

        // Markers need to be sorted ascending by time, to be properly displayed
        // in a waterfall view.
        this._markers = this._markers.sort((a, b) => (a.start > b.start));

        this._completed = true;
        break;
      }
    };
  },

}, PerformanceRecordingCommon));
Пример #23
0
var StyleSheetsActor = exports.StyleSheetsActor = protocol.ActorClass({
  typeName: "stylesheets",

  /**
   * The window we work with, taken from the parent actor.
   */
  get window() {
    return this.parentActor.window;
  },

  /**
   * The current content document of the window we work with.
   */
  get document() {
    return this.window.document;
  },

  form: function()
  {
    return { actor: this.actorID };
  },

  initialize: function (conn, tabActor) {
    protocol.Actor.prototype.initialize.call(this, null);

    this.parentActor = tabActor;
  },

  /**
   * Protocol method for getting a list of StyleSheetActors representing
   * all the style sheets in this document.
   */
  getStyleSheets: method(Task.async(function* () {
    // Iframe document can change during load (bug 1171919). Track their windows
    // instead.
    let windows = [this.window];
    let actors = [];

    for (let win of windows) {
      let sheets = yield this._addStyleSheets(win);
      actors = actors.concat(sheets);

      // Recursively handle style sheets of the documents in iframes.
      for (let iframe of win.document.querySelectorAll("iframe, browser, frame")) {
        if (iframe.contentDocument && iframe.contentWindow) {
          // Sometimes, iframes don't have any document, like the
          // one that are over deeply nested (bug 285395)
          windows.push(iframe.contentWindow);
        }
      }
    }
    return actors;
  }), {
    request: {},
    response: { styleSheets: RetVal("array:stylesheet") }
  }),

  /**
   * Check if we should be showing this stylesheet.
   *
   * @param {Document} doc
   *        Document for which we're checking
   * @param {DOMCSSStyleSheet} sheet
   *        Stylesheet we're interested in
   *
   * @return boolean
   *         Whether the stylesheet should be listed.
   */
  _shouldListSheet: function(doc, sheet) {
    // Special case about:PreferenceStyleSheet, as it is generated on the
    // fly and the URI is not registered with the about: handler.
    // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
    if (sheet.href && sheet.href.toLowerCase() == "about:preferencestylesheet") {
      return false;
    }

    return true;
  },

  /**
   * Add all the stylesheets for the document in this window to the map and
   * create an actor for each one if not already created.
   *
   * @param {Window} win
   *        Window for which to add stylesheets
   *
   * @return {Promise}
   *         Promise that resolves to an array of StyleSheetActors
   */
  _addStyleSheets: function(win)
  {
    return Task.spawn(function*() {
      let doc = win.document;
      // readyState can be uninitialized if an iframe has just been created but
      // it has not started to load yet.
      if (doc.readyState === "loading" || doc.readyState === "uninitialized") {
        // Wait for the document to load first.
        yield listenOnce(win, "DOMContentLoaded", true);

        // Make sure we have the actual document for this window. If the
        // readyState was initially uninitialized, the initial dummy document
        // was replaced with the actual document (bug 1171919).
        doc = win.document;
      }

      let isChrome = Services.scriptSecurityManager.isSystemPrincipal(doc.nodePrincipal);
      let styleSheets = isChrome ? DOMUtils.getAllStyleSheets(doc) : doc.styleSheets;
      let actors = [];
      for (let i = 0; i < styleSheets.length; i++) {
        let sheet = styleSheets[i];
        if (!this._shouldListSheet(doc, sheet)) {
          continue;
        }

        let actor = this.parentActor.createStyleSheetActor(sheet);
        actors.push(actor);

        // Get all sheets, including imported ones
        let imports = yield this._getImported(doc, actor);
        actors = actors.concat(imports);
      }
      return actors;
    }.bind(this));
  },

  /**
   * Get all the stylesheets @imported from a stylesheet.
   *
   * @param  {Document} doc
   *         The document including the stylesheet
   * @param  {DOMStyleSheet} styleSheet
   *         Style sheet to search
   * @return {Promise}
   *         A promise that resolves with an array of StyleSheetActors
   */
  _getImported: function(doc, styleSheet) {
    return Task.spawn(function*() {
      let rules = yield styleSheet.getCSSRules();
      let imported = [];

      for (let i = 0; i < rules.length; i++) {
        let rule = rules[i];
        if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
          // Associated styleSheet may be null if it has already been seen due
          // to duplicate @imports for the same URL.
          if (!rule.styleSheet || !this._shouldListSheet(doc, rule.styleSheet)) {
            continue;
          }
          let actor = this.parentActor.createStyleSheetActor(rule.styleSheet);
          imported.push(actor);

          // recurse imports in this stylesheet as well
          let children = yield this._getImported(doc, actor);
          imported = imported.concat(children);
        }
        else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
          // @import rules must precede all others except @charset
          break;
        }
      }

      return imported;
    }.bind(this));
  },


  /**
   * Create a new style sheet in the document with the given text.
   * Return an actor for it.
   *
   * @param  {object} request
   *         Debugging protocol request object, with 'text property'
   * @return {object}
   *         Object with 'styelSheet' property for form on new actor.
   */
  addStyleSheet: method(function(text) {
    let parent = this.document.documentElement;
    let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
    style.setAttribute("type", "text/css");

    if (text) {
      style.appendChild(this.document.createTextNode(text));
    }
    parent.appendChild(style);

    let actor = this.parentActor.createStyleSheetActor(style.sheet);
    return actor;
  }, {
    request: { text: Arg(0, "string") },
    response: { styleSheet: RetVal("stylesheet") }
  })
});
Пример #24
0
let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
  typeName: "audionode",

  /**
   * 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);

    try {
      this.type = getConstructorName(node);
    } catch (e) {
      this.type = "";
    }
  },

  /**
   * Returns the name of the audio type.
   * Examples: "OscillatorNode", "MediaElementAudioSourceNode"
   */
  getType: method(function () {
    return this.type;
  }, {
    response: { type: RetVal("string") }
  }),

  /**
   * Returns a boolean indicating if the node is a source node,
   * like BufferSourceNode, MediaElementAudioSourceNode, OscillatorNode, etc.
   */
  isSource: method(function () {
    return !!~this.type.indexOf("Source") || this.type === "OscillatorNode";
  }, {
    response: { source: RetVal("boolean") }
  }),

  /**
   * 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: method(function (param, value) {
    let node = this.node.get();

    if (node === null) {
      return CollectedAudioNodeError();
    }

    try {
      if (isAudioParam(node, param))
        node[param].value = value;
      else
        node[param] = value;
      return undefined;
    } catch (e) {
      return constructError(e);
    }
  }, {
    request: {
      param: Arg(0, "string"),
      value: Arg(1, "nullable:primitive")
    },
    response: { error: RetVal("nullable:json") }
  }),

  /**
   * Gets a param on the audio node.
   *
   * @param String param
   *        Name of the AudioParam to fetch.
   */
  getParam: method(function (param) {
    let 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.
    let 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.
    let grip;
    try {
      grip = ThreadActor.prototype.createValueGrip(value);
    }
    catch (e) {
      grip = createObjectGrip(value);
    }
    return grip;
  }, {
    request: {
      param: Arg(0, "string")
    },
    response: { text: RetVal("nullable:primitive") }
  }),

  /**
   * 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: method(function (param) {
    return (NODE_PROPERTIES[this.type] || {})[param];
  }, {
    request: { param: Arg(0, "string") },
    response: { flags: RetVal("nullable:primitive") }
  }),

  /**
   * 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: method(function (param) {
    let props = Object.keys(NODE_PROPERTIES[this.type]);
    return props.map(prop =>
      ({ param: prop, value: this.getParam(prop), flags: this.getParamFlags(prop) }));
  }, {
    response: { params: RetVal("json") }
  })
});
Пример #25
0
var ProfilerActor = exports.ProfilerActor = protocol.ActorClass({
  typeName: "profiler",

  /**
   * The set of events the ProfilerActor emits over RDP.
   */
  events: {
    "console-api-profiler": {
      data: Arg(0, "json"),
    },
    "profiler-started": {
      data: Arg(0, "json"),
    },
    "profiler-stopped": {
      data: Arg(0, "json"),
    },
    "profiler-status": {
      data: Arg(0, "json"),
    },

    // Only for older geckos, pre-protocol.js ProfilerActor (<Fx42).
    // Emitted on other events as a transition from older profiler events
    // to newer ones.
    "eventNotification": {
      subject: Option(0, "json"),
      topic: Option(0, "string"),
      details: Option(0, "json")
    }
  },

  initialize: function (conn) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this._onProfilerEvent = this._onProfilerEvent.bind(this);

    this.bridge = new Profiler();
    events.on(this.bridge, "*", this._onProfilerEvent);
  },

  /**
   * `disconnect` method required to call destroy, since this
   * actor is not managed by a parent actor.
   */
  disconnect: function() {
    this.destroy();
  },

  destroy: function() {
    events.off(this.bridge, "*", this._onProfilerEvent);
    this.bridge.destroy();
    protocol.Actor.prototype.destroy.call(this);
  },

  startProfiler: actorBridge("start", {
    // Write out every property in the request, since we want all these options to be
    // on the packet's top-level for backwards compatibility, when the profiler actor
    // was not using protocol.js (<Fx42)
    request: {
      entries: Option(0, "nullable:number"),
      interval: Option(0, "nullable:number"),
      features: Option(0, "nullable:array:string"),
      threadFilters: Option(0, "nullable:array:string"),
    },
    response: RetVal("json"),
  }),

  stopProfiler: actorBridge("stop", {
    response: RetVal("json"),
  }),

  getProfile: actorBridge("getProfile", {
    request: {
      startTime: Option(0, "nullable:number"),
      stringify: Option(0, "nullable:boolean")
    },
    response: RetVal("profiler-data")
  }),

  getFeatures: actorBridge("getFeatures", {
    response: RetVal("json")
  }),

  getBufferInfo: actorBridge("getBufferInfo", {
    response: RetVal("json")
  }),

  getStartOptions: actorBridge("getStartOptions", {
    response: RetVal("json")
  }),

  isActive: actorBridge("isActive", {
    response: RetVal("json")
  }),

  getSharedLibraryInformation: actorBridge("getSharedLibraryInformation", {
    response: RetVal("json")
  }),

  registerEventNotifications: actorBridge("registerEventNotifications", {
    // Explicitly enumerate the arguments
    // @see ProfilerActor#startProfiler
    request: {
      events: Option(0, "nullable:array:string"),
    },
    response: RetVal("json")
  }),

  unregisterEventNotifications: actorBridge("unregisterEventNotifications", {
    // Explicitly enumerate the arguments
    // @see ProfilerActor#startProfiler
    request: {
      events: Option(0, "nullable:array:string"),
    },
    response: RetVal("json")
  }),

  setProfilerStatusInterval: actorBridge("setProfilerStatusInterval", {
    request: { interval: Arg(0, "number") },
    oneway: true
  }),

  /**
   * Pipe events from Profiler module to this actor.
   */
  _onProfilerEvent: function (eventName, ...data) {
    events.emit(this, eventName, ...data);
  },
});
Пример #26
0
exports.LongStringActor = protocol.ActorClass({
  typeName: "longstractor",

  initialize: function(conn, str) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this.str = str;
    this.short = (this.str.length < LONG_STRING_LENGTH);
  },

  destroy: function() {
    this.str = null;
    protocol.Actor.prototype.destroy.call(this);
  },

  form: function() {
    if (this.short) {
      return this.str;
    }
    return {
      type: "longString",
      actor: this.actorID,
      length: this.str.length,
      initial: this.str.substring(0, LONG_STRING_INITIAL_LENGTH)
    }
  },

  substring: method(function(start, end) {
    return promise.resolve(this.str.substring(start, end));
  }, {
    request: {
      start: Arg(0),
      end: Arg(1)
    },
    response: { substring: RetVal() },
  }),

  release: method(function() { }, { release: true })
});
Пример #27
0
var FramerateActor = exports.FramerateActor = protocol.ActorClass({
  typeName: "framerate",
  initialize: function (conn, tabActor) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this.bridge = new Framerate(tabActor);
  },
  destroy: function(conn) {
    protocol.Actor.prototype.destroy.call(this, conn);
    this.bridge.destroy();
  },

  startRecording: actorBridge("startRecording", {}),

  stopRecording: actorBridge("stopRecording", {
    request: {
      beginAt: Arg(0, "nullable:number"),
      endAt: Arg(1, "nullable:number")
    },
    response: { ticks: RetVal("array:number") }
  }),

  cancelRecording: actorBridge("cancelRecording"),

  isRecording: actorBridge("isRecording", {
    response: { recording: RetVal("boolean") }
  }),

  getPendingTicks: actorBridge("getPendingTicks", {
    request: {
      beginAt: Arg(0, "nullable:number"),
      endAt: Arg(1, "nullable:number")
    },
    response: { ticks: RetVal("array:number") }
  }),
});
Пример #28
0
let FunctionCallActor = protocol.ActorClass({
  typeName: "function-call",

  /**
   * 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 array args
   *        The called function's arguments.
   * @param any result
   *        The value returned by the function call.
   */
  initialize: function(conn, [window, global, caller, type, name, stack, args, result]) {
    protocol.Actor.prototype.initialize.call(this, conn);

    this.details = {
      window: window,
      caller: caller,
      type: type,
      name: name,
      stack: stack,
      args: args,
      return: result
    };

    this.meta = {
      global: -1,
      previews: { caller: "", args: "" }
    };

    if (global == "WebGLRenderingContext") {
      this.meta.global = CallWatcherFront.CANVAS_WEBGL_CONTEXT;
    } else if (global == "CanvasRenderingContext2D") {
      this.meta.global = CallWatcherFront.CANVAS_2D_CONTEXT;
    } else if (global == "window") {
      this.meta.global = CallWatcherFront.UNKNOWN_SCOPE;
    } else {
      this.meta.global = CallWatcherFront.GLOBAL_SCOPE;
    }

    this.meta.previews.caller = this._generateCallerPreview();
    this.meta.previews.args = this._generateArgsPreview();
  },

  /**
   * 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,
      callerPreview: this.meta.previews.caller,
      argsPreview: this.meta.previews.args
    };
  },

  /**
   * Gets more information about this function call, which is not necessarily
   * available on the Front instance.
   */
  getDetails: method(function() {
    let { type, name, stack } = 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
    };
  }, {
    response: { info: RetVal("call-details") }
  }),

  /**
   * Serializes the caller's name so that it can be easily be transferred
   * as a string, but still be useful when displayed in a potential UI.
   *
   * @return string
   *         The caller's name as a string.
   */
  _generateCallerPreview: function() {
    let global = this.meta.global;
    if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
      return "gl";
    }
    if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
      return "ctx";
    }
    return "";
  },

  /**
   * Serializes the arguments so that they can be easily be transferred
   * as a string, but still be useful when displayed in a potential UI.
   *
   * @return string
   *         The arguments as a string.
   */
  _generateArgsPreview: function() {
    let { caller, args } = this.details;
    let { global } = this.meta;

    // XXX: All of this sucks. Make this smarter, so that the frontend
    // can inspect each argument, be it object or primitive. Bug 978960.
    let serializeArgs = () => args.map(arg => {
      if (typeof arg == "undefined") {
        return "undefined";
      }
      if (typeof arg == "function") {
        return "Function";
      }
      if (typeof arg == "object") {
        return "Object";
      }
      if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
        // XXX: This doesn't handle combined bitmasks. Bug 978964.
        return getEnumsLookupTable("webgl", caller)[arg] || arg;
      }
      if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
        return getEnumsLookupTable("2d", caller)[arg] || arg;
      }
      return arg;
    });

    return serializeArgs().join(", ");
  }
});
Пример #29
0
var FunctionCallActor = protocol.ActorClass({
  typeName: "function-call",

  /**
   * 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() },
      });
    }
    // Otherwise, hold strong references to the objects.
    else {
      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: method(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
    };
  }, {
    response: { info: RetVal("call-details") }
  }),

  /**
   * 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 + "";
  }
});
Пример #30
0
let TimelineActor = exports.TimelineActor = protocol.ActorClass({
  typeName: "timeline",

  events: {
    /**
     * "markers" events are emitted every DEFAULT_TIMELINE_DATA_PULL_TIMEOUT ms
     * at most, when profile markers are found. A marker has the following
     * properties:
     * - start {Number} ms
     * - end {Number} ms
     * - name {String}
     */
    "markers" : {
      type: "markers",
      markers: Arg(0, "array:json"),
      endTime: Arg(1, "number")
    },

    /**
     * "memory" events emitted in tandem with "markers", if this was enabled
     * when the recording started.
     */
    "memory" : {
      type: "memory",
      delta: Arg(0, "number"),
      measurement: Arg(1, "json")
    },

    /**
     * "ticks" events (from the refresh driver) emitted in tandem with "markers",
     * if this was enabled when the recording started.
     */
    "ticks" : {
      type: "ticks",
      delta: Arg(0, "number"),
      timestamps: Arg(1, "array:number")
    }
  },

  initialize: function(conn, tabActor) {
    protocol.Actor.prototype.initialize.call(this, conn);
    this.tabActor = tabActor;

    this._isRecording = false;
    this._startTime = 0;

    // Make sure to get markers from new windows as they become available
    this._onWindowReady = this._onWindowReady.bind(this);
    events.on(this.tabActor, "window-ready", this._onWindowReady);
  },

  /**
   * The timeline actor is the first (and last) in its hierarchy to use protocol.js
   * so it doesn't have a parent protocol actor that takes care of its lifetime.
   * So it needs a disconnect method to cleanup.
   */
  disconnect: function() {
    this.destroy();
  },

  destroy: function() {
    this.stop();

    events.off(this.tabActor, "window-ready", this._onWindowReady);
    this.tabActor = null;

    protocol.Actor.prototype.destroy.call(this);
  },

  /**
   * Get the list of docShells in the currently attached tabActor. Note that we
   * always list the docShells included in the real root docShell, even if the
   * tabActor was switched to a child frame. This is because for now, paint
   * markers are only recorded at parent frame level so switching the timeline
   * to a child frame would hide all paint markers.
   * See https://bugzilla.mozilla.org/show_bug.cgi?id=1050773#c14
   * @return {Array}
   */
  get docShells() {
    let originalDocShell;

    if (this.tabActor.isRootActor) {
      originalDocShell = this.tabActor.docShell;
    } else {
      originalDocShell = this.tabActor.originalDocShell;
    }

    let docShellsEnum = originalDocShell.getDocShellEnumerator(
      Ci.nsIDocShellTreeItem.typeAll,
      Ci.nsIDocShell.ENUMERATE_FORWARDS
    );

    let docShells = [];
    while (docShellsEnum.hasMoreElements()) {
      let docShell = docShellsEnum.getNext();
      docShells.push(docShell.QueryInterface(Ci.nsIDocShell));
    }

    return docShells;
  },

  /**
   * At regular intervals, pop the markers from the docshell, and forward
   * markers if any.
   */
  _pullTimelineData: function() {
    if (!this._isRecording) {
      return;
    }
    if (!this.docShells.length) {
      return;
    }

    let endTime = this.docShells[0].now();
    let markers = [];

    for (let docShell of this.docShells) {
      markers = [...markers, ...docShell.popProfileTimelineMarkers()];
    }

    if (markers.length > 0) {
      this._postProcessMarkers(markers);
      events.emit(this, "markers", markers, endTime);
    }
    if (this._memoryActor) {
      events.emit(this, "memory", endTime, this._memoryActor.measure());
    }
    if (this._framerateActor) {
      events.emit(this, "ticks", endTime, this._framerateActor.getPendingTicks());
    }

    this._dataPullTimeout = setTimeout(() => {
      this._pullTimelineData();
    }, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT);
  },

  /**
   * Some markers need post processing.
   * We will eventually do that platform side: bug 1069661
   */
  _postProcessMarkers: function(m) {
    m.forEach(m => {
      // A marker named "ConsoleTime:foobar" needs
      // to be renamed "ConsoleTime".
      let split = m.name.match(/ConsoleTime:(.*)/);
      if (split && split.length > 0) {
        if (!m.detail) {
          m.detail = {}
        }
        m.detail.causeName = split[1];
        m.name = "ConsoleTime";
      }
    });
  },

  /**
   * Are we recording profile markers currently?
   */
  isRecording: method(function() {
    return this._isRecording;
  }, {
    request: {},
    response: {
      value: RetVal("boolean")
    }
  }),

  /**
   * Start recording profile markers.
   */
  start: method(function({ withMemory, withTicks }) {
    if (this._isRecording) {
      return;
    }
    this._isRecording = true;
    this._startTime = this.docShells[0].now();

    for (let docShell of this.docShells) {
      docShell.recordProfileTimelineMarkers = true;
    }

    if (withMemory) {
      this._memoryActor = new MemoryActor(this.conn, this.tabActor);
      events.emit(this, "memory", this._startTime, this._memoryActor.measure());
    }
    if (withTicks) {
      this._framerateActor = new FramerateActor(this.conn, this.tabActor);
      this._framerateActor.startRecording();
    }

    this._pullTimelineData();
    return this._startTime;
  }, {
    request: {
      withMemory: Option(0, "boolean"),
      withTicks: Option(0, "boolean")
    },
    response: {
      value: RetVal("number")
    }
  }),

  /**
   * Stop recording profile markers.
   */
  stop: method(function() {
    if (!this._isRecording) {
      return;
    }
    this._isRecording = false;

    if (this._memoryActor) {
      this._memoryActor = null;
    }
    if (this._framerateActor) {
      this._framerateActor.stopRecording();
      this._framerateActor = null;
    }

    for (let docShell of this.docShells) {
      docShell.recordProfileTimelineMarkers = false;
    }

    clearTimeout(this._dataPullTimeout);
  }, {}),

  /**
   * When a new window becomes available in the tabActor, start recording its
   * markers if we were recording.
   */
  _onWindowReady: function({window}) {
    if (this._isRecording) {
      let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                           .getInterface(Ci.nsIWebNavigation)
                           .QueryInterface(Ci.nsIDocShell);
      docShell.recordProfileTimelineMarkers = true;
    }
  }
});