336 lines
10 KiB
JavaScript
336 lines
10 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";
|
|
|
|
/*
|
|
* Represents a WebExtension add-on in the parent process. This gives some metadata about
|
|
* the add-on and watches for uninstall events. This uses a proxy to access the
|
|
* WebExtension in the WebExtension process via the message manager.
|
|
*
|
|
* See devtools/docs/backend/actor-hierarchy.md for more details.
|
|
*/
|
|
|
|
const { Actor } = require("resource://devtools/shared/protocol.js");
|
|
const {
|
|
webExtensionDescriptorSpec,
|
|
} = require("resource://devtools/shared/specs/descriptors/webextension.js");
|
|
|
|
const {
|
|
connectToFrame,
|
|
} = require("resource://devtools/server/connectors/frame-connector.js");
|
|
const {
|
|
createWebExtensionSessionContext,
|
|
} = require("resource://devtools/server/actors/watcher/session-context.js");
|
|
|
|
const lazy = {};
|
|
loader.lazyGetter(lazy, "AddonManager", () => {
|
|
return ChromeUtils.importESModule(
|
|
"resource://gre/modules/AddonManager.sys.mjs",
|
|
{ global: "shared" }
|
|
).AddonManager;
|
|
});
|
|
loader.lazyGetter(lazy, "ExtensionParent", () => {
|
|
return ChromeUtils.importESModule(
|
|
"resource://gre/modules/ExtensionParent.sys.mjs",
|
|
{ global: "shared" }
|
|
).ExtensionParent;
|
|
});
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"WatcherActor",
|
|
"resource://devtools/server/actors/watcher.js",
|
|
true
|
|
);
|
|
|
|
const BGSCRIPT_STATUSES = {
|
|
RUNNING: "RUNNING",
|
|
STOPPED: "STOPPED",
|
|
};
|
|
|
|
/**
|
|
* Creates the actor that represents the addon in the parent process, which connects
|
|
* itself to a WebExtensionTargetActor counterpart which is created in the extension
|
|
* process (or in the main process if the WebExtensions OOP mode is disabled).
|
|
*
|
|
* The WebExtensionDescriptorActor subscribes itself as an AddonListener on the AddonManager
|
|
* and forwards this events to child actor (e.g. on addon reload or when the addon is
|
|
* uninstalled completely) and connects to the child extension process using a `browser`
|
|
* element provided by the extension internals (it is not related to any single extension,
|
|
* but it will be created automatically to the currently selected "WebExtensions OOP mode"
|
|
* and it persist across the extension reloads (it is destroyed once the actor exits).
|
|
* WebExtensionDescriptorActor is a child of RootActor, it can be retrieved via
|
|
* RootActor.listAddons request.
|
|
*
|
|
* @param {DevToolsServerConnection} conn
|
|
* The connection to the client.
|
|
* @param {AddonWrapper} addon
|
|
* The target addon.
|
|
*/
|
|
class WebExtensionDescriptorActor extends Actor {
|
|
constructor(conn, addon) {
|
|
super(conn, webExtensionDescriptorSpec);
|
|
this.addon = addon;
|
|
this.addonId = addon.id;
|
|
this._childFormPromise = null;
|
|
|
|
this._onChildExit = this._onChildExit.bind(this);
|
|
this.destroy = this.destroy.bind(this);
|
|
lazy.AddonManager.addAddonListener(this);
|
|
}
|
|
|
|
form() {
|
|
const { addonId } = this;
|
|
const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(addonId);
|
|
const persistentBackgroundScript =
|
|
lazy.ExtensionParent.DebugUtils.hasPersistentBackgroundScript(addonId);
|
|
const backgroundScriptStatus = this._getBackgroundScriptStatus();
|
|
|
|
return {
|
|
actor: this.actorID,
|
|
backgroundScriptStatus,
|
|
// Note that until the policy becomes active,
|
|
// getTarget/connectToFrame will fail attaching to the web extension:
|
|
// https://searchfox.org/mozilla-central/rev/526a5089c61db85d4d43eb0e46edaf1f632e853a/toolkit/components/extensions/WebExtensionPolicy.cpp#551-553
|
|
debuggable: policy?.active && this.addon.isDebuggable,
|
|
hidden: this.addon.hidden,
|
|
// iconDataURL is available after calling loadIconDataURL
|
|
iconDataURL: this._iconDataURL,
|
|
iconURL: this.addon.iconURL,
|
|
id: addonId,
|
|
isSystem: this.addon.isSystem,
|
|
isWebExtension: this.addon.isWebExtension,
|
|
manifestURL: policy && policy.getURL("manifest.json"),
|
|
name: this.addon.name,
|
|
persistentBackgroundScript,
|
|
temporarilyInstalled: this.addon.temporarilyInstalled,
|
|
traits: {
|
|
supportsReloadDescriptor: true,
|
|
// Supports the Watcher actor. Can be removed as part of Bug 1680280.
|
|
watcher: true,
|
|
},
|
|
url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined,
|
|
warnings: lazy.ExtensionParent.DebugUtils.getExtensionManifestWarnings(
|
|
this.addonId
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Return a Watcher actor, allowing to keep track of targets which
|
|
* already exists or will be created. It also helps knowing when they
|
|
* are destroyed.
|
|
*/
|
|
async getWatcher(config = {}) {
|
|
if (!this.watcher) {
|
|
// Ensure connecting to the webextension frame in order to populate this._form
|
|
await this._extensionFrameConnect();
|
|
this.watcher = new WatcherActor(
|
|
this.conn,
|
|
createWebExtensionSessionContext(
|
|
{
|
|
addonId: this.addonId,
|
|
browsingContextID: this._form.browsingContextID,
|
|
innerWindowId: this._form.innerWindowId,
|
|
},
|
|
config
|
|
)
|
|
);
|
|
this.manage(this.watcher);
|
|
}
|
|
return this.watcher;
|
|
}
|
|
|
|
async getTarget() {
|
|
const form = await this._extensionFrameConnect();
|
|
// Merge into the child actor form, some addon metadata
|
|
// (e.g. the addon name shown in the addon debugger window title).
|
|
return Object.assign(form, {
|
|
iconURL: this.addon.iconURL,
|
|
id: this.addon.id,
|
|
name: this.addon.name,
|
|
});
|
|
}
|
|
|
|
getChildren() {
|
|
return [];
|
|
}
|
|
|
|
async _extensionFrameConnect() {
|
|
if (this._form) {
|
|
return this._form;
|
|
}
|
|
|
|
this._browser =
|
|
await lazy.ExtensionParent.DebugUtils.getExtensionProcessBrowser(this);
|
|
|
|
const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(
|
|
this.addonId
|
|
);
|
|
this._form = await connectToFrame(this.conn, this._browser, this.destroy, {
|
|
addonId: this.addonId,
|
|
addonBrowsingContextGroupId: policy.browsingContextGroupId,
|
|
// Bug 1754452: This flag is passed by the client to getWatcher(), but the server
|
|
// doesn't support this anyway. So always pass false here and keep things simple.
|
|
// Once we enable this flag, we will stop using connectToFrame and instantiate
|
|
// the WebExtensionTargetActor from watcher code instead, so that shouldn't
|
|
// introduce an issue for the future.
|
|
isServerTargetSwitchingEnabled: false,
|
|
});
|
|
|
|
// connectToFrame may resolve to a null form,
|
|
// in case the browser element is destroyed before it is fully connected to it.
|
|
if (!this._form) {
|
|
throw new Error(
|
|
"browser element destroyed while connecting to it: " + this.addon.name
|
|
);
|
|
}
|
|
|
|
this._childActorID = this._form.actor;
|
|
|
|
// Exit the proxy child actor if the child actor has been destroyed.
|
|
this._mm.addMessageListener("debug:webext_child_exit", this._onChildExit);
|
|
|
|
return this._form;
|
|
}
|
|
|
|
/**
|
|
* Note that reloadDescriptor is the common API name for descriptors
|
|
* which support to be reloaded, while WebExtensionDescriptorActor::reload
|
|
* is a legacy API which is for instance used from web-ext.
|
|
*
|
|
* bypassCache has no impact for addon reloads.
|
|
*/
|
|
reloadDescriptor() {
|
|
return this.reload();
|
|
}
|
|
|
|
async reload() {
|
|
await this.addon.reload();
|
|
return {};
|
|
}
|
|
|
|
async terminateBackgroundScript() {
|
|
await lazy.ExtensionParent.DebugUtils.terminateBackgroundScript(
|
|
this.addonId
|
|
);
|
|
}
|
|
|
|
// This function will be called from RootActor in case that the devtools client
|
|
// retrieves list of addons with `iconDataURL` option.
|
|
async loadIconDataURL() {
|
|
this._iconDataURL = await this.getIconDataURL();
|
|
}
|
|
|
|
async getIconDataURL() {
|
|
if (!this.addon.iconURL) {
|
|
return null;
|
|
}
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.responseType = "blob";
|
|
xhr.open("GET", this.addon.iconURL, true);
|
|
|
|
if (this.addon.iconURL.toLowerCase().endsWith(".svg")) {
|
|
// Maybe SVG, thus force to change mime type.
|
|
xhr.overrideMimeType("image/svg+xml");
|
|
}
|
|
|
|
try {
|
|
const blob = await new Promise((resolve, reject) => {
|
|
xhr.onload = () => resolve(xhr.response);
|
|
xhr.onerror = reject;
|
|
xhr.send();
|
|
});
|
|
|
|
const reader = new FileReader();
|
|
return await new Promise((resolve, reject) => {
|
|
reader.onloadend = () => resolve(reader.result);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
} catch (_) {
|
|
console.warn(`Failed to create data url from [${this.addon.iconURL}]`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Private Methods
|
|
_getBackgroundScriptStatus() {
|
|
const isRunning = lazy.ExtensionParent.DebugUtils.isBackgroundScriptRunning(
|
|
this.addonId
|
|
);
|
|
// The background script status doesn't apply to this addon (e.g. the addon
|
|
// type doesn't have any code, like staticthemes/langpacks/dictionaries, or
|
|
// the extension does not have a background script at all).
|
|
if (isRunning === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
return isRunning ? BGSCRIPT_STATUSES.RUNNING : BGSCRIPT_STATUSES.STOPPED;
|
|
}
|
|
|
|
get _mm() {
|
|
return (
|
|
this._browser &&
|
|
(this._browser.messageManager || this._browser.frameLoader.messageManager)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle the child actor exit.
|
|
*/
|
|
_onChildExit(msg) {
|
|
if (msg.json.actor !== this._childActorID) {
|
|
return;
|
|
}
|
|
|
|
this.destroy();
|
|
}
|
|
|
|
// AddonManagerListener callbacks.
|
|
onInstalled(addon) {
|
|
if (addon.id != this.addonId) {
|
|
return;
|
|
}
|
|
|
|
// Update the AddonManager's addon object on reload/update.
|
|
this.addon = addon;
|
|
}
|
|
|
|
onUninstalled(addon) {
|
|
if (addon != this.addon) {
|
|
return;
|
|
}
|
|
|
|
this.destroy();
|
|
}
|
|
|
|
destroy() {
|
|
lazy.AddonManager.removeAddonListener(this);
|
|
|
|
this.addon = null;
|
|
if (this._mm) {
|
|
this._mm.removeMessageListener(
|
|
"debug:webext_child_exit",
|
|
this._onChildExit
|
|
);
|
|
|
|
this._mm.sendAsyncMessage("debug:webext_parent_exit", {
|
|
actor: this._childActorID,
|
|
});
|
|
|
|
lazy.ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(this);
|
|
}
|
|
|
|
this._browser = null;
|
|
this._childActorID = null;
|
|
|
|
this.emit("descriptor-destroyed");
|
|
|
|
super.destroy();
|
|
}
|
|
}
|
|
|
|
exports.WebExtensionDescriptorActor = WebExtensionDescriptorActor;
|