Backed out changeset 870b68c06bd7 (bug 1914438) for causing bc failures on browser_ne...
[gecko.git] / testing / geckodriver / src / browser.rs
blob2f7fe2a925feeea7a11f6cdf3e8cb3a3e72e8caa
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};
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::fs;
13 use std::io::Read;
14 use std::path::{Path, PathBuf};
15 use std::time;
16 use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
18 /// A running Gecko instance.
19 #[derive(Debug)]
20 #[allow(clippy::large_enum_variant)]
21 pub(crate) enum Browser {
22     Local(LocalBrowser),
23     Remote(RemoteBrowser),
25     /// An existing browser instance not controlled by GeckoDriver
26     Existing(u16),
29 impl Browser {
30     pub(crate) fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> {
31         match self {
32             Browser::Local(x) => x.close(wait_for_shutdown),
33             Browser::Remote(x) => x.close(),
34             Browser::Existing(_) => Ok(()),
35         }
36     }
38     pub(crate) fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
39         match self {
40             Browser::Local(x) => x.marionette_port(),
41             Browser::Remote(x) => x.marionette_port(),
42             Browser::Existing(x) => Ok(Some(*x)),
43         }
44     }
46     pub(crate) fn update_marionette_port(&mut self, port: u16) {
47         match self {
48             Browser::Local(x) => x.update_marionette_port(port),
49             Browser::Remote(x) => x.update_marionette_port(port),
50             Browser::Existing(x) => {
51                 if port != *x {
52                     error!(
53                         "Cannot re-assign Marionette port when connected to an existing browser"
54                     );
55                 }
56             }
57         }
58     }
61 #[derive(Debug)]
62 /// A local Firefox process, running on this (host) device.
63 pub(crate) struct LocalBrowser {
64     marionette_port: u16,
65     prefs_backup: Option<PrefsBackup>,
66     process: FirefoxProcess,
67     profile_path: Option<PathBuf>,
70 impl LocalBrowser {
71     pub(crate) fn new(
72         options: FirefoxOptions,
73         marionette_port: u16,
74         jsdebugger: bool,
75         profile_root: Option<&Path>,
76         enable_crash_reporter: bool,
77     ) -> WebDriverResult<LocalBrowser> {
78         let binary = options.binary.ok_or_else(|| {
79             WebDriverError::new(
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",
85             )
86         })?;
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)?),
94         };
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(
99                 marionette_port,
100                 profile,
101                 is_custom_profile,
102                 options.prefs,
103                 jsdebugger,
104             )
105             .map_err(|e| {
106                 WebDriverError::new(
107                     ErrorStatus::SessionNotCreated,
108                     format!("Failed to set preferences: {}", e),
109                 )
110             })?;
111             (Some(profile_path), prefs_backup)
112         } else {
113             warn!("Unable to set geckodriver prefs when using a named profile");
114             (None, None)
115         };
117         let mut runner = FirefoxRunner::new(&binary, profile);
119         runner.arg("--marionette");
120         if jsdebugger {
121             runner.arg("--jsdebugger");
122         }
123         if let Some(args) = options.args.as_ref() {
124             runner.args(args);
125         }
127         // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
128         if !enable_crash_reporter {
129             runner
130                 .env("MOZ_CRASHREPORTER", "1")
131                 .env("MOZ_CRASHREPORTER_NO_REPORT", "1")
132                 .env("MOZ_CRASHREPORTER_SHUTDOWN", "1");
133         }
135         let process = match runner.start() {
136             Ok(process) => process,
137             Err(e) => {
138                 if let Some(backup) = prefs_backup {
139                     backup.restore();
140                 }
141                 return Err(WebDriverError::new(
142                     ErrorStatus::SessionNotCreated,
143                     format!("Failed to start browser {}: {}", binary.display(), e),
144                 ));
145             }
146         };
148         Ok(LocalBrowser {
149             marionette_port,
150             prefs_backup,
151             process,
152             profile_path,
153         })
154     }
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),
164             }
165         }
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();
171         };
173         Ok(())
174     }
176     fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
177         if self.marionette_port != 0 {
178             return Ok(Some(self.marionette_port));
179         }
181         if let Some(profile_path) = self.profile_path.as_ref() {
182             return Ok(read_marionette_port(profile_path));
183         }
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",
189         ))
190     }
192     fn update_marionette_port(&mut self, port: u16) {
193         self.marionette_port = port;
194     }
196     pub(crate) fn check_status(&mut self) -> Option<String> {
197         match self.process.try_wait() {
198             Ok(Some(status)) => Some(
199                 status
200                     .code()
201                     .map(|c| c.to_string())
202                     .unwrap_or_else(|| "signal".into()),
203             ),
204             Ok(None) => None,
205             Err(_) => Some("{unknown}".into()),
206         }
207     }
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) {
214         Ok(file) => file,
215         Err(_) => {
216             trace!("Failed to open {}", &port_file.to_string_lossy());
217             return None;
218         }
219     };
220     if let Err(e) = file.read_to_string(&mut port_str) {
221         trace!("Failed to read {}: {}", &port_file.to_string_lossy(), e);
222         return None;
223     };
224     println!("Read port: {}", port_str);
225     let port = port_str.parse::<u16>().ok();
226     if port.is_none() {
227         warn!("Failed fo convert {} to u16", &port_str);
228     }
229     port
232 #[derive(Debug)]
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>,
240 impl RemoteBrowser {
241     pub(crate) fn new(
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",
258                 ));
259             }
260             ProfileType::Path(x) => (x, true),
261             ProfileType::Temporary => (Profile::new(profile_root)?, false),
262         };
264         let prefs_backup = set_prefs(
265             handler.marionette_target_port,
266             &mut profile,
267             is_custom_profile,
268             options.prefs,
269             false,
270         )
271         .map_err(|e| {
272             WebDriverError::new(
273                 ErrorStatus::SessionNotCreated,
274                 format!("Failed to set preferences: {}", e),
275             )
276         })?;
278         handler.prepare(
279             &profile,
280             options.args,
281             options.env.unwrap_or_default(),
282             enable_crash_reporter,
283         )?;
285         handler.launch()?;
287         Ok(RemoteBrowser {
288             handler,
289             marionette_port,
290             prefs_backup,
291         })
292     }
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();
300         };
302         Ok(())
303     }
305     fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
306         Ok(Some(self.marionette_port))
307     }
309     fn update_marionette_port(&mut self, port: u16) {
310         self.marionette_port = port;
311     }
314 fn set_prefs(
315     port: u16,
316     profile: &mut Profile,
317     custom_profile: bool,
318     extra_prefs: Vec<(String, Pref)>,
319     js_debugger: bool,
320 ) -> WebDriverResult<Option<PrefsBackup>> {
321     let prefs = profile.user_prefs().map_err(|_| {
322         WebDriverError::new(
323             ErrorStatus::UnknownError,
324             "Unable to read profile preferences file",
325         )
326     })?;
328     let backup_prefs = if custom_profile && prefs.path.exists() {
329         Some(PrefsBackup::new(prefs)?)
330     } else {
331         None
332     };
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());
337         }
338     }
340     prefs.insert_slice(&extra_prefs[..]);
342     if js_debugger {
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));
347     }
349     prefs.insert("marionette.port", Pref::new(port));
350     prefs.insert("remote.log.level", logging::max_level().into());
352     prefs.write().map_err(|e| {
353         WebDriverError::new(
354             ErrorStatus::UnknownError,
355             format!("Unable to write Firefox profile: {}", e),
356         )
357     })?;
358     Ok(backup_prefs)
361 #[derive(Debug)]
362 struct PrefsBackup {
363     orig_path: PathBuf,
364     backup_path: PathBuf,
367 impl PrefsBackup {
368     fn new(prefs: &PrefFile) -> WebDriverResult<PrefsBackup> {
369         let mut prefs_backup_path = prefs.path.clone();
370         let mut counter = 0;
371         while {
372             let ext = if counter > 0 {
373                 format!("geckodriver_backup_{}", counter)
374             } else {
375                 "geckodriver_backup".to_string()
376             };
377             prefs_backup_path.set_extension(ext);
378             prefs_backup_path.exists()
379         } {
380             counter += 1
381         }
382         debug!("Backing up prefs to {:?}", prefs_backup_path);
383         fs::copy(&prefs.path, &prefs_backup_path)?;
385         Ok(PrefsBackup {
386             orig_path: prefs.path.clone(),
387             backup_path: prefs_backup_path,
388         })
389     }
391     fn restore(self) {
392         if self.backup_path.exists() {
393             let _ = fs::rename(self.backup_path, self.orig_path);
394         }
395     }
398 #[cfg(test)]
399 mod tests {
400     use super::set_prefs;
401     use crate::browser::read_marionette_port;
402     use crate::capabilities::{FirefoxOptions, ProfileType};
403     use base64::prelude::BASE64_STANDARD;
404     use base64::Engine;
405     use mozprofile::preferences::{Pref, PrefValue};
406     use mozprofile::profile::Profile;
407     use serde_json::{Map, Value};
408     use std::fs::File;
409     use std::io::{Read, Write};
410     use std::path::Path;
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))
418     }
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.
423     #[test]
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,
432             _ => panic!(),
433         };
434         for (i, ch) in value.chars().enumerate() {
435             if i == 0 {
436                 assert!(ch.is_uppercase());
437             } else {
438                 assert!(ch.is_lowercase());
439             }
440         }
441     }
443     #[test]
444     fn test_prefs() {
445         let marionette_settings = Default::default();
447         let encoded_profile = example_profile();
448         let mut prefs: Map<String, Value> = Map::new();
449         prefs.insert(
450             "browser.display.background_color".into(),
451             Value::String("#00ff00".into()),
452         );
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"),
467         };
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);
474         assert_eq!(
475             prefs_set.get("startup.homepage_welcome_url"),
476             Some(&Pref::new("data:text/html,PASS"))
477         );
478         assert_eq!(
479             prefs_set.get("browser.display.background_color"),
480             Some(&Pref::new("#00ff00"))
481         );
482         assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828)));
483     }
485     #[test]
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)
507             .unwrap();
509         let backup = set_prefs(2828, &mut profile, true, vec![], false)
510             .unwrap()
511             .unwrap();
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)
532             .unwrap();
533         assert_eq!(backup_data, initial_prefs_data);
535         backup.restore();
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)
542             .unwrap();
543         assert_eq!(final_prefs_data, initial_prefs_data);
544     }
546     #[test]
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();
552         }
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);
564     }