1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 const EXPORTED_SYMBOLS = ["BroadcastConduit", "ConduitsParent"];
9 * This @file implements the parent side of Conduits, an abstraction over
10 * Fission IPC for extension Contexts, API managers, Ports/Messengers, and
11 * other types of "subjects" participating in implementation of extension APIs.
13 * Additionally, knowledge about conduits from all child processes is gathered
14 * here, and used together with the full CanonicalBrowsingContext tree to route
15 * IPC messages and queries directly to the right subjects.
17 * Each Conduit is tied to one subject, attached to a ConduitAddress descriptor,
18 * and exposes an API for sending/receiving via an actor, or multiple actors in
19 * case of the parent process.
21 * @typedef {number|string} ConduitID
23 * @typedef {object} ConduitAddress
24 * @prop {ConduitID} id Globally unique across all processes.
25 * @prop {string[]} [recv]
26 * @prop {string[]} [send]
27 * @prop {string[]} [query]
28 * @prop {string[]} [cast]
29 * Lists of recvX, sendX, queryX and castX methods this subject will use.
31 * @typedef {"messenger"|"port"|"tab"} BroadcastKind
32 * Kinds of broadcast targeting filters.
37 * this.conduit = actor.openConduit(this, {
39 * recv: ["recvAddNumber"],
40 * send: ["sendNumberUpdate"],
44 * recvAddNumber({ num }, { actor, sender }) {
46 * this.conduit.sendNumberUpdate(sender.id, { num });
52 ExtensionUtils: { DefaultWeakMap, ExtensionError },
53 } = ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
55 const { BaseConduit } = ChromeUtils.import(
56 "resource://gre/modules/ConduitsChild.jsm"
59 const { WebNavigationFrames } = ChromeUtils.import(
60 "resource://gre/modules/WebNavigationFrames.jsm"
63 const BATCH_TIMEOUT_MS = 250;
64 const ADDON_ENV = new Set(["addon_child", "devtools_child"]);
67 * Internal, keeps track of all parent and remote (child) conduits.
70 /** @type Map<ConduitID, ConduitAddress> Info about all child conduits. */
73 /** @type Map<ConduitID, BroadcastConduit> All open parent conduits. */
76 /** @type Map<string, BroadcastConduit> Parent conduits by recvMethod. */
79 /** @type WeakMap<ConduitsParent, Set<ConduitAddress>> Conduits by actor. */
80 byActor: new DefaultWeakMap(() => new Set()),
82 /** @type Map<string, BroadcastConduit> */
83 reportOnClosed: new Map(),
86 * Save info about a new parent conduit, register it as a global listener.
87 * @param {BroadcastConduit} conduit
89 openConduit(conduit) {
90 this.conduits.set(conduit.id, conduit);
91 for (let name of conduit.address.recv || []) {
92 if (this.byMethod.get(name)) {
93 // For now, we only allow one parent conduit handling each recv method.
94 throw new Error(`Duplicate BroadcastConduit method name recv${name}`);
96 this.byMethod.set(name, conduit);
102 * @param {BroadcastConduit} conduit
104 closeConduit({ id, address }) {
105 this.conduits.delete(id);
106 for (let name of address.recv || []) {
107 this.byMethod.remove(name);
112 * Confirm that a remote conduit comes from an extension page.
113 * @see ExtensionPolicyService::CheckParentFrames
114 * @param {ConduitAddress} remote
117 verifyEnv({ actor, envType, extensionId }) {
118 if (!extensionId || !ADDON_ENV.has(envType)) {
121 let windowGlobal = actor.manager;
123 while (windowGlobal) {
124 let { browsingContext: bc, documentPrincipal: prin } = windowGlobal;
126 if (prin.addonId !== extensionId) {
127 throw new Error(`Bad ${extensionId} principal: ${prin.URI.spec}`);
129 if (bc.currentRemoteType !== prin.addonPolicy.extension.remoteType) {
130 throw new Error(`Bad ${extensionId} process: ${bc.currentRemoteType}`);
136 windowGlobal = bc.embedderWindowGlobal;
138 throw new Error(`Missing WindowGlobalParent for ${extensionId}`);
142 * Fill in common address fields knowable from the parent process.
143 * @param {ConduitAddress} address
144 * @param {ConduitsParent} actor
146 fillInAddress(address, actor) {
147 address.actor = actor;
148 address.verified = this.verifyEnv(address);
149 address.frameId = WebNavigationFrames.getFrameId(actor.browsingContext);
150 address.url = actor.browsingContext.currentURI.spec;
154 * Save info about a new remote conduit.
155 * @param {ConduitAddress} address
156 * @param {ConduitsParent} actor
158 recvConduitOpened(address, actor) {
159 this.fillInAddress(address, actor);
160 this.remotes.set(address.id, address);
161 this.byActor.get(actor).add(address);
165 * Notifies listeners and cleans up after the remote conduit is closed.
166 * @param {ConduitAddress} remote
168 recvConduitClosed(remote) {
169 this.remotes.delete(remote.id);
170 this.byActor.get(remote.actor).delete(remote);
173 for (let [key, conduit] of Hub.reportOnClosed.entries()) {
175 conduit.subject.recvConduitClosed(remote);
181 * Close all remote conduits when the actor goes away.
182 * @param {ConduitsParent} actor
185 for (let remote of this.byActor.get(actor)) {
186 // When a Port is closed, we notify the other side, but it might share
187 // an actor, so we shouldn't sendQeury() in that case (see bug 1623976).
188 this.remotes.delete(remote.id);
190 for (let remote of this.byActor.get(actor)) {
191 this.recvConduitClosed(remote);
193 this.byActor.delete(actor);
198 * Parent side conduit, registers as a global listeners for certain messages,
199 * and can target specific child conduits when sending.
201 class BroadcastConduit extends BaseConduit {
203 * @param {object} subject
204 * @param {ConduitAddress} address
206 constructor(subject, address) {
207 super(subject, address);
209 // Create conduit.castX() bidings.
210 for (let name of address.cast || []) {
211 this[`cast${name}`] = this._cast.bind(this, name);
214 // Wants to know when conduits with a specific attribute are closed.
215 // `subject.recvConduitClosed(address)` method will be called.
216 if (address.reportOnClosed) {
217 Hub.reportOnClosed.set(address.reportOnClosed, this);
221 Hub.openConduit(this);
225 * Internal, sends a message to a specific conduit, used by sendX stubs.
226 * @param {string} method
227 * @param {boolean} query
228 * @param {ConduitID} target
229 * @param {object?} arg
230 * @returns {Promise<any>}
232 _send(method, query, target, arg = {}) {
234 throw new Error(`send${method} on closed conduit ${this.id}`);
237 let sender = this.id;
238 let { actor } = Hub.remotes.get(target);
240 if (method === "RunListener" && arg.path.startsWith("webRequest.")) {
241 return actor.batch(method, { target, arg, query, sender });
243 return super._send(method, query, actor, { target, arg, query, sender });
247 * Broadcasts a method call to all conduits of kind that satisfy filtering by
248 * kind-specific properties from arg, returns an array of response promises.
249 * @param {string} method
250 * @param {BroadcastKind} kind
251 * @param {object} arg
252 * @returns {Promise[]}
254 _cast(method, kind, arg) {
256 // Target Ports by portId and side (connect caller/onConnect receiver).
258 remote.portId === arg.portId &&
259 (arg.source == null || remote.source === arg.source),
261 // Target Messengers in extension pages by extensionId and envType.
264 r.id !== arg.sender.contextId &&
265 r.extensionId === arg.extensionId &&
266 r.recv.includes(method) &&
267 // TODO: Bug 1453343 - get rid of this:
268 (r.envType === "addon_child" || arg.sender.envType !== "content_child"),
270 // Target Messengers by extensionId, tabId (topBC) and frameId.
272 remote.extensionId === arg.extensionId &&
273 remote.actor.manager.browsingContext.top.id === arg.topBC &&
274 (arg.frameId == null || remote.frameId === arg.frameId) &&
275 remote.recv.includes(method),
278 let targets = Array.from(Hub.remotes.values()).filter(filters[kind]);
279 let promises = targets.map(c => this._send(method, true, c.id, arg));
281 return arg.firstResponse
282 ? this._raceResponses(promises)
283 : Promise.allSettled(promises);
287 * Custom Promise.race() function that ignores certain resolutions and errors.
288 * @param {Promise<response>[]} promises
289 * @returns {Promise<response?>}
291 _raceResponses(promises) {
292 return new Promise((resolve, reject) => {
297 if (value.response) {
298 // We have an explicit response, resolve immediately.
300 } else if (value.received) {
301 // Message was received, but no response.
302 // Resolve with this only if there is no other explicit response.
307 // Forward errors that are exposed to extension, but ignore
308 // internal errors such as actor destruction and DataCloneError.
309 if (err instanceof ExtensionError || err?.mozWebExtLocation) {
316 // Ensure resolving when there are no responses.
317 Promise.allSettled(promises).then(() => resolve(result));
323 Hub.closeConduit(this);
328 * Implements the parent side of the Conduits actor.
330 class ConduitsParent extends JSWindowActorParent {
334 this.batchPromise = null;
335 this.batchResolve = null;
336 this.timerActive = false;
340 * Group webRequest events to send them as a batch, reducing IPC overhead.
341 * @param {string} name
342 * @param {MessageData} data
343 * @returns {Promise<object>}
346 let pos = this.batchData.length;
347 this.batchData.push(data);
349 let sendNow = idleDispatch => {
350 if (this.batchData.length && this.manager) {
351 this.batchResolve(this.sendQuery(name, this.batchData));
353 this.batchResolve([]);
356 this.timerActive = !idleDispatch;
360 this.batchPromise = new Promise(r => (this.batchResolve = r));
361 if (!this.timerActive) {
362 ChromeUtils.idleDispatch(sendNow, { timeout: BATCH_TIMEOUT_MS });
363 this.timerActive = true;
367 if (data.arg.urgentSend) {
368 // If this is an urgent blocking event, run this batch right away.
372 return this.batchPromise.then(results => results[pos]);
376 * JSWindowActor method, routes the message to the target subject.
377 * @param {string} name
378 * @param {MessageData} data
379 * @returns {Promise?}
381 async receiveMessage({ name, data: { arg, query, sender } }) {
382 if (name === "ConduitOpened") {
383 return Hub.recvConduitOpened(arg, this);
386 sender = Hub.remotes.get(sender);
387 if (!sender || sender.actor !== this) {
388 throw new Error(`Unknown sender or wrong actor for recv${name}`);
391 if (name === "ConduitClosed") {
392 return Hub.recvConduitClosed(sender);
395 let conduit = Hub.byMethod.get(name);
397 throw new Error(`Parent conduit for recv${name} not found`);
400 return conduit._recv(name, arg, { actor: this, query, sender });
404 * JSWindowActor method, ensure cleanup.
407 Hub.actorClosed(this);