/* 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::env; use std::fs; use std::io::prelude::*; 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> { 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, process: FirefoxProcess, pub(crate) profile_path: Option, } impl LocalBrowser { pub(crate) fn new( options: FirefoxOptions, marionette_port: u16, jsdebugger: bool, system_access: bool, profile_root: Option<&Path>, ) -> 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 = 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 system_access { runner.arg("--remote-allow-system-access"); } 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()?; Ok(()) } fn marionette_port(&mut self) -> WebDriverResult> { 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 { 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()), } } } impl Drop for LocalBrowser { fn drop(&mut self) { if let Some(prefs_backup) = self.prefs_backup.take() { debug!("Restore user preferences"); prefs_backup.restore(); } if let Some(profile_path) = &self.profile_path { // Save minidump files of potential crashes from the profile if requested. if let Err(e) = copy_minidumps_files(profile_path) { error!( "Failed to save crash minidumps to the specified location: {}", e ); } } } } fn read_marionette_port(profile_path: &Path) -> Option { 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::().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 { pub(crate) handler: AndroidHandler, marionette_port: u16, prefs_backup: Option, } impl RemoteBrowser { pub(crate) fn new( options: FirefoxOptions, marionette_port: u16, websocket_port: Option, system_access: bool, profile_root: Option<&Path>, ) -> WebDriverResult { let android_options = options.android.unwrap(); let handler = AndroidHandler::new( &android_options, marionette_port, system_access, 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()?; Ok(()) } fn marionette_port(&mut self) -> WebDriverResult> { Ok(Some(self.marionette_port)) } fn update_marionette_port(&mut self, port: u16) { self.marionette_port = port; } } impl Drop for RemoteBrowser { fn drop(&mut self) { // Restore preferences which had custom values set. if let Some(prefs_backup) = self.prefs_backup.take() { prefs_backup.restore(); } // Save minidump files of potential crashes from the profile if requested. if let Err(e) = self.handler.copy_minidumps_files() { error!( "Failed to save crash minidumps to the specified location: {}", e ); } } } 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 &(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 { 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); } } } fn copy_minidumps_files(profile_path: &Path) -> WebDriverResult<()> { let mut minidumps_path = profile_path.to_path_buf(); minidumps_path.push("minidumps"); match std::fs::read_dir(&minidumps_path) { Ok(entries) => { let save_path = match env::var("MINIDUMP_SAVE_PATH").map(PathBuf::from) { Ok(path) => path, Err(_) => { debug!("Set MINIDUMP_SAVE_PATH to store crash minidumps."); return Ok(()); } }; for result_entry in entries { let entry = result_entry?; let file_type = entry.file_type()?; if file_type.is_dir() { continue; } let path = entry.path(); let extension = path .extension() .and_then(|ext| ext.to_str()) .map(|ext| ext.to_lowercase()) .unwrap_or(String::from("")); // Copy only *.dmp and *.extra files. if extension == "dmp" || extension == "extra" { let dest_path = save_path.join(entry.file_name()); fs::copy(path, &dest_path)?; debug!( "Copied minidump file {:?} to {:?}.", entry.file_name(), save_path.display() ); } } } Err(_) => { warn!( "Couldn't read files from minidumps folder '{}'", minidumps_path.display(), ); return Ok(()); } } Ok(()) } #[cfg(test)] mod tests { #![allow(unsafe_code)] use super::*; 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 std::sync::{LazyLock, Mutex, MutexGuard}; 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 = 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::new().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); } fn assert_minidump_files(minidumps_path: &Path, filename: &str) { let mut dmp_file_present = false; let mut extra_file_present = false; for result_entry in std::fs::read_dir(minidumps_path).unwrap() { let entry = result_entry.unwrap(); let path: PathBuf = entry.path(); let filename_from_path = path.file_stem().unwrap().to_str().unwrap(); if filename == filename_from_path { let extension = path.extension().and_then(|ext| ext.to_str()).unwrap(); if extension == "dmp" { dmp_file_present = true; } if extension == "extra" { extra_file_present = true; } } } assert!(dmp_file_present); assert!(extra_file_present); } static ENV_MUTEX: LazyLock> = LazyLock::new(Mutex::default); static MINIDUMP_KEY: &str = "MINIDUMP_SAVE_PATH"; pub(crate) struct MinidumpEnvironment<'environment> { initial_environment: Option, #[allow(dead_code)] guard: MutexGuard<'environment, ()>, } impl<'environment> MinidumpEnvironment<'environment> { pub(crate) fn new() -> MinidumpEnvironment<'environment> { MinidumpEnvironment { initial_environment: env::var(MINIDUMP_KEY).ok(), guard: ENV_MUTEX.lock().unwrap(), } } pub(crate) fn set(&self, value: Option<&str>) { fn set_env(key: &str, value: Option<&str>) { // SAFETY: Safe as long as no other threads try to modify the environment // This is enforced by Environment taking a mutex, so tests can't run // in parallel. unsafe { match value { Some(value) => env::set_var(key, value), None => env::remove_var(key), } } } set_env(MINIDUMP_KEY, value); } } impl Drop for MinidumpEnvironment<'_> { fn drop(&mut self) { self.set(self.initial_environment.clone().as_deref()); } } fn create_file(folder: &Path, filename: &str) { let file = folder.join(filename); File::create(&file).unwrap(); } fn create_minidump_files(profile_path: &Path, filename: &str) { let folder = create_minidump_folder(profile_path); let mut file_extensions = [".dmp", ".extra"]; for file_extension in file_extensions.iter_mut() { let mut filename_with_extension: String = filename.to_owned(); filename_with_extension.push_str(file_extension); create_file(&folder, &filename_with_extension); } } fn create_minidump_folder(profile_path: &Path) -> PathBuf { let minidumps_folder = profile_path.join("minidumps"); if !minidumps_folder.is_dir() { fs::create_dir(&minidumps_folder).unwrap(); } minidumps_folder } #[test] fn test_copy_minidumps() { let env = MinidumpEnvironment::new(); let tmp_dir_profile = TempDir::new().unwrap(); let profile_path = tmp_dir_profile.path(); let filename = "test"; create_minidump_files(profile_path, filename); let tmp_dir_minidumps = TempDir::new().unwrap(); let minidumps_path = tmp_dir_minidumps.path(); env.set(minidumps_path.to_str()); assert!(copy_minidumps_files(profile_path).is_ok()); assert_minidump_files(minidumps_path, filename); tmp_dir_profile.close().unwrap(); tmp_dir_minidumps.close().unwrap(); } #[test] fn test_copy_multiple_minidumps() { let env = MinidumpEnvironment::new(); let tmp_dir_profile = TempDir::new().unwrap(); let profile_path = tmp_dir_profile.path(); let filename_1 = "test_1"; create_minidump_files(profile_path, filename_1); let filename_2 = "test_2"; create_minidump_files(profile_path, filename_2); let tmp_dir_minidumps = TempDir::new().unwrap(); let minidumps_path = tmp_dir_minidumps.path(); env.set(minidumps_path.to_str()); assert!(copy_minidumps_files(profile_path).is_ok()); assert_minidump_files(minidumps_path, filename_1); assert_minidump_files(minidumps_path, filename_1); tmp_dir_profile.close().unwrap(); tmp_dir_minidumps.close().unwrap(); } #[test] fn test_copy_minidumps_with_non_existent_manifest_path() { let env = MinidumpEnvironment::new(); let tmp_dir_profile = TempDir::new().unwrap(); let profile_path = tmp_dir_profile.path(); create_minidump_folder(profile_path); env.set(Path::new("/non-existent").to_str()); assert!(copy_minidumps_files(profile_path).is_ok()); tmp_dir_profile.close().unwrap(); } #[test] fn test_copy_minidumps_with_non_existent_profile_path() { let env = MinidumpEnvironment::new(); let tmp_dir_profile = TempDir::new().unwrap(); let profile_path = tmp_dir_profile.path(); env.set(Path::new("/non-existent").to_str()); assert!(copy_minidumps_files(profile_path).is_ok()); tmp_dir_profile.close().unwrap(); } #[test] fn test_copy_minidumps_with_no_minidump_files() { let env = MinidumpEnvironment::new(); let tmp_dir_profile = TempDir::new().unwrap(); let profile_path = tmp_dir_profile.path(); let minidumps_folder = create_minidump_folder(profile_path); // Create a folder. let test_folder_binding = profile_path.join("test"); let test_folder = test_folder_binding.as_path(); fs::create_dir(test_folder).unwrap(); // Create a file with non minidumps extension. create_file(&minidumps_folder, "test.txt"); let tmp_dir_minidumps = TempDir::new().unwrap(); let minidumps_path = tmp_dir_minidumps.path(); env.set(minidumps_path.to_str()); assert!(copy_minidumps_files(profile_path).is_ok()); // Check that the non minidump file and the folder were not copied. assert!(minidumps_path.read_dir().unwrap().next().is_none()); tmp_dir_profile.close().unwrap(); tmp_dir_minidumps.close().unwrap(); } }