зеркало из https://github.com/mozilla/gecko-dev.git
696 строки
22 KiB
JavaScript
696 строки
22 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";
|
|
|
|
const protocol = require("devtools/shared/protocol");
|
|
|
|
const { Cc, Ci, Cu, Cr } = require("chrome");
|
|
|
|
const { DebuggerServer } = require("devtools/server/debugger-server");
|
|
const Services = require("Services");
|
|
const ChromeUtils = require("ChromeUtils");
|
|
|
|
loader.lazyGetter(
|
|
this,
|
|
"NodeActor",
|
|
() => require("devtools/server/actors/inspector/node").NodeActor,
|
|
true
|
|
);
|
|
|
|
const {
|
|
webExtensionInspectedWindowSpec,
|
|
} = require("devtools/shared/specs/addon/webextension-inspected-window");
|
|
|
|
const { WebExtensionPolicy } = Cu.getGlobalForObject(
|
|
require("resource://gre/modules/XPCOMUtils.jsm")
|
|
);
|
|
|
|
// A weak set of the documents for which a warning message has been
|
|
// already logged (so that we don't keep emitting the same warning if an
|
|
// extension keeps calling the devtools.inspectedWindow.eval API method
|
|
// when it fails to retrieve a result, but we do log the warning message
|
|
// if the user reloads the window):
|
|
//
|
|
// WeakSet<Document>
|
|
const deniedWarningDocuments = new WeakSet();
|
|
|
|
function isSystemPrincipalWindow(window) {
|
|
return window.document.nodePrincipal.isSystemPrincipal;
|
|
}
|
|
|
|
// Create the exceptionInfo property in the format expected by a
|
|
// WebExtension inspectedWindow.eval API calls.
|
|
function createExceptionInfoResult(props) {
|
|
return {
|
|
exceptionInfo: {
|
|
isError: true,
|
|
code: "E_PROTOCOLERROR",
|
|
description: "Unknown Inspector protocol error",
|
|
|
|
// Apply the passed properties.
|
|
...props,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Show a warning message in the webconsole when an extension
|
|
// eval request has been denied, so that the user knows about it
|
|
// even if the extension doesn't report the error itself.
|
|
function logAccessDeniedWarning(window, callerInfo, extensionPolicy) {
|
|
// Do not log the same warning multiple times for the same document.
|
|
if (deniedWarningDocuments.has(window.document)) {
|
|
return;
|
|
}
|
|
|
|
deniedWarningDocuments.add(window.document);
|
|
|
|
const { name } = extensionPolicy;
|
|
|
|
// System principals have a null nodePrincipal.URI and so we use
|
|
// the url from window.location.href.
|
|
const reportedURI = isSystemPrincipalWindow(window)
|
|
? Services.io.newURI(window.location.href)
|
|
: window.document.nodePrincipal.URI;
|
|
|
|
const error = Cc["@mozilla.org/scripterror;1"].createInstance(
|
|
Ci.nsIScriptError
|
|
);
|
|
|
|
const msg = `The extension "${name}" is not allowed to access ${
|
|
reportedURI.spec
|
|
}`;
|
|
|
|
const innerWindowId = window.windowUtils.currentInnerWindowID;
|
|
|
|
const errorFlag = 0;
|
|
|
|
let { url, lineNumber } = callerInfo;
|
|
|
|
const callerURI = callerInfo.url && Services.io.newURI(callerInfo.url);
|
|
|
|
// callerInfo.url is not the full path to the file that called the WebExtensions
|
|
// API yet (Bug 1448878), and so we associate the error to the url of the extension
|
|
// manifest.json file as a fallback.
|
|
if (callerURI.filePath === "/") {
|
|
url = extensionPolicy.getURL("/manifest.json");
|
|
lineNumber = null;
|
|
}
|
|
|
|
error.initWithWindowID(
|
|
msg,
|
|
url,
|
|
lineNumber,
|
|
0,
|
|
0,
|
|
errorFlag,
|
|
"webExtensions",
|
|
innerWindowId
|
|
);
|
|
Services.console.logMessage(error);
|
|
}
|
|
|
|
function CustomizedReload(params) {
|
|
this.docShell = params.targetActor.window.docShell;
|
|
this.docShell.QueryInterface(Ci.nsIWebProgress);
|
|
|
|
this.inspectedWindowEval = params.inspectedWindowEval;
|
|
this.callerInfo = params.callerInfo;
|
|
|
|
this.ignoreCache = params.ignoreCache;
|
|
this.injectedScript = params.injectedScript;
|
|
this.userAgent = params.userAgent;
|
|
|
|
this.customizedReloadWindows = new WeakSet();
|
|
}
|
|
|
|
CustomizedReload.prototype = {
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
Ci.nsIWebProgressListener,
|
|
Ci.nsISupportsWeakReference,
|
|
]),
|
|
get window() {
|
|
return this.docShell.DOMWindow;
|
|
},
|
|
|
|
get webNavigation() {
|
|
return this.docShell
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation);
|
|
},
|
|
|
|
start() {
|
|
if (!this.waitForReloadCompleted) {
|
|
this.waitForReloadCompleted = new Promise((resolve, reject) => {
|
|
this.resolveReloadCompleted = resolve;
|
|
this.rejectReloadCompleted = reject;
|
|
|
|
if (this.userAgent) {
|
|
this.docShell.customUserAgent = this.userAgent;
|
|
}
|
|
|
|
let reloadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
|
|
|
if (this.ignoreCache) {
|
|
reloadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
|
|
}
|
|
|
|
try {
|
|
if (this.injectedScript) {
|
|
// Listen to the newly created document elements only if there is an
|
|
// injectedScript to evaluate.
|
|
Services.obs.addObserver(this, "initial-document-element-inserted");
|
|
}
|
|
|
|
// Watch the loading progress and clear the current CustomizedReload once the
|
|
// page has been reloaded (or if its reloading has been interrupted).
|
|
this.docShell.addProgressListener(
|
|
this,
|
|
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
|
|
);
|
|
|
|
this.webNavigation.reload(reloadFlags);
|
|
} catch (err) {
|
|
// Cancel the injected script listener if the reload fails
|
|
// (which will also report the error by rejecting the promise).
|
|
this.stop(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
return this.waitForReloadCompleted;
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
if (topic !== "initial-document-element-inserted") {
|
|
return;
|
|
}
|
|
|
|
const document = subject;
|
|
const window = document && document.defaultView;
|
|
|
|
// Filter out non interesting documents.
|
|
if (!document || !document.location || !window) {
|
|
return;
|
|
}
|
|
|
|
const subjectDocShell = window.docShell;
|
|
|
|
// Keep track of the set of window objects where we are going to inject
|
|
// the injectedScript: the top level window and all its descendant
|
|
// that are still of type content (filtering out loaded XUL pages, if any).
|
|
if (window == this.window) {
|
|
this.customizedReloadWindows.add(window);
|
|
} else if (subjectDocShell.sameTypeParent) {
|
|
const parentWindow = subjectDocShell.sameTypeParent.domWindow;
|
|
if (parentWindow && this.customizedReloadWindows.has(parentWindow)) {
|
|
this.customizedReloadWindows.add(window);
|
|
}
|
|
}
|
|
|
|
if (this.customizedReloadWindows.has(window)) {
|
|
const { apiErrorResult } = this.inspectedWindowEval(
|
|
this.callerInfo,
|
|
this.injectedScript,
|
|
{},
|
|
window
|
|
);
|
|
|
|
// Log only apiErrorResult, because no one is waiting for the
|
|
// injectedScript result, and any exception is going to be logged
|
|
// in the inspectedWindow webconsole.
|
|
if (apiErrorResult) {
|
|
console.error(
|
|
"Unexpected Error in injectedScript during inspectedWindow.reload for",
|
|
`${this.callerInfo.url}:${this.callerInfo.lineNumber}`,
|
|
apiErrorResult
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
onStateChange(webProgress, request, state, status) {
|
|
if (webProgress.DOMWindow !== this.window) {
|
|
return;
|
|
}
|
|
|
|
if (state & Ci.nsIWebProgressListener.STATE_STOP) {
|
|
if (status == Cr.NS_BINDING_ABORTED) {
|
|
// The customized reload has been interrupted and we can clear
|
|
// the CustomizedReload and reject the promise.
|
|
const url = this.window.location.href;
|
|
this.stop(
|
|
new Error(
|
|
`devtools.inspectedWindow.reload on ${url} has been interrupted`
|
|
)
|
|
);
|
|
} else {
|
|
// Once the top level frame has been loaded, we can clear the customized reload
|
|
// and resolve the promise.
|
|
this.stop();
|
|
}
|
|
}
|
|
},
|
|
|
|
stop(error) {
|
|
if (this.stopped) {
|
|
return;
|
|
}
|
|
|
|
this.docShell.removeProgressListener(this);
|
|
|
|
if (this.injectedScript) {
|
|
Services.obs.removeObserver(this, "initial-document-element-inserted");
|
|
}
|
|
|
|
// Reset the customized user agent.
|
|
if (this.userAgent && this.docShell.customUserAgent == this.userAgent) {
|
|
this.docShell.customUserAgent = null;
|
|
}
|
|
|
|
if (error) {
|
|
this.rejectReloadCompleted(error);
|
|
} else {
|
|
this.resolveReloadCompleted();
|
|
}
|
|
|
|
this.stopped = true;
|
|
},
|
|
};
|
|
|
|
var WebExtensionInspectedWindowActor = protocol.ActorClassWithSpec(
|
|
webExtensionInspectedWindowSpec,
|
|
{
|
|
/**
|
|
* Created the WebExtension InspectedWindow actor
|
|
*/
|
|
initialize(conn, targetActor) {
|
|
protocol.Actor.prototype.initialize.call(this, conn);
|
|
this.targetActor = targetActor;
|
|
},
|
|
|
|
destroy(conn) {
|
|
protocol.Actor.prototype.destroy.call(this, conn);
|
|
|
|
if (this.customizedReload) {
|
|
this.customizedReload.stop(
|
|
new Error("WebExtensionInspectedWindowActor destroyed")
|
|
);
|
|
delete this.customizedReload;
|
|
}
|
|
|
|
if (this._dbg) {
|
|
this._dbg.disable();
|
|
delete this._dbg;
|
|
}
|
|
},
|
|
|
|
get dbg() {
|
|
if (this._dbg) {
|
|
return this._dbg;
|
|
}
|
|
|
|
this._dbg = this.targetActor.makeDebugger();
|
|
return this._dbg;
|
|
},
|
|
|
|
get window() {
|
|
return this.targetActor.window;
|
|
},
|
|
|
|
get webNavigation() {
|
|
return this.targetActor.webNavigation;
|
|
},
|
|
|
|
createEvalBindings(dbgWindow, options) {
|
|
const bindings = Object.create(null);
|
|
|
|
let selectedDOMNode;
|
|
|
|
if (options.toolboxSelectedNodeActorID) {
|
|
const actor = DebuggerServer.searchAllConnectionsForActor(
|
|
options.toolboxSelectedNodeActorID
|
|
);
|
|
if (actor && actor instanceof NodeActor) {
|
|
selectedDOMNode = actor.rawNode;
|
|
}
|
|
}
|
|
|
|
Object.defineProperty(bindings, "$0", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get: () => {
|
|
if (selectedDOMNode && !Cu.isDeadWrapper(selectedDOMNode)) {
|
|
return dbgWindow.makeDebuggeeValue(selectedDOMNode);
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
});
|
|
|
|
// This function is used by 'eval' and 'reload' requests, but only 'eval'
|
|
// passes 'toolboxConsoleActor' from the client side in order to set
|
|
// the 'inspect' binding.
|
|
Object.defineProperty(bindings, "inspect", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
value: dbgWindow.makeDebuggeeValue(object => {
|
|
const dbgObj = dbgWindow.makeDebuggeeValue(object);
|
|
|
|
const consoleActor = DebuggerServer.searchAllConnectionsForActor(
|
|
options.toolboxConsoleActorID
|
|
);
|
|
if (consoleActor) {
|
|
consoleActor.inspectObject(
|
|
dbgObj,
|
|
"webextension-devtools-inspectedWindow-eval"
|
|
);
|
|
} else {
|
|
// TODO(rpl): evaluate if it would be better to raise an exception
|
|
// to the caller code instead.
|
|
console.error("Toolbox Console RDP Actor not found");
|
|
}
|
|
}),
|
|
});
|
|
|
|
return bindings;
|
|
},
|
|
|
|
/**
|
|
* Reload the target tab, optionally bypass cache, customize the userAgent and/or
|
|
* inject a script in targeted document or any of its sub-frame.
|
|
*
|
|
* @param {webExtensionCallerInfo} callerInfo
|
|
* the addonId and the url (the addon base url or the url of the actual caller
|
|
* filename and lineNumber) used to log useful debugging information in the
|
|
* produced error logs and eval stack trace.
|
|
*
|
|
* @param {webExtensionReloadOptions} options
|
|
* used to optionally enable the reload customizations.
|
|
* @param {boolean|undefined} options.ignoreCache
|
|
* enable/disable the cache bypass headers.
|
|
* @param {string|undefined} options.userAgent
|
|
* customize the userAgent during the page reload.
|
|
* @param {string|undefined} options.injectedScript
|
|
* evaluate the provided javascript code in the top level and every sub-frame
|
|
* created during the page reload, before any other script in the page has been
|
|
* executed.
|
|
*/
|
|
reload(callerInfo, { ignoreCache, userAgent, injectedScript }) {
|
|
if (isSystemPrincipalWindow(this.window)) {
|
|
console.error(
|
|
"Ignored inspectedWindow.reload on system principal target for " +
|
|
`${callerInfo.url}:${callerInfo.lineNumber}`
|
|
);
|
|
return {};
|
|
}
|
|
|
|
const delayedReload = () => {
|
|
// This won't work while the browser is shutting down and we don't really
|
|
// care.
|
|
if (Services.startup.shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
if (injectedScript || userAgent) {
|
|
if (this.customizedReload) {
|
|
// TODO(rpl): check what chrome does, and evaluate if queue the new reload
|
|
// after the current one has been completed.
|
|
console.error(
|
|
"Reload already in progress. Ignored inspectedWindow.reload for " +
|
|
`${callerInfo.url}:${callerInfo.lineNumber}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.customizedReload = new CustomizedReload({
|
|
targetActor: this.targetActor,
|
|
inspectedWindowEval: this.eval.bind(this),
|
|
callerInfo,
|
|
injectedScript,
|
|
userAgent,
|
|
ignoreCache,
|
|
});
|
|
|
|
this.customizedReload
|
|
.start()
|
|
.then(() => {
|
|
delete this.customizedReload;
|
|
})
|
|
.catch(err => {
|
|
delete this.customizedReload;
|
|
console.error(err);
|
|
});
|
|
} catch (err) {
|
|
// Cancel the customized reload (if any) on exception during the
|
|
// reload setup.
|
|
if (this.customizedReload) {
|
|
this.customizedReload.stop(err);
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
} else {
|
|
// If there is no custom user agent and/or injected script, then
|
|
// we can reload the target without subscribing any observer/listener.
|
|
let reloadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
|
if (ignoreCache) {
|
|
reloadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
|
|
}
|
|
this.webNavigation.reload(reloadFlags);
|
|
}
|
|
};
|
|
|
|
// Execute the reload in a dispatched runnable, so that we can
|
|
// return the reply to the caller before the reload is actually
|
|
// started.
|
|
Services.tm.dispatchToMainThread(delayedReload);
|
|
|
|
return {};
|
|
},
|
|
|
|
/**
|
|
* Evaluate the provided javascript code in a target window (that is always the
|
|
* targetActor window when called through RDP protocol, or the passed
|
|
* customTargetWindow when called directly from the CustomizedReload instances).
|
|
*
|
|
* @param {webExtensionCallerInfo} callerInfo
|
|
* the addonId and the url (the addon base url or the url of the actual caller
|
|
* filename and lineNumber) used to log useful debugging information in the
|
|
* produced error logs and eval stack trace.
|
|
*
|
|
* @param {string} expression
|
|
* the javascript code to be evaluated in the target window
|
|
*
|
|
* @param {webExtensionEvalOptions} evalOptions
|
|
* used to optionally enable the eval customizations.
|
|
* NOTE: none of the eval options is currently implemented, they will be already
|
|
* reported as unsupported by the WebExtensions schema validation wrappers, but
|
|
* an additional level of error reporting is going to be applied here, so that
|
|
* if the server and the client have different ideas of which option is supported
|
|
* the eval call result will contain detailed informations (in the format usually
|
|
* expected for errors not raised in the evaluated javascript code).
|
|
*
|
|
* @param {DOMWindow|undefined} customTargetWindow
|
|
* Used in the CustomizedReload instances to evaluate the `injectedScript`
|
|
* javascript code in every sub-frame of the target window during the tab reload.
|
|
* NOTE: this parameter is not part of the RDP protocol exposed by this actor, when
|
|
* it is called over the remote debugging protocol the target window is always
|
|
* `targetActor.window`.
|
|
*/
|
|
// eslint-disable-next-line complexity
|
|
eval(callerInfo, expression, options, customTargetWindow) {
|
|
const window = customTargetWindow || this.window;
|
|
options = options || {};
|
|
|
|
const extensionPolicy = WebExtensionPolicy.getByID(callerInfo.addonId);
|
|
|
|
if (!extensionPolicy) {
|
|
return createExceptionInfoResult({
|
|
description: "Inspector protocol error: %s %s",
|
|
details: ["Caller extension not found for", callerInfo.url],
|
|
});
|
|
}
|
|
|
|
if (!window) {
|
|
return createExceptionInfoResult({
|
|
description: "Inspector protocol error: %s",
|
|
details: [
|
|
"The target window is not defined. inspectedWindow.eval not executed.",
|
|
],
|
|
});
|
|
}
|
|
|
|
// Log the error for the user to know that the extension request has been denied
|
|
// (the extension may not warn the user at all).
|
|
const logEvalDenied = () => {
|
|
logAccessDeniedWarning(window, callerInfo, extensionPolicy);
|
|
};
|
|
|
|
if (isSystemPrincipalWindow(window)) {
|
|
logEvalDenied();
|
|
|
|
// On denied JS evaluation, report it to the extension using the same data format
|
|
// used in the corresponding chrome API method to report issues that are
|
|
// not exceptions raised in the evaluated javascript code.
|
|
return createExceptionInfoResult({
|
|
description: "Inspector protocol error: %s",
|
|
details: [
|
|
"This target has a system principal. inspectedWindow.eval denied.",
|
|
],
|
|
});
|
|
}
|
|
|
|
const docPrincipalURI = window.document.nodePrincipal.URI;
|
|
|
|
// Deny on document principals listed as restricted or
|
|
// related to the about: pages (only about:blank and about:srcdoc are
|
|
// allowed and their are expected to not have their about URI associated
|
|
// to the principal).
|
|
if (
|
|
WebExtensionPolicy.isRestrictedURI(docPrincipalURI) ||
|
|
docPrincipalURI.schemeIs("about")
|
|
) {
|
|
logEvalDenied();
|
|
|
|
return createExceptionInfoResult({
|
|
description: "Inspector protocol error: %s %s",
|
|
details: [
|
|
"This extension is not allowed on the current inspected window origin",
|
|
docPrincipalURI.spec,
|
|
],
|
|
});
|
|
}
|
|
|
|
const windowAddonId = window.document.nodePrincipal.addonId;
|
|
|
|
if (windowAddonId && extensionPolicy.id !== windowAddonId) {
|
|
logEvalDenied();
|
|
|
|
return createExceptionInfoResult({
|
|
description: "Inspector protocol error: %s on %s",
|
|
details: [
|
|
"This extension is not allowed to access this extension page.",
|
|
window.document.location.origin,
|
|
],
|
|
});
|
|
}
|
|
|
|
// Raise an error on the unsupported options.
|
|
if (
|
|
options.frameURL ||
|
|
options.contextSecurityOrigin ||
|
|
options.useContentScriptContext
|
|
) {
|
|
return createExceptionInfoResult({
|
|
description: "Inspector protocol error: %s",
|
|
details: [
|
|
"The inspectedWindow.eval options are currently not supported",
|
|
],
|
|
});
|
|
}
|
|
|
|
const dbgWindow = this.dbg.makeGlobalObjectReference(window);
|
|
|
|
let evalCalledFrom = callerInfo.url;
|
|
if (callerInfo.lineNumber) {
|
|
evalCalledFrom += `:${callerInfo.lineNumber}`;
|
|
}
|
|
|
|
const bindings = this.createEvalBindings(dbgWindow, options);
|
|
|
|
const result = dbgWindow.executeInGlobalWithBindings(
|
|
expression,
|
|
bindings,
|
|
{
|
|
url: `debugger eval called from ${evalCalledFrom} - eval code`,
|
|
}
|
|
);
|
|
|
|
let evalResult;
|
|
|
|
if (result) {
|
|
if ("return" in result) {
|
|
evalResult = result.return;
|
|
} else if ("yield" in result) {
|
|
evalResult = result.yield;
|
|
} else if ("throw" in result) {
|
|
const throwErr = result.throw;
|
|
|
|
// XXXworkers: Calling unsafeDereference() returns an object with no
|
|
// toString method in workers. See Bug 1215120.
|
|
const unsafeDereference =
|
|
throwErr &&
|
|
typeof throwErr === "object" &&
|
|
throwErr.unsafeDereference();
|
|
const message =
|
|
unsafeDereference && unsafeDereference.toString
|
|
? unsafeDereference.toString()
|
|
: String(throwErr);
|
|
const stack =
|
|
unsafeDereference && unsafeDereference.stack
|
|
? unsafeDereference.stack
|
|
: null;
|
|
|
|
return {
|
|
exceptionInfo: {
|
|
isException: true,
|
|
value: `${message}\n\t${stack}`,
|
|
},
|
|
};
|
|
}
|
|
} else {
|
|
// TODO(rpl): can the result of executeInGlobalWithBinding be null or
|
|
// undefined? (which means that it is not a return, a yield or a throw).
|
|
console.error(
|
|
"Unexpected empty inspectedWindow.eval result for",
|
|
`${callerInfo.url}:${callerInfo.lineNumber}`
|
|
);
|
|
}
|
|
|
|
if (evalResult) {
|
|
try {
|
|
// Return the evalResult as a grip (used by the WebExtensions
|
|
// devtools inspector's sidebar.setExpression API method).
|
|
if (options.evalResultAsGrip) {
|
|
if (!options.toolboxConsoleActorID) {
|
|
return createExceptionInfoResult({
|
|
description: "Inspector protocol error: %s - %s",
|
|
details: [
|
|
"Unexpected invalid sidebar panel expression request",
|
|
"missing toolboxConsoleActorID",
|
|
],
|
|
});
|
|
}
|
|
|
|
const consoleActor = DebuggerServer.searchAllConnectionsForActor(
|
|
options.toolboxConsoleActorID
|
|
);
|
|
|
|
return { valueGrip: consoleActor.createValueGrip(evalResult) };
|
|
}
|
|
|
|
if (evalResult && typeof evalResult === "object") {
|
|
evalResult = evalResult.unsafeDereference();
|
|
}
|
|
evalResult = JSON.parse(JSON.stringify(evalResult));
|
|
} catch (err) {
|
|
// The evaluation result cannot be sent over the RDP Protocol,
|
|
// report it as with the same data format used in the corresponding
|
|
// chrome API method.
|
|
return createExceptionInfoResult({
|
|
description: "Inspector protocol error: %s",
|
|
details: [String(err)],
|
|
});
|
|
}
|
|
}
|
|
|
|
return { value: evalResult };
|
|
},
|
|
}
|
|
);
|
|
|
|
exports.WebExtensionInspectedWindowActor = WebExtensionInspectedWindowActor;
|