Mypal/devtools/shared/fronts/inspector.js

1008 lines
30 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
require("devtools/shared/fronts/styles");
require("devtools/shared/fronts/highlighters");
require("devtools/shared/fronts/layout");
const { SimpleStringFront } = require("devtools/shared/fronts/string");
const {
Front,
FrontClassWithSpec,
custom,
preEvent,
types
} = require("devtools/shared/protocol.js");
const {
inspectorSpec,
nodeSpec,
nodeListSpec,
walkerSpec
} = require("devtools/shared/specs/inspector");
const promise = require("promise");
const defer = require("devtools/shared/defer");
const { Task } = require("devtools/shared/task");
const { Class } = require("sdk/core/heritage");
const events = require("sdk/event/core");
const object = require("sdk/util/object");
const nodeConstants = require("devtools/shared/dom-node-constants.js");
loader.lazyRequireGetter(this, "CommandUtils",
"devtools/client/shared/developer-toolbar", true);
const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
/**
* Convenience API for building a list of attribute modifications
* for the `modifyAttributes` request.
*/
const AttributeModificationList = Class({
initialize: function (node) {
this.node = node;
this.modifications = [];
},
apply: function () {
let ret = this.node.modifyAttributes(this.modifications);
return ret;
},
destroy: function () {
this.node = null;
this.modification = null;
},
setAttributeNS: function (ns, name, value) {
this.modifications.push({
attributeNamespace: ns,
attributeName: name,
newValue: value
});
},
setAttribute: function (name, value) {
this.setAttributeNS(undefined, name, value);
},
removeAttributeNS: function (ns, name) {
this.setAttributeNS(ns, name, undefined);
},
removeAttribute: function (name) {
this.setAttributeNS(undefined, name, undefined);
}
});
/**
* Client side of the node actor.
*
* Node fronts are strored in a tree that mirrors the DOM tree on the
* server, but with a few key differences:
* - Not all children will be necessary loaded for each node.
* - The order of children isn't guaranteed to be the same as the DOM.
* Children are stored in a doubly-linked list, to make addition/removal
* and traversal quick.
*
* Due to the order/incompleteness of the child list, it is safe to use
* the parent node from clients, but the `children` request should be used
* to traverse children.
*/
const NodeFront = FrontClassWithSpec(nodeSpec, {
initialize: function (conn, form, detail, ctx) {
// The parent node
this._parent = null;
// The first child of this node.
this._child = null;
// The next sibling of this node.
this._next = null;
// The previous sibling of this node.
this._prev = null;
Front.prototype.initialize.call(this, conn, form, detail, ctx);
},
/**
* Destroy a node front. The node must have been removed from the
* ownership tree before this is called, unless the whole walker front
* is being destroyed.
*/
destroy: function () {
Front.prototype.destroy.call(this);
},
// Update the object given a form representation off the wire.
form: function (form, detail, ctx) {
if (detail === "actorid") {
this.actorID = form;
return;
}
// backward-compatibility: shortValue indicates we are connected to old server
if (form.shortValue) {
// If the value is not complete, set nodeValue to null, it will be fetched
// when calling getNodeValue()
form.nodeValue = form.incompleteValue ? null : form.shortValue;
}
// Shallow copy of the form. We could just store a reference, but
// eventually we'll want to update some of the data.
this._form = object.merge(form);
this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
if (form.parent) {
// Get the owner actor for this actor (the walker), and find the
// parent node of this actor from it, creating a standin node if
// necessary.
let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent);
this.reparent(parentNodeFront);
}
if (form.inlineTextChild) {
this.inlineTextChild =
types.getType("domnode").read(form.inlineTextChild, ctx);
} else {
this.inlineTextChild = undefined;
}
},
/**
* Returns the parent NodeFront for this NodeFront.
*/
parentNode: function () {
return this._parent;
},
/**
* Process a mutation entry as returned from the walker's `getMutations`
* request. Only tries to handle changes of the node's contents
* themselves (character data and attribute changes), the walker itself
* will keep the ownership tree up to date.
*/
updateMutation: function (change) {
if (change.type === "attributes") {
// We'll need to lazily reparse the attributes after this change.
this._attrMap = undefined;
// Update any already-existing attributes.
let found = false;
for (let i = 0; i < this.attributes.length; i++) {
let attr = this.attributes[i];
if (attr.name == change.attributeName &&
attr.namespace == change.attributeNamespace) {
if (change.newValue !== null) {
attr.value = change.newValue;
} else {
this.attributes.splice(i, 1);
}
found = true;
break;
}
}
// This is a new attribute. The null check is because of Bug 1192270,
// in the case of a newly added then removed attribute
if (!found && change.newValue !== null) {
this.attributes.push({
name: change.attributeName,
namespace: change.attributeNamespace,
value: change.newValue
});
}
} else if (change.type === "characterData") {
this._form.nodeValue = change.newValue;
} else if (change.type === "pseudoClassLock") {
this._form.pseudoClassLocks = change.pseudoClassLocks;
} else if (change.type === "events") {
this._form.hasEventListeners = change.hasEventListeners;
}
},
// Some accessors to make NodeFront feel more like an nsIDOMNode
get id() {
return this.getAttribute("id");
},
get nodeType() {
return this._form.nodeType;
},
get namespaceURI() {
return this._form.namespaceURI;
},
get nodeName() {
return this._form.nodeName;
},
get displayName() {
let {displayName, nodeName} = this._form;
// Keep `nodeName.toLowerCase()` for backward compatibility
return displayName || nodeName.toLowerCase();
},
get doctypeString() {
return "<!DOCTYPE " + this._form.name +
(this._form.publicId ? " PUBLIC \"" + this._form.publicId + "\"" : "") +
(this._form.systemId ? " \"" + this._form.systemId + "\"" : "") +
">";
},
get baseURI() {
return this._form.baseURI;
},
get className() {
return this.getAttribute("class") || "";
},
get hasChildren() {
return this._form.numChildren > 0;
},
get numChildren() {
return this._form.numChildren;
},
get hasEventListeners() {
return this._form.hasEventListeners;
},
get isBeforePseudoElement() {
return this._form.isBeforePseudoElement;
},
get isAfterPseudoElement() {
return this._form.isAfterPseudoElement;
},
get isPseudoElement() {
return this.isBeforePseudoElement || this.isAfterPseudoElement;
},
get isAnonymous() {
return this._form.isAnonymous;
},
get isInHTMLDocument() {
return this._form.isInHTMLDocument;
},
get tagName() {
return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null;
},
get isDocumentElement() {
return !!this._form.isDocumentElement;
},
// doctype properties
get name() {
return this._form.name;
},
get publicId() {
return this._form.publicId;
},
get systemId() {
return this._form.systemId;
},
getAttribute: function (name) {
let attr = this._getAttribute(name);
return attr ? attr.value : null;
},
hasAttribute: function (name) {
this._cacheAttributes();
return (name in this._attrMap);
},
get hidden() {
let cls = this.getAttribute("class");
return cls && cls.indexOf(HIDDEN_CLASS) > -1;
},
get attributes() {
return this._form.attrs;
},
get pseudoClassLocks() {
return this._form.pseudoClassLocks || [];
},
hasPseudoClassLock: function (pseudo) {
return this.pseudoClassLocks.some(locked => locked === pseudo);
},
get isDisplayed() {
// The NodeActor's form contains the isDisplayed information as a boolean
// starting from FF32. Before that, the property is missing
return "isDisplayed" in this._form ? this._form.isDisplayed : true;
},
get isTreeDisplayed() {
let parent = this;
while (parent) {
if (!parent.isDisplayed) {
return false;
}
parent = parent.parentNode();
}
return true;
},
getNodeValue: custom(function () {
// backward-compatibility: if nodevalue is null and shortValue is defined, the actual
// value of the node needs to be fetched on the server.
if (this._form.nodeValue === null && this._form.shortValue) {
return this._getNodeValue();
}
let str = this._form.nodeValue || "";
return promise.resolve(new SimpleStringFront(str));
}, {
impl: "_getNodeValue"
}),
// Accessors for custom form properties.
getFormProperty: function (name) {
return this._form.props ? this._form.props[name] : null;
},
hasFormProperty: function (name) {
return this._form.props ? (name in this._form.props) : null;
},
get formProperties() {
return this._form.props;
},
/**
* Return a new AttributeModificationList for this node.
*/
startModifyingAttributes: function () {
return AttributeModificationList(this);
},
_cacheAttributes: function () {
if (typeof this._attrMap != "undefined") {
return;
}
this._attrMap = {};
for (let attr of this.attributes) {
this._attrMap[attr.name] = attr;
}
},
_getAttribute: function (name) {
this._cacheAttributes();
return this._attrMap[name] || undefined;
},
/**
* Set this node's parent. Note that the children saved in
* this tree are unordered and incomplete, so shouldn't be used
* instead of a `children` request.
*/
reparent: function (parent) {
if (this._parent === parent) {
return;
}
if (this._parent && this._parent._child === this) {
this._parent._child = this._next;
}
if (this._prev) {
this._prev._next = this._next;
}
if (this._next) {
this._next._prev = this._prev;
}
this._next = null;
this._prev = null;
this._parent = parent;
if (!parent) {
// Subtree is disconnected, we're done
return;
}
this._next = parent._child;
if (this._next) {
this._next._prev = this;
}
parent._child = this;
},
/**
* Return all the known children of this node.
*/
treeChildren: function () {
let ret = [];
for (let child = this._child; child != null; child = child._next) {
ret.push(child);
}
return ret;
},
/**
* Do we use a local target?
* Useful to know if a rawNode is available or not.
*
* This will, one day, be removed. External code should
* not need to know if the target is remote or not.
*/
isLocalToBeDeprecated: function () {
return !!this.conn._transport._serverConnection;
},
/**
* Get an nsIDOMNode for the given node front. This only works locally,
* and is only intended as a stopgap during the transition to the remote
* protocol. If you depend on this you're likely to break soon.
*/
rawNode: function (rawNode) {
if (!this.isLocalToBeDeprecated()) {
console.warn("Tried to use rawNode on a remote connection.");
return null;
}
const { DebuggerServer } = require("devtools/server/main");
let actor = DebuggerServer._searchAllConnectionsForActor(this.actorID);
if (!actor) {
// Can happen if we try to get the raw node for an already-expired
// actor.
return null;
}
return actor.rawNode;
}
});
exports.NodeFront = NodeFront;
/**
* Client side of a node list as returned by querySelectorAll()
*/
const NodeListFront = FrontClassWithSpec(nodeListSpec, {
initialize: function (client, form) {
Front.prototype.initialize.call(this, client, form);
},
destroy: function () {
Front.prototype.destroy.call(this);
},
marshallPool: function () {
return this.parent();
},
// Update the object given a form representation off the wire.
form: function (json) {
this.length = json.length;
},
item: custom(function (index) {
return this._item(index).then(response => {
return response.node;
});
}, {
impl: "_item"
}),
items: custom(function (start, end) {
return this._items(start, end).then(response => {
return response.nodes;
});
}, {
impl: "_items"
})
});
exports.NodeListFront = NodeListFront;
/**
* Client side of the DOM walker.
*/
const WalkerFront = FrontClassWithSpec(walkerSpec, {
// Set to true if cleanup should be requested after every mutation list.
autoCleanup: true,
/**
* This is kept for backward-compatibility reasons with older remote target.
* Targets previous to bug 916443
*/
pick: custom(function () {
return this._pick().then(response => {
return response.node;
});
}, {impl: "_pick"}),
initialize: function (client, form) {
this._createRootNodePromise();
Front.prototype.initialize.call(this, client, form);
this._orphaned = new Set();
this._retainedOrphans = new Set();
},
destroy: function () {
Front.prototype.destroy.call(this);
},
// Update the object given a form representation off the wire.
form: function (json) {
this.actorID = json.actor;
this.rootNode = types.getType("domnode").read(json.root, this);
this._rootNodeDeferred.resolve(this.rootNode);
// FF42+ the actor starts exposing traits
this.traits = json.traits || {};
},
/**
* Clients can use walker.rootNode to get the current root node of the
* walker, but during a reload the root node might be null. This
* method returns a promise that will resolve to the root node when it is
* set.
*/
getRootNode: function () {
return this._rootNodeDeferred.promise;
},
/**
* Create the root node promise, triggering the "new-root" notification
* on resolution.
*/
_createRootNodePromise: function () {
this._rootNodeDeferred = defer();
this._rootNodeDeferred.promise.then(() => {
events.emit(this, "new-root");
});
},
/**
* When reading an actor form off the wire, we want to hook it up to its
* parent front. The protocol guarantees that the parent will be seen
* by the client in either a previous or the current request.
* So if we've already seen this parent return it, otherwise create
* a bare-bones stand-in node. The stand-in node will be updated
* with a real form by the end of the deserialization.
*/
ensureParentFront: function (id) {
let front = this.get(id);
if (front) {
return front;
}
return types.getType("domnode").read({ actor: id }, this, "standin");
},
/**
* See the documentation for WalkerActor.prototype.retainNode for
* information on retained nodes.
*
* From the client's perspective, `retainNode` can fail if the node in
* question is removed from the ownership tree before the `retainNode`
* request reaches the server. This can only happen if the client has
* asked the server to release nodes but hasn't gotten a response
* yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
* set is outstanding.
*
* If either of those requests is outstanding AND releases the retained
* node, this request will fail with noSuchActor, but the ownership tree
* will stay in a consistent state.
*
* Because the protocol guarantees that requests will be processed and
* responses received in the order they were sent, we get the right
* semantics by setting our local retained flag on the node only AFTER
* a SUCCESSFUL retainNode call.
*/
retainNode: custom(function (node) {
return this._retainNode(node).then(() => {
node.retained = true;
});
}, {
impl: "_retainNode",
}),
unretainNode: custom(function (node) {
return this._unretainNode(node).then(() => {
node.retained = false;
if (this._retainedOrphans.has(node)) {
this._retainedOrphans.delete(node);
this._releaseFront(node);
}
});
}, {
impl: "_unretainNode"
}),
releaseNode: custom(function (node, options = {}) {
// NodeFront.destroy will destroy children in the ownership tree too,
// mimicking what the server will do here.
let actorID = node.actorID;
this._releaseFront(node, !!options.force);
return this._releaseNode({ actorID: actorID });
}, {
impl: "_releaseNode"
}),
findInspectingNode: custom(function () {
return this._findInspectingNode().then(response => {
return response.node;
});
}, {
impl: "_findInspectingNode"
}),
querySelector: custom(function (queryNode, selector) {
return this._querySelector(queryNode, selector).then(response => {
return response.node;
});
}, {
impl: "_querySelector"
}),
getNodeActorFromObjectActor: custom(function (objectActorID) {
return this._getNodeActorFromObjectActor(objectActorID).then(response => {
return response ? response.node : null;
});
}, {
impl: "_getNodeActorFromObjectActor"
}),
getStyleSheetOwnerNode: custom(function (styleSheetActorID) {
return this._getStyleSheetOwnerNode(styleSheetActorID).then(response => {
return response ? response.node : null;
});
}, {
impl: "_getStyleSheetOwnerNode"
}),
getNodeFromActor: custom(function (actorID, path) {
return this._getNodeFromActor(actorID, path).then(response => {
return response ? response.node : null;
});
}, {
impl: "_getNodeFromActor"
}),
/*
* Incrementally search the document for a given string.
* For modern servers, results will be searched with using the WalkerActor
* `search` function (includes tag names, attributes, and text contents).
* Only 1 result is sent back, and calling the method again with the same
* query will send the next result. When there are no more results to be sent
* back, null is sent.
* @param {String} query
* @param {Object} options
* - "reverse": search backwards
* - "selectorOnly": treat input as a selector string (don't search text
* tags, attributes, etc)
*/
search: custom(Task.async(function* (query, options = { }) {
let nodeList;
let searchType;
let searchData = this.searchData = this.searchData || { };
let selectorOnly = !!options.selectorOnly;
// Backwards compat. Use selector only search if the new
// search functionality isn't implemented, or if the caller (tests)
// want it.
if (selectorOnly || !this.traits.textSearch) {
searchType = "selector";
if (this.traits.multiFrameQuerySelectorAll) {
nodeList = yield this.multiFrameQuerySelectorAll(query);
} else {
nodeList = yield this.querySelectorAll(this.rootNode, query);
}
} else {
searchType = "search";
let result = yield this._search(query, options);
nodeList = result.list;
}
// If this is a new search, start at the beginning.
if (searchData.query !== query ||
searchData.selectorOnly !== selectorOnly) {
searchData.selectorOnly = selectorOnly;
searchData.query = query;
searchData.index = -1;
}
if (!nodeList.length) {
return null;
}
// Move search result cursor and cycle if necessary.
searchData.index = options.reverse ? searchData.index - 1 :
searchData.index + 1;
if (searchData.index >= nodeList.length) {
searchData.index = 0;
}
if (searchData.index < 0) {
searchData.index = nodeList.length - 1;
}
// Send back the single node, along with any relevant search data
let node = yield nodeList.item(searchData.index);
return {
type: searchType,
node: node,
resultsLength: nodeList.length,
resultsIndex: searchData.index,
};
}), {
impl: "_search"
}),
_releaseFront: function (node, force) {
if (node.retained && !force) {
node.reparent(null);
this._retainedOrphans.add(node);
return;
}
if (node.retained) {
// Forcing a removal.
this._retainedOrphans.delete(node);
}
// Release any children
for (let child of node.treeChildren()) {
this._releaseFront(child, force);
}
// All children will have been removed from the node by this point.
node.reparent(null);
node.destroy();
},
/**
* Get any unprocessed mutation records and process them.
*/
getMutations: custom(function (options = {}) {
return this._getMutations(options).then(mutations => {
let emitMutations = [];
for (let change of mutations) {
// The target is only an actorID, get the associated front.
let targetID;
let targetFront;
if (change.type === "newRoot") {
// We may receive a new root without receiving any documentUnload
// beforehand. Like when opening tools in middle of a document load.
if (this.rootNode) {
this._createRootNodePromise();
}
this.rootNode = types.getType("domnode").read(change.target, this);
this._rootNodeDeferred.resolve(this.rootNode);
targetID = this.rootNode.actorID;
targetFront = this.rootNode;
} else {
targetID = change.target;
targetFront = this.get(targetID);
}
if (!targetFront) {
console.trace("Got a mutation for an unexpected actor: " + targetID +
", please file a bug on bugzilla.mozilla.org!");
continue;
}
let emittedMutation = object.merge(change, { target: targetFront });
if (change.type === "childList" ||
change.type === "nativeAnonymousChildList") {
// Update the ownership tree according to the mutation record.
let addedFronts = [];
let removedFronts = [];
for (let removed of change.removed) {
let removedFront = this.get(removed);
if (!removedFront) {
console.error("Got a removal of an actor we didn't know about: " +
removed);
continue;
}
// Remove from the ownership tree
removedFront.reparent(null);
// This node is orphaned unless we get it in the 'added' list
// eventually.
this._orphaned.add(removedFront);
removedFronts.push(removedFront);
}
for (let added of change.added) {
let addedFront = this.get(added);
if (!addedFront) {
console.error("Got an addition of an actor we didn't know " +
"about: " + added);
continue;
}
addedFront.reparent(targetFront);
// The actor is reconnected to the ownership tree, unorphan
// it.
this._orphaned.delete(addedFront);
addedFronts.push(addedFront);
}
// Before passing to users, replace the added and removed actor
// ids with front in the mutation record.
emittedMutation.added = addedFronts;
emittedMutation.removed = removedFronts;
// If this is coming from a DOM mutation, the actor's numChildren
// was passed in. Otherwise, it is simulated from a frame load or
// unload, so don't change the front's form.
if ("numChildren" in change) {
targetFront._form.numChildren = change.numChildren;
}
} else if (change.type === "frameLoad") {
// Nothing we need to do here, except verify that we don't have any
// document children, because we should have gotten a documentUnload
// first.
for (let child of targetFront.treeChildren()) {
if (child.nodeType === nodeConstants.DOCUMENT_NODE) {
console.trace("Got an unexpected frameLoad in the inspector, " +
"please file a bug on bugzilla.mozilla.org!");
}
}
} else if (change.type === "documentUnload") {
if (targetFront === this.rootNode) {
this._createRootNodePromise();
}
// We try to give fronts instead of actorIDs, but these fronts need
// to be destroyed now.
emittedMutation.target = targetFront.actorID;
emittedMutation.targetParent = targetFront.parentNode();
// Release the document node and all of its children, even retained.
this._releaseFront(targetFront, true);
} else if (change.type === "unretained") {
// Retained orphans were force-released without the intervention of
// client (probably a navigated frame).
for (let released of change.nodes) {
let releasedFront = this.get(released);
this._retainedOrphans.delete(released);
this._releaseFront(releasedFront, true);
}
} else {
targetFront.updateMutation(change);
}
// Update the inlineTextChild property of the target for a selected list of
// mutation types.
if (change.type === "inlineTextChild" ||
change.type === "childList" ||
change.type === "nativeAnonymousChildList") {
if (change.inlineTextChild) {
targetFront.inlineTextChild =
types.getType("domnode").read(change.inlineTextChild, this);
} else {
targetFront.inlineTextChild = undefined;
}
}
emitMutations.push(emittedMutation);
}
if (options.cleanup) {
for (let node of this._orphaned) {
// This will move retained nodes to this._retainedOrphans.
this._releaseFront(node);
}
this._orphaned = new Set();
}
events.emit(this, "mutations", emitMutations);
});
}, {
impl: "_getMutations"
}),
/**
* Handle the `new-mutations` notification by fetching the
* available mutation records.
*/
onMutations: preEvent("new-mutations", function () {
// Fetch and process the mutations.
this.getMutations({cleanup: this.autoCleanup}).catch(() => {});
}),
isLocal: function () {
return !!this.conn._transport._serverConnection;
},
// XXX hack during transition to remote inspector: get a proper NodeFront
// for a given local node. Only works locally.
frontForRawNode: function (rawNode) {
if (!this.isLocal()) {
console.warn("Tried to use frontForRawNode on a remote connection.");
return null;
}
const { DebuggerServer } = require("devtools/server/main");
let walkerActor = DebuggerServer._searchAllConnectionsForActor(this.actorID);
if (!walkerActor) {
throw Error("Could not find client side for actor " + this.actorID);
}
let nodeActor = walkerActor._ref(rawNode);
// Pass the node through a read/write pair to create the client side actor.
let nodeType = types.getType("domnode");
let returnNode = nodeType.read(
nodeType.write(nodeActor, walkerActor), this);
let top = returnNode;
let extras = walkerActor.parents(nodeActor, {sameTypeRootTreeItem: true});
for (let extraActor of extras) {
top = nodeType.read(nodeType.write(extraActor, walkerActor), this);
}
if (top !== this.rootNode) {
// Imported an already-orphaned node.
this._orphaned.add(top);
walkerActor._orphaned
.add(DebuggerServer._searchAllConnectionsForActor(top.actorID));
}
return returnNode;
},
removeNode: custom(Task.async(function* (node) {
let previousSibling = yield this.previousSibling(node);
let nextSibling = yield this._removeNode(node);
return {
previousSibling: previousSibling,
nextSibling: nextSibling,
};
}), {
impl: "_removeNode"
}),
});
exports.WalkerFront = WalkerFront;
/**
* Client side of the inspector actor, which is used to create
* inspector-related actors, including the walker.
*/
var InspectorFront = FrontClassWithSpec(inspectorSpec, {
initialize: function (client, tabForm) {
Front.prototype.initialize.call(this, client);
this.actorID = tabForm.inspectorActor;
// XXX: This is the first actor type in its hierarchy to use the protocol
// library, so we're going to self-own on the client side for now.
this.manage(this);
},
destroy: function () {
delete this.walker;
Front.prototype.destroy.call(this);
},
getWalker: custom(function (options = {}) {
return this._getWalker(options).then(walker => {
this.walker = walker;
return walker;
});
}, {
impl: "_getWalker"
}),
getPageStyle: custom(function () {
return this._getPageStyle().then(pageStyle => {
// We need a walker to understand node references from the
// node style.
if (this.walker) {
return pageStyle;
}
return this.getWalker().then(() => {
return pageStyle;
});
});
}, {
impl: "_getPageStyle"
}),
pickColorFromPage: custom(Task.async(function* (toolbox, options) {
if (toolbox) {
// If the eyedropper was already started using the gcli command, hide it so we don't
// end up with 2 instances of the eyedropper on the page.
let {target} = toolbox;
let requisition = yield CommandUtils.createRequisition(target, {
environment: CommandUtils.createEnvironment({target})
});
yield requisition.updateExec("eyedropper --hide");
}
yield this._pickColorFromPage(options);
}), {
impl: "_pickColorFromPage"
})
});
exports.InspectorFront = InspectorFront;