summaryrefslogtreecommitdiffstats
path: root/testing/geckodriver/src/browser.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/geckodriver/src/browser.rs556
1 files changed, 556 insertions, 0 deletions
diff --git a/testing/geckodriver/src/browser.rs b/testing/geckodriver/src/browser.rs
new file mode 100644
index 0000000000..a33a755fa2
--- /dev/null
+++ b/testing/geckodriver/src/browser.rs
@@ -0,0 +1,556 @@
+/* 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, ProfileType};
+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::io::Read;
+use std::path::{Path, PathBuf};
+use std::time;
+use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
+
+/// A running Gecko instance.
+#[derive(Debug)]
+#[allow(clippy::large_enum_variant)]
+pub(crate) enum Browser {
+ Local(LocalBrowser),
+ Remote(RemoteBrowser),
+
+ /// An existing browser instance not controlled by GeckoDriver
+ Existing(u16),
+}
+
+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(()),
+ }
+ }
+
+ pub(crate) fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
+ match self {
+ Browser::Local(x) => x.marionette_port(),
+ Browser::Remote(x) => x.marionette_port(),
+ Browser::Existing(x) => Ok(Some(*x)),
+ }
+ }
+
+ pub(crate) fn update_marionette_port(&mut self, port: u16) {
+ match self {
+ Browser::Local(x) => x.update_marionette_port(port),
+ Browser::Remote(x) => x.update_marionette_port(port),
+ Browser::Existing(x) => {
+ if port != *x {
+ error!(
+ "Cannot re-assign Marionette port when connected to an existing browser"
+ );
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+/// A local Firefox process, running on this (host) device.
+pub(crate) struct LocalBrowser {
+ marionette_port: u16,
+ prefs_backup: Option<PrefsBackup>,
+ process: FirefoxProcess,
+ profile_path: Option<PathBuf>,
+}
+
+impl LocalBrowser {
+ pub(crate) fn new(
+ options: FirefoxOptions,
+ marionette_port: u16,
+ jsdebugger: bool,
+ profile_root: Option<&Path>,
+ ) -> WebDriverResult<LocalBrowser> {
+ 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 = matches!(options.profile, ProfileType::Path(_));
+
+ let mut profile = match options.profile {
+ ProfileType::Named => None,
+ ProfileType::Path(x) => Some(x),
+ ProfileType::Temporary => Some(Profile::new(profile_root)?),
+ };
+
+ let (profile_path, prefs_backup) = if let Some(ref mut profile) = profile {
+ let profile_path = profile.path.clone();
+ let prefs_backup = set_prefs(
+ marionette_port,
+ profile,
+ is_custom_profile,
+ options.prefs,
+ jsdebugger,
+ )
+ .map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ format!("Failed to set preferences: {}", e),
+ )
+ })?;
+ (Some(profile_path), prefs_backup)
+ } else {
+ warn!("Unable to set geckodriver prefs when using a named profile");
+ (None, None)
+ };
+
+ 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 {
+ marionette_port,
+ prefs_backup,
+ process,
+ profile_path,
+ })
+ }
+
+ 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(())
+ }
+
+ fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
+ if self.marionette_port != 0 {
+ return Ok(Some(self.marionette_port));
+ }
+
+ if let Some(profile_path) = self.profile_path.as_ref() {
+ return Ok(read_marionette_port(profile_path));
+ }
+
+ // This should be impossible, but it isn't enforced
+ Err(WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Port not known when using named profile",
+ ))
+ }
+
+ fn update_marionette_port(&mut self, port: u16) {
+ self.marionette_port = port;
+ }
+
+ pub(crate) fn check_status(&mut self) -> Option<String> {
+ 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()),
+ }
+ }
+}
+
+fn read_marionette_port(profile_path: &Path) -> Option<u16> {
+ let port_file = profile_path.join("MarionetteActivePort");
+ let mut port_str = String::with_capacity(6);
+ let mut file = match fs::File::open(&port_file) {
+ Ok(file) => file,
+ Err(_) => {
+ trace!("Failed to open {}", &port_file.to_string_lossy());
+ return None;
+ }
+ };
+ if let Err(e) = file.read_to_string(&mut port_str) {
+ trace!("Failed to read {}: {}", &port_file.to_string_lossy(), e);
+ return None;
+ };
+ println!("Read port: {}", port_str);
+ let port = port_str.parse::<u16>().ok();
+ if port.is_none() {
+ warn!("Failed fo convert {} to u16", &port_str);
+ }
+ port
+}
+
+#[derive(Debug)]
+/// A remote instance, running on a (target) Android device.
+pub(crate) struct RemoteBrowser {
+ handler: AndroidHandler,
+ marionette_port: u16,
+ prefs_backup: Option<PrefsBackup>,
+}
+
+impl RemoteBrowser {
+ pub(crate) fn new(
+ options: FirefoxOptions,
+ marionette_port: u16,
+ websocket_port: Option<u16>,
+ profile_root: Option<&Path>,
+ ) -> WebDriverResult<RemoteBrowser> {
+ let android_options = options.android.unwrap();
+
+ let handler = AndroidHandler::new(&android_options, marionette_port, websocket_port)?;
+
+ // Profile management.
+ let (mut profile, is_custom_profile) = match options.profile {
+ ProfileType::Named => {
+ return Err(WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Cannot use a named profile on Android",
+ ));
+ }
+ ProfileType::Path(x) => (x, true),
+ ProfileType::Temporary => (Profile::new(profile_root)?, false),
+ };
+
+ let prefs_backup = 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,
+ marionette_port,
+ prefs_backup,
+ })
+ }
+
+ fn close(self) -> WebDriverResult<()> {
+ self.handler.force_stop()?;
+
+ // 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(())
+ }
+
+ fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
+ Ok(Some(self.marionette_port))
+ }
+
+ fn update_marionette_port(&mut self, port: u16) {
+ self.marionette_port = port;
+ }
+}
+
+fn set_prefs(
+ port: u16,
+ profile: &mut Profile,
+ custom_profile: bool,
+ extra_prefs: Vec<(String, Pref)>,
+ js_debugger: bool,
+) -> WebDriverResult<Option<PrefsBackup>> {
+ 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 &(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());
+
+ 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<PrefsBackup> {
+ 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::browser::read_marionette_port;
+ use crate::capabilities::{FirefoxOptions, ProfileType};
+ use base64::prelude::BASE64_STANDARD;
+ use base64::Engine;
+ use mozprofile::preferences::{Pref, PrefValue};
+ use mozprofile::profile::Profile;
+ use serde_json::{Map, Value};
+ use std::fs::File;
+ use std::io::{Read, Write};
+ use std::path::Path;
+ use tempfile::tempdir;
+
+ 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_STANDARD.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(None).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<String, Value> = 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 = match opts.profile {
+ ProfileType::Path(profile) => profile,
+ _ => panic!("Expected ProfileType::Path"),
+ };
+
+ 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(None).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");
+ 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");
+
+ 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);
+ }
+
+ #[test]
+ fn test_local_read_marionette_port() {
+ fn create_port_file(profile_path: &Path, data: &[u8]) {
+ let port_path = profile_path.join("MarionetteActivePort");
+ let mut file = File::create(&port_path).unwrap();
+ file.write_all(data).unwrap();
+ }
+
+ let profile_dir = tempdir().unwrap();
+ let profile_path = profile_dir.path();
+ assert_eq!(read_marionette_port(profile_path), None);
+ assert_eq!(read_marionette_port(profile_path), None);
+ create_port_file(profile_path, b"");
+ assert_eq!(read_marionette_port(profile_path), None);
+ create_port_file(profile_path, b"1234");
+ assert_eq!(read_marionette_port(profile_path), Some(1234));
+ create_port_file(profile_path, b"1234abc");
+ assert_eq!(read_marionette_port(profile_path), None);
+ }
+}