RootTransport.sys.mjs (8551B)
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/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 ContextDescriptorType: 9 "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs", 10 error: "chrome://remote/content/shared/messagehandler/Errors.sys.mjs", 11 isBrowsingContextCompatible: 12 "chrome://remote/content/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs", 13 isInitialDocument: 14 "chrome://remote/content/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs", 15 Log: "chrome://remote/content/shared/Log.sys.mjs", 16 MessageHandlerFrameActor: 17 "chrome://remote/content/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameActor.sys.mjs", 18 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", 19 waitForCurrentWindowGlobal: 20 "chrome://remote/content/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs", 21 }); 22 23 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); 24 25 ChromeUtils.defineLazyGetter(lazy, "prefRetryOnAbort", () => { 26 return Services.prefs.getBoolPref("remote.retry-on-abort", false); 27 }); 28 29 const MAX_RETRY_ATTEMPTS = 10; 30 31 /** 32 * RootTransport is intended to be used from a ROOT MessageHandler to communicate 33 * with WINDOW_GLOBAL MessageHandlers via the MessageHandlerFrame JSWindow 34 * actors. 35 */ 36 export class RootTransport { 37 /** 38 * @param {MessageHandler} messageHandler 39 * The MessageHandler instance which owns this RootTransport instance. 40 */ 41 constructor(messageHandler) { 42 this._messageHandler = messageHandler; 43 44 // RootTransport will rely on the MessageHandlerFrame JSWindow actors. 45 // Make sure they are registered when instantiating a RootTransport. 46 lazy.MessageHandlerFrameActor.register(); 47 } 48 49 /** 50 * Forward the provided command to WINDOW_GLOBAL MessageHandlers via the 51 * MessageHandlerFrame actors. 52 * 53 * @param {Command} command 54 * The command to forward. See type definition in MessageHandler.js 55 * @returns {Promise} 56 * Returns a promise that resolves with the result of the command after 57 * being processed by WINDOW_GLOBAL MessageHandlers. 58 */ 59 forwardCommand(command) { 60 if (command.destination.id && command.destination.contextDescriptor) { 61 throw new Error( 62 "Invalid command destination with both 'id' and 'contextDescriptor' properties" 63 ); 64 } 65 66 // With an id given forward the command to only this specific destination. 67 if (command.destination.id) { 68 const browsingContext = BrowsingContext.get(command.destination.id); 69 if (!browsingContext) { 70 throw new lazy.error.DiscardedBrowsingContextError( 71 `Unable to find a BrowsingContext for id "${command.destination.id}"` 72 ); 73 } 74 return this._sendCommandToBrowsingContext(command, browsingContext); 75 } 76 77 // ... otherwise broadcast to destinations matching the contextDescriptor. 78 if (command.destination.contextDescriptor) { 79 return this._broadcastCommand(command); 80 } 81 82 throw new Error( 83 "Unrecognized command destination, missing 'id' or 'contextDescriptor' properties" 84 ); 85 } 86 87 _broadcastCommand(command) { 88 const { contextDescriptor } = command.destination; 89 const browsingContexts = 90 this._getBrowsingContextsForDescriptor(contextDescriptor); 91 92 return Promise.all( 93 browsingContexts.map(async browsingContext => { 94 try { 95 return await this._sendCommandToBrowsingContext( 96 command, 97 browsingContext 98 ); 99 } catch (e) { 100 console.error( 101 `Failed to broadcast a command to browsingContext ${browsingContext.id}`, 102 e 103 ); 104 return null; 105 } 106 }) 107 ); 108 } 109 110 async _sendCommandToBrowsingContext(command, browsingContext) { 111 const name = `${command.moduleName}.${command.commandName}`; 112 113 let retryOnAbort = true; 114 if (command.retryOnAbort !== undefined) { 115 // The caller should always be able to force a value. 116 retryOnAbort = command.retryOnAbort; 117 } else if (!lazy.prefRetryOnAbort) { 118 // If we don't retry by default do it at least for the initial document. 119 retryOnAbort = lazy.isInitialDocument(browsingContext); 120 } 121 122 // If a top-level content browsing context was replaced and 123 // retrying is allowed, retrieve the new one for the current browser. 124 if ( 125 browsingContext && 126 browsingContext.isContent && 127 browsingContext.isReplaced && 128 browsingContext.top === browsingContext && 129 retryOnAbort 130 ) { 131 browsingContext = BrowsingContext.getCurrentTopByBrowserId( 132 browsingContext.browserId 133 ); 134 } 135 136 if (!browsingContext || browsingContext.isDiscarded) { 137 throw new lazy.error.DiscardedBrowsingContextError( 138 `BrowsingContext does no longer exist` 139 ); 140 } 141 142 // For content browsing contexts keep a reference to the 143 // webProgress, which will persist, and always use it to 144 // retrieve the currently valid browsing context. 145 const webProgress = browsingContext.webProgress; 146 147 let attempts = 0; 148 while (true) { 149 try { 150 if (browsingContext.isContent) { 151 browsingContext = webProgress.browsingContext; 152 } 153 154 if (!browsingContext.currentWindowGlobal) { 155 await lazy.waitForCurrentWindowGlobal(browsingContext); 156 } 157 158 return await browsingContext.currentWindowGlobal 159 .getActor("MessageHandlerFrame") 160 .sendCommand(command, this._messageHandler.sessionId); 161 } catch (e) { 162 // Re-throw the error in case it is not an AbortError. 163 if (e.name != "AbortError") { 164 throw e; 165 } 166 167 // Only retry if the command supports retryOnAbort and when the 168 // JSWindowActor pair gets destroyed. 169 if (!retryOnAbort) { 170 throw new lazy.error.DiscardedBrowsingContextError(e.message); 171 } 172 173 if (++attempts > MAX_RETRY_ATTEMPTS) { 174 lazy.logger.trace( 175 `RootTransport reached the limit of retry attempts (${MAX_RETRY_ATTEMPTS})` + 176 ` for command ${name} and browsing context ${browsingContext.id}.` 177 ); 178 throw new lazy.error.DiscardedBrowsingContextError(e.message); 179 } 180 181 lazy.logger.trace( 182 `RootTransport retrying command ${name} for ` + 183 `browsing context ${browsingContext.id}, attempt: ${attempts}.` 184 ); 185 await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); 186 } 187 } 188 } 189 190 toString() { 191 return `[object ${this.constructor.name} ${this._messageHandler.name}]`; 192 } 193 194 _getBrowsingContextsForDescriptor(contextDescriptor) { 195 const { id, type } = contextDescriptor; 196 197 if (type === lazy.ContextDescriptorType.All) { 198 return this._getBrowsingContexts(); 199 } 200 201 if (type === lazy.ContextDescriptorType.TopBrowsingContext) { 202 return this._getBrowsingContexts({ browserId: id }); 203 } 204 205 if (type === lazy.ContextDescriptorType.UserContext) { 206 return this._getBrowsingContexts({ userContext: id }); 207 } 208 209 // TODO: Handle other types of context descriptors. 210 throw new Error( 211 `Unsupported contextDescriptor type for broadcasting: ${type}` 212 ); 213 } 214 215 /** 216 * Get all browsing contexts, optionally matching the provided options. 217 * 218 * @param {object} options 219 * @param {string=} options.browserId 220 * The id of the browser to filter the browsing contexts by (optional). 221 * @param {string=} options.userContext 222 * The id of the user context to filter the browsing contexts by (optional). 223 * @returns {Array<BrowsingContext>} 224 * The browsing contexts matching the provided options or all browsing contexts 225 * if no options are provided. 226 */ 227 _getBrowsingContexts(options = {}) { 228 // extract browserId from options 229 const { browserId, userContext } = options; 230 let browsingContexts = []; 231 232 // Fetch all tab related browsing contexts for top-level windows. 233 for (const { browsingContext } of lazy.TabManager.getBrowsers()) { 234 if ( 235 lazy.isBrowsingContextCompatible(browsingContext, { 236 browserId, 237 userContext, 238 }) 239 ) { 240 browsingContexts = browsingContexts.concat( 241 browsingContext.getAllBrowsingContextsInSubtree() 242 ); 243 } 244 } 245 246 return browsingContexts; 247 } 248 }