forked from mirrors/gecko-dev
		
	Bug 1726800 - [remote] Verify commands as early as possible in MessageHandler r=webdriver-reviewers,whimboo
Depends on D123655 With this patch, the MessageHandler can immediately check if a command is implemented by the modules, and therefore reject as early as possible. This is implemented via a checkCommand method on MessageHandler. Other required changes: - ModuleRegistry now owns the logic to import BiDi modules. - ModuleCache exposes a `getAllModuleClasses` to get all the relevant modules for a moduleName+destination pair. Error messages have been improved and are verified with a dedicated test Differential Revision: https://phabricator.services.mozilla.com/D123655
This commit is contained in:
		
							parent
							
								
									773ea0006d
								
							
						
					
					
						commit
						69fa6f5492
					
				
					 11 changed files with 420 additions and 90 deletions
				
			
		|  | @ -160,6 +160,9 @@ remote/cdp/Protocol.jsm | ||||||
| remote/cdp/test/browser/chrome-remote-interface.js | remote/cdp/test/browser/chrome-remote-interface.js | ||||||
| remote/marionette/atom.js | remote/marionette/atom.js | ||||||
| 
 | 
 | ||||||
|  | # This file explicitly has a syntax error and cannot be parsed by eslint. | ||||||
|  | remote/shared/messagehandler/test/browser/resources/modules/root/invalid.jsm | ||||||
|  | 
 | ||||||
| # services/ exclusions | # services/ exclusions | ||||||
| 
 | 
 | ||||||
