summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/rust/mozrunner
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /testing/mozbase/rust/mozrunner
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/mozbase/rust/mozrunner')
-rw-r--r--testing/mozbase/rust/mozrunner/Cargo.toml27
-rw-r--r--testing/mozbase/rust/mozrunner/src/bin/firefox-default-path.rs21
-rw-r--r--testing/mozbase/rust/mozrunner/src/firefox_args.rs384
-rw-r--r--testing/mozbase/rust/mozrunner/src/lib.rs20
-rw-r--r--testing/mozbase/rust/mozrunner/src/path.rs48
-rw-r--r--testing/mozbase/rust/mozrunner/src/runner.rs570
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 == '-'
+ }
+}