MessageHandlerFrameParent.sys.mjs (7097B)
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 clearTimeout: "resource://gre/modules/Timer.sys.mjs", 9 setTimeout: "resource://gre/modules/Timer.sys.mjs", 10 11 error: "chrome://remote/content/shared/messagehandler/Errors.sys.mjs", 12 Log: "chrome://remote/content/shared/Log.sys.mjs", 13 RootMessageHandlerRegistry: 14 "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs", 15 WindowGlobalMessageHandler: 16 "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs", 17 }); 18 19 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); 20 21 ChromeUtils.defineLazyGetter(lazy, "WebDriverError", () => { 22 return ChromeUtils.importESModule( 23 "chrome://remote/content/shared/webdriver/Errors.sys.mjs" 24 ).error.WebDriverError; 25 }); 26 27 // Set the timeout delay before a command is considered as potentially timing 28 // out. This can be customized by a preference mostly for tests. Regular 29 // implementation should use DEFAULT_COMMAND_DELAY; 30 const DEFAULT_COMMAND_DELAY = 10000; 31 const PREF_REMOTE_COMMAND_DELAY = "remote.messagehandler.test.command.delay"; 32 33 ChromeUtils.defineLazyGetter(lazy, "commandDelay", () => 34 Services.prefs.getIntPref(PREF_REMOTE_COMMAND_DELAY, DEFAULT_COMMAND_DELAY) 35 ); 36 37 const PING_DELAY = 1000; 38 const PING_TIMEOUT = Symbol(); 39 40 /** 41 * Parent actor for the MessageHandlerFrame JSWindowActor. The 42 * MessageHandlerFrame actor is used by RootTransport to communicate between 43 * ROOT MessageHandlers and WINDOW_GLOBAL MessageHandlers. 44 */ 45 export class MessageHandlerFrameParent extends JSWindowActorParent { 46 #destroyed; 47 48 constructor() { 49 super(); 50 this.#destroyed = false; 51 } 52 53 didDestroy() { 54 this.#destroyed = true; 55 } 56 57 async receiveMessage(message) { 58 switch (message.name) { 59 case "MessageHandlerFrameChild:sendCommand": { 60 return this.#handleSendCommandMessage(message.data); 61 } 62 case "MessageHandlerFrameChild:messageHandlerEvent": { 63 return this.#handleMessageHandlerEventMessage(message.data); 64 } 65 default: 66 throw new Error("Unsupported message:" + message.name); 67 } 68 } 69 70 /** 71 * Send a command to the corresponding MessageHandlerFrameChild actor via a 72 * JSWindowActor query. 73 * 74 * @param {Command} command 75 * The command to forward. See type definition in MessageHandler.js 76 * @param {string} sessionId 77 * ID of the session that sent the command. 78 * @returns {Promise} 79 * Promise that will resolve with the result of query sent to the 80 * MessageHandlerFrameChild actor. 81 */ 82 async sendCommand(command, sessionId) { 83 const timer = lazy.setTimeout( 84 () => this.#sendPing(command), 85 lazy.commandDelay 86 ); 87 88 const result = await this.sendQuery( 89 "MessageHandlerFrameParent:sendCommand", 90 { 91 command, 92 sessionId, 93 } 94 ); 95 96 lazy.clearTimeout(timer); 97 98 if (result?.error) { 99 if (result.isMessageHandlerError) { 100 throw lazy.error.MessageHandlerError.fromJSON(result.error); 101 } 102 103 // TODO: Do not assume WebDriver is the session protocol, see Bug 1779026. 104 throw lazy.WebDriverError.fromJSON(result.error); 105 } 106 107 return result; 108 } 109 110 async #handleMessageHandlerEventMessage(messageData) { 111 const { name, contextInfo, data, sessionId } = messageData; 112 const [moduleName] = name.split("."); 113 114 // Re-emit the event on the RootMessageHandler. 115 const messageHandler = 116 lazy.RootMessageHandlerRegistry.getExistingMessageHandler(sessionId); 117 118 if (!messageHandler) { 119 // If there is no message handler for the provided session id, this is a 120 // late event for an already destroyed session, bail out. 121 // Bug 1730913: A trace could be added here once Bug 1730913 is resolved. 122 // Until that, this would lead to too much log pollution, because content 123 // process modules will not be destroyed until the corresponding window 124 // is destroyed (either closed or navigates). 125 return; 126 } 127 128 // TODO: getModuleInstance expects a CommandDestination in theory, 129 // but only uses the MessageHandler type in practice, see Bug 1776389. 130 const module = messageHandler.moduleCache.getModuleInstance(moduleName, { 131 type: lazy.WindowGlobalMessageHandler.type, 132 }); 133 let eventPayload = data; 134 135 // Modify an event payload if there is a special method in the targeted module. 136 // If present it can be found in windowglobal-in-root module. 137 if (module?.interceptEvent) { 138 eventPayload = await module.interceptEvent(name, data); 139 140 if (eventPayload === null) { 141 lazy.logger.trace( 142 `${moduleName}.interceptEvent returned null, skipping event: ${name}, data: ${data}` 143 ); 144 return; 145 } 146 // Make sure that an event payload is returned. 147 if (!eventPayload) { 148 throw new Error( 149 `${moduleName}.interceptEvent doesn't return the event payload` 150 ); 151 } 152 } 153 messageHandler.emitEvent(name, eventPayload, contextInfo); 154 } 155 156 async #handleSendCommandMessage(messageData) { 157 const { sessionId, command } = messageData; 158 const messageHandler = 159 lazy.RootMessageHandlerRegistry.getExistingMessageHandler(sessionId); 160 try { 161 return await messageHandler.handleCommand(command); 162 } catch (e) { 163 if (e?.isRemoteError) { 164 return { 165 error: e.toJSON(), 166 isMessageHandlerError: e.isMessageHandlerError, 167 }; 168 } 169 throw e; 170 } 171 } 172 173 async #sendPing(command) { 174 const commandName = `${command.moduleName}.${command.commandName}`; 175 const destination = command.destination.id; 176 177 if (this.#destroyed) { 178 // If the JSWindowActor was destroyed already, no need to send a ping. 179 return; 180 } 181 182 lazy.logger.trace( 183 `MessageHandlerFrameParent command ${commandName} to ${destination} ` + 184 `takes more than ${lazy.commandDelay / 1000} seconds to resolve, sending ping` 185 ); 186 187 try { 188 const result = await Promise.race([ 189 this.sendQuery("MessageHandlerFrameParent:sendPing"), 190 new Promise(r => lazy.setTimeout(() => r(PING_TIMEOUT), PING_DELAY)), 191 ]); 192 193 if (result === PING_TIMEOUT) { 194 lazy.logger.warn( 195 `MessageHandlerFrameParent ping for command ${commandName} to ${destination} timed out` 196 ); 197 } else { 198 lazy.logger.trace( 199 `MessageHandlerFrameParent ping for command ${commandName} to ${destination} was successful` 200 ); 201 } 202 } catch (e) { 203 if (!this.#destroyed) { 204 // Only swallow errors if the JSWindowActor pair was destroyed while 205 // waiting for the ping response. 206 throw e; 207 } 208 209 lazy.logger.trace( 210 `MessageHandlerFrameParent ping for command ${commandName} to ${destination}` + 211 ` lost after JSWindowActor was destroyed` 212 ); 213 } 214 } 215 }