browser.rs (19950B)
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/. */ 4 5 use crate::android::{AndroidError, AndroidHandler}; 6 use crate::capabilities::{FirefoxOptions, ProfileType}; 7 use crate::logging; 8 use crate::prefs; 9 use mozprofile::preferences::Pref; 10 use mozprofile::profile::{PrefFile, Profile}; 11 use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess}; 12 use std::env; 13 use std::fs; 14 use std::io::prelude::*; 15 use std::path::{Path, PathBuf}; 16 use std::time; 17 use uuid::Uuid; 18 use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult}; 19 20 /// A running Gecko instance. 21 #[derive(Debug)] 22 #[allow(clippy::large_enum_variant)] 23 pub(crate) enum Browser { 24 Local(LocalBrowser), 25 Remote(RemoteBrowser), 26 27 /// An existing browser instance not controlled by GeckoDriver 28 Existing(u16), 29 } 30 31 impl Browser { 32 pub(crate) fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> { 33 match self { 34 Browser::Local(x) => x.close(wait_for_shutdown), 35 Browser::Remote(x) => x.close(), 36 Browser::Existing(_) => Ok(()), 37 } 38 } 39 40 pub(crate) fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> { 41 match self { 42 Browser::Local(x) => x.marionette_port(), 43 Browser::Remote(x) => x.marionette_port(), 44 Browser::Existing(x) => Ok(Some(*x)), 45 } 46 } 47 48 pub(crate) fn update_marionette_port(&mut self, port: u16) { 49 match self { 50 Browser::Local(x) => x.update_marionette_port(port), 51 Browser::Remote(x) => x.update_marionette_port(port), 52 Browser::Existing(x) => { 53 if port != *x { 54 error!( 55 "Cannot re-assign Marionette port when connected to an existing browser" 56 ); 57 } 58 } 59 } 60 } 61 62 pub(crate) fn create_file(&self, content: &[u8]) -> WebDriverResult<String> { 63 let addon_file = format!("addon-{}.xpi", Uuid::new_v4()); 64 match self { 65 Browser::Remote(x) => { 66 let path = x.push_file(content, &addon_file).map_err(|e| { 67 WebDriverError::new( 68 ErrorStatus::UnknownError, 69 format!("Failed to create an addon file: {}", e), 70 ) 71 })?; 72 73 Ok(path) 74 } 75 Browser::Local(_) | Browser::Existing(_) => { 76 let path = env::temp_dir().as_path().join(addon_file); 77 let mut xpi_file = fs::File::create(&path).map_err(|e| { 78 WebDriverError::new( 79 ErrorStatus::UnknownError, 80 format!("Failed to create an addon file: {}", e), 81 ) 82 })?; 83 xpi_file.write_all(content).map_err(|e| { 84 WebDriverError::new( 85 ErrorStatus::UnknownError, 86 format!("Failed to write data to the addon file: {}", e), 87 ) 88 })?; 89 90 Ok(path.display().to_string()) 91 } 92 } 93 } 94 } 95 96 #[derive(Debug)] 97 /// A local Firefox process, running on this (host) device. 98 pub(crate) struct LocalBrowser { 99 marionette_port: u16, 100 prefs_backup: Option<PrefsBackup>, 101 process: FirefoxProcess, 102 pub(crate) profile_path: Option<PathBuf>, 103 } 104 105 impl LocalBrowser { 106 pub(crate) fn new( 107 options: FirefoxOptions, 108 marionette_port: u16, 109 jsdebugger: bool, 110 system_access: bool, 111 profile_root: Option<&Path>, 112 ) -> WebDriverResult<LocalBrowser> { 113 let binary = options.binary.ok_or_else(|| { 114 WebDriverError::new( 115 ErrorStatus::SessionNotCreated, 116 "Expected browser binary location, but unable to find \ 117 binary in default location, no \ 118 'moz:firefoxOptions.binary' capability provided, and \ 119 no binary flag set on the command line", 120 ) 121 })?; 122 123 let is_custom_profile = matches!(options.profile, ProfileType::Path(_)); 124 125 let mut profile = match options.profile { 126 ProfileType::Named => None, 127 ProfileType::Path(x) => Some(x), 128 ProfileType::Temporary => Some(Profile::new(profile_root)?), 129 }; 130 131 let (profile_path, prefs_backup) = if let Some(ref mut profile) = profile { 132 let profile_path = profile.path.clone(); 133 let prefs_backup = set_prefs( 134 marionette_port, 135 profile, 136 is_custom_profile, 137 options.prefs, 138 jsdebugger, 139 ) 140 .map_err(|e| { 141 WebDriverError::new( 142 ErrorStatus::SessionNotCreated, 143 format!("Failed to set preferences: {}", e), 144 ) 145 })?; 146 (Some(profile_path), prefs_backup) 147 } else { 148 warn!("Unable to set geckodriver prefs when using a named profile"); 149 (None, None) 150 }; 151 152 let mut runner = FirefoxRunner::new(&binary, profile); 153 154 runner.arg("--marionette"); 155 if jsdebugger { 156 runner.arg("--jsdebugger"); 157 } 158 if system_access { 159 runner.arg("--remote-allow-system-access"); 160 } 161 if let Some(args) = options.args.as_ref() { 162 runner.args(args); 163 } 164 165 // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting 166 runner 167 .env("MOZ_CRASHREPORTER", "1") 168 .env("MOZ_CRASHREPORTER_NO_REPORT", "1") 169 .env("MOZ_CRASHREPORTER_SHUTDOWN", "1"); 170 171 let process = match runner.start() { 172 Ok(process) => process, 173 Err(e) => { 174 if let Some(backup) = prefs_backup { 175 backup.restore(); 176 } 177 return Err(WebDriverError::new( 178 ErrorStatus::SessionNotCreated, 179 format!("Failed to start browser {}: {}", binary.display(), e), 180 )); 181 } 182 }; 183 184 Ok(LocalBrowser { 185 marionette_port, 186 prefs_backup, 187 process, 188 profile_path, 189 }) 190 } 191 192 fn close(mut self, wait_for_shutdown: bool) -> WebDriverResult<()> { 193 if wait_for_shutdown { 194 // TODO(https://bugzil.la/1443922): 195 // Use toolkit.asyncshutdown.crash_timout pref 196 let duration = time::Duration::from_secs(70); 197 match self.process.wait(duration) { 198 Ok(x) => debug!("Browser process stopped: {}", x), 199 Err(e) => error!("Failed to stop browser process: {}", e), 200 } 201 } 202 self.process.kill()?; 203 204 // Restoring the prefs if the browser fails to stop perhaps doesn't work anyway 205 if let Some(prefs_backup) = self.prefs_backup { 206 prefs_backup.restore(); 207 }; 208 209 Ok(()) 210 } 211 212 fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> { 213 if self.marionette_port != 0 { 214 return Ok(Some(self.marionette_port)); 215 } 216 217 if let Some(profile_path) = self.profile_path.as_ref() { 218 return Ok(read_marionette_port(profile_path)); 219 } 220 221 // This should be impossible, but it isn't enforced 222 Err(WebDriverError::new( 223 ErrorStatus::SessionNotCreated, 224 "Port not known when using named profile", 225 )) 226 } 227 228 fn update_marionette_port(&mut self, port: u16) { 229 self.marionette_port = port; 230 } 231 232 pub(crate) fn check_status(&mut self) -> Option<String> { 233 match self.process.try_wait() { 234 Ok(Some(status)) => Some( 235 status 236 .code() 237 .map(|c| c.to_string()) 238 .unwrap_or_else(|| "signal".into()), 239 ), 240 Ok(None) => None, 241 Err(_) => Some("{unknown}".into()), 242 } 243 } 244 } 245 246 fn read_marionette_port(profile_path: &Path) -> Option<u16> { 247 let port_file = profile_path.join("MarionetteActivePort"); 248 let mut port_str = String::with_capacity(6); 249 let mut file = match fs::File::open(&port_file) { 250 Ok(file) => file, 251 Err(_) => { 252 trace!("Failed to open {}", &port_file.to_string_lossy()); 253 return None; 254 } 255 }; 256 if let Err(e) = file.read_to_string(&mut port_str) { 257 trace!("Failed to read {}: {}", &port_file.to_string_lossy(), e); 258 return None; 259 }; 260 println!("Read port: {}", port_str); 261 let port = port_str.parse::<u16>().ok(); 262 if port.is_none() { 263 warn!("Failed fo convert {} to u16", &port_str); 264 } 265 port 266 } 267 268 #[derive(Debug)] 269 /// A remote instance, running on a (target) Android device. 270 pub(crate) struct RemoteBrowser { 271 pub(crate) handler: AndroidHandler, 272 marionette_port: u16, 273 prefs_backup: Option<PrefsBackup>, 274 } 275 276 impl RemoteBrowser { 277 pub(crate) fn new( 278 options: FirefoxOptions, 279 marionette_port: u16, 280 websocket_port: Option<u16>, 281 system_access: bool, 282 profile_root: Option<&Path>, 283 ) -> WebDriverResult<RemoteBrowser> { 284 let android_options = options.android.unwrap(); 285 286 let handler = AndroidHandler::new( 287 &android_options, 288 marionette_port, 289 system_access, 290 websocket_port, 291 )?; 292 293 // Profile management. 294 let (mut profile, is_custom_profile) = match options.profile { 295 ProfileType::Named => { 296 return Err(WebDriverError::new( 297 ErrorStatus::SessionNotCreated, 298 "Cannot use a named profile on Android", 299 )); 300 } 301 ProfileType::Path(x) => (x, true), 302 ProfileType::Temporary => (Profile::new(profile_root)?, false), 303 }; 304 305 let prefs_backup = set_prefs( 306 handler.marionette_target_port, 307 &mut profile, 308 is_custom_profile, 309 options.prefs, 310 false, 311 ) 312 .map_err(|e| { 313 WebDriverError::new( 314 ErrorStatus::SessionNotCreated, 315 format!("Failed to set preferences: {}", e), 316 ) 317 })?; 318 319 handler.prepare(&profile, options.args, options.env.unwrap_or_default())?; 320 321 handler.launch()?; 322 323 Ok(RemoteBrowser { 324 handler, 325 marionette_port, 326 prefs_backup, 327 }) 328 } 329 330 fn close(self) -> WebDriverResult<()> { 331 self.handler.force_stop()?; 332 333 // Restoring the prefs if the browser fails to stop perhaps doesn't work anyway 334 if let Some(prefs_backup) = self.prefs_backup { 335 prefs_backup.restore(); 336 }; 337 338 Ok(()) 339 } 340 341 fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> { 342 Ok(Some(self.marionette_port)) 343 } 344 345 fn update_marionette_port(&mut self, port: u16) { 346 self.marionette_port = port; 347 } 348 349 fn push_file(&self, content: &[u8], path: &str) -> Result<String, AndroidError> { 350 self.handler.push_as_file(content, path) 351 } 352 } 353 354 fn set_prefs( 355 port: u16, 356 profile: &mut Profile, 357 custom_profile: bool, 358 extra_prefs: Vec<(String, Pref)>, 359 js_debugger: bool, 360 ) -> WebDriverResult<Option<PrefsBackup>> { 361 let prefs = profile.user_prefs().map_err(|_| { 362 WebDriverError::new( 363 ErrorStatus::UnknownError, 364 "Unable to read profile preferences file", 365 ) 366 })?; 367 368 let backup_prefs = if custom_profile && prefs.path.exists() { 369 Some(PrefsBackup::new(prefs)?) 370 } else { 371 None 372 }; 373 374 for &(name, ref value) in prefs::DEFAULT.iter() { 375 if !custom_profile || !prefs.contains_key(name) { 376 prefs.insert(name.to_string(), (*value).clone()); 377 } 378 } 379 380 prefs.insert_slice(&extra_prefs[..]); 381 382 if js_debugger { 383 prefs.insert("devtools.browsertoolbox.panel", Pref::new("jsdebugger")); 384 prefs.insert("devtools.debugger.remote-enabled", Pref::new(true)); 385 prefs.insert("devtools.chrome.enabled", Pref::new(true)); 386 prefs.insert("devtools.debugger.prompt-connection", Pref::new(false)); 387 } 388 389 prefs.insert("marionette.port", Pref::new(port)); 390 prefs.insert("remote.log.level", logging::max_level().into()); 391 392 prefs.write().map_err(|e| { 393 WebDriverError::new( 394 ErrorStatus::UnknownError, 395 format!("Unable to write Firefox profile: {}", e), 396 ) 397 })?; 398 Ok(backup_prefs) 399 } 400 401 #[derive(Debug)] 402 struct PrefsBackup { 403 orig_path: PathBuf, 404 backup_path: PathBuf, 405 } 406 407 impl PrefsBackup { 408 fn new(prefs: &PrefFile) -> WebDriverResult<PrefsBackup> { 409 let mut prefs_backup_path = prefs.path.clone(); 410 let mut counter = 0; 411 while { 412 let ext = if counter > 0 { 413 format!("geckodriver_backup_{}", counter) 414 } else { 415 "geckodriver_backup".to_string() 416 }; 417 prefs_backup_path.set_extension(ext); 418 prefs_backup_path.exists() 419 } { 420 counter += 1 421 } 422 debug!("Backing up prefs to {:?}", prefs_backup_path); 423 fs::copy(&prefs.path, &prefs_backup_path)?; 424 425 Ok(PrefsBackup { 426 orig_path: prefs.path.clone(), 427 backup_path: prefs_backup_path, 428 }) 429 } 430 431 fn restore(self) { 432 if self.backup_path.exists() { 433 let _ = fs::rename(self.backup_path, self.orig_path); 434 } 435 } 436 } 437 438 #[cfg(test)] 439 mod tests { 440 use super::set_prefs; 441 use crate::browser::read_marionette_port; 442 use crate::capabilities::{FirefoxOptions, ProfileType}; 443 use base64::prelude::BASE64_STANDARD; 444 use base64::Engine; 445 use mozprofile::preferences::{Pref, PrefValue}; 446 use mozprofile::profile::Profile; 447 use serde_json::{Map, Value}; 448 use std::fs::File; 449 use std::io::{Read, Write}; 450 use std::path::Path; 451 use tempfile::tempdir; 452 453 fn example_profile() -> Value { 454 let mut profile_data = Vec::with_capacity(1024); 455 let mut profile = File::open("src/tests/profile.zip").unwrap(); 456 profile.read_to_end(&mut profile_data).unwrap(); 457 Value::String(BASE64_STANDARD.encode(&profile_data)) 458 } 459 460 // This is not a pretty test, mostly due to the nature of 461 // mozprofile's and MarionetteHandler's APIs, but we have had 462 // several regressions related to remote.log.level. 463 #[test] 464 fn test_remote_log_level() { 465 let mut profile = Profile::new(None).unwrap(); 466 set_prefs(2828, &mut profile, false, vec![], false).ok(); 467 let user_prefs = profile.user_prefs().unwrap(); 468 469 let pref = user_prefs.get("remote.log.level").unwrap(); 470 let value = match pref.value { 471 PrefValue::String(ref s) => s, 472 _ => panic!(), 473 }; 474 for (i, ch) in value.chars().enumerate() { 475 if i == 0 { 476 assert!(ch.is_uppercase()); 477 } else { 478 assert!(ch.is_lowercase()); 479 } 480 } 481 } 482 483 #[test] 484 fn test_prefs() { 485 let marionette_settings = Default::default(); 486 487 let encoded_profile = example_profile(); 488 let mut prefs: Map<String, Value> = Map::new(); 489 prefs.insert( 490 "browser.display.background_color".into(), 491 Value::String("#00ff00".into()), 492 ); 493 494 let mut firefox_opts = Map::new(); 495 firefox_opts.insert("profile".into(), encoded_profile); 496 firefox_opts.insert("prefs".into(), Value::Object(prefs)); 497 498 let mut caps = Map::new(); 499 caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts)); 500 501 let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps) 502 .expect("Valid profile and prefs"); 503 504 let mut profile = match opts.profile { 505 ProfileType::Path(profile) => profile, 506 _ => panic!("Expected ProfileType::Path"), 507 }; 508 509 set_prefs(2828, &mut profile, true, opts.prefs, false).expect("set preferences"); 510 511 let prefs_set = profile.user_prefs().expect("valid user preferences"); 512 println!("{:#?}", prefs_set.prefs); 513 514 assert_eq!( 515 prefs_set.get("startup.homepage_welcome_url"), 516 Some(&Pref::new("data:text/html,PASS")) 517 ); 518 assert_eq!( 519 prefs_set.get("browser.display.background_color"), 520 Some(&Pref::new("#00ff00")) 521 ); 522 assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828))); 523 } 524 525 #[test] 526 fn test_pref_backup() { 527 let mut profile = Profile::new(None).unwrap(); 528 529 // Create some prefs in the profile 530 let initial_prefs = profile.user_prefs().unwrap(); 531 initial_prefs.insert("geckodriver.example", Pref::new("example")); 532 initial_prefs.write().unwrap(); 533 534 let prefs_path = initial_prefs.path.clone(); 535 536 let mut conflicting_backup_path = initial_prefs.path.clone(); 537 conflicting_backup_path.set_extension("geckodriver_backup"); 538 println!("{:?}", conflicting_backup_path); 539 let mut file = File::create(&conflicting_backup_path).unwrap(); 540 file.write_all(b"test").unwrap(); 541 assert!(conflicting_backup_path.exists()); 542 543 let mut initial_prefs_data = String::new(); 544 File::open(&prefs_path) 545 .expect("Initial prefs exist") 546 .read_to_string(&mut initial_prefs_data) 547 .unwrap(); 548 549 let backup = set_prefs(2828, &mut profile, true, vec![], false) 550 .unwrap() 551 .unwrap(); 552 let user_prefs = profile.user_prefs().unwrap(); 553 554 assert!(user_prefs.path.exists()); 555 let mut backup_path = user_prefs.path.clone(); 556 backup_path.set_extension("geckodriver_backup_1"); 557 558 assert!(backup_path.exists()); 559 560 // Ensure the actual prefs contain both the existing ones and the ones we added 561 let pref = user_prefs.get("marionette.port").unwrap(); 562 assert_eq!(pref.value, PrefValue::Int(2828)); 563 564 let pref = user_prefs.get("geckodriver.example").unwrap(); 565 assert_eq!(pref.value, PrefValue::String("example".into())); 566 567 // Ensure the backup prefs don't contain the new settings 568 let mut backup_data = String::new(); 569 File::open(&backup_path) 570 .expect("Backup prefs exist") 571 .read_to_string(&mut backup_data) 572 .unwrap(); 573 assert_eq!(backup_data, initial_prefs_data); 574 575 backup.restore(); 576 577 assert!(!backup_path.exists()); 578 let mut final_prefs_data = String::new(); 579 File::open(&prefs_path) 580 .expect("Initial prefs exist") 581 .read_to_string(&mut final_prefs_data) 582 .unwrap(); 583 assert_eq!(final_prefs_data, initial_prefs_data); 584 } 585 586 #[test] 587 fn test_local_read_marionette_port() { 588 fn create_port_file(profile_path: &Path, data: &[u8]) { 589 let port_path = profile_path.join("MarionetteActivePort"); 590 let mut file = File::create(&port_path).unwrap(); 591 file.write_all(data).unwrap(); 592 } 593 594 let profile_dir = tempdir().unwrap(); 595 let profile_path = profile_dir.path(); 596 assert_eq!(read_marionette_port(profile_path), None); 597 assert_eq!(read_marionette_port(profile_path), None); 598 create_port_file(profile_path, b""); 599 assert_eq!(read_marionette_port(profile_path), None); 600 create_port_file(profile_path, b"1234"); 601 assert_eq!(read_marionette_port(profile_path), Some(1234)); 602 create_port_file(profile_path, b"1234abc"); 603 assert_eq!(read_marionette_port(profile_path), None); 604 } 605 }