diff options
Diffstat (limited to 'testing/mozbase/rust/mozrunner')
-rw-r--r-- | testing/mozbase/rust/mozrunner/Cargo.toml | 27 | ||||
-rw-r--r-- | testing/mozbase/rust/mozrunner/src/bin/firefox-default-path.rs | 21 | ||||
-rw-r--r-- | testing/mozbase/rust/mozrunner/src/firefox_args.rs | 384 | ||||
-rw-r--r-- | testing/mozbase/rust/mozrunner/src/lib.rs | 20 | ||||
-rw-r--r-- | testing/mozbase/rust/mozrunner/src/path.rs | 48 | ||||
-rw-r--r-- | testing/mozbase/rust/mozrunner/src/runner.rs | 570 |
6 files changed, 1070 insertions, 0 deletions
diff --git a/testing/mozbase/rust/mozrunner/Cargo.toml b/testing/mozbase/rust/mozrunner/Cargo.toml new file mode 100644 index 0000000000..5baa6356fe --- /dev/null +++ b/testing/mozbase/rust/mozrunner/Cargo.toml @@ -0,0 +1,27 @@ +[package] +edition = "2018" +name = "mozrunner" +version = "0.15.1" +authors = ["Mozilla"] +description = "Reliable Firefox process management." +keywords = [ + "firefox", + "mozilla", + "process-manager", +] +license = "MPL-2.0" +repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/rust/mozrunner" + +[dependencies] +log = "0.4" +mozprofile = { path = "../mozprofile", version = "0.9" } +plist = "1.0" + +[target.'cfg(target_os = "windows")'.dependencies] +winreg = "0.10.1" + +[target.'cfg(target_os = "macos")'.dependencies] +dirs = "4" + +[[bin]] +name = "firefox-default-path" diff --git a/testing/mozbase/rust/mozrunner/src/bin/firefox-default-path.rs b/testing/mozbase/rust/mozrunner/src/bin/firefox-default-path.rs new file mode 100644 index 0000000000..94958aac90 --- /dev/null +++ b/testing/mozbase/rust/mozrunner/src/bin/firefox-default-path.rs @@ -0,0 +1,21 @@ +/* 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/. */ + +extern crate mozrunner; + +use mozrunner::runner::platform; +use std::io::Write; + +fn main() { + let (path, code) = platform::firefox_default_path() + .map(|x| (x.to_string_lossy().into_owned(), 0)) + .unwrap_or(("Firefox binary not found".to_owned(), 1)); + + let mut writer: Box<dyn Write> = match code { + 0 => Box::new(std::io::stdout()), + _ => Box::new(std::io::stderr()), + }; + writeln!(&mut writer, "{}", &*path).unwrap(); + std::process::exit(code); +} diff --git a/testing/mozbase/rust/mozrunner/src/firefox_args.rs b/testing/mozbase/rust/mozrunner/src/firefox_args.rs new file mode 100644 index 0000000000..49f873f9dc --- /dev/null +++ b/testing/mozbase/rust/mozrunner/src/firefox_args.rs @@ -0,0 +1,384 @@ +/* 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/. */ + +//! Argument string parsing and matching functions for Firefox. +//! +//! Which arguments Firefox accepts and in what style depends on the platform. +//! On Windows only, arguments can be prefixed with `/` (slash), such as +//! `/screenshot`. Elsewhere, including Windows, arguments may be prefixed +//! with both single (`-screenshot`) and double (`--screenshot`) dashes. +//! +//! An argument's name is determined by a space or an assignment operator (`=`) +//! so that for the string `-foo=bar`, `foo` is considered the argument's +//! basename. + +use crate::runner::platform; +use std::ffi::{OsStr, OsString}; +use std::fmt; + +/// Parse an argument string into a name and value +/// +/// Given an argument like `"--arg=value"` this will split it into +/// `(Some("arg"), Some("value")). For a case like `"--arg"` it will +/// return `(Some("arg"), None)` and where the input doesn't look like +/// an argument e.g. `"value"` it will return `(None, Some("value"))` +fn parse_arg_name_value<T>(arg: T) -> (Option<String>, Option<String>) +where + T: AsRef<OsStr>, +{ + let arg_os_str: &OsStr = arg.as_ref(); + let arg_str = arg_os_str.to_string_lossy(); + + let mut name_start = 0; + let mut name_end = 0; + + // Look for an argument name at the start of the + // string + for (i, c) in arg_str.chars().enumerate() { + if i == 0 { + if !platform::arg_prefix_char(c) { + break; + } + } else if i == 1 { + if name_end_char(c) { + break; + } else if c != '-' { + name_start = i; + name_end = name_start + 1; + } else { + name_start = i + 1; + name_end = name_start; + } + } else { + name_end += 1; + if name_end_char(c) { + name_end -= 1; + break; + } + } + } + + let name = if name_start > 0 && name_end > name_start { + Some(arg_str[name_start..name_end].into()) + } else { + None + }; + + // If there are characters in the string after the argument, read + // them as the value, excluding the seperator (e.g. "=") if + // present. + let mut value_start = name_end; + let value_end = arg_str.len(); + let value = if value_start < value_end { + if let Some(c) = arg_str[value_start..value_end].chars().next() { + if name_end_char(c) { + value_start += 1; + } + } + Some(arg_str[value_start..value_end].into()) + } else { + None + }; + (name, value) +} + +fn name_end_char(c: char) -> bool { + c == ' ' || c == '=' +} + +/// Represents a Firefox command-line argument. +#[derive(Debug, PartialEq)] +pub enum Arg { + /// `-foreground` ensures application window gets focus, which is not the + /// default on macOS. As such Firefox only supports it on MacOS. + Foreground, + + /// --marionette enables Marionette in the application which is used + /// by WebDriver HTTP. + Marionette, + + /// `-no-remote` prevents remote commands to this instance of Firefox, and + /// ensure we always start a new instance. + NoRemote, + + /// `-P NAME` starts Firefox with a profile with a given name. + NamedProfile, + + /// `-profile PATH` starts Firefox with the profile at the specified path. + Profile, + + /// `-ProfileManager` starts Firefox with the profile chooser dialogue. + ProfileManager, + + /// All other arguments. + Other(String), + + /// --remote-allow-hosts contains comma-separated values of the Host header + /// to allow for incoming WebSocket requests of the Remote Agent. + RemoteAllowHosts, + + /// --remote-allow-origins contains comma-separated values of the Origin header + /// to allow for incoming WebSocket requests of the Remote Agent. + RemoteAllowOrigins, + + /// --remote-debugging-port enables the Remote Agent in the application + /// which is used for the WebDriver BiDi and CDP remote debugging protocols. + RemoteDebuggingPort, + + /// Not an argument. + None, +} + +impl Arg { + pub fn new(name: &str) -> Arg { + match name { + "foreground" => Arg::Foreground, + "marionette" => Arg::Marionette, + "no-remote" => Arg::NoRemote, + "profile" => Arg::Profile, + "P" => Arg::NamedProfile, + "ProfileManager" => Arg::ProfileManager, + "remote-allow-hosts" => Arg::RemoteAllowHosts, + "remote-allow-origins" => Arg::RemoteAllowOrigins, + "remote-debugging-port" => Arg::RemoteDebuggingPort, + _ => Arg::Other(name.into()), + } + } +} + +impl<'a> From<&'a OsString> for Arg { + fn from(arg_str: &OsString) -> Arg { + if let (Some(name), _) = parse_arg_name_value(arg_str) { + Arg::new(&name) + } else { + Arg::None + } + } +} + +impl fmt::Display for Arg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&match self { + Arg::Foreground => "--foreground".to_string(), + Arg::Marionette => "--marionette".to_string(), + Arg::NamedProfile => "-P".to_string(), + Arg::None => "".to_string(), + Arg::NoRemote => "--no-remote".to_string(), + Arg::Other(x) => format!("--{}", x), + Arg::Profile => "--profile".to_string(), + Arg::ProfileManager => "--ProfileManager".to_string(), + Arg::RemoteAllowHosts => "--remote-allow-hosts".to_string(), + Arg::RemoteAllowOrigins => "--remote-allow-origins".to_string(), + Arg::RemoteDebuggingPort => "--remote-debugging-port".to_string(), + }) + } +} + +/// Parse an iterator over arguments into an vector of (name, value) +/// tuples +/// +/// Each entry in the input argument will produce a single item in the +/// output. Because we don't know anything about the specific +/// arguments, something that doesn't parse as a named argument may +/// either be the value of a previous named argument, or may be a +/// positional argument. +pub fn parse_args<'a>( + args: impl Iterator<Item = &'a OsString>, +) -> Vec<(Option<Arg>, Option<String>)> { + args.map(parse_arg_name_value) + .map(|(name, value)| { + if let Some(arg_name) = name { + (Some(Arg::new(&arg_name)), value) + } else { + (None, value) + } + }) + .collect() +} + +/// Given an iterator over all arguments, get the value of an argument +/// +/// This assumes that the argument takes a single value and that is +/// either provided as a single argument entry +/// (e.g. `["--name=value"]`) or as the following argument +/// (e.g. `["--name", "value"]) +pub fn get_arg_value<'a>( + mut parsed_args: impl Iterator<Item = &'a (Option<Arg>, Option<String>)>, + arg: Arg, +) -> Option<String> { + let mut found_value = None; + for (arg_name, arg_value) in &mut parsed_args { + if let (Some(name), value) = (arg_name, arg_value) { + if *name == arg { + found_value = value.clone(); + break; + } + } + } + if found_value.is_none() { + // If there wasn't a value, check if the following argument is a value + if let Some((None, value)) = parsed_args.next() { + found_value = value.clone(); + } + } + found_value +} + +#[cfg(test)] +mod tests { + use super::{get_arg_value, parse_arg_name_value, parse_args, Arg}; + use std::ffi::OsString; + + fn parse(arg: &str, name: Option<&str>) { + let (result, _) = parse_arg_name_value(arg); + assert_eq!(result, name.map(|x| x.to_string())); + } + + #[test] + fn test_parse_arg_name_value() { + parse("-p", Some("p")); + parse("--p", Some("p")); + parse("--profile foo", Some("profile")); + parse("--profile", Some("profile")); + parse("--", None); + parse("", None); + parse("-=", None); + parse("--=", None); + parse("-- foo", None); + parse("foo", None); + parse("/ foo", None); + parse("/- foo", None); + parse("/=foo", None); + parse("foo", None); + parse("-profile", Some("profile")); + parse("-profile=foo", Some("profile")); + parse("-profile = foo", Some("profile")); + parse("-profile abc", Some("profile")); + parse("-profile /foo", Some("profile")); + } + + #[cfg(target_os = "windows")] + #[test] + fn test_parse_arg_name_value_windows() { + parse("/profile", Some("profile")); + } + + #[cfg(not(target_os = "windows"))] + #[test] + fn test_parse_arg_name_value_non_windows() { + parse("/profile", None); + } + + #[test] + fn test_arg_from_osstring() { + assert_eq!(Arg::from(&OsString::from("--foreground")), Arg::Foreground); + assert_eq!(Arg::from(&OsString::from("-foreground")), Arg::Foreground); + + assert_eq!(Arg::from(&OsString::from("--marionette")), Arg::Marionette); + assert_eq!(Arg::from(&OsString::from("-marionette")), Arg::Marionette); + + assert_eq!(Arg::from(&OsString::from("--no-remote")), Arg::NoRemote); + assert_eq!(Arg::from(&OsString::from("-no-remote")), Arg::NoRemote); + + assert_eq!(Arg::from(&OsString::from("-- profile")), Arg::None); + assert_eq!(Arg::from(&OsString::from("profile")), Arg::None); + assert_eq!(Arg::from(&OsString::from("profile -P")), Arg::None); + assert_eq!( + Arg::from(&OsString::from("-profiled")), + Arg::Other("profiled".into()) + ); + assert_eq!( + Arg::from(&OsString::from("-PROFILEMANAGER")), + Arg::Other("PROFILEMANAGER".into()) + ); + + assert_eq!(Arg::from(&OsString::from("--profile")), Arg::Profile); + assert_eq!(Arg::from(&OsString::from("-profile foo")), Arg::Profile); + + assert_eq!( + Arg::from(&OsString::from("--ProfileManager")), + Arg::ProfileManager + ); + assert_eq!( + Arg::from(&OsString::from("-ProfileManager")), + Arg::ProfileManager + ); + + // TODO: -Ptest is valid + //assert_eq!(Arg::from(&OsString::from("-Ptest")), Arg::NamedProfile); + assert_eq!(Arg::from(&OsString::from("-P")), Arg::NamedProfile); + assert_eq!(Arg::from(&OsString::from("-P test")), Arg::NamedProfile); + + assert_eq!( + Arg::from(&OsString::from("--remote-debugging-port")), + Arg::RemoteDebuggingPort + ); + assert_eq!( + Arg::from(&OsString::from("-remote-debugging-port")), + Arg::RemoteDebuggingPort + ); + assert_eq!( + Arg::from(&OsString::from("--remote-debugging-port 9222")), + Arg::RemoteDebuggingPort + ); + + assert_eq!( + Arg::from(&OsString::from("--remote-allow-hosts")), + Arg::RemoteAllowHosts + ); + assert_eq!( + Arg::from(&OsString::from("-remote-allow-hosts")), + Arg::RemoteAllowHosts + ); + assert_eq!( + Arg::from(&OsString::from("--remote-allow-hosts 9222")), + Arg::RemoteAllowHosts + ); + + assert_eq!( + Arg::from(&OsString::from("--remote-allow-origins")), + Arg::RemoteAllowOrigins + ); + assert_eq!( + Arg::from(&OsString::from("-remote-allow-origins")), + Arg::RemoteAllowOrigins + ); + assert_eq!( + Arg::from(&OsString::from("--remote-allow-origins http://foo")), + Arg::RemoteAllowOrigins + ); + } + + #[test] + fn test_get_arg_value() { + let args = vec!["-P", "ProfileName", "--profile=/path/", "--no-remote"] + .iter() + .map(|x| OsString::from(x)) + .collect::<Vec<OsString>>(); + let parsed_args = parse_args(args.iter()); + assert_eq!( + get_arg_value(parsed_args.iter(), Arg::NamedProfile), + Some("ProfileName".into()) + ); + assert_eq!( + get_arg_value(parsed_args.iter(), Arg::Profile), + Some("/path/".into()) + ); + assert_eq!(get_arg_value(parsed_args.iter(), Arg::NoRemote), None); + + let args = vec!["--profile=", "-P test"] + .iter() + .map(|x| OsString::from(x)) + .collect::<Vec<OsString>>(); + let parsed_args = parse_args(args.iter()); + assert_eq!( + get_arg_value(parsed_args.iter(), Arg::NamedProfile), + Some("test".into()) + ); + assert_eq!( + get_arg_value(parsed_args.iter(), Arg::Profile), + Some("".into()) + ); + } +} diff --git a/testing/mozbase/rust/mozrunner/src/lib.rs b/testing/mozbase/rust/mozrunner/src/lib.rs new file mode 100644 index 0000000000..5634de11bf --- /dev/null +++ b/testing/mozbase/rust/mozrunner/src/lib.rs @@ -0,0 +1,20 @@ +#![forbid(unsafe_code)] +/* 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/. */ + +#[macro_use] +extern crate log; +#[cfg(target_os = "macos")] +extern crate dirs; +extern crate mozprofile; +#[cfg(target_os = "macos")] +extern crate plist; +#[cfg(target_os = "windows")] +extern crate winreg; + +pub mod firefox_args; +pub mod path; +pub mod runner; + +pub use crate::runner::platform::firefox_default_path; diff --git a/testing/mozbase/rust/mozrunner/src/path.rs b/testing/mozbase/rust/mozrunner/src/path.rs new file mode 100644 index 0000000000..0bf1eda9ed --- /dev/null +++ b/testing/mozbase/rust/mozrunner/src/path.rs @@ -0,0 +1,48 @@ +/* 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/. */ + +//! Provides utilities for searching the system path. + +use std::env; +use std::path::{Path, PathBuf}; + +#[cfg(unix)] +fn is_executable(path: &Path) -> bool { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + // Permissions are a set of four 4-bit bitflags, represented by a single octal + // digit. The lowest bit of each of the last three values represents the + // executable permission for all, group and user, repsectively. We assume the + // file is executable if any of these are set. + match fs::metadata(path).ok() { + Some(meta) => meta.permissions().mode() & 0o111 != 0, + None => false, + } +} + +#[cfg(not(unix))] +fn is_executable(_: &Path) -> bool { + true +} + +/// Determines if the path is an executable binary. That is, if it exists, is +/// a file, and is executable where applicable. +pub fn is_binary(path: &Path) -> bool { + path.exists() && path.is_file() && is_executable(path) +} + +/// Searches the system path (`PATH`) for an executable binary and returns the +/// first match, or `None` if not found. +pub fn find_binary(binary_name: &str) -> Option<PathBuf> { + env::var_os("PATH").and_then(|path_env| { + for mut path in env::split_paths(&path_env) { + path.push(binary_name); + if is_binary(&path) { + return Some(path); + } + } + None + }) +} diff --git a/testing/mozbase/rust/mozrunner/src/runner.rs b/testing/mozbase/rust/mozrunner/src/runner.rs new file mode 100644 index 0000000000..b3deafe27b --- /dev/null +++ b/testing/mozbase/rust/mozrunner/src/runner.rs @@ -0,0 +1,570 @@ +/* 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 mozprofile::prefreader::PrefReaderError; +use mozprofile::profile::Profile; +use std::collections::HashMap; +use std::convert::From; +use std::error::Error; +use std::ffi::{OsStr, OsString}; +use std::fmt; +use std::io; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::process; +use std::process::{Child, Command, Stdio}; +use std::thread; +use std::time; + +use crate::firefox_args::Arg; + +pub trait Runner { + type Process; + + fn arg<S>(&mut self, arg: S) -> &mut Self + where + S: AsRef<OsStr>; + + fn args<I, S>(&mut self, args: I) -> &mut Self + where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>; + + fn env<K, V>(&mut self, key: K, value: V) -> &mut Self + where + K: AsRef<OsStr>, + V: AsRef<OsStr>; + + fn envs<I, K, V>(&mut self, envs: I) -> &mut Self + where + I: IntoIterator<Item = (K, V)>, + K: AsRef<OsStr>, + V: AsRef<OsStr>; + + fn stdout<T>(&mut self, stdout: T) -> &mut Self + where + T: Into<Stdio>; + + fn stderr<T>(&mut self, stderr: T) -> &mut Self + where + T: Into<Stdio>; + + fn start(self) -> Result<Self::Process, RunnerError>; +} + +pub trait RunnerProcess { + /// Attempts to collect the exit status of the process if it has already exited. + /// + /// This function will not block the calling thread and will only advisorily check to see if + /// the child process has exited or not. If the process has exited then on Unix the process ID + /// is reaped. This function is guaranteed to repeatedly return a successful exit status so + /// long as the child has already exited. + /// + /// If the process has exited, then `Ok(Some(status))` is returned. If the exit status is not + /// available at this time then `Ok(None)` is returned. If an error occurs, then that error is + /// returned. + fn try_wait(&mut self) -> io::Result<Option<process::ExitStatus>>; + + /// Waits for the process to exit completely, killing it if it does not stop within `timeout`, + /// and returns the status that it exited with. + /// + /// Firefox' integrated background monitor observes long running threads during shutdown and + /// kills these after 63 seconds. If the process fails to exit within the duration of + /// `timeout`, it is forcefully killed. + /// + /// This function will continue to have the same return value after it has been called at least + /// once. + fn wait(&mut self, timeout: time::Duration) -> io::Result<process::ExitStatus>; + + /// Determine if the process is still running. + fn running(&mut self) -> bool; + + /// Forces the process to exit and returns the exit status. This is + /// equivalent to sending a SIGKILL on Unix platforms. + fn kill(&mut self) -> io::Result<process::ExitStatus>; +} + +#[derive(Debug)] +pub enum RunnerError { + Io(io::Error), + PrefReader(PrefReaderError), +} + +impl fmt::Display for RunnerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + RunnerError::Io(ref err) => match err.kind() { + ErrorKind::NotFound => "no such file or directory".fmt(f), + _ => err.fmt(f), + }, + RunnerError::PrefReader(ref err) => err.fmt(f), + } + } +} + +impl Error for RunnerError { + fn cause(&self) -> Option<&dyn Error> { + Some(match *self { + RunnerError::Io(ref err) => err as &dyn Error, + RunnerError::PrefReader(ref err) => err as &dyn Error, + }) + } +} + +impl From<io::Error> for RunnerError { + fn from(value: io::Error) -> RunnerError { + RunnerError::Io(value) + } +} + +impl From<PrefReaderError> for RunnerError { + fn from(value: PrefReaderError) -> RunnerError { + RunnerError::PrefReader(value) + } +} + +#[derive(Debug)] +pub struct FirefoxProcess { + process: Child, + // The profile field is not directly used, but it is kept to avoid its + // Drop removing the (temporary) profile directory. + #[allow(dead_code)] + profile: Option<Profile>, +} + +impl RunnerProcess for FirefoxProcess { + fn try_wait(&mut self) -> io::Result<Option<process::ExitStatus>> { + self.process.try_wait() + } + + fn wait(&mut self, timeout: time::Duration) -> io::Result<process::ExitStatus> { + let start = time::Instant::now(); + loop { + match self.try_wait() { + // child has already exited, reap its exit code + Ok(Some(status)) => return Ok(status), + + // child still running and timeout elapsed, kill it + Ok(None) if start.elapsed() >= timeout => return self.kill(), + + // child still running, let's give it more time + Ok(None) => thread::sleep(time::Duration::from_millis(100)), + + Err(e) => return Err(e), + } + } + } + + fn running(&mut self) -> bool { + self.try_wait().unwrap().is_none() + } + + fn kill(&mut self) -> io::Result<process::ExitStatus> { + match self.try_wait() { + // child has already exited, reap its exit code + Ok(Some(status)) => Ok(status), + + // child still running, kill it + Ok(None) => { + debug!("Killing process {}", self.process.id()); + self.process.kill()?; + self.process.wait() + } + + Err(e) => Err(e), + } + } +} + +#[derive(Debug)] +pub struct FirefoxRunner { + path: PathBuf, + profile: Option<Profile>, + args: Vec<OsString>, + envs: HashMap<OsString, OsString>, + stdout: Option<Stdio>, + stderr: Option<Stdio>, +} + +impl FirefoxRunner { + /// Initialise Firefox process runner. + /// + /// On macOS, `path` can optionally point to an application bundle, + /// i.e. _/Applications/Firefox.app_, as well as to an executable program + /// such as _/Applications/Firefox.app/Content/MacOS/firefox-bin_. + pub fn new(path: &Path, profile: Option<Profile>) -> FirefoxRunner { + let mut envs: HashMap<OsString, OsString> = HashMap::new(); + envs.insert("MOZ_NO_REMOTE".into(), "1".into()); + + FirefoxRunner { + path: path.to_path_buf(), + envs, + profile, + args: vec![], + stdout: None, + stderr: None, + } + } +} + +impl Runner for FirefoxRunner { + type Process = FirefoxProcess; + + fn arg<S>(&mut self, arg: S) -> &mut FirefoxRunner + where + S: AsRef<OsStr>, + { + self.args.push((&arg).into()); + self + } + + fn args<I, S>(&mut self, args: I) -> &mut FirefoxRunner + where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>, + { + for arg in args { + self.args.push((&arg).into()); + } + self + } + + fn env<K, V>(&mut self, key: K, value: V) -> &mut FirefoxRunner + where + K: AsRef<OsStr>, + V: AsRef<OsStr>, + { + self.envs.insert((&key).into(), (&value).into()); + self + } + + fn envs<I, K, V>(&mut self, envs: I) -> &mut FirefoxRunner + where + I: IntoIterator<Item = (K, V)>, + K: AsRef<OsStr>, + V: AsRef<OsStr>, + { + for (key, value) in envs { + self.envs.insert((&key).into(), (&value).into()); + } + self + } + + fn stdout<T>(&mut self, stdout: T) -> &mut Self + where + T: Into<Stdio>, + { + self.stdout = Some(stdout.into()); + self + } + + fn stderr<T>(&mut self, stderr: T) -> &mut Self + where + T: Into<Stdio>, + { + self.stderr = Some(stderr.into()); + self + } + + fn start(mut self) -> Result<FirefoxProcess, RunnerError> { + if let Some(ref mut profile) = self.profile { + profile.user_prefs()?.write()?; + } + + let stdout = self.stdout.unwrap_or_else(Stdio::inherit); + let stderr = self.stderr.unwrap_or_else(Stdio::inherit); + + let binary_path = platform::resolve_binary_path(&mut self.path); + let mut cmd = Command::new(binary_path); + cmd.args(&self.args[..]) + .envs(&self.envs) + .stdout(stdout) + .stderr(stderr); + + let mut seen_foreground = false; + let mut seen_no_remote = false; + let mut seen_profile = false; + for arg in self.args.iter() { + match arg.into() { + Arg::Foreground => seen_foreground = true, + Arg::NoRemote => seen_no_remote = true, + Arg::Profile | Arg::NamedProfile | Arg::ProfileManager => seen_profile = true, + Arg::Marionette + | Arg::None + | Arg::Other(_) + | Arg::RemoteAllowHosts + | Arg::RemoteAllowOrigins + | Arg::RemoteDebuggingPort => {} + } + } + // -foreground is only supported on Mac, and shouldn't be passed + // to Firefox on other platforms (bug 1720502). + if cfg!(target_os = "macos") && !seen_foreground { + cmd.arg("-foreground"); + } + if !seen_no_remote { + cmd.arg("-no-remote"); + } + if let Some(ref profile) = self.profile { + if !seen_profile { + cmd.arg("-profile").arg(&profile.path); + } + } + + info!("Running command: {:?}", cmd); + let process = cmd.spawn()?; + Ok(FirefoxProcess { + process, + profile: self.profile, + }) + } +} + +#[cfg(all(not(target_os = "macos"), unix))] +pub mod platform { + use crate::path::find_binary; + use std::path::PathBuf; + + pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf { + path + } + + fn running_as_snap() -> bool { + std::env::var("SNAP_INSTANCE_NAME") + .or_else(|_| { + // Compatibility for snapd <= 2.35 + std::env::var("SNAP_NAME") + }) + .map(|name| !name.is_empty()) + .unwrap_or(false) + } + + /// Searches the system path for `firefox`. + pub fn firefox_default_path() -> Option<PathBuf> { + if running_as_snap() { + return Some(PathBuf::from("/snap/firefox/current/firefox.launcher")); + } + find_binary("firefox") + } + + pub fn arg_prefix_char(c: char) -> bool { + c == '-' + } + + #[cfg(test)] + mod tests { + use crate::firefox_default_path; + use std::env; + use std::ops::Drop; + use std::path::PathBuf; + + static SNAP_KEY: &str = "SNAP_INSTANCE_NAME"; + static SNAP_LEGACY_KEY: &str = "SNAP_NAME"; + + struct SnapEnvironment { + initial_environment: (Option<String>, Option<String>), + } + + impl SnapEnvironment { + fn new() -> SnapEnvironment { + SnapEnvironment { + initial_environment: (env::var(SNAP_KEY).ok(), env::var(SNAP_LEGACY_KEY).ok()), + } + } + + fn set(&self, value: Option<String>, legacy_value: Option<String>) { + fn set_env(key: &str, value: Option<String>) { + match value { + Some(value) => env::set_var(key, value), + None => env::remove_var(key), + } + } + set_env(SNAP_KEY, value); + set_env(SNAP_LEGACY_KEY, legacy_value); + } + } + + impl Drop for SnapEnvironment { + fn drop(&mut self) { + self.set( + self.initial_environment.0.clone(), + self.initial_environment.1.clone(), + ) + } + } + + #[test] + fn test_default_path() { + let snap_path = Some(PathBuf::from("/snap/firefox/current/firefox.launcher")); + + let snap_env = SnapEnvironment::new(); + + snap_env.set(None, None); + assert_ne!(firefox_default_path(), snap_path); + + snap_env.set(Some("value".into()), None); + assert_eq!(firefox_default_path(), snap_path); + + snap_env.set(None, Some("value".into())); + assert_eq!(firefox_default_path(), snap_path); + } + } +} + +#[cfg(target_os = "macos")] +pub mod platform { + use crate::path::{find_binary, is_binary}; + use dirs; + use plist::Value; + use std::path::PathBuf; + + /// Searches for the binary file inside the path passed as parameter. + /// If the binary is not found, the path remains unaltered. + /// Else, it gets updated by the new binary path. + pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf { + if path.as_path().is_dir() { + let mut info_plist = path.clone(); + info_plist.push("Contents"); + info_plist.push("Info.plist"); + if let Ok(plist) = Value::from_file(&info_plist) { + if let Some(dict) = plist.as_dictionary() { + if let Some(Value::String(s)) = dict.get("CFBundleExecutable") { + path.push("Contents"); + path.push("MacOS"); + path.push(s); + } + } + } + } + path + } + + /// Searches the system path for `firefox-bin`, then looks for + /// `Applications/Firefox.app/Contents/MacOS/firefox-bin` as well + /// as `Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin` + /// under both `/` (system root) and the user home directory. + pub fn firefox_default_path() -> Option<PathBuf> { + if let Some(path) = find_binary("firefox-bin") { + return Some(path); + } + + let home = dirs::home_dir(); + for &(prefix_home, trial_path) in [ + ( + false, + "/Applications/Firefox.app/Contents/MacOS/firefox-bin", + ), + (true, "Applications/Firefox.app/Contents/MacOS/firefox-bin"), + ( + false, + "/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin", + ), + ( + true, + "Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin", + ), + ] + .iter() + { + let path = match (home.as_ref(), prefix_home) { + (Some(home_dir), true) => home_dir.join(trial_path), + (None, true) => continue, + (_, false) => PathBuf::from(trial_path), + }; + if is_binary(&path) { + return Some(path); + } + } + + None + } + + pub fn arg_prefix_char(c: char) -> bool { + c == '-' + } +} + +#[cfg(target_os = "windows")] +pub mod platform { + use crate::path::{find_binary, is_binary}; + use std::io::Error; + use std::path::PathBuf; + use winreg::enums::*; + use winreg::RegKey; + + pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf { + path + } + + /// Searches the Windows registry, then the system path for `firefox.exe`. + /// + /// It _does not_ currently check the `HKEY_CURRENT_USER` tree. + pub fn firefox_default_path() -> Option<PathBuf> { + if let Ok(Some(path)) = firefox_registry_path() { + if is_binary(&path) { + return Some(path); + } + }; + find_binary("firefox.exe") + } + + fn firefox_registry_path() -> Result<Option<PathBuf>, Error> { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + for subtree_key in ["SOFTWARE", "SOFTWARE\\WOW6432Node"].iter() { + let subtree = hklm.open_subkey_with_flags(subtree_key, KEY_READ)?; + let mozilla_org = match subtree.open_subkey_with_flags("mozilla.org\\Mozilla", KEY_READ) + { + Ok(val) => val, + Err(_) => continue, + }; + let current_version: String = mozilla_org.get_value("CurrentVersion")?; + let mozilla = subtree.open_subkey_with_flags("Mozilla", KEY_READ)?; + for key_res in mozilla.enum_keys() { + let key = key_res?; + let section_data = mozilla.open_subkey_with_flags(&key, KEY_READ)?; + let version: Result<String, _> = section_data.get_value("GeckoVer"); + if let Ok(ver) = version { + if ver == current_version { + let mut bin_key = key.to_owned(); + bin_key.push_str("\\bin"); + if let Ok(bin_subtree) = mozilla.open_subkey_with_flags(bin_key, KEY_READ) { + let path_to_exe: Result<String, _> = bin_subtree.get_value("PathToExe"); + if let Ok(path_to_exe) = path_to_exe { + let path = PathBuf::from(path_to_exe); + if is_binary(&path) { + return Ok(Some(path)); + } + } + } + } + } + } + } + Ok(None) + } + + pub fn arg_prefix_char(c: char) -> bool { + c == '/' || c == '-' + } +} + +#[cfg(not(any(unix, target_os = "windows")))] +pub mod platform { + use std::path::PathBuf; + + /// Returns an unaltered path for all operating systems other than macOS. + pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf { + path + } + + /// Returns `None` for all other operating systems than Linux, macOS, and + /// Windows. + pub fn firefox_default_path() -> Option<PathBuf> { + None + } + + pub fn arg_prefix_char(c: char) -> bool { + c == '-' + } +} |