1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 use crate::android::AndroidHandler;
6 use crate::capabilities::{FirefoxOptions, ProfileType};
9 use mozprofile::preferences::Pref;
10 use mozprofile::profile::{PrefFile, Profile};
11 use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess};
14 use std::path::{Path, PathBuf};
16 use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
18 /// A running Gecko instance.
20 #[allow(clippy::large_enum_variant)]
21 pub(crate) enum Browser {
23 Remote(RemoteBrowser),
25 /// An existing browser instance not controlled by GeckoDriver
30 pub(crate) fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> {
32 Browser::Local(x) => x.close(wait_for_shutdown),
33 Browser::Remote(x) => x.close(),
34 Browser::Existing(_) => Ok(()),
38 pub(crate) fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
40 Browser::Local(x) => x.marionette_port(),
41 Browser::Remote(x) => x.marionette_port(),
42 Browser::Existing(x) => Ok(Some(*x)),
46 pub(crate) fn update_marionette_port(&mut self, port: u16) {
48 Browser::Local(x) => x.update_marionette_port(port),
49 Browser::Remote(x) => x.update_marionette_port(port),
50 Browser::Existing(x) => {
53 "Cannot re-assign Marionette port when connected to an existing browser"
62 /// A local Firefox process, running on this (host) device.
63 pub(crate) struct LocalBrowser {
65 prefs_backup: Option<PrefsBackup>,
66 process: FirefoxProcess,
67 profile_path: Option<PathBuf>,
72 options: FirefoxOptions,
75 profile_root: Option<&Path>,
76 enable_crash_reporter: bool,
77 ) -> WebDriverResult<LocalBrowser> {
78 let binary = options.binary.ok_or_else(|| {
80 ErrorStatus::SessionNotCreated,
81 "Expected browser binary location, but unable to find \
82 binary in default location, no \
83 'moz:firefoxOptions.binary' capability provided, and \
84 no binary flag set on the command line",
88 let is_custom_profile = matches!(options.profile, ProfileType::Path(_));
90 let mut profile = match options.profile {
91 ProfileType::Named => None,
92 ProfileType::Path(x) => Some(x),
93 ProfileType::Temporary => Some(Profile::new(profile_root)?),
96 let (profile_path, prefs_backup) = if let Some(ref mut profile) = profile {
97 let profile_path = profile.path.clone();
98 let prefs_backup = set_prefs(
107 ErrorStatus::SessionNotCreated,
108 format!("Failed to set preferences: {}", e),
111 (Some(profile_path), prefs_backup)
113 warn!("Unable to set geckodriver prefs when using a named profile");
117 let mut runner = FirefoxRunner::new(&binary, profile);
119 runner.arg("--marionette");
121 runner.arg("--jsdebugger");
123 if let Some(args) = options.args.as_ref() {
127 // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
128 if !enable_crash_reporter {
130 .env("MOZ_CRASHREPORTER", "1")
131 .env("MOZ_CRASHREPORTER_NO_REPORT", "1")
132 .env("MOZ_CRASHREPORTER_SHUTDOWN", "1");
135 let process = match runner.start() {
136 Ok(process) => process,
138 if let Some(backup) = prefs_backup {
141 return Err(WebDriverError::new(
142 ErrorStatus::SessionNotCreated,
143 format!("Failed to start browser {}: {}", binary.display(), e),
156 fn close(mut self, wait_for_shutdown: bool) -> WebDriverResult<()> {
157 if wait_for_shutdown {
158 // TODO(https://bugzil.la/1443922):
159 // Use toolkit.asyncshutdown.crash_timout pref
160 let duration = time::Duration::from_secs(70);
161 match self.process.wait(duration) {
162 Ok(x) => debug!("Browser process stopped: {}", x),
163 Err(e) => error!("Failed to stop browser process: {}", e),
166 self.process.kill()?;
168 // Restoring the prefs if the browser fails to stop perhaps doesn't work anyway
169 if let Some(prefs_backup) = self.prefs_backup {
170 prefs_backup.restore();
176 fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
177 if self.marionette_port != 0 {
178 return Ok(Some(self.marionette_port));
181 if let Some(profile_path) = self.profile_path.as_ref() {
182 return Ok(read_marionette_port(profile_path));
185 // This should be impossible, but it isn't enforced
186 Err(WebDriverError::new(
187 ErrorStatus::SessionNotCreated,
188 "Port not known when using named profile",
192 fn update_marionette_port(&mut self, port: u16) {
193 self.marionette_port = port;
196 pub(crate) fn check_status(&mut self) -> Option<String> {
197 match self.process.try_wait() {
198 Ok(Some(status)) => Some(
201 .map(|c| c.to_string())
202 .unwrap_or_else(|| "signal".into()),
205 Err(_) => Some("{unknown}".into()),
210 fn read_marionette_port(profile_path: &Path) -> Option<u16> {
211 let port_file = profile_path.join("MarionetteActivePort");
212 let mut port_str = String::with_capacity(6);
213 let mut file = match fs::File::open(&port_file) {
216 trace!("Failed to open {}", &port_file.to_string_lossy());
220 if let Err(e) = file.read_to_string(&mut port_str) {
221 trace!("Failed to read {}: {}", &port_file.to_string_lossy(), e);
224 println!("Read port: {}", port_str);
225 let port = port_str.parse::<u16>().ok();
227 warn!("Failed fo convert {} to u16", &port_str);
233 /// A remote instance, running on a (target) Android device.
234 pub(crate) struct RemoteBrowser {
235 handler: AndroidHandler,
236 marionette_port: u16,
237 prefs_backup: Option<PrefsBackup>,
242 options: FirefoxOptions,
243 marionette_port: u16,
244 websocket_port: Option<u16>,
245 profile_root: Option<&Path>,
246 enable_crash_reporter: bool,
247 ) -> WebDriverResult<RemoteBrowser> {
248 let android_options = options.android.unwrap();
250 let handler = AndroidHandler::new(&android_options, marionette_port, websocket_port)?;
252 // Profile management.
253 let (mut profile, is_custom_profile) = match options.profile {
254 ProfileType::Named => {
255 return Err(WebDriverError::new(
256 ErrorStatus::SessionNotCreated,
257 "Cannot use a named profile on Android",
260 ProfileType::Path(x) => (x, true),
261 ProfileType::Temporary => (Profile::new(profile_root)?, false),
264 let prefs_backup = set_prefs(
265 handler.marionette_target_port,
273 ErrorStatus::SessionNotCreated,
274 format!("Failed to set preferences: {}", e),
281 options.env.unwrap_or_default(),
282 enable_crash_reporter,
294 fn close(self) -> WebDriverResult<()> {
295 self.handler.force_stop()?;
297 // Restoring the prefs if the browser fails to stop perhaps doesn't work anyway
298 if let Some(prefs_backup) = self.prefs_backup {
299 prefs_backup.restore();
305 fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
306 Ok(Some(self.marionette_port))
309 fn update_marionette_port(&mut self, port: u16) {
310 self.marionette_port = port;
316 profile: &mut Profile,
317 custom_profile: bool,
318 extra_prefs: Vec<(String, Pref)>,
320 ) -> WebDriverResult<Option<PrefsBackup>> {
321 let prefs = profile.user_prefs().map_err(|_| {
323 ErrorStatus::UnknownError,
324 "Unable to read profile preferences file",
328 let backup_prefs = if custom_profile && prefs.path.exists() {
329 Some(PrefsBackup::new(prefs)?)
334 for &(name, ref value) in prefs::DEFAULT.iter() {
335 if !custom_profile || !prefs.contains_key(name) {
336 prefs.insert(name.to_string(), (*value).clone());
340 prefs.insert_slice(&extra_prefs[..]);
343 prefs.insert("devtools.browsertoolbox.panel", Pref::new("jsdebugger"));
344 prefs.insert("devtools.debugger.remote-enabled", Pref::new(true));
345 prefs.insert("devtools.chrome.enabled", Pref::new(true));
346 prefs.insert("devtools.debugger.prompt-connection", Pref::new(false));
349 prefs.insert("marionette.port", Pref::new(port));
350 prefs.insert("remote.log.level", logging::max_level().into());
352 prefs.write().map_err(|e| {
354 ErrorStatus::UnknownError,
355 format!("Unable to write Firefox profile: {}", e),
364 backup_path: PathBuf,
368 fn new(prefs: &PrefFile) -> WebDriverResult<PrefsBackup> {
369 let mut prefs_backup_path = prefs.path.clone();
372 let ext = if counter > 0 {
373 format!("geckodriver_backup_{}", counter)
375 "geckodriver_backup".to_string()
377 prefs_backup_path.set_extension(ext);
378 prefs_backup_path.exists()
382 debug!("Backing up prefs to {:?}", prefs_backup_path);
383 fs::copy(&prefs.path, &prefs_backup_path)?;
386 orig_path: prefs.path.clone(),
387 backup_path: prefs_backup_path,
392 if self.backup_path.exists() {
393 let _ = fs::rename(self.backup_path, self.orig_path);
400 use super::set_prefs;
401 use crate::browser::read_marionette_port;
402 use crate::capabilities::{FirefoxOptions, ProfileType};
403 use base64::prelude::BASE64_STANDARD;
405 use mozprofile::preferences::{Pref, PrefValue};
406 use mozprofile::profile::Profile;
407 use serde_json::{Map, Value};
409 use std::io::{Read, Write};
411 use tempfile::tempdir;
413 fn example_profile() -> Value {
414 let mut profile_data = Vec::with_capacity(1024);
415 let mut profile = File::open("src/tests/profile.zip").unwrap();
416 profile.read_to_end(&mut profile_data).unwrap();
417 Value::String(BASE64_STANDARD.encode(&profile_data))
420 // This is not a pretty test, mostly due to the nature of
421 // mozprofile's and MarionetteHandler's APIs, but we have had
422 // several regressions related to remote.log.level.
424 fn test_remote_log_level() {
425 let mut profile = Profile::new(None).unwrap();
426 set_prefs(2828, &mut profile, false, vec![], false).ok();
427 let user_prefs = profile.user_prefs().unwrap();
429 let pref = user_prefs.get("remote.log.level").unwrap();
430 let value = match pref.value {
431 PrefValue::String(ref s) => s,
434 for (i, ch) in value.chars().enumerate() {
436 assert!(ch.is_uppercase());
438 assert!(ch.is_lowercase());
445 let marionette_settings = Default::default();
447 let encoded_profile = example_profile();
448 let mut prefs: Map<String, Value> = Map::new();
450 "browser.display.background_color".into(),
451 Value::String("#00ff00".into()),
454 let mut firefox_opts = Map::new();
455 firefox_opts.insert("profile".into(), encoded_profile);
456 firefox_opts.insert("prefs".into(), Value::Object(prefs));
458 let mut caps = Map::new();
459 caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts));
461 let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
462 .expect("Valid profile and prefs");
464 let mut profile = match opts.profile {
465 ProfileType::Path(profile) => profile,
466 _ => panic!("Expected ProfileType::Path"),
469 set_prefs(2828, &mut profile, true, opts.prefs, false).expect("set preferences");
471 let prefs_set = profile.user_prefs().expect("valid user preferences");
472 println!("{:#?}", prefs_set.prefs);
475 prefs_set.get("startup.homepage_welcome_url"),
476 Some(&Pref::new("data:text/html,PASS"))
479 prefs_set.get("browser.display.background_color"),
480 Some(&Pref::new("#00ff00"))
482 assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828)));
486 fn test_pref_backup() {
487 let mut profile = Profile::new(None).unwrap();
489 // Create some prefs in the profile
490 let initial_prefs = profile.user_prefs().unwrap();
491 initial_prefs.insert("geckodriver.example", Pref::new("example"));
492 initial_prefs.write().unwrap();
494 let prefs_path = initial_prefs.path.clone();
496 let mut conflicting_backup_path = initial_prefs.path.clone();
497 conflicting_backup_path.set_extension("geckodriver_backup");
498 println!("{:?}", conflicting_backup_path);
499 let mut file = File::create(&conflicting_backup_path).unwrap();
500 file.write_all(b"test").unwrap();
501 assert!(conflicting_backup_path.exists());
503 let mut initial_prefs_data = String::new();
504 File::open(&prefs_path)
505 .expect("Initial prefs exist")
506 .read_to_string(&mut initial_prefs_data)
509 let backup = set_prefs(2828, &mut profile, true, vec![], false)
512 let user_prefs = profile.user_prefs().unwrap();
514 assert!(user_prefs.path.exists());
515 let mut backup_path = user_prefs.path.clone();
516 backup_path.set_extension("geckodriver_backup_1");
518 assert!(backup_path.exists());
520 // Ensure the actual prefs contain both the existing ones and the ones we added
521 let pref = user_prefs.get("marionette.port").unwrap();
522 assert_eq!(pref.value, PrefValue::Int(2828));
524 let pref = user_prefs.get("geckodriver.example").unwrap();
525 assert_eq!(pref.value, PrefValue::String("example".into()));
527 // Ensure the backup prefs don't contain the new settings
528 let mut backup_data = String::new();
529 File::open(&backup_path)
530 .expect("Backup prefs exist")
531 .read_to_string(&mut backup_data)
533 assert_eq!(backup_data, initial_prefs_data);
537 assert!(!backup_path.exists());
538 let mut final_prefs_data = String::new();
539 File::open(&prefs_path)
540 .expect("Initial prefs exist")
541 .read_to_string(&mut final_prefs_data)
543 assert_eq!(final_prefs_data, initial_prefs_data);
547 fn test_local_read_marionette_port() {
548 fn create_port_file(profile_path: &Path, data: &[u8]) {
549 let port_path = profile_path.join("MarionetteActivePort");
550 let mut file = File::create(&port_path).unwrap();
551 file.write_all(data).unwrap();
554 let profile_dir = tempdir().unwrap();
555 let profile_path = profile_dir.path();
556 assert_eq!(read_marionette_port(profile_path), None);
557 assert_eq!(read_marionette_port(profile_path), None);
558 create_port_file(profile_path, b"");
559 assert_eq!(read_marionette_port(profile_path), None);
560 create_port_file(profile_path, b"1234");
561 assert_eq!(read_marionette_port(profile_path), Some(1234));
562 create_port_file(profile_path, b"1234abc");
563 assert_eq!(read_marionette_port(profile_path), None);