diff --git a/CHANGES.md b/CHANGES.md index ddf9073..85f5c3d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,12 +3,83 @@ Change log All notable changes to this program are documented in this file. -0.29.1 (2021-04-09), `970ef713fe58`) +0.30.0 (2021-09-16, `d372710b98a6`) +------------------------------------ + +### Known problems + +- _macOS 10.15 (Catalina) and later:_ + + Due to the requirement from Apple that all programs must be + notarized, geckodriver will not work on Catalina if you manually + download it through another notarized program, such as Firefox. + + Whilst we are working on a repackaging fix for this problem, you can + find more details on how to work around this issue in the [macOS + notarization] section of the documentation. + +- _Android:_ + + For releases of Firefox 89.0 and earlier Marionette will only be enabled in + GeckoView based applications when the Firefox preference + `devtools.debugger.remote-enabled` is set to `true` via [`moz:firefoxOptions`]. + +### Added + +- Support for WebDriver clients to opt in to WebDriver BiDi. + + Introduced the new boolean capability [`webSocketUrl`] that can be used by + WebDriver clients to opt in to a bidirectional connection. A string capability + with the same name will be returned by [`NewSession`], which contains the + WebSocket URL of the newly created WebDriver session in the form of: + `ws://host:port/session/`. + + When running on Android a port forward will be set on the host machine, + which is using the exact same port as on the device. + + All the supported WebDriver BiDi commands depend on the version of + Firefox, and not geckodriver. The first commands will be shipped in + Firefox 94. + +- It's now possible to set additional preferences when a custom profile has been + specified. At the end of the session they will be removed. + +### Fixed + +- Improved Host header checks to reject requests not sent to a well-known + local hostname or IP, or the server-specified hostname. + +- Added validation that the `--host` argument resolves to a local IP address. + +- Limit the `--foreground` argument of Firefox to MacOS only. + +- Increased Marionette handshake timeout to not fail for slow connections. + +- `Marionette:Quit` is no longer sent twice during session deletion. + +- When deleting a session that was attached to an already running browser + instance, the browser is not getting closed anymore. + +- Android + + - Starting Firefox on Android from a Windows based host will now succeed as + we are using the correct Unix path separator to construct on-device paths. + + - Arguments as specified in [`moz:firefoxOptions`] are now used when starting + Firefox. + + - Port forwards set for Marionette and the WebSocket server (WebDriver BiDi) + are now correctly removed when geckodriver exits. + + - The test root folder is now removed when geckodriver exists. + + +0.29.1 (2021-04-09, `970ef713fe58`) ------------------------------------- ### Known problems -- _macOS 10.15 (Catalina):_ +- _macOS 10.15 (Catalina) and later:_ Due to the requirement from Apple that all programs must be notarized, geckodriver will not work on Catalina if you manually @@ -21,9 +92,9 @@ All notable changes to this program are documented in this file. - _Android:_ Marionette will only be enabled in GeckoView based applications when the - Firefox preference `devtools.debugger.remote-enabled` is set to `True` via - [`moz:firefoxOptions`]. This will be fixed in one of the upcoming Firefox - for Android releases. + Firefox preference `devtools.debugger.remote-enabled` is set to `true` via + [`moz:firefoxOptions`]. This will be fixed in the Firefox 90 release for + Android. ### Added @@ -58,7 +129,7 @@ All notable changes to this program are documented in this file. ### Known problems -- _macOS 10.15 (Catalina):_ +- _macOS 10.15 (Catalina) and later:_ Due to the requirement from Apple that all programs must be notarized, geckodriver will not work on Catalina if you manually @@ -71,7 +142,7 @@ All notable changes to this program are documented in this file. - _Android:_ Marionette will only be enabled in GeckoView based applications when the - Firefox preference `devtools.debugger.remote-enabled` is set to `True` via + Firefox preference `devtools.debugger.remote-enabled` is set to `true` via [`moz:firefoxOptions`]. This will be fixed in one of the upcoming Firefox for Android releases. @@ -84,7 +155,7 @@ All notable changes to this program are documented in this file. ### Added -- Introduced the new boolean capability `moz:debuggerAddress` that can be used +- Introduced the new boolean capability [`moz:debuggerAddress`] that can be used to opt-in to the experimental Chrome DevTools Protocol (CDP) implementation. A string capability with the same name will be returned by [`NewSession`], which contains the `host:port` combination of the HTTP server that can be @@ -98,7 +169,7 @@ All notable changes to this program are documented in this file. ### Known problems -- _macOS 10.15 (Catalina):_ +- _macOS 10.15 (Catalina) and later:_ Due to the requirement from Apple that all programs must be notarized, geckodriver will not work on Catalina if you manually @@ -111,7 +182,7 @@ All notable changes to this program are documented in this file. - _Android:_ Marionette will only be enabled in GeckoView based applications when the - Firefox preference `devtools.debugger.remote-enabled` is set to `True` via + Firefox preference `devtools.debugger.remote-enabled` is set to `true` via [`moz:firefoxOptions`]. This will be fixed in one of the upcoming Firefox for Android releases. @@ -160,7 +231,7 @@ All notable changes to this program are documented in this file. ### Known problems -- _macOS 10.15 (Catalina):_ +- _macOS 10.15 (Catalina) and later:_ Due to the requirement from Apple that all programs must be notarized, geckodriver will not work on Catalina if you manually @@ -173,7 +244,7 @@ All notable changes to this program are documented in this file. - _Android:_ Marionette will only be enabled in GeckoView based applications when the - Firefox preference `devtools.debugger.remote-enabled` is set to `True` via + Firefox preference `devtools.debugger.remote-enabled` is set to `true` via [`moz:firefoxOptions`]. This will be fixed in one of the upcoming Firefox for Android releases. @@ -221,7 +292,7 @@ has changed to Firefox ≥60. ### Known problems -- _macOS 10.15 (Catalina):_ +- _macOS 10.15 (Catalina) and later:_ Due to the recent requirement from Apple that all programs must be notarized, geckodriver will not work on Catalina if you manually @@ -240,7 +311,7 @@ has changed to Firefox ≥60. - _Android:_ Marionette will only be enabled in GeckoView based applications when the - Firefox preference `devtools.debugger.remote-enabled` is set to `True` via + Firefox preference `devtools.debugger.remote-enabled` is set to `true` via [`moz:firefoxOptions`]. This will be fixed in one of the upcoming Firefox for Android releases. @@ -427,8 +498,7 @@ with this particular release that we intend to release a fix for soon. - ARMv7 HF builds have been discontinued - We [announced](https://lists.mozilla.org/pipermail/tools-marionette/2018-September/000035.html) - back in September 2018 that we would stop building for ARM, + We announced back in September 2018 that we would stop building for ARM, but builds can be self-serviced by building from source. To cross-compile from another host system, you can use this command: @@ -1491,6 +1561,7 @@ and greater. [README]: https://github.com/mozilla/geckodriver/blob/master/README.md [Browser Toolbox]: https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox [WebDriver conformance]: https://wpt.fyi/results/webdriver/tests?label=experimental +[`webSocketUrl`]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/webSocketUrl [`moz:firefoxOptions`]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions [`moz:debuggerAddress`]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html#moz-debuggeraddress [Microsoft Visual Studio redistributable runtime]: https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads diff --git a/Cargo.toml b/Cargo.toml index 395604e..4203098 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "geckodriver" -version = "0.29.1" +version = "0.30.0" description = "Proxy for using WebDriver clients to interact with Gecko-based browsers." keywords = ["webdriver", "w3c", "httpd", "mozilla", "firefox"] repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver" @@ -17,17 +17,17 @@ hyper = "0.13" lazy_static = "1.0" log = { version = "0.4", features = ["std"] } marionette = { path = "./marionette" } -mozdevice = "0.3.2" -mozprofile = "0.7.2" -mozrunner = "0.12.1" -mozversion = "0.4.1" +mozdevice = "0.4.0" +mozprofile = "0.7.3" +mozrunner = "0.13.0" +mozversion = "0.4.2" regex = { version="1.0", default-features = false, features = ["perf", "std"] } serde = "1.0" serde_derive = "1.0" serde_json = "1.0" serde_yaml = "0.8" uuid = { version = "0.8", features = ["v4"] } -webdriver = "0.43.1" +webdriver = "0.44.0" zip = { version = "0.4", default-features = false, features = ["deflate"] } [[bin]] diff --git a/README.md b/README.md index 78e4414..a1d65e8 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,7 @@ Contact ------- The mailing list for geckodriver discussion is -tools-marionette@lists.mozilla.org ([subscribe], [archive]). +https://groups.google.com/a/mozilla.org/g/dev-webdriver. -There is also a Matrix channel to talk about using and developing -geckodriver on `#interop:mozilla.org `__ - -[subscribe]: https://lists.mozilla.org/listinfo/tools-marionette -[archive]: https://lists.mozilla.org/pipermail/tools-marionette/ +There is also an Element channel to talk about using and developing +geckodriver on `#webdriver:mozilla.org `__ diff --git a/doc/ARM.md b/doc/ARM.md index 5627cbd..99ab6dd 100644 --- a/doc/ARM.md +++ b/doc/ARM.md @@ -1,7 +1,7 @@ Self-serving an ARM build ========================= -Mozilla [announced the intent] to deprecate ARMv7 HF builds of +Mozilla announced the intent to deprecate ARMv7 HF builds of geckodriver in September 2018. This does not mean you can no longer use geckodriver on ARM systems, and this document explains how you can self-service a build for ARMv7 HF. @@ -35,5 +35,4 @@ cross-compile ARMv7 from a Linux host system is as follows: % cd testing/geckodriver % cargo build --release --target armv7-unknown-linux-gnueabihf -[announced the intent]: https://lists.mozilla.org/pipermail/tools-marionette/2018-September/000035.html [central]: https://hg.mozilla.org/mozilla-central/ diff --git a/doc/Patches.md b/doc/Patches.md new file mode 100644 index 0000000..5559280 --- /dev/null +++ b/doc/Patches.md @@ -0,0 +1,28 @@ +Submitting patches +================== + +You can submit patches by using [Phabricator]. Walk through its documentation +in how to set it up, and uploading patches for review. Don't worry about which +person to select for reviewing your code. It will be done automatically. + +Please also make sure to follow the [commit creation guidelines]. + +Once you have contributed a couple of patches, we are happy to sponsor you in +[becoming a Mozilla committer]. When you have been granted commit access +level 1, you will have permission to use the [Firefox CI] to trigger your own +“try runs” to test your changes. You can use the following [try preset] to run +the most relevant tests: + + mach try --preset geckodriver + +This preset will schedule geckodriver-related tests on various platforms. You can +reduce the number of tasks by filtering on platforms (e.g. linux) or build type +(e.g. opt): + + mach try --preset geckodriver -xq "'linux 'opt" + +[Phabricator]: https://moz-conduit.readthedocs.io/en/latest/phabricator-user.html +[commit creation guidelines]: https://mozilla-version-control-tools.readthedocs.io/en/latest/devguide/contributing.html?highlight=phabricator#submitting-patches-for-review +[becoming a Mozilla committer]: https://www.mozilla.org/en-US/about/governance/policies/commit/ +[Firefox CI]: https://treeherder.mozilla.org/ +[try preset]: https://firefox-source-docs.mozilla.org/tools/try/presets.html diff --git a/doc/Profiles.md b/doc/Profiles.md index c7ff656..d9a7c9b 100644 --- a/doc/Profiles.md +++ b/doc/Profiles.md @@ -75,7 +75,7 @@ Firefox (1), and a set of recommended preferences set on startup (2). These can be perused here: 1. [testing/geckodriver/src/prefs.rs](https://searchfox.org/mozilla-central/source/testing/geckodriver/src/prefs.rs) - 2. [testing/marionette/components/marionette/marionette.js](https://searchfox.org/mozilla-central/source/testing/marionette/components/marionette.js) + 2. [remote/components/marionette.js](https://searchfox.org/mozilla-central/source/remote/components/marionette.js) As mentioned, these are _recommended_ preferences, and any user-defined preferences in the [user.js file] or as part of the [`prefs` capability] @@ -84,7 +84,9 @@ take precedence. This means for example that the user can tweak starting the browser with a blank page. The recommended preferences set at runtime (see 2 above) may also -be disabled entirely by setting `marionette.prefs.recommended`. +be disabled entirely by setting `remote.prefs.recommended` starting with Firefox +91. For older versions of Firefox, the preference to use was +`marionette.prefs.recommended`. This may however cause geckodriver to not behave correctly according to the WebDriver standard, so it should be used with caution. diff --git a/doc/Releasing.md b/doc/Releasing.md index 7cf8b33..fa34e08 100644 --- a/doc/Releasing.md +++ b/doc/Releasing.md @@ -45,7 +45,8 @@ For each crate: 1. Bump the version number in Cargo.toml 2. Update the crate: `cargo update -p ` 3. Commit the changes for the modified `Cargo.toml`, and `Cargo.lock` - (can be found in the repositories root folder) + (can be found in the repositories root folder). Use a commit message + like `Bug XYZ - [rust-] Release version .` Update the change log diff --git a/doc/Support.md b/doc/Support.md index 741aab8..689522e 100644 --- a/doc/Support.md +++ b/doc/Support.md @@ -22,6 +22,11 @@ and required versions of Selenium and Firefox: max + + 0.30.0 + ≥ 3.11 (3.14 Python) + 78 ESR + n/a 0.29.1 ≥ 3.11 (3.14 Python) diff --git a/doc/index.rst b/doc/index.rst index be9fc88..863c058 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -40,6 +40,7 @@ For developers Building.md Testing.md + Patches.md Releasing.md ARM.md @@ -48,10 +49,7 @@ Communication ============= The mailing list for geckodriver discussion is -tools-marionette@lists.mozilla.org (`subscribe`_, `archive`_). +https://groups.google.com/a/mozilla.org/g/dev-webdriver. If you prefer real-time chat, ask your questions -on `#interop:mozilla.org `__. - -.. _subscribe: https://lists.mozilla.org/listinfo/tools-marionette -.. _archive: https://lists.mozilla.org/pipermail/tools-marionette/ +on `#webdriver:mozilla.org `__. diff --git a/mach_commands.py b/mach_commands.py index bf5fe59..e557cae 100644 --- a/mach_commands.py +++ b/mach_commands.py @@ -55,12 +55,14 @@ class GeckoDriver(MachCommandBase): help="Flags to pass to the debugger itself; " "split as the Bourne shell would.", ) - def run(self, binary, params, debug, debugger, debugger_args): + def run(self, command_context, binary, params, debug, debugger, debugger_args): try: - binpath = self.get_binary_path("geckodriver") + binpath = command_context.get_binary_path("geckodriver") except BinaryNotFoundException as e: - self.log(logging.ERROR, "geckodriver", {"error": str(e)}, "ERROR: {error}") - self.log( + command_context.log( + logging.ERROR, "geckodriver", {"error": str(e)}, "ERROR: {error}" + ) + command_context.log( logging.INFO, "geckodriver", {}, @@ -78,19 +80,21 @@ def run(self, binary, params, debug, debugger, debugger_args): if binary is None: try: - binary = self.get_binary_path("app") + binary = command_context.get_binary_path("app") except BinaryNotFoundException as e: - self.log( + command_context.log( logging.ERROR, "geckodriver", {"error": str(e)}, "ERROR: {error}" ) - self.log(logging.INFO, "geckodriver", {"help": e.help()}, "{help}") + command_context.log( + logging.INFO, "geckodriver", {"help": e.help()}, "{help}" + ) return 1 args.extend(["--binary", binary]) if debug or debugger or debugger_args: if "INSIDE_EMACS" in os.environ: - self.log_manager.terminal_handler.setLevel(logging.WARNING) + command_context.log_manager.terminal_handler.setLevel(logging.WARNING) import mozdebug @@ -102,8 +106,8 @@ def run(self, binary, params, debug, debugger, debugger_args): ) if debugger: - self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args) - if not self.debuggerInfo: + debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args) + if not debuggerInfo: print("Could not find a suitable debugger in your PATH.") return 1 @@ -122,6 +126,8 @@ def run(self, binary, params, debug, debugger, debugger_args): return 1 # Prepend the debugger args. - args = [self.debuggerInfo.path] + self.debuggerInfo.args + args + args = [debuggerInfo.path] + debuggerInfo.args + args - return self.run_process(args=args, ensure_exit_code=False, pass_thru=True) + return command_context.run_process( + args=args, ensure_exit_code=False, pass_thru=True + ) diff --git a/marionette/src/error.rs b/marionette/src/error.rs index 5db502e..6b05410 100644 --- a/marionette/src/error.rs +++ b/marionette/src/error.rs @@ -64,9 +64,9 @@ fn empty_string() -> String { "".to_owned() } -impl Into for MarionetteError { - fn into(self) -> Error { - Error::Marionette(self) +impl From for Error { + fn from(error: MarionetteError) -> Error { + Error::Marionette(error) } } diff --git a/marionette/src/message.rs b/marionette/src/message.rs index 33741ca..f9aac74 100644 --- a/marionette/src/message.rs +++ b/marionette/src/message.rs @@ -213,7 +213,7 @@ mod tests { let json = json!([0, 42, "WebDriver:FindElement", {"using": "css selector", "value": "value"}]); let find_element = webdriver::Command::FindElement(webdriver::Locator { - using: webdriver::Selector::CSS, + using: webdriver::Selector::Css, value: "value".into(), }); let req = Request(42, Command::WebDriver(find_element)); diff --git a/marionette/src/webdriver.rs b/marionette/src/webdriver.rs index b1069ed..6877313 100644 --- a/marionette/src/webdriver.rs +++ b/marionette/src/webdriver.rs @@ -26,7 +26,7 @@ pub struct Locator { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Selector { #[serde(rename = "css selector")] - CSS, + Css, #[serde(rename = "link text")] LinkText, #[serde(rename = "partial link text")] @@ -149,8 +149,7 @@ pub struct Script { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Command { - // Needs to be updated to "WebDriver:AcceptAlert" for Firefox 63 - #[serde(rename = "WebDriver:AcceptDialog")] + #[serde(rename = "WebDriver:AcceptAlert")] AcceptAlert, #[serde( rename = "WebDriver:AddCookie", @@ -168,6 +167,8 @@ pub enum Command { DeleteCookie(String), #[serde(rename = "WebDriver:DeleteAllCookies")] DeleteCookies, + #[serde(rename = "WebDriver:DeleteSession")] + DeleteSession, #[serde(rename = "WebDriver:DismissAlert")] DismissAlert, #[serde(rename = "WebDriver:ElementClear")] @@ -302,7 +303,7 @@ mod tests { #[test] fn test_json_selector_css() { - assert_ser_de(&Selector::CSS, json!("css selector")); + assert_ser_de(&Selector::Css, json!("css selector")); } #[test] @@ -376,7 +377,7 @@ mod tests { #[test] fn test_command_with_params() { let locator = Locator { - using: Selector::CSS, + using: Selector::Css, value: "value".into(), }; let json = json!({"WebDriver:FindElement": {"using": "css selector", "value": "value"}}); diff --git a/src/android.rs b/src/android.rs index e21c146..8969fb1 100644 --- a/src/android.rs +++ b/src/android.rs @@ -1,17 +1,16 @@ use crate::capabilities::AndroidOptions; -use mozdevice::{AndroidStorage, Device, Host}; +use mozdevice::{AndroidStorage, Device, Host, UnixPathBuf}; use mozprofile::profile::Profile; use serde::Serialize; use serde_yaml::{Mapping, Value}; use std::fmt; use std::io; -use std::path::PathBuf; use std::time; use webdriver::error::{ErrorStatus, WebDriverError}; // TODO: avoid port clashes across GeckoView-vehicles. // For now, we always use target port 2829, leading to issues like bug 1533704. -const TARGET_PORT: u16 = 2829; +const MARIONETTE_TARGET_PORT: u16 = 2829; const CONFIG_FILE_HEADING: &str = r#"## GeckoView configuration YAML ## @@ -97,15 +96,18 @@ impl AndroidProcess { #[derive(Debug)] pub struct AndroidHandler { - pub config: PathBuf, + pub config: UnixPathBuf, pub options: AndroidOptions, pub process: AndroidProcess, - pub profile: PathBuf, - pub test_root: PathBuf, + pub profile: UnixPathBuf, + pub test_root: UnixPathBuf, - // For port forwarding host => target - pub host_port: u16, - pub target_port: u16, + // Port forwarding for Marionette: host => target + pub marionette_host_port: u16, + pub marionette_target_port: u16, + + // Port forwarding for WebSocket connections (WebDriver BiDi and CDP) + pub websocket_port: Option, } impl Drop for AndroidHandler { @@ -126,21 +128,44 @@ impl Drop for AndroidHandler { Err(e) => error!("Failed deleting GeckoView configuration file: {}", e), } - match self.process.device.kill_forward_port(self.host_port) { + match self.process.device.remove(&self.test_root) { + Ok(_) => debug!("Deleted test root folder: {}", &self.test_root.display()), + Err(e) => error!("Failed deleting test root folder: {}", e), + } + + match self + .process + .device + .kill_forward_port(self.marionette_host_port) + { Ok(_) => debug!( - "Android port forward ({} -> {}) stopped", - &self.host_port, &self.target_port + "Marionette port forward ({} -> {}) stopped", + &self.marionette_host_port, &self.marionette_target_port ), Err(e) => error!( - "Android port forward ({} -> {}) failed to stop: {}", - &self.host_port, &self.target_port, e + "Marionette port forward ({} -> {}) failed to stop: {}", + &self.marionette_host_port, &self.marionette_target_port, e ), } + + if let Some(port) = self.websocket_port { + match self.process.device.kill_forward_port(port) { + Ok(_) => debug!("WebSocket port forward ({0} -> {0}) stopped", &port), + Err(e) => error!( + "WebSocket port forward ({0} -> {0}) failed to stop: {1}", + &port, e + ), + } + } } } impl AndroidHandler { - pub fn new(options: &AndroidOptions, host_port: u16) -> Result { + pub fn new( + options: &AndroidOptions, + marionette_host_port: u16, + websocket_port: Option, + ) -> Result { // We need to push profile.pathbuf to a safe space on the device. // Make it per-Android package to avoid clashes and confusion. // This naming scheme follows GeckoView's configuration file naming scheme, @@ -155,29 +180,35 @@ impl AndroidHandler { let mut device = host.device_or_default(options.device_serial.as_ref(), options.storage)?; - // Set up port forward. Port forwarding will be torn down, if possible, - device.forward_port(host_port, TARGET_PORT)?; + // Set up port forwarding for Marionette. + device.forward_port(marionette_host_port, MARIONETTE_TARGET_PORT)?; debug!( - "Android port forward ({} -> {}) started", - host_port, TARGET_PORT + "Marionette port forward ({} -> {}) started", + marionette_host_port, MARIONETTE_TARGET_PORT ); + if let Some(port) = websocket_port { + // Set up port forwarding for WebSocket connections (WebDriver BiDi, and CDP). + device.forward_port(port, port)?; + debug!("WebSocket port forward ({} -> {}) started", port, port); + } + let test_root = match device.storage { AndroidStorage::App => { device.run_as_package = Some(options.package.to_owned()); - let mut buf = PathBuf::from("/data/data"); + let mut buf = UnixPathBuf::from("/data/data"); buf.push(&options.package); buf.push("test_root"); buf } - AndroidStorage::Internal => PathBuf::from("/data/local/tmp/test_root"), + AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"), AndroidStorage::Sdcard => { // We need to push the profile to a location on the device that can also // be read and write by the application, and works for unrooted devices. // The only location that meets this criteria is under: // $EXTERNAL_STORAGE/Android/data/%options.package%/files let response = device.execute_host_shell_command("echo $EXTERNAL_STORAGE")?; - let mut buf = PathBuf::from(response.trim_end_matches('\n')); + let mut buf = UnixPathBuf::from(response.trim_end_matches('\n')); buf.push("Android/data"); buf.push(&options.package); buf.push("files/test_root"); @@ -199,17 +230,16 @@ impl AndroidHandler { // Check if the specified package is installed let response = device.execute_host_shell_command(&format!("pm list packages {}", &options.package))?; - let packages = response + let mut packages = response .trim() .split_terminator('\n') .filter(|line| line.starts_with("package:")) - .map(|line| line.rsplit(':').next().expect("Package name found")) - .collect::>(); - if !packages.contains(&options.package.as_str()) { + .map(|line| line.rsplit(':').next().expect("Package name found")); + if packages.find(|x| x == &options.package.as_str()).is_none() { return Err(AndroidError::PackageNotFound(options.package.clone())); } - let config = PathBuf::from(format!( + let config = UnixPathBuf::from(format!( "/data/local/tmp/{}-geckoview-config.yaml", &options.package )); @@ -238,17 +268,22 @@ impl AndroidHandler { let process = AndroidProcess::new(device, options.package.clone(), activity)?; Ok(AndroidHandler { - options: options.clone(), config, process, profile, test_root, - host_port, - target_port: TARGET_PORT, + marionette_host_port, + marionette_target_port: MARIONETTE_TARGET_PORT, + options: options.clone(), + websocket_port, }) } - pub fn generate_config_file(&self, envs: I) -> Result + pub fn generate_config_file( + &self, + args: Option>, + envs: I, + ) -> Result where I: IntoIterator, K: ToString, @@ -259,19 +294,20 @@ impl AndroidHandler { #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Config { pub env: Mapping, - pub args: Value, + pub args: Vec, } - // TODO: Allow to write custom arguments and preferences from moz:firefoxOptions let mut config = Config { - args: Value::Sequence(vec![ - Value::String("--marionette".into()), - Value::String("--profile".into()), - Value::String(self.profile.display().to_string()), - ]), + args: vec![ + "--marionette".into(), + "--profile".into(), + self.profile.display().to_string(), + ], env: Mapping::new(), }; + config.args.append(&mut args.unwrap_or_default()); + for (key, value) in envs { config.env.insert( Value::String(key.to_string()), @@ -298,7 +334,12 @@ impl AndroidHandler { Ok(contents.concat()) } - pub fn prepare(&self, profile: &Profile, env: I) -> Result<()> + pub fn prepare( + &self, + profile: &Profile, + args: Option>, + env: I, + ) -> Result<()> where I: IntoIterator, K: ToString, @@ -324,7 +365,7 @@ impl AndroidHandler { .device .push_dir(&profile.path, &self.profile, 0o777)?; - let contents = self.generate_config_file(env)?; + let contents = self.generate_config_file(args, env)?; debug!("Content of generated GeckoView config file:\n{}", contents); let reader = &mut io::BufReader::new(contents.as_bytes()); @@ -409,17 +450,16 @@ mod test { use crate::android::AndroidHandler; use crate::capabilities::AndroidOptions; - use mozdevice::{AndroidStorage, AndroidStorageInput}; - use std::path::PathBuf; + use mozdevice::{AndroidStorage, AndroidStorageInput, UnixPathBuf}; fn run_handler_storage_test(package: &str, storage: AndroidStorageInput) { let options = AndroidOptions::new(package.to_owned(), storage); - let handler = AndroidHandler::new(&options, 4242).expect("has valid Android handler"); + let handler = AndroidHandler::new(&options, 4242, None).expect("has valid Android handler"); assert_eq!(handler.options, options); assert_eq!(handler.process.package, package); - let expected_config_path = PathBuf::from(format!( + let expected_config_path = UnixPathBuf::from(format!( "/data/local/tmp/{}-geckoview-config.yaml", &package )); @@ -436,12 +476,12 @@ mod test { let test_root = match handler.process.device.storage { AndroidStorage::App => { - let mut buf = PathBuf::from("/data/data"); + let mut buf = UnixPathBuf::from("/data/data"); buf.push(&package); buf.push("test_root"); buf } - AndroidStorage::Internal => PathBuf::from("/data/local/tmp/test_root"), + AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"), AndroidStorage::Sdcard => { let response = handler .process @@ -449,7 +489,7 @@ mod test { .execute_host_shell_command("echo $EXTERNAL_STORAGE") .unwrap(); - let mut buf = PathBuf::from(response.trim_end_matches('\n')); + let mut buf = UnixPathBuf::from(response.trim_end_matches('\n')); buf.push("Android/data/"); buf.push(&package); buf.push("files/test_root"); diff --git a/src/browser.rs b/src/browser.rs new file mode 100644 index 0000000..b777a0a --- /dev/null +++ b/src/browser.rs @@ -0,0 +1,423 @@ +/* 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 crate::android::AndroidHandler; +use crate::capabilities::FirefoxOptions; +use crate::logging; +use crate::prefs; +use mozprofile::preferences::Pref; +use mozprofile::profile::{PrefFile, Profile}; +use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess}; +use std::fs; +use std::path::PathBuf; +use std::time; + +use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult}; + +/// A running Gecko instance. +#[derive(Debug)] +pub(crate) enum Browser { + Local(LocalBrowser), + Remote(RemoteBrowser), + + /// An existing browser instance not controlled by GeckoDriver + Existing, +} + +impl Browser { + pub(crate) fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> { + match self { + Browser::Local(x) => x.close(wait_for_shutdown), + Browser::Remote(x) => x.close(), + Browser::Existing => Ok(()), + } + } +} + +#[derive(Debug)] +/// A local Firefox process, running on this (host) device. +pub(crate) struct LocalBrowser { + process: FirefoxProcess, + prefs_backup: Option, +} + +impl LocalBrowser { + pub(crate) fn new( + options: FirefoxOptions, + marionette_port: u16, + jsdebugger: bool, + ) -> WebDriverResult { + let binary = options.binary.ok_or_else(|| { + WebDriverError::new( + ErrorStatus::SessionNotCreated, + "Expected browser binary location, but unable to find \ + binary in default location, no \ + 'moz:firefoxOptions.binary' capability provided, and \ + no binary flag set on the command line", + ) + })?; + + let is_custom_profile = options.profile.is_some(); + + let mut profile = match options.profile { + Some(x) => x, + None => Profile::new()?, + }; + + let prefs_backup = set_prefs( + marionette_port, + &mut profile, + is_custom_profile, + options.prefs, + jsdebugger, + ) + .map_err(|e| { + WebDriverError::new( + ErrorStatus::SessionNotCreated, + format!("Failed to set preferences: {}", e), + ) + })?; + + let mut runner = FirefoxRunner::new(&binary, profile); + + runner.arg("--marionette"); + if jsdebugger { + runner.arg("--jsdebugger"); + } + if let Some(args) = options.args.as_ref() { + runner.args(args); + } + + // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting + runner + .env("MOZ_CRASHREPORTER", "1") + .env("MOZ_CRASHREPORTER_NO_REPORT", "1") + .env("MOZ_CRASHREPORTER_SHUTDOWN", "1"); + + let process = match runner.start() { + Ok(process) => process, + Err(e) => { + if let Some(backup) = prefs_backup { + backup.restore(); + } + return Err(WebDriverError::new( + ErrorStatus::SessionNotCreated, + format!("Failed to start browser {}: {}", binary.display(), e), + )); + } + }; + + Ok(LocalBrowser { + process, + prefs_backup, + }) + } + + fn close(mut self, wait_for_shutdown: bool) -> WebDriverResult<()> { + if wait_for_shutdown { + // TODO(https://bugzil.la/1443922): + // Use toolkit.asyncshutdown.crash_timout pref + let duration = time::Duration::from_secs(70); + match self.process.wait(duration) { + Ok(x) => debug!("Browser process stopped: {}", x), + Err(e) => error!("Failed to stop browser process: {}", e), + } + } + self.process.kill()?; + // Restoring the prefs if the browser fails to stop perhaps doesn't work anyway + if let Some(prefs_backup) = self.prefs_backup { + prefs_backup.restore(); + }; + Ok(()) + } + + pub(crate) fn check_status(&mut self) -> Option { + match self.process.try_wait() { + Ok(Some(status)) => Some( + status + .code() + .map(|c| c.to_string()) + .unwrap_or_else(|| "signal".into()), + ), + Ok(None) => None, + Err(_) => Some("{unknown}".into()), + } + } +} + +#[derive(Debug)] +/// A remote instance, running on a (target) Android device. +pub(crate) struct RemoteBrowser { + handler: AndroidHandler, +} + +impl RemoteBrowser { + pub(crate) fn new( + options: FirefoxOptions, + marionette_port: u16, + websocket_port: Option, + ) -> WebDriverResult { + let android_options = options.android.unwrap(); + + let handler = AndroidHandler::new(&android_options, marionette_port, websocket_port)?; + + // Profile management. + let is_custom_profile = options.profile.is_some(); + + let mut profile = options.profile.unwrap_or(Profile::new()?); + + set_prefs( + handler.marionette_target_port, + &mut profile, + is_custom_profile, + options.prefs, + false, + ) + .map_err(|e| { + WebDriverError::new( + ErrorStatus::SessionNotCreated, + format!("Failed to set preferences: {}", e), + ) + })?; + + handler.prepare(&profile, options.args, options.env.unwrap_or_default())?; + + handler.launch()?; + + Ok(RemoteBrowser { handler }) + } + + fn close(self) -> WebDriverResult<()> { + self.handler.force_stop()?; + Ok(()) + } +} + +fn set_prefs( + port: u16, + profile: &mut Profile, + custom_profile: bool, + extra_prefs: Vec<(String, Pref)>, + js_debugger: bool, +) -> WebDriverResult> { + let prefs = profile.user_prefs().map_err(|_| { + WebDriverError::new( + ErrorStatus::UnknownError, + "Unable to read profile preferences file", + ) + })?; + + let backup_prefs = if custom_profile && prefs.path.exists() { + Some(PrefsBackup::new(&prefs)?) + } else { + None + }; + + for &(ref name, ref value) in prefs::DEFAULT.iter() { + if !custom_profile || !prefs.contains_key(name) { + prefs.insert((*name).to_string(), (*value).clone()); + } + } + + prefs.insert_slice(&extra_prefs[..]); + + if js_debugger { + prefs.insert("devtools.browsertoolbox.panel", Pref::new("jsdebugger")); + prefs.insert("devtools.debugger.remote-enabled", Pref::new(true)); + prefs.insert("devtools.chrome.enabled", Pref::new(true)); + prefs.insert("devtools.debugger.prompt-connection", Pref::new(false)); + } + + prefs.insert("marionette.port", Pref::new(port)); + prefs.insert("remote.log.level", logging::max_level().into()); + + // Deprecated since Firefox 91. + prefs.insert("marionette.log.level", logging::max_level().into()); + + prefs.write().map_err(|e| { + WebDriverError::new( + ErrorStatus::UnknownError, + format!("Unable to write Firefox profile: {}", e), + ) + })?; + Ok(backup_prefs) +} + +#[derive(Debug)] +struct PrefsBackup { + orig_path: PathBuf, + backup_path: PathBuf, +} + +impl PrefsBackup { + fn new(prefs: &PrefFile) -> WebDriverResult { + let mut prefs_backup_path = prefs.path.clone(); + let mut counter = 0; + while { + let ext = if counter > 0 { + format!("geckodriver_backup_{}", counter) + } else { + "geckodriver_backup".to_string() + }; + prefs_backup_path.set_extension(ext); + prefs_backup_path.exists() + } { + counter += 1 + } + debug!("Backing up prefs to {:?}", prefs_backup_path); + fs::copy(&prefs.path, &prefs_backup_path)?; + + Ok(PrefsBackup { + orig_path: prefs.path.clone(), + backup_path: prefs_backup_path, + }) + } + + fn restore(self) { + if self.backup_path.exists() { + let _ = fs::rename(self.backup_path, self.orig_path); + } + } +} + +#[cfg(test)] +mod tests { + use super::set_prefs; + use crate::capabilities::FirefoxOptions; + use mozprofile::preferences::{Pref, PrefValue}; + use mozprofile::profile::Profile; + use serde_json::{Map, Value}; + use std::fs::File; + use std::io::{Read, Write}; + + fn example_profile() -> Value { + let mut profile_data = Vec::with_capacity(1024); + let mut profile = File::open("src/tests/profile.zip").unwrap(); + profile.read_to_end(&mut profile_data).unwrap(); + Value::String(base64::encode(&profile_data)) + } + + // This is not a pretty test, mostly due to the nature of + // mozprofile's and MarionetteHandler's APIs, but we have had + // several regressions related to remote.log.level. + #[test] + fn test_remote_log_level() { + let mut profile = Profile::new().unwrap(); + set_prefs(2828, &mut profile, false, vec![], false).ok(); + let user_prefs = profile.user_prefs().unwrap(); + + let pref = user_prefs.get("remote.log.level").unwrap(); + let value = match pref.value { + PrefValue::String(ref s) => s, + _ => panic!(), + }; + for (i, ch) in value.chars().enumerate() { + if i == 0 { + assert!(ch.is_uppercase()); + } else { + assert!(ch.is_lowercase()); + } + } + } + + #[test] + fn test_prefs() { + let marionette_settings = Default::default(); + + let encoded_profile = example_profile(); + let mut prefs: Map = Map::new(); + prefs.insert( + "browser.display.background_color".into(), + Value::String("#00ff00".into()), + ); + + let mut firefox_opts = Map::new(); + firefox_opts.insert("profile".into(), encoded_profile); + firefox_opts.insert("prefs".into(), Value::Object(prefs)); + + let mut caps = Map::new(); + caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts)); + + let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps) + .expect("Valid profile and prefs"); + + let mut profile = opts.profile.expect("valid firefox profile"); + + set_prefs(2828, &mut profile, true, opts.prefs, false).expect("set preferences"); + + let prefs_set = profile.user_prefs().expect("valid user preferences"); + println!("{:#?}", prefs_set.prefs); + + assert_eq!( + prefs_set.get("startup.homepage_welcome_url"), + Some(&Pref::new("data:text/html,PASS")) + ); + assert_eq!( + prefs_set.get("browser.display.background_color"), + Some(&Pref::new("#00ff00")) + ); + assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828))); + } + + #[test] + fn test_pref_backup() { + let mut profile = Profile::new().unwrap(); + + // Create some prefs in the profile + let initial_prefs = profile.user_prefs().unwrap(); + initial_prefs.insert("geckodriver.example", Pref::new("example")); + initial_prefs.write().unwrap(); + + let prefs_path = initial_prefs.path.clone(); + + let mut conflicting_backup_path = initial_prefs.path.clone(); + conflicting_backup_path.set_extension("geckodriver_backup".to_string()); + println!("{:?}", conflicting_backup_path); + let mut file = File::create(&conflicting_backup_path).unwrap(); + file.write_all(b"test").unwrap(); + assert!(conflicting_backup_path.exists()); + + let mut initial_prefs_data = String::new(); + File::open(&prefs_path) + .expect("Initial prefs exist") + .read_to_string(&mut initial_prefs_data) + .unwrap(); + + let backup = set_prefs(2828, &mut profile, true, vec![], false) + .unwrap() + .unwrap(); + let user_prefs = profile.user_prefs().unwrap(); + + assert!(user_prefs.path.exists()); + let mut backup_path = user_prefs.path.clone(); + backup_path.set_extension("geckodriver_backup_1".to_string()); + + assert!(backup_path.exists()); + + // Ensure the actual prefs contain both the existing ones and the ones we added + let pref = user_prefs.get("marionette.port").unwrap(); + assert_eq!(pref.value, PrefValue::Int(2828)); + + let pref = user_prefs.get("geckodriver.example").unwrap(); + assert_eq!(pref.value, PrefValue::String("example".into())); + + // Ensure the backup prefs don't contain the new settings + let mut backup_data = String::new(); + File::open(&backup_path) + .expect("Backup prefs exist") + .read_to_string(&mut backup_data) + .unwrap(); + assert_eq!(backup_data, initial_prefs_data); + + backup.restore(); + + assert!(!backup_path.exists()); + let mut final_prefs_data = String::new(); + File::open(&prefs_path) + .expect("Initial prefs exist") + .read_to_string(&mut final_prefs_data) + .unwrap(); + assert_eq!(final_prefs_data, initial_prefs_data); + } +} diff --git a/src/build.rs b/src/build.rs index 7ba3144..f77a336 100644 --- a/src/build.rs +++ b/src/build.rs @@ -35,10 +35,8 @@ impl fmt::Display for BuildInfo { } } -// TODO(Henrik): Change into From -//std::convert::From<&str>` is not implemented for `rustc_serialize::json::Json -impl Into for BuildInfo { - fn into(self) -> Value { +impl From for Value { + fn from(_: BuildInfo) -> Value { Value::String(BuildInfo::version().to_string()) } } diff --git a/src/capabilities.rs b/src/capabilities.rs index a409cfe..278530e 100644 --- a/src/capabilities.rs +++ b/src/capabilities.rs @@ -4,16 +4,18 @@ use crate::command::LogOptions; use crate::logging::Level; -use base64; +use crate::marionette::MarionetteSettings; use mozdevice::AndroidStorageInput; use mozprofile::preferences::Pref; use mozprofile::profile::Profile; +use mozrunner::firefox_args::{get_arg_value, parse_args, Arg}; use mozrunner::runner::platform::firefox_default_path; use mozversion::{self, firefox_binary_version, firefox_version, Version}; use regex::bytes::Regex; use serde_json::{Map, Value}; use std::collections::BTreeMap; use std::default::Default; +use std::ffi::OsString; use std::fmt::{self, Display}; use std::fs; use std::io; @@ -23,7 +25,6 @@ use std::path::{Path, PathBuf}; use std::str::{self, FromStr}; use webdriver::capabilities::{BrowserCapabilities, Capabilities}; use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult}; -use zip; #[derive(Clone, Debug)] enum VersionError { @@ -154,6 +155,10 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> { Ok(true) } + fn accept_proxy(&mut self, _: &Capabilities, _: &Capabilities) -> WebDriverResult { + Ok(true) + } + fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult { Ok(true) } @@ -164,7 +169,7 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> { comparison: &str, ) -> WebDriverResult { Version::from_str(version) - .map_err(|err| VersionError::from(err))? + .map_err(VersionError::from)? .matches(comparison) .map_err(|err| VersionError::from(err).into()) } @@ -173,8 +178,10 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> { Ok(true) } - fn accept_proxy(&mut self, _: &Capabilities, _: &Capabilities) -> WebDriverResult { - Ok(true) + fn web_socket_url(&mut self, caps: &Capabilities) -> WebDriverResult { + self.browser_version(caps)? + .map(|v| self.compare_browser_version(&v, ">=90")) + .unwrap_or(Ok(false)) } fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()> { @@ -377,6 +384,7 @@ pub struct FirefoxOptions { pub log: LogOptions, pub prefs: Vec<(String, Pref)>, pub android: Option, + pub use_websocket: bool, } impl FirefoxOptions { @@ -384,9 +392,9 @@ impl FirefoxOptions { Default::default() } - pub fn from_capabilities( + pub(crate) fn from_capabilities( binary_path: Option, - android_storage: AndroidStorageInput, + settings: &MarionetteSettings, matched: &mut Capabilities, ) -> WebDriverResult { let mut rv = FirefoxOptions::new(); @@ -401,7 +409,7 @@ impl FirefoxOptions { ) })?; - rv.android = FirefoxOptions::load_android(android_storage, &options)?; + rv.android = FirefoxOptions::load_android(settings.android_storage, &options)?; rv.args = FirefoxOptions::load_args(&options)?; rv.env = FirefoxOptions::load_env(&options)?; rv.log = FirefoxOptions::load_log(&options)?; @@ -409,32 +417,62 @@ impl FirefoxOptions { rv.profile = FirefoxOptions::load_profile(&options)?; } - if let Some(json) = matched.remove("moz:debuggerAddress") { - let use_web_socket = json.as_bool().ok_or_else(|| { - WebDriverError::new( - ErrorStatus::InvalidArgument, - "moz:debuggerAddress is not a boolean", - ) - })?; + if let Some(args) = rv.args.as_ref() { + let os_args = parse_args(args.iter().map(OsString::from).collect::>().iter()); + if let Some(path) = get_arg_value(os_args.iter(), Arg::Profile) { + if rv.profile.is_some() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "Can't provide both a --profile argument and a profile", + )); + } + let path_buf = PathBuf::from(path); + rv.profile = Some(Profile::new_from_path(&path_buf)?); + } - if use_web_socket { - let mut remote_args = Vec::new(); - remote_args.push("--remote-debugging-port".to_owned()); - remote_args.push("0".to_owned()); + if get_arg_value(os_args.iter(), Arg::NamedProfile).is_some() && rv.profile.is_some() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "Can't provide both a -P argument and a profile", + )); + } + } - if let Some(ref mut args) = rv.args { - args.append(&mut remote_args); - } else { - rv.args = Some(remote_args); - } + let has_web_socket_url = matched + .get("webSocketUrl") + .and_then(|x| x.as_bool()) + .unwrap_or(false); + + let has_debugger_address = matched + .remove("moz:debuggerAddress") + .and_then(|x| x.as_bool()) + .unwrap_or(false); + + // Set a command line provided port for the Remote Agent for now. + // It needs to be the same on the host and the Android device. + if has_web_socket_url || has_debugger_address { + rv.use_websocket = true; + + // Bug 1722863: Setting of command line arguments would be + // better suited in the individual Browser implementations. + let mut remote_args = Vec::new(); + remote_args.push("--remote-debugging-port".to_owned()); + remote_args.push(settings.websocket_port.to_string()); + + if let Some(ref mut args) = rv.args { + args.append(&mut remote_args); + } else { + rv.args = Some(remote_args); + } + } - // Force Fission disabled until Remote Agent is compatible, - // and preference hasn't been already set - let has_fission_pref = rv.prefs.iter().find(|&x| x.0 == "fission.autostart"); - if has_fission_pref.is_none() { - rv.prefs - .push(("fission.autostart".to_owned(), Pref::new(false))); - } + // Force Fission disabled until the CDP implementation is compatible, + // and preference hasn't been already set + if has_debugger_address { + let has_fission_pref = rv.prefs.iter().find(|&x| x.0 == "fission.autostart"); + if has_fission_pref.is_none() { + rv.prefs + .push(("fission.autostart".to_owned(), Pref::new(false))); } } @@ -597,7 +635,7 @@ impl FirefoxOptions { })? .to_owned(); - if activity.contains("/") { + if activity.contains('/') { return Err(WebDriverError::new( ErrorStatus::InvalidArgument, "androidActivity should not contain '/", @@ -671,7 +709,7 @@ impl FirefoxOptions { "-d".to_string(), "about:blank".to_string(), ]) - }, + } }; Ok(Some(android)) @@ -751,10 +789,7 @@ mod tests { use self::mozprofile::preferences::Pref; use super::*; - use crate::marionette::MarionetteHandler; - use mozdevice::AndroidStorageInput; - use serde_json::json; - use std::default::Default; + use serde_json::{json, Map, Value}; use std::fs::File; use std::io::Read; @@ -767,16 +802,19 @@ mod tests { Value::String(base64::encode(&profile_data)) } - fn make_options(firefox_opts: Capabilities) -> WebDriverResult { + fn make_options( + firefox_opts: Capabilities, + marionette_settings: Option, + ) -> WebDriverResult { let mut caps = Capabilities::new(); caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts)); - FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + FirefoxOptions::from_capabilities(None, &marionette_settings.unwrap_or_default(), &mut caps) } #[test] fn fx_options_default() { - let opts = FirefoxOptions::new(); + let opts: FirefoxOptions = Default::default(); assert_eq!(opts.android, None); assert_eq!(opts.args, None); assert_eq!(opts.binary, None); @@ -787,11 +825,12 @@ mod tests { } #[test] - fn fx_options_from_capabilities_no_binary_and_caps() { + fn fx_options_from_capabilities_no_binary_and_empty_caps() { let mut caps = Capabilities::new(); - let opts = - FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps).unwrap(); + let marionette_settings = Default::default(); + let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps) + .expect("valid firefox options"); assert_eq!(opts.android, None); assert_eq!(opts.args, None); assert_eq!(opts.binary, None); @@ -808,13 +847,14 @@ mod tests { ); let binary = PathBuf::from("foo"); + let marionette_settings = Default::default(); let opts = FirefoxOptions::from_capabilities( Some(binary.clone()), - AndroidStorageInput::Auto, + &marionette_settings, &mut caps, ) - .unwrap(); + .expect("valid firefox options"); assert_eq!(opts.android, None); assert_eq!(opts.args, None); assert_eq!(opts.binary, Some(binary)); @@ -823,10 +863,11 @@ mod tests { } #[test] - fn fx_options_from_capabilities_with_debugger_address_not_set() { + fn fx_options_from_capabilities_with_websocket_url_not_set() { let mut caps = Capabilities::new(); - let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + let marionette_settings = Default::default(); + let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps) .expect("Valid Firefox options"); assert!( @@ -836,16 +877,63 @@ mod tests { } #[test] - fn fx_options_from_capabilities_with_debugger_address_false() { + fn fx_options_from_capabilities_with_websocket_url_false() { let mut caps = Capabilities::new(); - caps.insert("moz:debuggerAddress".into(), json!(false)); + caps.insert("webSocketUrl".into(), json!(false)); - let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + let marionette_settings = Default::default(); + let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps) .expect("Valid Firefox options"); assert!( opts.args.is_none(), - "CLI arguments for remote protocol unexpectedly found" + "CLI arguments for Firefox unexpectedly found" + ); + } + + #[test] + fn fx_options_from_capabilities_with_websocket_url_true() { + let mut caps = Capabilities::new(); + caps.insert("webSocketUrl".into(), json!(true)); + + let settings = MarionetteSettings { + websocket_port: 1234, + ..Default::default() + }; + let opts = FirefoxOptions::from_capabilities(None, &settings, &mut caps) + .expect("Valid Firefox options"); + + if let Some(args) = opts.args { + let mut iter = args.iter(); + assert!(iter + .find(|&arg| arg == &"--remote-debugging-port".to_owned()) + .is_some()); + assert_eq!(iter.next(), Some(&"1234".to_owned())); + } else { + assert!(false, "CLI arguments for Firefox not found"); + } + } + + #[test] + fn fx_options_from_capabilities_with_debugger_address_not_set() { + let caps = Capabilities::new(); + + let opts = make_options(caps, None).expect("valid firefox options"); + assert!( + opts.args.is_none(), + "CLI arguments for Firefox unexpectedly found" + ); + } + + #[test] + fn fx_options_from_capabilities_with_debugger_address_false() { + let mut caps = Capabilities::new(); + caps.insert("moz:debuggerAddress".into(), json!(false)); + + let opts = make_options(caps, None).expect("valid firefox options"); + assert!( + opts.args.is_none(), + "CLI arguments for Firefox unexpectedly found" ); } @@ -854,7 +942,11 @@ mod tests { let mut caps = Capabilities::new(); caps.insert("moz:debuggerAddress".into(), json!(true)); - let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + let settings = MarionetteSettings { + websocket_port: 1234, + ..Default::default() + }; + let opts = FirefoxOptions::from_capabilities(None, &settings, &mut caps) .expect("Valid Firefox options"); if let Some(args) = opts.args { @@ -862,9 +954,9 @@ mod tests { assert!(iter .find(|&arg| arg == &"--remote-debugging-port".to_owned()) .is_some()); - assert_eq!(iter.next(), Some(&"0".to_owned())); + assert_eq!(iter.next(), Some(&"1234".to_owned())); } else { - assert!(false, "CLI arguments for remote protocol not found"); + assert!(false, "CLI arguments for Firefox not found"); } assert!(opts @@ -878,7 +970,8 @@ mod tests { let mut caps = Capabilities::new(); caps.insert("moz:firefoxOptions".into(), json!(42)); - FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + let marionette_settings = Default::default(); + FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps) .expect_err("Firefox options need to be of type object"); } @@ -887,7 +980,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("androidAvtivity".into(), json!("foo")); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); assert_eq!(opts.android, None); } @@ -897,7 +990,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("androidPackage".into(), json!(value)); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); assert_eq!(opts.android.unwrap().package, value.to_string()); } } @@ -907,7 +1000,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("androidPackage".into(), json!(42)); - make_options(firefox_opts).expect_err("invalid firefox options"); + make_options(firefox_opts, None).expect_err("invalid firefox options"); } #[test] @@ -915,7 +1008,7 @@ mod tests { for value in ["../foo", "\\foo\n", "foo", "_foo", "0foo"].iter() { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("androidPackage".into(), json!(value)); - make_options(firefox_opts).expect_err("invalid firefox options"); + make_options(firefox_opts, None).expect_err("invalid firefox options"); } } @@ -937,7 +1030,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("androidPackage".into(), json!(package)); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); assert!(opts .android .unwrap() @@ -955,7 +1048,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("androidPackage".into(), json!(package)); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); assert_eq!(opts.android.unwrap().activity, None); } @@ -965,7 +1058,7 @@ mod tests { json!("org.mozilla.geckoview_example"), ); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); assert_eq!(opts.android.unwrap().activity, None); } @@ -975,7 +1068,7 @@ mod tests { firefox_opts.insert("androidPackage".into(), json!("foo.bar")); firefox_opts.insert("androidActivity".into(), json!("foo")); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); assert_eq!(opts.android.unwrap().activity, Some("foo".to_string())); } @@ -985,7 +1078,7 @@ mod tests { firefox_opts.insert("androidPackage".into(), json!("foo.bar")); firefox_opts.insert("androidActivity".into(), json!(42)); - make_options(firefox_opts).expect_err("invalid firefox options"); + make_options(firefox_opts, None).expect_err("invalid firefox options"); } #[test] @@ -994,7 +1087,7 @@ mod tests { firefox_opts.insert("androidPackage".into(), json!("foo.bar")); firefox_opts.insert("androidActivity".into(), json!("foo.bar/cheese")); - make_options(firefox_opts).expect_err("invalid firefox options"); + make_options(firefox_opts, None).expect_err("invalid firefox options"); } #[test] @@ -1003,7 +1096,7 @@ mod tests { firefox_opts.insert("androidPackage".into(), json!("foo.bar")); firefox_opts.insert("androidDeviceSerial".into(), json!("cheese")); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); assert_eq!( opts.android.unwrap().device_serial, Some("cheese".to_string()) @@ -1016,7 +1109,7 @@ mod tests { firefox_opts.insert("androidPackage".into(), json!("foo.bar")); firefox_opts.insert("androidDeviceSerial".into(), json!(42)); - make_options(firefox_opts).expect_err("invalid firefox options"); + make_options(firefox_opts, None).expect_err("invalid firefox options"); } #[test] @@ -1035,7 +1128,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("androidPackage".into(), json!(package)); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); assert_eq!( opts.android.unwrap().intent_arguments, Some(vec![ @@ -1054,7 +1147,7 @@ mod tests { firefox_opts.insert("androidPackage".into(), json!("foo.bar")); firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", "ipsum"])); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); assert_eq!( opts.android.unwrap().intent_arguments, Some(vec!["lorem".to_string(), "ipsum".to_string()]) @@ -1067,7 +1160,7 @@ mod tests { firefox_opts.insert("androidPackage".into(), json!("foo.bar")); firefox_opts.insert("androidIntentArguments".into(), json!(42)); - make_options(firefox_opts).expect_err("invalid firefox options"); + make_options(firefox_opts, None).expect_err("invalid firefox options"); } #[test] @@ -1076,7 +1169,7 @@ mod tests { firefox_opts.insert("androidPackage".into(), json!("foo.bar")); firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", 42])); - make_options(firefox_opts).expect_err("invalid firefox options"); + make_options(firefox_opts, None).expect_err("invalid firefox options"); } #[test] @@ -1088,7 +1181,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("env".into(), env.into()); - let mut opts = make_options(firefox_opts).expect("valid firefox options"); + let mut opts = make_options(firefox_opts, None).expect("valid firefox options"); for sorted in opts.env.iter_mut() { sorted.sort() } @@ -1108,7 +1201,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("env".into(), env.into()); - make_options(firefox_opts).expect_err("invalid firefox options"); + make_options(firefox_opts, None).expect_err("invalid firefox options"); } #[test] @@ -1119,7 +1212,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("env".into(), env.into()); - make_options(firefox_opts).expect_err("invalid firefox options"); + make_options(firefox_opts, None).expect_err("invalid firefox options"); } #[test] @@ -1128,7 +1221,7 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("profile".into(), encoded_profile); - let opts = make_options(firefox_opts).expect("valid firefox options"); + let opts = make_options(firefox_opts, None).expect("valid firefox options"); let mut profile = opts.profile.expect("valid firefox profile"); let prefs = profile.user_prefs().expect("valid preferences"); @@ -1141,37 +1234,28 @@ mod tests { } #[test] - fn test_prefs() { - let encoded_profile = example_profile(); - let mut prefs: Map = Map::new(); - prefs.insert( - "browser.display.background_color".into(), - Value::String("#00ff00".into()), - ); - + fn fx_options_args_profile() { let mut firefox_opts = Capabilities::new(); - firefox_opts.insert("profile".into(), encoded_profile); - firefox_opts.insert("prefs".into(), Value::Object(prefs)); + firefox_opts.insert("args".into(), json!(["--profile", "foo"])); - let opts = make_options(firefox_opts).expect("valid profile and prefs"); - let mut profile = opts.profile.expect("valid firefox profile"); + make_options(firefox_opts, None).expect("Valid args"); + } - let handler = MarionetteHandler::new(Default::default()); - handler - .set_prefs(2828, &mut profile, true, opts.prefs) - .expect("set preferences"); + #[test] + fn fx_options_args_profile_and_profile() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("args".into(), json!(["--profile", "foo"])); + firefox_opts.insert("profile".into(), json!("foo")); - let prefs_set = profile.user_prefs().expect("valid user preferences"); - println!("{:#?}", prefs_set.prefs); + make_options(firefox_opts, None).expect_err("Invalid args"); + } - assert_eq!( - prefs_set.get("startup.homepage_welcome_url"), - Some(&Pref::new("data:text/html,PASS")) - ); - assert_eq!( - prefs_set.get("browser.display.background_color"), - Some(&Pref::new("#00ff00")) - ); - assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828))); + #[test] + fn fx_options_args_p_and_profile() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("args".into(), json!(["-P"])); + firefox_opts.insert("profile".into(), json!("foo")); + + make_options(firefox_opts, None).expect_err("Invalid args"); } } diff --git a/src/command.rs b/src/command.rs index b3ccdb7..abd65f1 100644 --- a/src/command.rs +++ b/src/command.rs @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::logging; -use base64; use hyper::Method; use serde::de::{self, Deserialize, Deserializer}; use serde_json::{self, Value}; diff --git a/src/logging.rs b/src/logging.rs index 7721bb7..de477dc 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -37,8 +37,6 @@ use std::io::Write; use std::str; use std::sync::atomic::{AtomicUsize, Ordering}; -use chrono; -use log; use mozprofile::preferences::Pref; static MAX_LOG_LEVEL: AtomicUsize = AtomicUsize::new(0); @@ -116,10 +114,10 @@ impl str::FromStr for Level { } } -impl Into for Level { - fn into(self) -> log::Level { +impl From for log::Level { + fn from(level: Level) -> log::Level { use self::Level::*; - match self { + match level { Fatal | Error => log::Level::Error, Warn => log::Level::Warn, Info => log::Level::Info, @@ -129,10 +127,10 @@ impl Into for Level { } } -impl Into for Level { - fn into(self) -> Pref { +impl From for Pref { + fn from(level: Level) -> Pref { use self::Level::*; - Pref::new(match self { + Pref::new(match level { Fatal => "Fatal", Error => "Error", Warn => "Warn", diff --git a/src/main.rs b/src/main.rs index ca894e7..c6c5503 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,6 +44,7 @@ macro_rules! try_opt { } mod android; +mod browser; mod build; mod capabilities; mod command; @@ -79,10 +80,7 @@ impl FatalError { } fn help_included(&self) -> bool { - match *self { - FatalError::Parsing(_) => true, - _ => false, - } + matches!(*self, FatalError::Parsing(_)) } } @@ -128,6 +126,7 @@ enum Operation { Version, Server { log_level: Option, + host: String, address: SocketAddr, settings: MarionetteSettings, deprecated_storage_arg: bool, @@ -159,6 +158,12 @@ fn parse_args(app: &mut App) -> ProgramResult { Ok(addr) => SocketAddr::new(addr, port), Err(e) => usage!("{}: {}:{}", e, host, port), }; + if !address.ip().is_loopback() { + usage!( + "invalid --host: {}. Must be a local loopback interface", + host + ) + } let android_storage = value_t!(matches, "android_storage", AndroidStorageInput) .unwrap_or(AndroidStorageInput::Auto); @@ -174,21 +179,33 @@ fn parse_args(app: &mut App) -> ProgramResult { None => None, }; + // For Android the port on the device must be the same as the one on the + // host. For now default to 9222, which is the default for --remote-debugging-port. + let websocket_port = match matches.value_of("websocket_port") { + Some(s) => match u16::from_str(s) { + Ok(n) => n, + Err(e) => usage!("invalid --websocket-port: {}", e), + }, + None => 9222, + }; + let op = if matches.is_present("help") { Operation::Help } else if matches.is_present("version") { Operation::Version } else { let settings = MarionetteSettings { - host: marionette_host.to_string(), - port: marionette_port, binary, connect_existing: matches.is_present("connect_existing"), + host: marionette_host.to_string(), + port: marionette_port, + websocket_port, jsdebugger: matches.is_present("jsdebugger"), android_storage, }; Operation::Server { log_level, + host: host.into(), address, settings, deprecated_storage_arg: matches.is_present("android_storage"), @@ -205,6 +222,7 @@ fn inner_main(app: &mut App) -> ProgramResult<()> { Operation::Server { log_level, + host, address, settings, deprecated_storage_arg, @@ -220,7 +238,7 @@ fn inner_main(app: &mut App) -> ProgramResult<()> { }; let handler = MarionetteHandler::new(settings); - let listening = webdriver::server::start(address, handler, extension_routes())?; + let listening = webdriver::server::start(host, address, handler, extension_routes())?; info!("Listening on {}", listening.socket); } } @@ -291,6 +309,14 @@ fn make_app<'a, 'b>() -> App<'a, 'b> { .value_name("PORT") .help("Port to use to connect to Gecko [default: system-allocated port]"), ) + .arg( + Arg::with_name("websocket_port") + .long("websocket-port") + .takes_value(true) + .value_name("PORT") + .conflicts_with("connect_existing") + .help("Port to use to connect to WebDriver BiDi [default: 9222]"), + ) .arg( Arg::with_name("connect_existing") .long("connect-existing") diff --git a/src/marionette.rs b/src/marionette.rs index 87657ba..cf3f40e 100644 --- a/src/marionette.rs +++ b/src/marionette.rs @@ -2,11 +2,14 @@ * 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 crate::android::AndroidHandler; +use crate::browser::{Browser, LocalBrowser, RemoteBrowser}; +use crate::build; +use crate::capabilities::{FirefoxCapabilities, FirefoxOptions}; use crate::command::{ AddonInstallParameters, AddonUninstallParameters, GeckoContextParameters, GeckoExtensionCommand, GeckoExtensionRoute, CHROME_ELEMENT_KEY, }; +use crate::logging; use marionette_rs::common::{ Cookie as MarionetteCookie, Date as MarionetteDate, Frame as MarionetteFrame, Timeouts as MarionetteTimeouts, WebElement as MarionetteWebElement, Window, @@ -22,9 +25,6 @@ use marionette_rs::webdriver::{ Url as MarionetteUrl, WindowRect as MarionetteWindowRect, }; use mozdevice::AndroidStorageInput; -use mozprofile::preferences::Pref; -use mozprofile::profile::Profile; -use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess}; use serde::de::{self, Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use serde_json::{self, Map, Value}; @@ -32,12 +32,11 @@ use std::io::prelude::*; use std::io::Error as IoError; use std::io::ErrorKind; use std::io::Result as IoResult; -use std::net::{TcpListener, TcpStream}; +use std::net::{Shutdown, TcpListener, TcpStream}; use std::path::PathBuf; use std::sync::Mutex; use std::thread; use std::time; -use webdriver::capabilities::CapabilitiesMatching; use webdriver::command::WebDriverCommand::{ AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert, ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension, @@ -66,24 +65,10 @@ use webdriver::response::{ NewWindowResponse, TimeoutsResponse, ValueResponse, WebDriverResponse, WindowRectResponse, }; use webdriver::server::{Session, WebDriverHandler}; - -use crate::build; -use crate::capabilities::{FirefoxCapabilities, FirefoxOptions}; -use crate::logging; -use crate::prefs; - -/// A running Gecko instance. -#[derive(Debug)] -pub enum Browser { - /// A local Firefox process, running on this (host) device. - Host(FirefoxProcess), - - /// A remote instance, running on a (target) Android device. - Target(AndroidHandler), -} +use webdriver::{capabilities::CapabilitiesMatching, server::SessionTeardownKind}; #[derive(Debug, PartialEq, Deserialize)] -pub struct MarionetteHandshake { +struct MarionetteHandshake { #[serde(rename = "marionetteProtocol")] protocol: u16, #[serde(rename = "applicationType")] @@ -91,41 +76,40 @@ pub struct MarionetteHandshake { } #[derive(Default)] -pub struct MarionetteSettings { - pub host: String, - pub port: Option, - pub binary: Option, - pub connect_existing: bool, +pub(crate) struct MarionetteSettings { + pub(crate) binary: Option, + pub(crate) connect_existing: bool, + pub(crate) host: String, + pub(crate) port: Option, + pub(crate) websocket_port: u16, /// Brings up the Browser Toolbox when starting Firefox, /// letting you debug internals. - pub jsdebugger: bool, + pub(crate) jsdebugger: bool, - pub android_storage: AndroidStorageInput, + pub(crate) android_storage: AndroidStorageInput, } #[derive(Default)] -pub struct MarionetteHandler { - pub connection: Mutex>, - pub settings: MarionetteSettings, - pub browser: Option, +pub(crate) struct MarionetteHandler { + connection: Mutex>, + settings: MarionetteSettings, } impl MarionetteHandler { - pub fn new(settings: MarionetteSettings) -> MarionetteHandler { + pub(crate) fn new(settings: MarionetteSettings) -> MarionetteHandler { MarionetteHandler { connection: Mutex::new(None), settings, - browser: None, } } - pub fn create_connection( - &mut self, - session_id: &Option, + fn create_connection( + &self, + session_id: Option, new_session_parameters: &NewSessionParameters, - ) -> WebDriverResult> { - let (options, capabilities) = { + ) -> WebDriverResult { + let (capabilities, options) = { let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref()); let mut capabilities = new_session_parameters .match_browser(&mut fx_capabilities)? @@ -138,188 +122,64 @@ impl MarionetteHandler { let options = FirefoxOptions::from_capabilities( fx_capabilities.chosen_binary, - self.settings.android_storage, + &self.settings, &mut capabilities, )?; - (options, capabilities) + (capabilities, options) }; if let Some(l) = options.log.level { logging::set_max_level(l); } - let host = self.settings.host.to_owned(); - let port = self.settings.port.unwrap_or(get_free_port(&host)?); - - match options.android { - Some(_) => { - // TODO: support connecting to running Apps. There's no real obstruction here, - // just some details about port forwarding to work through. We can't follow - // `chromedriver` here since it uses an abstract socket rather than a TCP socket: - // see bug 1240830 for thoughts on doing that for Marionette. - if self.settings.connect_existing { - return Err(WebDriverError::new( - ErrorStatus::SessionNotCreated, - "Cannot connect to an existing Android App yet", - )); - } - - self.start_android(port, options)?; - } - None => { - if !self.settings.connect_existing { - self.start_browser(port, options)?; - } - } - } - - let mut connection = MarionetteConnection::new(host, port, session_id.clone()); - connection.connect(&mut self.browser).or_else(|e| { - match self.browser { - Some(Browser::Host(ref mut runner)) => { - runner.kill()?; - } - Some(Browser::Target(ref mut handler)) => { - handler.force_stop().map_err(|e| { - WebDriverError::new(ErrorStatus::UnknownError, e.to_string()) - })?; - } - _ => {} - } - - Err(e) - })?; - self.connection = Mutex::new(Some(connection)); - Ok(capabilities) - } - - fn start_android(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> { - let android_options = options.android.unwrap(); - - let handler = AndroidHandler::new(&android_options, port)?; - - // Profile management. - let is_custom_profile = options.profile.is_some(); - - let mut profile = options.profile.unwrap_or(Profile::new()?); - - self.set_prefs( - handler.target_port, - &mut profile, - is_custom_profile, - options.prefs, - ) - .map_err(|e| { - WebDriverError::new( - ErrorStatus::SessionNotCreated, - format!("Failed to set preferences: {}", e), - ) - })?; - - handler - .prepare(&profile, options.env.unwrap_or_default()) - .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?; - - handler - .launch() - .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?; - - self.browser = Some(Browser::Target(handler)); + let marionette_host = self.settings.host.to_owned(); + let marionette_port = self + .settings + .port + .unwrap_or(get_free_port(&marionette_host)?); - Ok(()) - } - - fn start_browser(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> { - let binary = options.binary.ok_or_else(|| { - WebDriverError::new( - ErrorStatus::SessionNotCreated, - "Expected browser binary location, but unable to find \ - binary in default location, no \ - 'moz:firefoxOptions.binary' capability provided, and \ - no binary flag set on the command line", - ) - })?; - - let is_custom_profile = options.profile.is_some(); - - let mut profile = match options.profile { - Some(x) => x, - None => Profile::new()?, + let websocket_port = match options.use_websocket { + true => Some(self.settings.websocket_port), + false => None, }; - self.set_prefs(port, &mut profile, is_custom_profile, options.prefs) - .map_err(|e| { - WebDriverError::new( + let browser = if options.android.is_some() { + // TODO: support connecting to running Apps. There's no real obstruction here, + // just some details about port forwarding to work through. We can't follow + // `chromedriver` here since it uses an abstract socket rather than a TCP socket: + // see bug 1240830 for thoughts on doing that for Marionette. + if self.settings.connect_existing { + return Err(WebDriverError::new( ErrorStatus::SessionNotCreated, - format!("Failed to set preferences: {}", e), - ) - })?; - - let mut runner = FirefoxRunner::new(&binary, profile); - - runner.arg("--marionette"); - if self.settings.jsdebugger { - runner.arg("--jsdebugger"); - } - if let Some(args) = options.args.as_ref() { - runner.args(args); - } - - // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting - runner - .env("MOZ_CRASHREPORTER", "1") - .env("MOZ_CRASHREPORTER_NO_REPORT", "1") - .env("MOZ_CRASHREPORTER_SHUTDOWN", "1"); - - let browser_proc = runner.start().map_err(|e| { - WebDriverError::new( - ErrorStatus::SessionNotCreated, - format!("Failed to start browser {}: {}", binary.display(), e), - ) - })?; - self.browser = Some(Browser::Host(browser_proc)); - - Ok(()) + "Cannot connect to an existing Android App yet", + )); + } + Browser::Remote(RemoteBrowser::new( + options, + marionette_port, + websocket_port, + )?) + } else if !self.settings.connect_existing { + Browser::Local(LocalBrowser::new( + options, + marionette_port, + self.settings.jsdebugger, + )?) + } else { + Browser::Existing + }; + let session = MarionetteSession::new(session_id, capabilities); + MarionetteConnection::new(marionette_host, marionette_port, browser, session) } - pub fn set_prefs( - &self, - port: u16, - profile: &mut Profile, - custom_profile: bool, - extra_prefs: Vec<(String, Pref)>, - ) -> WebDriverResult<()> { - let prefs = profile.user_prefs().map_err(|_| { - WebDriverError::new( - ErrorStatus::UnknownError, - "Unable to read profile preferences file", - ) - })?; - - for &(ref name, ref value) in prefs::DEFAULT.iter() { - if !custom_profile || !prefs.contains_key(name) { - prefs.insert((*name).to_string(), (*value).clone()); + fn close_connection(&mut self, wait_for_shutdown: bool) { + if let Ok(connection) = self.connection.get_mut() { + if let Some(conn) = connection.take() { + if let Err(e) = conn.close(wait_for_shutdown) { + error!("Failed to close browser connection: {}", e) + } } } - - prefs.insert_slice(&extra_prefs[..]); - - if self.settings.jsdebugger { - prefs.insert("devtools.browsertoolbox.panel", Pref::new("jsdebugger")); - prefs.insert("devtools.debugger.remote-enabled", Pref::new(true)); - prefs.insert("devtools.chrome.enabled", Pref::new(true)); - prefs.insert("devtools.debugger.prompt-connection", Pref::new(false)); - } - - prefs.insert("marionette.log.level", logging::max_level().into()); - prefs.insert("marionette.port", Pref::new(port)); - - prefs.write().map_err(|e| { - WebDriverError::new( - ErrorStatus::UnknownError, - format!("Unable to write Firefox profile: {}", e), - ) - }) } } @@ -329,75 +189,49 @@ impl WebDriverHandler for MarionetteHandler { _: &Option, msg: WebDriverMessage, ) -> WebDriverResult { - let mut resolved_capabilities = None; - { - let mut capabilities_options = None; - // First handle the status message which doesn't actually require a marionette - // connection or message - if let Status = msg.command { - let (ready, message) = self - .connection - .lock() - .map(|ref connection| { - connection - .as_ref() - .map(|_| (false, "Session already started")) - .unwrap_or((true, "")) - }) - .unwrap_or((false, "geckodriver internal error")); - let mut value = Map::new(); - value.insert("ready".to_string(), Value::Bool(ready)); - value.insert("message".to_string(), Value::String(message.into())); - return Ok(WebDriverResponse::Generic(ValueResponse(Value::Object( - value, - )))); - } - - match self.connection.lock() { - Ok(ref connection) => { - if connection.is_none() { - match msg.command { - NewSession(ref capabilities) => { - capabilities_options = Some(capabilities); - } - _ => { - return Err(WebDriverError::new( - ErrorStatus::InvalidSessionId, - "Tried to run command without establishing a connection", - )); - } - } - } - } - Err(_) => { - return Err(WebDriverError::new( - ErrorStatus::UnknownError, - "Failed to aquire Marionette connection", - )) - } - } - if let Some(capabilities) = capabilities_options { - resolved_capabilities = - Some(self.create_connection(&msg.session_id, &capabilities)?); - } + // First handle the status message which doesn't actually require a marionette + // connection or message + if let Status = msg.command { + let (ready, message) = self + .connection + .get_mut() + .map(|ref connection| { + connection + .as_ref() + .map(|_| (false, "Session already started")) + .unwrap_or((true, "")) + }) + .unwrap_or((false, "geckodriver internal error")); + let mut value = Map::new(); + value.insert("ready".to_string(), Value::Bool(ready)); + value.insert("message".to_string(), Value::String(message.into())); + return Ok(WebDriverResponse::Generic(ValueResponse(Value::Object( + value, + )))); } match self.connection.lock() { - Ok(ref mut connection) => { - match connection.as_mut() { - Some(conn) => { - conn.send_command(resolved_capabilities, &msg) - .map_err(|mut err| { - // Shutdown the browser if no session can - // be established due to errors. - if let NewSession(_) = msg.command { - err.delete_session = true; - } - err - }) + Ok(mut connection) => { + if connection.is_none() { + if let NewSession(ref capabilities) = msg.command { + let conn = self.create_connection(msg.session_id.clone(), &capabilities)?; + *connection = Some(conn); + } else { + return Err(WebDriverError::new( + ErrorStatus::InvalidSessionId, + "Tried to run command without establishing a connection", + )); } - None => panic!("Connection missing"), } + let conn = connection.as_mut().expect("Missing connection"); + conn.send_command(&msg).map_err(|mut err| { + // Shutdown the browser if no session can + // be established due to errors. + if let NewSession(_) = msg.command { + err.delete_session = true; + } + err + }) } Err(_) => Err(WebDriverError::new( ErrorStatus::UnknownError, @@ -406,64 +240,38 @@ impl WebDriverHandler for MarionetteHandler { } } - fn delete_session(&mut self, session: &Option) { - if let Some(ref s) = *session { - let delete_session = WebDriverMessage { - session_id: Some(s.id.clone()), - command: WebDriverCommand::DeleteSession, - }; - let _ = self.handle_command(session, delete_session); - } - - if let Ok(ref mut connection) = self.connection.lock() { - if let Some(conn) = connection.as_mut() { - conn.close(); - } - } - - match self.browser { - Some(Browser::Host(ref mut runner)) => { - // TODO(https://bugzil.la/1443922): - // Use toolkit.asyncshutdown.crash_timout pref - match runner.wait(time::Duration::from_secs(70)) { - Ok(x) => debug!("Browser process stopped: {}", x), - Err(e) => error!("Failed to stop browser process: {}", e), - } - } - Some(Browser::Target(ref mut handler)) => { - // Try to force-stop the process on the target device - match handler.force_stop() { - Ok(_) => debug!("Android package force-stopped"), - Err(e) => error!("Failed to force-stop Android package: {}", e), - } - } - None => {} - } + fn teardown_session(&mut self, kind: SessionTeardownKind) { + let wait_for_shutdown = match kind { + SessionTeardownKind::Deleted => true, + SessionTeardownKind::NotDeleted => false, + }; + self.close_connection(wait_for_shutdown); + } +} - self.connection = Mutex::new(None); - self.browser = None; +impl Drop for MarionetteHandler { + fn drop(&mut self) { + self.close_connection(false); } } -pub struct MarionetteSession { - pub session_id: String, - protocol: Option, - application_type: Option, +struct MarionetteSession { + session_id: String, + capabilities: Map, command_id: MessageId, } impl MarionetteSession { - pub fn new(session_id: Option) -> MarionetteSession { + fn new(session_id: Option, capabilities: Map) -> MarionetteSession { let initital_id = session_id.unwrap_or_else(|| "".to_string()); MarionetteSession { session_id: initital_id, - protocol: None, - application_type: None, + capabilities, command_id: 0, } } - pub fn update( + fn update( &mut self, msg: &WebDriverMessage, resp: &MarionetteResponse, @@ -479,7 +287,7 @@ impl MarionetteSession { ErrorStatus::SessionNotCreated, "Unable to convert session id to string" ); - self.session_id = session_id.to_string().clone(); + self.session_id = session_id.to_string(); }; Ok(()) } @@ -514,12 +322,12 @@ impl MarionetteSession { Ok(WebElement(id)) } - pub fn next_command_id(&mut self) -> MessageId { + fn next_command_id(&mut self) -> MessageId { self.command_id += 1; self.command_id } - pub fn response( + fn response( &mut self, msg: &WebDriverMessage, resp: MarionetteResponse, @@ -859,7 +667,7 @@ impl MarionetteSession { WebDriverResponse::NewSession(NewSessionResponse::new( session_id.to_string(), - Value::Object(capabilities.clone()), + Value::Object(capabilities), )) } DeleteSession => WebDriverResponse::DeleteSession, @@ -876,6 +684,7 @@ impl MarionetteSession { fn try_convert_to_marionette_message( msg: &WebDriverMessage, + browser: &Browser, ) -> WebDriverResult> { use self::GeckoExtensionCommand::*; use self::WebDriverCommand::*; @@ -892,11 +701,16 @@ fn try_convert_to_marionette_message( DeleteCookies => Some(Command::WebDriver( MarionetteWebDriverCommand::DeleteCookies, )), - DeleteSession => Some(Command::Marionette( - marionette_rs::marionette::Command::DeleteSession { - flags: vec![AppStatus::eForceQuit], - }, - )), + DeleteSession => match browser { + Browser::Local(_) | Browser::Remote(_) => Some(Command::Marionette( + marionette_rs::marionette::Command::DeleteSession { + flags: vec![AppStatus::eForceQuit], + }, + )), + Browser::Existing => Some(Command::WebDriver( + MarionetteWebDriverCommand::DeleteSession, + )), + }, DismissAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::DismissAlert)), ElementClear(ref e) => Some(Command::WebDriver( MarionetteWebDriverCommand::ElementClear(e.to_marionette()?), @@ -910,7 +724,7 @@ fn try_convert_to_marionette_message( MarionetteWebDriverCommand::ElementSendKeys { id: e.clone().to_string(), text: keys.text.clone(), - value: keys.value.clone(), + value: keys.value, }, )) } @@ -932,7 +746,7 @@ fn try_convert_to_marionette_message( MarionetteWebDriverCommand::FindElementElement { element: e.clone().to_string(), using: locator.using.clone(), - value: locator.value.clone(), + value: locator.value, }, )) } @@ -942,7 +756,7 @@ fn try_convert_to_marionette_message( MarionetteWebDriverCommand::FindElementElements { element: e.clone().to_string(), using: locator.using.clone(), - value: locator.value.clone(), + value: locator.value, }, )) } @@ -1068,28 +882,25 @@ fn try_convert_to_marionette_message( MarionetteWebDriverCommand::TakeScreenshot(screenshot), )) } - Extension(ref extension) => match extension { - TakeFullScreenshot => { - let screenshot = ScreenshotOptions { - id: None, - highlights: vec![], - full: true, - }; - Some(Command::WebDriver( - MarionetteWebDriverCommand::TakeFullScreenshot(screenshot), - )) - } - _ => None, - }, + Extension(TakeFullScreenshot) => { + let screenshot = ScreenshotOptions { + id: None, + highlights: vec![], + full: true, + }; + Some(Command::WebDriver( + MarionetteWebDriverCommand::TakeFullScreenshot(screenshot), + )) + } _ => None, }) } #[derive(Debug, PartialEq)] -pub struct MarionetteCommand { - pub id: MessageId, - pub name: String, - pub params: Map, +struct MarionetteCommand { + id: MessageId, + name: String, + params: Map, } impl Serialize for MarionetteCommand { @@ -1118,23 +929,21 @@ impl MarionetteCommand { fn from_webdriver_message( id: MessageId, - capabilities: Option>, + capabilities: &Map, + browser: &Browser, msg: &WebDriverMessage, ) -> WebDriverResult { use self::GeckoExtensionCommand::*; - if let Some(cmd) = try_convert_to_marionette_message(msg)? { + if let Some(cmd) = try_convert_to_marionette_message(msg, browser)? { let req = Message::Incoming(Request(id, cmd)); MarionetteCommand::encode_msg(req) } else { let (opt_name, opt_parameters) = match msg.command { Status => panic!("Got status command that should already have been handled"), NewSession(_) => { - let caps = capabilities - .expect("Tried to create new session without processing capabilities"); - let mut data = Map::new(); - for (k, v) in caps.iter() { + for (k, v) in capabilities.iter() { data.insert(k.to_string(), serde_json::to_value(v)?); } @@ -1167,10 +976,10 @@ impl MarionetteCommand { } #[derive(Debug, PartialEq)] -pub struct MarionetteResponse { - pub id: MessageId, - pub error: Option, - pub result: Value, +struct MarionetteResponse { + id: MessageId, + error: Option, + result: Value, } impl<'de> Deserialize<'de> for MarionetteResponse { @@ -1219,19 +1028,19 @@ impl MarionetteResponse { } #[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct MarionetteError { +struct MarionetteError { #[serde(rename = "error")] - pub code: String, - pub message: String, - pub stacktrace: Option, + code: String, + message: String, + stacktrace: Option, } -impl Into for MarionetteError { - fn into(self) -> WebDriverError { - let status = ErrorStatus::from(self.code); - let message = self.message; +impl From for WebDriverError { + fn from(error: MarionetteError) -> WebDriverError { + let status = ErrorStatus::from(error.code); + let message = error.message; - if let Some(stack) = self.stacktrace { + if let Some(stack) = error.stacktrace { WebDriverError::new_with_stack(status, message, stack) } else { WebDriverError::new(status, message) @@ -1245,25 +1054,36 @@ fn get_free_port(host: &str) -> IoResult { .map(|x| x.port()) } -pub struct MarionetteConnection { - host: String, - port: u16, - stream: Option, - pub session: MarionetteSession, +struct MarionetteConnection { + browser: Browser, + session: MarionetteSession, + stream: TcpStream, } impl MarionetteConnection { - pub fn new(host: String, port: u16, session_id: Option) -> MarionetteConnection { - let session = MarionetteSession::new(session_id); - MarionetteConnection { - host, - port, - stream: None, + fn new( + host: String, + port: u16, + mut browser: Browser, + session: MarionetteSession, + ) -> WebDriverResult { + let stream = match MarionetteConnection::connect(&host, port, &mut browser) { + Ok(stream) => stream, + Err(e) => { + if let Err(e) = browser.close(true) { + error!("Failed to stop browser: {:?}", e); + } + return Err(e); + } + }; + Ok(MarionetteConnection { + browser, session, - } + stream, + }) } - pub fn connect(&mut self, browser: &mut Option) -> WebDriverResult<()> { + fn connect(host: &str, port: u16, browser: &mut Browser) -> WebDriverResult { let timeout = time::Duration::from_secs(60); let poll_interval = time::Duration::from_millis(100); let now = time::Instant::now(); @@ -1271,52 +1091,29 @@ impl MarionetteConnection { debug!( "Waiting {}s to connect to browser on {}:{}", timeout.as_secs(), - self.host, - self.port + host, + port ); loop { // immediately abort connection attempts if process disappears - if let Some(Browser::Host(ref mut runner)) = *browser { - let exit_status = match runner.try_wait() { - Ok(Some(status)) => Some( - status - .code() - .map(|c| c.to_string()) - .unwrap_or_else(|| "signal".into()), - ), - Ok(None) => None, - Err(_) => Some("{unknown}".into()), - }; - if let Some(s) = exit_status { + if let Browser::Local(browser) = browser { + if let Some(status) = browser.check_status() { return Err(WebDriverError::new( ErrorStatus::UnknownError, - format!("Process unexpectedly closed with status {}", s), + format!("Process unexpectedly closed with status {}", status), )); } } - let try_connect = || -> WebDriverResult<(TcpStream, MarionetteHandshake)> { - let mut stream = TcpStream::connect((&self.host[..], self.port))?; - let data = MarionetteConnection::handshake(&mut stream)?; - - Ok((stream, data)) - }; - - match try_connect() { - Ok((stream, data)) => { - debug!( - "Connection to Marionette established on {}:{}.", - self.host, self.port, - ); - - self.stream = Some(stream); - self.session.application_type = Some(data.application_type); - self.session.protocol = Some(data.protocol); - break; + match MarionetteConnection::try_connect(host, port) { + Ok(stream) => { + debug!("Connection to Marionette established on {}:{}.", host, port); + return Ok(stream); } Err(e) => { if now.elapsed() < timeout { + trace!("{}. Retrying in {:?}", e.to_string(), poll_interval); thread::sleep(poll_interval); } else { return Err(WebDriverError::new(ErrorStatus::Timeout, e.to_string())); @@ -1324,17 +1121,22 @@ impl MarionetteConnection { } } } + } - Ok(()) + fn try_connect(host: &str, port: u16) -> WebDriverResult { + let mut stream = TcpStream::connect((host, port))?; + MarionetteConnection::handshake(&mut stream)?; + Ok(stream) } fn handshake(stream: &mut TcpStream) -> WebDriverResult { let resp = (match stream.read_timeout() { Ok(timeout) => { // If platform supports changing the read timeout of the stream, - // use a short one only for the handshake with Marionette. + // use a short one only for the handshake with Marionette. Don't + // make it shorter as 1000ms to not fail on slow connections. stream - .set_read_timeout(Some(time::Duration::from_millis(100))) + .set_read_timeout(Some(time::Duration::from_millis(1000))) .ok(); let data = MarionetteConnection::read_resp(stream); stream.set_read_timeout(timeout).ok(); @@ -1372,15 +1174,23 @@ impl MarionetteConnection { Ok(data) } - pub fn close(&self) {} + fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> { + self.stream.shutdown(Shutdown::Both)?; + self.browser.close(wait_for_shutdown)?; + Ok(()) + } - pub fn send_command( + fn send_command( &mut self, - capabilities: Option>, msg: &WebDriverMessage, ) -> WebDriverResult { let id = self.session.next_command_id(); - let enc_cmd = MarionetteCommand::from_webdriver_message(id, capabilities, msg)?; + let enc_cmd = MarionetteCommand::from_webdriver_message( + id, + &self.session.capabilities, + &self.browser, + msg, + )?; let resp_data = self.send(enc_cmd)?; let data: MarionetteResponse = serde_json::from_str(&resp_data)?; @@ -1388,30 +1198,16 @@ impl MarionetteConnection { } fn send(&mut self, data: String) -> WebDriverResult { - let stream = match self.stream { - Some(ref mut stream) => { - if stream.write(&*data.as_bytes()).is_err() { - let mut err = WebDriverError::new( - ErrorStatus::UnknownError, - "Failed to write request to stream", - ); - err.delete_session = true; - return Err(err); - } - - stream - } - None => { - let mut err = WebDriverError::new( - ErrorStatus::UnknownError, - "Tried to write before opening stream", - ); - err.delete_session = true; - return Err(err); - } - }; + if self.stream.write(&*data.as_bytes()).is_err() { + let mut err = WebDriverError::new( + ErrorStatus::UnknownError, + "Failed to write request to stream", + ); + err.delete_session = true; + return Err(err); + } - match MarionetteConnection::read_resp(stream) { + match MarionetteConnection::read_resp(&mut self.stream) { Ok(resp) => Ok(resp), Err(_) => { let mut err = WebDriverError::new( @@ -1428,7 +1224,7 @@ impl MarionetteConnection { let mut bytes = 0usize; loop { - let buf = &mut [0 as u8]; + let buf = &mut [0u8]; let num_read = stream.read(buf)?; let byte = match num_read { 0 => { @@ -1437,9 +1233,9 @@ impl MarionetteConnection { "EOF reading marionette message", )) } - 1 => buf[0] as char, + 1 => buf[0], _ => panic!("Expected one byte got more"), - }; + } as char; match byte { '0'..='9' => { bytes *= 10; @@ -1450,7 +1246,7 @@ impl MarionetteConnection { } } - let buf = &mut [0 as u8; 8192]; + let buf = &mut [0u8; 8192]; let mut payload = Vec::with_capacity(bytes); let mut total_read = 0; while total_read < bytes { @@ -1628,7 +1424,7 @@ impl ToMarionette for LocatorStrategy { fn to_marionette(&self) -> WebDriverResult { use self::LocatorStrategy::*; match self { - CSSSelector => Ok(MarionetteSelector::CSS), + CSSSelector => Ok(MarionetteSelector::Css), LinkText => Ok(MarionetteSelector::LinkText), PartialLinkText => Ok(MarionetteSelector::PartialLinkText), TagName => Ok(MarionetteSelector::TagName), @@ -1662,7 +1458,7 @@ impl ToMarionette for SwitchToFrameParameters { fn to_marionette(&self) -> WebDriverResult { Ok(match &self.id { Some(x) => match x { - FrameId::Short(n) => MarionetteFrame::Index(n.clone()), + FrameId::Short(n) => MarionetteFrame::Index(*n), FrameId::Element(el) => MarionetteFrame::Element(el.0.clone()), }, None => MarionetteFrame::Parent, @@ -1715,34 +1511,3 @@ impl ToMarionette for WindowRectParameters { }) } } - -#[cfg(test)] -mod tests { - use super::{MarionetteHandler, MarionetteSettings}; - use mozprofile::preferences::PrefValue; - use mozprofile::profile::Profile; - - // This is not a pretty test, mostly due to the nature of - // mozprofile's and MarionetteHandler's APIs, but we have had - // several regressions related to marionette.log.level. - #[test] - fn test_marionette_log_level() { - let mut profile = Profile::new().unwrap(); - let handler = MarionetteHandler::new(MarionetteSettings::default()); - handler.set_prefs(2828, &mut profile, false, vec![]).ok(); - let user_prefs = profile.user_prefs().unwrap(); - - let pref = user_prefs.get("marionette.log.level").unwrap(); - let value = match pref.value { - PrefValue::String(ref s) => s, - _ => panic!(), - }; - for (i, ch) in value.chars().enumerate() { - if i == 0 { - assert!(ch.is_uppercase()); - } else { - assert!(ch.is_lowercase()); - } - } - } -} diff --git a/src/prefs.rs b/src/prefs.rs index 33da0e5..e9a4032 100644 --- a/src/prefs.rs +++ b/src/prefs.rs @@ -145,13 +145,32 @@ lazy_static! { ("security.certerrors.mitm.priming.enabled", Pref::new(false)), // Ensure blocklist updates don't hit the network - ("services.settings.server", Pref::new("http://%(server)s/dummy/blocklist/")), + ("services.settings.server", Pref::new("")), // Disable first run pages ("startup.homepage_welcome_url", Pref::new("about:blank")), ("startup.homepage_welcome_url.additional", Pref::new("")), + // asrouter expects a plain object or null + ("browser.newtabpage.activity-stream.asrouter.providers.cfr", Pref::new("null")), + // TODO: Remove once minimum supported Firefox release is 93. + ("browser.newtabpage.activity-stream.asrouter.providers.cfr-fxa", Pref::new("null")), + ("browser.newtabpage.activity-stream.asrouter.providers.snippets", Pref::new("null")), + ("browser.newtabpage.activity-stream.asrouter.providers.message-groups", Pref::new("null")), + ("browser.newtabpage.activity-stream.asrouter.providers.whats-new-panel", Pref::new("null")), + ("browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", Pref::new("null")), + ("browser.newtabpage.activity-stream.feeds.system.topstories", Pref::new(false)), + ("browser.newtabpage.activity-stream.feeds.snippets", Pref::new(false)), + ("browser.newtabpage.activity-stream.tippyTop.service.endpoint", Pref::new("")), + ("browser.newtabpage.activity-stream.discoverystream.config", Pref::new("[]")), + + // For Activity Stream firstrun page, use an empty string to avoid fetching. + ("browser.newtabpage.activity-stream.fxaccounts.endpoint", Pref::new("")), + // Prevent starting into safe mode after application crashes ("toolkit.startup.max_resumed_crashes", Pref::new(-1)), + + // Disable webapp updates. + ("browser.webapps.checkForUpdates", Pref::new(0)), ]; }