| # Third party services | # Third party services | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ const { XPCOMUtils } = ChromeUtils.import( | ||||||
| XPCOMUtils.defineLazyModuleGetters(this, { | XPCOMUtils.defineLazyModuleGetters(this, { | ||||||
|   EventEmitter: "resource://gre/modules/EventEmitter.jsm", |   EventEmitter: "resource://gre/modules/EventEmitter.jsm", | ||||||
| 
 | 
 | ||||||
|  |   error: "chrome://remote/content/shared/messagehandler/Errors.jsm", | ||||||
|   Log: "chrome://remote/content/shared/Log.jsm", |   Log: "chrome://remote/content/shared/Log.jsm", | ||||||
|   ModuleCache: "chrome://remote/content/shared/messagehandler/ModuleCache.jsm", |   ModuleCache: "chrome://remote/content/shared/messagehandler/ModuleCache.jsm", | ||||||
| }); | }); | ||||||
|  | @ -73,6 +74,32 @@ class MessageHandler extends EventEmitter { | ||||||
|     return this._sessionId; |     return this._sessionId; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Check if the command can be handled from this MessageHandler node. | ||||||
|  |    * | ||||||
|  |    * @param {Command} command | ||||||
|  |    *     The command to check. See type definition in MessageHandler.js | ||||||
|  |    * @throws {Error} | ||||||
|  |    *     Throws if there is no module supporting the provided command on the | ||||||
|  |    *     path to the command's destination | ||||||
|  |    */ | ||||||
|  |   checkCommand(command) { | ||||||
|  |     const { commandName, destination, moduleName } = command; | ||||||
|  | 
 | ||||||
|  |     // Retrieve all the modules classes which can be used to reach the
 | ||||||
|  |     // command's destination.
 | ||||||
|  |     const moduleClasses = this._moduleCache.getAllModuleClasses( | ||||||
|  |       moduleName, | ||||||
|  |       destination | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     if (!moduleClasses.some(cls => cls.supportsMethod(commandName))) { | ||||||
|  |       throw new error.UnsupportedCommandError( | ||||||
|  |         `${moduleName}.${commandName} not supported for destination ${destination?.type}` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   destroy() { |   destroy() { | ||||||
|     logger.trace( |     logger.trace( | ||||||
|       `MessageHandler ${this.constructor.type} for session ${this.sessionId} is being destroyed` |       `MessageHandler ${this.constructor.type} for session ${this.sessionId} is being destroyed` | ||||||
|  | @ -139,8 +166,10 @@ class MessageHandler extends EventEmitter { | ||||||
|       `Received command ${moduleName}.${commandName} for destination ${destination.type}` |       `Received command ${moduleName}.${commandName} for destination ${destination.type}` | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  |     this.checkCommand(command); | ||||||
|  | 
 | ||||||
|     const module = this._moduleCache.getModuleInstance(moduleName, destination); |     const module = this._moduleCache.getModuleInstance(moduleName, destination); | ||||||
|     if (this._isCommandSupportedByModule(commandName, module)) { |     if (module && module.supportsMethod(commandName)) { | ||||||
|       return module[commandName](params, destination); |       return module[commandName](params, destination); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -151,14 +180,6 @@ class MessageHandler extends EventEmitter { | ||||||
|     return `[object ${this.constructor.name} ${this.name}]`; |     return `[object ${this.constructor.name} ${this.name}]`; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   _isCommandSupportedByModule(commandName, module) { |  | ||||||
|     // TODO: With the current implementation, all functions of a given module
 |  | ||||||
|     // are considered as valid commands.
 |  | ||||||
|     // This should probably be replaced by a more explicit declaration, via a
 |  | ||||||
|     // manifest for instance.
 |  | ||||||
|     return module && typeof module[commandName] === "function"; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |   /** | ||||||
|    * Returns the module path corresponding to this MessageHandler class. |    * Returns the module path corresponding to this MessageHandler class. | ||||||
|    * |    * | ||||||
|  |  | ||||||
|  | @ -26,7 +26,19 @@ class Module { | ||||||
|     throw new Error("Not implemented"); |     throw new Error("Not implemented"); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Instance shortcut for supportsMethod to avoid reaching the constructor for | ||||||
|  |    * consumers which directly deal with an instance. | ||||||
|  |    */ | ||||||
|  |   supportsMethod(methodName) { | ||||||
|  |     return this.constructor.supportsMethod(methodName); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   get messageHandler() { |   get messageHandler() { | ||||||
|     return this._messageHandler; |     return this._messageHandler; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   static supportsMethod(methodName) { | ||||||
|  |     return typeof this.prototype[methodName] === "function"; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,29 +10,28 @@ const { XPCOMUtils } = ChromeUtils.import( | ||||||
|   "resource://gre/modules/XPCOMUtils.jsm" |   "resource://gre/modules/XPCOMUtils.jsm" | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| // Import the ModuleRegistry so that all modules are properly referenced.
 |  | ||||||
| ChromeUtils.import( |  | ||||||
|   "chrome://remote/content/webdriver-bidi/modules/ModuleRegistry.jsm" |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| XPCOMUtils.defineLazyModuleGetters(this, { | XPCOMUtils.defineLazyModuleGetters(this, { | ||||||
|   Services: "resource://gre/modules/Services.jsm", |   Services: "resource://gre/modules/Services.jsm", | ||||||
| 
 | 
 | ||||||
|   getMessageHandlerClass: |   getMessageHandlerClass: | ||||||
|     "chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.jsm", |     "chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.jsm", | ||||||
|  |   // Additional protocols might use a different registry for their modules,
 | ||||||
|  |   // in which case this will no longer be a constant but will instead depend on
 | ||||||
|  |   // the protocol owning the MessageHandler. See Bug 1722464.
 | ||||||
|  |   getModuleClass: | ||||||
|  |     "chrome://remote/content/webdriver-bidi/modules/ModuleRegistry.jsm", | ||||||
|   Log: "chrome://remote/content/shared/Log.jsm", |   Log: "chrome://remote/content/shared/Log.jsm", | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | XPCOMUtils.defineLazyModuleGetter( | ||||||
|  |   this, | ||||||
|  |   "getTestModuleClass", | ||||||
|  |   "chrome://mochitests/content/browser/remote/shared/messagehandler/test/browser/resources/modules/ModuleRegistry.jsm", | ||||||
|  |   "getModuleClass" | ||||||
|  | ); | ||||||
|  | 
 | ||||||
| XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get()); | XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get()); | ||||||
| 
 | 
 | ||||||
| // Additional protocols might use a different root folder for their modules,
 |  | ||||||
| // in which case this will no longer be a constant but will instead depend on
 |  | ||||||
| // the protocol owning the MessageHandler. See Bug 1722464.
 |  | ||||||
| const MODULES_FOLDER = "chrome://remote/content/webdriver-bidi/modules"; |  | ||||||
| 
 |  | ||||||
| const TEST_MODULES_FOLDER = |  | ||||||
|   "chrome://mochitests/content/browser/remote/shared/messagehandler/test/browser/resources/modules"; |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * ModuleCache instances are dedicated to lazily create and cache the instances |  * ModuleCache instances are dedicated to lazily create and cache the instances | ||||||
|  * of all the modules related to a specific MessageHandler instance. |  * of all the modules related to a specific MessageHandler instance. | ||||||
|  | @ -71,20 +70,12 @@ class ModuleCache { | ||||||
|    */ |    */ | ||||||
|   constructor(messageHandler) { |   constructor(messageHandler) { | ||||||
|     this.messageHandler = messageHandler; |     this.messageHandler = messageHandler; | ||||||
|     this._messageHandlerPath = messageHandler.constructor.modulePath; |     this._messageHandlerType = messageHandler.constructor.type; | ||||||
| 
 | 
 | ||||||
|     this._modulesRootFolder = MODULES_FOLDER; |     this._useTestModules = Services.prefs.getBoolPref( | ||||||
|     // TODO: Temporary workaround to use a different folder for tests.
 |       "remote.messagehandler.modulecache.useBrowserTestRoot", | ||||||
|     // After Bug 1722464 lands, we should be able to set custom root folders
 |       false | ||||||
|     // per session and we can remove this workaround.
 |     ); | ||||||
|     if ( |  | ||||||
|       Services.prefs.getBoolPref( |  | ||||||
|         "remote.messagehandler.modulecache.useBrowserTestRoot", |  | ||||||
|         false |  | ||||||
|       ) |  | ||||||
|     ) { |  | ||||||
|       this._modulesRootFolder = TEST_MODULES_FOLDER; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     // Map of absolute module paths to module instances.
 |     // Map of absolute module paths to module instances.
 | ||||||
|     this._modules = new Map(); |     this._modules = new Map(); | ||||||
|  | @ -97,6 +88,41 @@ class ModuleCache { | ||||||
|     this._modules.forEach(module => module?.destroy()); |     this._modules.forEach(module => module?.destroy()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Retrieve all module classes matching the provided module name to reach the | ||||||
|  |    * provided destination, from the current context. | ||||||
|  |    * | ||||||
|  |    * This corresponds to the path a command can take to reach its destination. | ||||||
|  |    * A command's method must be implemented in one of the classes returned by | ||||||
|  |    * getAllModuleClasses in order to be successfully handled. | ||||||
|  |    * | ||||||
|  |    * @param {String} moduleName | ||||||
|  |    *     The name of the module. | ||||||
|  |    * @param {Destination} destination | ||||||
|  |    *     The destination. | ||||||
|  |    * @return {Array.<class<Module>=>} | ||||||
|  |    *     An array of Module classes. Will contain `null` items if the module | ||||||
|  |    *     name is not implemented in a given layer. | ||||||
|  |    */ | ||||||
|  |   getAllModuleClasses(moduleName, destination) { | ||||||
|  |     const destinationType = destination.type; | ||||||
|  |     const folders = [ | ||||||
|  |       this._getModuleFolder(this._messageHandlerType, destinationType), | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     // Bug 1733242: Extend the implementation of this method to handle workers.
 | ||||||
|  |     // It assumes layers have at most one level of nesting, for instance
 | ||||||
|  |     // "root -> windowglobal", but it wouldn't work for something such as
 | ||||||
|  |     // "root -> windowglobal -> worker".
 | ||||||
|  |     if (destinationType !== this._messageHandlerType) { | ||||||
|  |       folders.push(this._getModuleFolder(destinationType, destinationType)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return folders | ||||||
|  |       .map(folder => this._getModuleClass(moduleName, folder)) | ||||||
|  |       .filter(cls => !!cls); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Get a module instance corresponding to the provided moduleName and |    * Get a module instance corresponding to the provided moduleName and | ||||||
|    * destination. If no existing module can be found in the cache, ModuleCache |    * destination. If no existing module can be found in the cache, ModuleCache | ||||||
|  | @ -113,43 +139,56 @@ class ModuleCache { | ||||||
|    *     destination, or null if it could not be instantiated. |    *     destination, or null if it could not be instantiated. | ||||||
|    */ |    */ | ||||||
|   getModuleInstance(moduleName, destination) { |   getModuleInstance(moduleName, destination) { | ||||||
|     const moduleFullPath = this._getModuleFullPath(moduleName, destination); |     const key = `${moduleName}-${destination.type}`; | ||||||
| 
 | 
 | ||||||
|     if (!this._modules.has(moduleFullPath)) { |     if (this._modules.has(key)) { | ||||||
|       try { |       // If there is already a cached instance (potentially null) for the
 | ||||||
|         const ModuleClass = ChromeUtils.import(moduleFullPath)[moduleName]; |       // module name + destination type pair, return it.
 | ||||||
|         this._modules.set(moduleFullPath, new ModuleClass(this.messageHandler)); |       return this._modules.get(key); | ||||||
|         logger.trace(`Module ${moduleName} created for ${moduleFullPath}`); |  | ||||||
|       } catch (e) { |  | ||||||
|         // If the module could not be imported, set null in the module cache
 |  | ||||||
|         // so that the root MessageHandler can forward the message to the next
 |  | ||||||
|         // layer.
 |  | ||||||
|         this._modules.set(moduleFullPath, null); |  | ||||||
|         logger.trace(`No module ${moduleName} found for ${moduleFullPath}`); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return this._modules.get(moduleFullPath); |     const moduleFolder = this._getModuleFolder( | ||||||
|  |       this._messageHandlerType, | ||||||
|  |       destination.type | ||||||
|  |     ); | ||||||
|  |     const ModuleClass = this._getModuleClass(moduleName, moduleFolder); | ||||||
|  | 
 | ||||||
|  |     let module = null; | ||||||
|  |     if (ModuleClass) { | ||||||
|  |       module = new ModuleClass(this.messageHandler); | ||||||
|  |       logger.trace(`Module ${moduleName} created for ${destination.type}`); | ||||||
|  |     } else { | ||||||
|  |       logger.trace(`No module ${moduleName} found for ${destination.type}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this._modules.set(key, module); | ||||||
|  |     return module; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   toString() { |   toString() { | ||||||
|     return `[object ${this.constructor.name} ${this.messageHandler.name}]`; |     return `[object ${this.constructor.name} ${this.messageHandler.name}]`; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   _getModuleFullPath(moduleName, destination) { |   _getModuleClass(moduleName, moduleFolder) { | ||||||
|     let moduleFolder; |     if (this._useTestModules) { | ||||||
|     if (this.messageHandler.constructor.type === destination.type) { |       return getTestModuleClass(moduleName, moduleFolder); | ||||||
|       // If the command is targeting the current type, the module is expected to
 |  | ||||||
|       // be in eg "windowglobal/${moduleName}.jsm".
 |  | ||||||
|       moduleFolder = this._messageHandlerPath; |  | ||||||
|     } else { |  | ||||||
|       // If the command is targeting another type, the module is expected to
 |  | ||||||
|       // be in a composed folder eg "windowglobal-in-root/${moduleName}.jsm".
 |  | ||||||
|       const MessageHandlerClass = getMessageHandlerClass(destination.type); |  | ||||||
|       const destinationPath = MessageHandlerClass.modulePath; |  | ||||||
|       moduleFolder = `${destinationPath}-in-${this._messageHandlerPath}`; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return `${this._modulesRootFolder}/${moduleFolder}/${moduleName}.jsm`; |     // Retrieve the module class from the WebDriverBiDi ModuleRegistry if we
 | ||||||
|  |     // are not using test modules.
 | ||||||
|  |     return getModuleClass(moduleName, moduleFolder); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _getModuleFolder(originType, destinationType) { | ||||||
|  |     const originPath = getMessageHandlerClass(originType).modulePath; | ||||||
|  |     if (originType === destinationType) { | ||||||
|  |       // If the command is targeting the current type, the module is expected to
 | ||||||
|  |       // be in eg "windowglobal/${moduleName}.jsm".
 | ||||||
|  |       return originPath; | ||||||
|  |     } | ||||||
|  |     // If the command is targeting another type, the module is expected to
 | ||||||
|  |     // be in a composed folder eg "windowglobal-in-root/${moduleName}.jsm".
 | ||||||
|  |     const destinationPath = getMessageHandlerClass(destinationType).modulePath; | ||||||
|  |     return `${destinationPath}-in-${originPath}`; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,6 +3,9 @@ | ||||||
| 
 | 
 | ||||||
| "use strict"; | "use strict"; | ||||||
| 
 | 
 | ||||||
|  | const { RootMessageHandler } = ChromeUtils.import( | ||||||
|  |   "chrome://remote/content/shared/messagehandler/RootMessageHandler.jsm" | ||||||
|  | ); | ||||||
| const { WindowGlobalMessageHandler } = ChromeUtils.import( | const { WindowGlobalMessageHandler } = ChromeUtils.import( | ||||||
|   "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.jsm" |   "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.jsm" | ||||||
| ); | ); | ||||||
|  | @ -37,25 +40,182 @@ add_task(async function test_module_error() { | ||||||
| add_task(async function test_destination_error() { | add_task(async function test_destination_error() { | ||||||
|   const rootMessageHandler = createRootMessageHandler("session-id-error"); |   const rootMessageHandler = createRootMessageHandler("session-id-error"); | ||||||
| 
 | 
 | ||||||
|  |   const fakeBrowsingContextId = -1; | ||||||
|  |   ok( | ||||||
|  |     !BrowsingContext.get(fakeBrowsingContextId), | ||||||
|  |     "No browsing context matches fakeBrowsingContextId" | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|   info("Call a valid module method, but on a non-existent browsing context id"); |   info("Call a valid module method, but on a non-existent browsing context id"); | ||||||
|   try { |   Assert.throws( | ||||||
|     const fakeBrowsingContextId = -1; |     () => | ||||||
|     ok( |       rootMessageHandler.handleCommand({ | ||||||
|       !BrowsingContext.get(fakeBrowsingContextId), |         moduleName: "commandwindowglobalonly", | ||||||
|       "No browsing context matches fakeBrowsingContextId" |         commandName: "testOnlyInWindowGlobal", | ||||||
|     ); |         destination: { | ||||||
|     await rootMessageHandler.handleCommand({ |           type: WindowGlobalMessageHandler.type, | ||||||
|       moduleName: "commandwindowglobalonly", |           id: fakeBrowsingContextId, | ||||||
|       commandName: "testOnlyInWindowGlobal", |         }, | ||||||
|       destination: { |       }), | ||||||
|         type: WindowGlobalMessageHandler.type, |     err => err.message == `Unable to find a BrowsingContext for id -1` | ||||||
|         id: fakeBrowsingContextId, |   ); | ||||||
|       }, | 
 | ||||||
|     }); |   rootMessageHandler.destroy(); | ||||||
|     ok(false, "Incorrect destination error was not caught"); | }); | ||||||
|   } catch (e) { | 
 | ||||||
|     ok(true, "Incorrect destination error was caught"); | add_task(async function test_invalid_module_error() { | ||||||
|   } |   const rootMessageHandler = createRootMessageHandler( | ||||||
|  |     "session-id-missing_module" | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   info("Attempt to call a Root module which has a syntax error"); | ||||||
|  |   Assert.throws( | ||||||
|  |     () => | ||||||
|  |       rootMessageHandler.handleCommand({ | ||||||
|  |         moduleName: "invalid", | ||||||
|  |         commandName: "someMethod", | ||||||
|  |         destination: { | ||||||
|  |           type: RootMessageHandler.type, | ||||||
|  |         }, | ||||||
|  |       }), | ||||||
|  |     err => | ||||||
|  |       err.name === "SyntaxError" && | ||||||
|  |       err.message == "expected expression, got ';'" | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   rootMessageHandler.destroy(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | add_task(async function test_missing_root_module_error() { | ||||||
|  |   const rootMessageHandler = createRootMessageHandler( | ||||||
|  |     "session-id-missing_module" | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   info("Attempt to call a Root module which doesn't exist"); | ||||||
|  |   Assert.throws( | ||||||
|  |     () => | ||||||
|  |       rootMessageHandler.handleCommand({ | ||||||
|  |         moduleName: "missingmodule", | ||||||
|  |         commandName: "someMethod", | ||||||
|  |         destination: { | ||||||
|  |           type: RootMessageHandler.type, | ||||||
|  |         }, | ||||||
|  |       }), | ||||||
|  |     err => | ||||||
|  |       err.name == "UnsupportedCommandError" && | ||||||
|  |       err.message == | ||||||
|  |         `missingmodule.someMethod not supported for destination ROOT` | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   rootMessageHandler.destroy(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | add_task(async function test_missing_windowglobal_module_error() { | ||||||
|  |   const browsingContextId = gBrowser.selectedBrowser.browsingContext.id; | ||||||
|  |   const rootMessageHandler = createRootMessageHandler( | ||||||
|  |     "session-id-missing_windowglobal_module" | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   info("Attempt to call a WindowGlobal module which doesn't exist"); | ||||||
|  |   Assert.throws( | ||||||
|  |     () => | ||||||
|  |       rootMessageHandler.handleCommand({ | ||||||
|  |         moduleName: "missingmodule", | ||||||
|  |         commandName: "someMethod", | ||||||
|  |         destination: { | ||||||
|  |           type: WindowGlobalMessageHandler.type, | ||||||
|  |           id: browsingContextId, | ||||||
|  |         }, | ||||||
|  |       }), | ||||||
|  |     err => | ||||||
|  |       err.name == "UnsupportedCommandError" && | ||||||
|  |       err.message == | ||||||
|  |         `missingmodule.someMethod not supported for destination WINDOW_GLOBAL` | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   rootMessageHandler.destroy(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | add_task(async function test_missing_root_method_error() { | ||||||
|  |   const rootMessageHandler = createRootMessageHandler( | ||||||
|  |     "session-id-missing_root_method" | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   info("Attempt to call an invalid method on a Root module"); | ||||||
|  |   Assert.throws( | ||||||
|  |     () => | ||||||
|  |       rootMessageHandler.handleCommand({ | ||||||
|  |         moduleName: "command", | ||||||
|  |         commandName: "wrongMethod", | ||||||
|  |         destination: { | ||||||
|  |           type: RootMessageHandler.type, | ||||||
|  |         }, | ||||||
|  |       }), | ||||||
|  |     err => | ||||||
|  |       err.name == "UnsupportedCommandError" && | ||||||
|  |       err.message == `command.wrongMethod not supported for destination ROOT` | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   rootMessageHandler.destroy(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | add_task(async function test_missing_windowglobal_method_error() { | ||||||
|  |   const browsingContextId = gBrowser.selectedBrowser.browsingContext.id; | ||||||
|  |   const rootMessageHandler = createRootMessageHandler( | ||||||
|  |     "session-id-missing_windowglobal_method" | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   info("Attempt to call an invalid method on a WindowGlobal module"); | ||||||
|  |   Assert.throws( | ||||||
|  |     () => | ||||||
|  |       rootMessageHandler.handleCommand({ | ||||||
|  |         moduleName: "commandwindowglobalonly", | ||||||
|  |         commandName: "wrongMethod", | ||||||
|  |         destination: { | ||||||
|  |           type: WindowGlobalMessageHandler.type, | ||||||
|  |           id: browsingContextId, | ||||||
|  |         }, | ||||||
|  |       }), | ||||||
|  |     err => | ||||||
|  |       err.name == "UnsupportedCommandError" && | ||||||
|  |       err.message == | ||||||
|  |         `commandwindowglobalonly.wrongMethod not supported for destination WINDOW_GLOBAL` | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   rootMessageHandler.destroy(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This test checks that even if a command is rerouted to another command after | ||||||
|  |  * the RootMessageHandler, we still check the new command and log a useful | ||||||
|  |  * error message. | ||||||
|  |  * | ||||||
|  |  * This illustrates why it is important to perform the command check at each | ||||||
|  |  * layer of the MessageHandler network. | ||||||
|  |  */ | ||||||
|  | add_task(async function test_missing_intermediary_method_error() { | ||||||
|  |   const browsingContextId = gBrowser.selectedBrowser.browsingContext.id; | ||||||
|  |   const rootMessageHandler = createRootMessageHandler( | ||||||
|  |     "session-id-missing_intermediary_method" | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   info( | ||||||
|  |     "Call a (valid) command that relies on another (missing) command on a WindowGlobal module" | ||||||
|  |   ); | ||||||
|  |   await Assert.rejects( | ||||||
|  |     rootMessageHandler.handleCommand({ | ||||||
|  |       moduleName: "commandwindowglobalonly", | ||||||
|  |       commandName: "testMissingIntermediaryMethod", | ||||||
|  |       destination: { | ||||||
|  |         type: WindowGlobalMessageHandler.type, | ||||||
|  |         id: browsingContextId, | ||||||
|  |       }, | ||||||
|  |     }), | ||||||
|  |     err => | ||||||
|  |       err.name == "UnsupportedCommandError" && | ||||||
|  |       err.message == | ||||||
|  |         `commandwindowglobalonly.missingMethod not supported for destination WINDOW_GLOBAL` | ||||||
|  |   ); | ||||||
| 
 | 
 | ||||||
|   rootMessageHandler.destroy(); |   rootMessageHandler.destroy(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,33 @@ | ||||||
|  | /* 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"; | ||||||
|  | 
 | ||||||
|  | var EXPORTED_SYMBOLS = ["getModuleClass"]; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Retrieve the WebDriver BiDi module class matching the provided module name | ||||||
|  |  * and folder. | ||||||
|  |  * | ||||||
|  |  * @param {String} moduleName | ||||||
|  |  *     The name of the module to get the class for. | ||||||
|  |  * @param {String} moduleFolder | ||||||
|  |  *     A valid folder name for modules. | ||||||
|  |  * @return {Class=} | ||||||
|  |  *     The class corresponding to the module name and folder, null if no match | ||||||
|  |  *     was found. | ||||||
|  |  * @throws {Error} | ||||||
|  |  *     If the provided module folder is unexpected. | ||||||
|  |  **/ | ||||||
|  | const getModuleClass = function(moduleName, moduleFolder) { | ||||||
|  |   const path = `chrome://mochitests/content/browser/remote/shared/messagehandler/test/browser/resources/modules/${moduleFolder}/${moduleName}.jsm`; | ||||||
|  |   try { | ||||||
|  |     return ChromeUtils.import(path)[moduleName]; | ||||||
|  |   } catch (e) { | ||||||
|  |     if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     throw e; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | // This module is meant to check error reporting when importing a module fails
 | ||||||
|  | // due to an actual issue (syntax error etc...).
 | ||||||
|  | 
 | ||||||
|  | SyntaxError(; | ||||||
|  | @ -32,6 +32,16 @@ class CommandWindowGlobalOnly extends Module { | ||||||
|   testError() { |   testError() { | ||||||
|     throw new Error("error-from-module"); |     throw new Error("error-from-module"); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   testMissingIntermediaryMethod(params, destination) { | ||||||
|  |     // Spawn a new internal command, but with a commandName which doesn't match
 | ||||||
|  |     // any method.
 | ||||||
|  |     return this.messageHandler.handleCommand({ | ||||||
|  |       moduleName: "commandwindowglobalonly", | ||||||
|  |       commandName: "missingMethod", | ||||||
|  |       destination, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const commandwindowglobalonly = CommandWindowGlobalOnly; | const commandwindowglobalonly = CommandWindowGlobalOnly; | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ const { XPCOMUtils } = ChromeUtils.import( | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| XPCOMUtils.defineLazyModuleGetters(this, { | XPCOMUtils.defineLazyModuleGetters(this, { | ||||||
|  |   error: "chrome://remote/content/shared/messagehandler/Errors.jsm", | ||||||
|   MessageHandlerRegistry: |   MessageHandlerRegistry: | ||||||
|     "chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.jsm", |     "chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.jsm", | ||||||
|   WindowGlobalMessageHandler: |   WindowGlobalMessageHandler: | ||||||
|  | @ -37,13 +38,22 @@ class MessageHandlerFrameChild extends JSWindowActorChild { | ||||||
|     this._registry.on("message-handler-registry-event", this._onRegistryEvent); |     this._registry.on("message-handler-registry-event", this._onRegistryEvent); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   receiveMessage(message) { |   async receiveMessage(message) { | ||||||
|     if (message.name === "MessageHandlerFrameParent:sendCommand") { |     if (message.name === "MessageHandlerFrameParent:sendCommand") { | ||||||
|       const { sessionId, command } = message.data; |       const { sessionId, command } = message.data; | ||||||
|       const messageHandler = this._registry.getOrCreateMessageHandler( |       const messageHandler = this._registry.getOrCreateMessageHandler( | ||||||
|         sessionId |         sessionId | ||||||
|       ); |       ); | ||||||
|       return messageHandler.handleCommand(command); |       try { | ||||||
|  |         return await messageHandler.handleCommand(command); | ||||||
|  |       } catch (e) { | ||||||
|  |         if (e instanceof error.MessageHandlerError) { | ||||||
|  |           return { | ||||||
|  |             error: e.toJSON(), | ||||||
|  |           }; | ||||||
|  |         } | ||||||
|  |         throw e; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return null; |     return null; | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ const { XPCOMUtils } = ChromeUtils.import( | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| XPCOMUtils.defineLazyModuleGetters(this, { | XPCOMUtils.defineLazyModuleGetters(this, { | ||||||
|  |   error: "chrome://remote/content/shared/messagehandler/Errors.jsm", | ||||||
|   RootMessageHandlerRegistry: |   RootMessageHandlerRegistry: | ||||||
|     "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.jsm", |     "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.jsm", | ||||||
| }); | }); | ||||||
|  | @ -51,10 +52,19 @@ class MessageHandlerFrameParent extends JSWindowActorParent { | ||||||
|    *     Promise that will resolve with the result of query sent to the |    *     Promise that will resolve with the result of query sent to the | ||||||
|    *     MessageHandlerFrameChild actor. |    *     MessageHandlerFrameChild actor. | ||||||
|    */ |    */ | ||||||
|   sendCommand(command, sessionId) { |   async sendCommand(command, sessionId) { | ||||||
|     return this.sendQuery("MessageHandlerFrameParent:sendCommand", { |     const result = await this.sendQuery( | ||||||
|       command, |       "MessageHandlerFrameParent:sendCommand", | ||||||
|       sessionId, |       { | ||||||
|     }); |         command, | ||||||
|  |         sessionId, | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     if (result?.error) { | ||||||
|  |       throw error.MessageHandlerError.fromJSON(result.error); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return result; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| "use strict"; | "use strict"; | ||||||
| 
 | 
 | ||||||
| var EXPORTED_SYMBOLS = []; | var EXPORTED_SYMBOLS = ["getModuleClass"]; | ||||||
| 
 | 
 | ||||||
| const { XPCOMUtils } = ChromeUtils.import( | const { XPCOMUtils } = ChromeUtils.import( | ||||||
|   "resource://gre/modules/XPCOMUtils.jsm" |   "resource://gre/modules/XPCOMUtils.jsm" | ||||||
|  | @ -16,8 +16,6 @@ const modules = { | ||||||
|   windowglobal: {}, |   windowglobal: {}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // Modules are not exported here and the lazy getters are only here to avoid
 |  | ||||||
| // errors in browser_all_files_referenced.js
 |  | ||||||
| XPCOMUtils.defineLazyModuleGetters(modules.root, { | XPCOMUtils.defineLazyModuleGetters(modules.root, { | ||||||
|   log: "chrome://remote/content/webdriver-bidi/modules/root/log.jsm", |   log: "chrome://remote/content/webdriver-bidi/modules/root/log.jsm", | ||||||
|   session: "chrome://remote/content/webdriver-bidi/modules/root/session.jsm", |   session: "chrome://remote/content/webdriver-bidi/modules/root/session.jsm", | ||||||
|  | @ -31,3 +29,33 @@ XPCOMUtils.defineLazyModuleGetters(modules["windowglobal-in-root"], { | ||||||
| XPCOMUtils.defineLazyModuleGetters(modules.windowglobal, { | XPCOMUtils.defineLazyModuleGetters(modules.windowglobal, { | ||||||
|   log: "chrome://remote/content/webdriver-bidi/modules/windowglobal/log.jsm", |   log: "chrome://remote/content/webdriver-bidi/modules/windowglobal/log.jsm", | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Retrieve the WebDriver BiDi module class matching the provided module name | ||||||
|  |  * and folder. | ||||||
|  |  * | ||||||
|  |  * @param {String} moduleName | ||||||
|  |  *     The name of the module to get the class for. | ||||||
|  |  * @param {String} moduleFolder | ||||||
|  |  *     A valid folder name for modules. | ||||||
|  |  * @return {Class=} | ||||||
|  |  *     The class corresponding to the module name and folder, null if no match | ||||||
|  |  *     was found. | ||||||
|  |  * @throws {Error} | ||||||
|  |  *     If the provided module folder is unexpected. | ||||||
|  |  **/ | ||||||
|  | const getModuleClass = function(moduleName, moduleFolder) { | ||||||
|  |   if (!modules[moduleFolder]) { | ||||||
|  |     throw new Error( | ||||||
|  |       `Invalid module folder "${moduleFolder}", expected one of "${Object.keys( | ||||||
|  |         modules | ||||||
|  |       )}"` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!modules[moduleFolder][moduleName]) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return modules[moduleFolder][moduleName]; | ||||||
|  | }; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Julian Descottes
						Julian Descottes