Bug 1429511 - Move Rust port of mozrunner to central. r?ahal draft
authorAndreas Tolfsen <ato@sny.no>
Fri, 12 Jan 2018 15:31:20 +0000
changeset 722158 41315e1e17aea38007429a98cfa5f998fbb075a6
parent 722069 abbe0ca5d9693e06484fcb52bbcac189c3461f50
child 722159 fdca9c450a48ad723623ba98d6a1c44906ed3984
push id96064
push userbmo:ato@sny.no
push dateThu, 18 Jan 2018 14:41:40 +0000
reviewersahal
bugs1429511
milestone59.0a1
Bug 1429511 - Move Rust port of mozrunner to central. r?ahal This moves the Rust crate mozrunner into central from GitHub. The old repository will be graveyarded: https://github.com/jgraham/rust_mozrunner The git history is not considered important, hence this does not overlay that onto central like we did for testing/geckodriver and testing/webdriver. MozReview-Commit-ID: J4ZYdow2Lkw
.gitignore
testing/mozbase/rust/mozrunner/Cargo.toml
testing/mozbase/rust/mozrunner/moz.build
testing/mozbase/rust/mozrunner/src/bin/firefox-default-path.rs
testing/mozbase/rust/mozrunner/src/lib.rs
testing/mozbase/rust/mozrunner/src/runner.rs
--- a/.gitignore
+++ b/.gitignore
@@ -99,16 +99,20 @@ testing/web-platform/products/
 # Android Gradle artifacts.
 mobile/android/gradle/.gradle
 
 # XCode project cruft
 /*.xcodeproj/
 embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/project.xcworkspace/xcuserdata
 embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/xcuserdata
 
+# Rust port of mozbase are libraries
+testing/mozbase/rust/*/target
+testing/mozbase/rust/*/Cargo.lock
+
 # Ignore mozharness execution files
 testing/mozharness/.tox/
 testing/mozharness/build/
 testing/mozharness/logs/
 testing/mozharness/.coverage
 testing/mozharness/nosetests.xml
 
 # Ignore ESLint node_modules
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/rust/mozrunner/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "mozrunner"
+version = "0.5.0"
+authors = ["Mozilla Tools and Automation <auto-tools@mozilla.com>"]
+description = "Library for starting Firefox binaries."
+repository = "https://github.com/jgraham/rust_mozrunner"
+license = "MPL-2.0"
+
+[dependencies]
+log = "0.3"
+mozprofile = "0.3"
+
+[target.'cfg(target_os = "windows")'.dependencies]
+winreg = "0.3.5"
+
+[[bin]]
+name = "firefox-default-path"
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/rust/mozrunner/moz.build
@@ -0,0 +1,6 @@
+# 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/.
+
+with Files("**"):
+    BUG_COMPONENT = ("Testing", "Mozbase Rust")
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/rust/mozrunner/src/bin/firefox-default-path.rs
@@ -0,0 +1,17 @@
+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<Write> = match code {
+        0 => Box::new(std::io::stdout()),
+        _ => Box::new(std::io::stderr())
+    };
+    writeln!(&mut writer, "{}", &*path).unwrap();
+    std::process::exit(code);
+}
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/rust/mozrunner/src/lib.rs
@@ -0,0 +1,8 @@
+#[macro_use] extern crate log;
+extern crate mozprofile;
+#[cfg(target_os = "windows")]
+extern crate winreg;
+
+pub mod runner;
+
+pub use runner::platform::firefox_default_path;
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/rust/mozrunner/src/runner.rs
@@ -0,0 +1,387 @@
+use mozprofile::prefreader::PrefReaderError;
+use mozprofile::profile::Profile;
+use std::ascii::AsciiExt;
+use std::collections::HashMap;
+use std::convert::From;
+use std::env;
+use std::error::Error;
+use std::fmt;
+use std::io::{Result as IoResult, Error as IoError, ErrorKind};
+use std::path::{Path, PathBuf};
+use std::process;
+use std::process::{Command, Stdio};
+
+pub trait Runner {
+    fn args(&mut self) -> &mut Vec<String>;
+    fn build_command(&self, &mut Command);
+    fn envs(&mut self) -> &mut HashMap<String, String>;
+    fn is_running(&mut self) -> bool;
+    fn start(&mut self) -> Result<(), RunnerError>;
+    fn status(&mut self) -> IoResult<Option<process::ExitStatus>>;
+    fn stop(&mut self) -> IoResult<Option<process::ExitStatus>>;
+}
+
+#[derive(Debug)]
+pub enum RunnerError {
+    Io(IoError),
+    PrefReader(PrefReaderError),
+}
+
+impl fmt::Display for RunnerError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.description().fmt(f)
+    }
+}
+
+impl Error for RunnerError {
+    fn description(&self) -> &str {
+        match *self {
+            RunnerError::Io(ref err) => {
+                match err.kind() {
+                    ErrorKind::NotFound => "no such file or directory",
+                    _ => err.description(),
+                }
+            }
+            RunnerError::PrefReader(ref err) => err.description(),
+        }
+    }
+
+    fn cause(&self) -> Option<&Error> {
+        Some(match *self {
+            RunnerError::Io(ref err) => err as &Error,
+            RunnerError::PrefReader(ref err) => err as &Error,
+        })
+    }
+}
+
+impl From<IoError> for RunnerError {
+    fn from(value: IoError) -> RunnerError {
+        RunnerError::Io(value)
+    }
+}
+
+impl From<PrefReaderError> for RunnerError {
+    fn from(value: PrefReaderError) -> RunnerError {
+        RunnerError::PrefReader(value)
+    }
+}
+
+#[derive(Debug)]
+pub struct FirefoxRunner {
+    pub binary: PathBuf,
+    args: Vec<String>,
+    envs: HashMap<String, String>,
+    process: Option<process::Child>,
+    pub profile: Profile
+}
+
+impl FirefoxRunner {
+    pub fn new(binary: &Path, profile: Option<Profile>) -> IoResult<FirefoxRunner> {
+        let prof = match profile {
+            Some(p) => p,
+            None => try!(Profile::new(None))
+        };
+
+        let mut envs = HashMap::new();
+        envs.insert("MOZ_NO_REMOTE".to_string(), "1".to_string());
+        envs.insert("NO_EM_RESTART".to_string(), "1".to_string());
+
+        Ok(FirefoxRunner {
+            binary: binary.to_path_buf(),
+            process: None,
+            args: Vec::new(),
+            envs: envs,
+            profile: prof
+        })
+    }
+}
+
+impl Runner for FirefoxRunner {
+    fn args(&mut self) -> &mut Vec<String> {
+        &mut self.args
+    }
+
+    fn build_command(&self, command: &mut Command) {
+        command
+            .args(&self.args[..])
+            .envs(&self.envs);
+
+        if !self.args.iter().any(|x| is_profile_arg(x)) {
+            command.arg("-profile").arg(&self.profile.path);
+        }
+        command.stdout(Stdio::inherit())
+            .stderr(Stdio::inherit());
+    }
+
+    fn envs(&mut self) -> &mut HashMap<String, String> {
+        &mut self.envs
+    }
+
+    fn is_running(&mut self) -> bool {
+        self.process.is_some() && self.status().unwrap().is_none()
+    }
+
+    fn start(&mut self) -> Result<(), RunnerError> {
+        let mut cmd = Command::new(&self.binary);
+        self.build_command(&mut cmd);
+
+        let prefs = try!(self.profile.user_prefs());
+        try!(prefs.write());
+
+        info!("Running command: {:?}", cmd);
+        let process = try!(cmd.spawn());
+        self.process = Some(process);
+        Ok(())
+    }
+
+    fn status(&mut self) -> IoResult<Option<process::ExitStatus>> {
+        self.process.as_mut().map(|p| p.try_wait()).unwrap_or(Ok(None))
+    }
+
+    fn stop(&mut self) -> IoResult<Option<process::ExitStatus>> {
+        let mut retval = None;
+
+        if let Some(ref mut p) = self.process {
+            try!(p.kill());
+            retval = Some(try!(p.wait()));
+        };
+        Ok(retval)
+    }
+}
+
+fn parse_arg_name(arg: &str) -> Option<&str> {
+    let mut start = 0;
+    let mut end = 0;
+
+    for (i, c) in arg.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 != '-' {
+                start = i;
+                end = start + 1;
+            } else {
+                start = i + 1;
+                end = start;
+            }
+        } else {
+            end += 1;
+            if name_end_char(c) {
+                end -= 1;
+                break;
+            }
+        }
+    }
+
+    if start > 0 && end > start {
+        Some(&arg[start..end])
+    } else {
+        None
+    }
+}
+
+fn name_end_char(c: char) -> bool {
+    c == ' ' || c == '='
+}
+
+/// Check if an argument string affects the Firefox profile
+///
+/// Returns a boolean indicating whether a given string
+/// contains one of the `-P`, `-Profile` or `-ProfileManager`
+/// arguments, respecting the various platform-specific conventions.
+pub fn is_profile_arg(arg: &str) -> bool {
+    if let Some(name) = parse_arg_name(arg) {
+        name.eq_ignore_ascii_case("profile") ||
+            name.eq_ignore_ascii_case("p") ||
+            name.eq_ignore_ascii_case("profilemanager")
+    } else {
+        false
+    }
+}
+
+fn find_binary(name: &str) -> Option<PathBuf> {
+    env::var("PATH")
+        .ok()
+        .and_then(|path_env| {
+            for mut path in env::split_paths(&*path_env) {
+                path.push(name);
+                if path.exists() {
+                    return Some(path)
+                }
+            }
+            None
+        })
+}
+
+#[cfg(target_os = "linux")]
+pub mod platform {
+    use super::find_binary;
+    use std::path::PathBuf;
+
+    pub fn firefox_default_path() -> Option<PathBuf> {
+        find_binary("firefox")
+    }
+
+    pub fn arg_prefix_char(c: char) -> bool {
+        c == '-'
+    }
+}
+
+#[cfg(target_os = "macos")]
+pub mod platform {
+    use super::find_binary;
+    use std::env;
+    use std::path::PathBuf;
+
+    pub fn firefox_default_path() -> Option<PathBuf> {
+        if let Some(path) = find_binary("firefox-bin") {
+            return Some(path)
+        }
+        let home = env::home_dir();
+        for &(prefix_home, trial_path) in [
+            (false, "/Applications/Firefox.app/Contents/MacOS/firefox-bin"),
+            (true, "Applications/Firefox.app/Contents/MacOS/firefox-bin")].iter() {
+            let path = match (home.as_ref(), prefix_home) {
+                (Some(ref home_dir), true) => home_dir.join(trial_path),
+                (None, true) => continue,
+                (_, false) => PathBuf::from(trial_path)
+            };
+            if path.exists() {
+                return Some(path)
+            }
+        }
+        None
+    }
+
+    pub fn arg_prefix_char(c: char) -> bool {
+        c == '-'
+    }
+}
+
+#[cfg(target_os = "windows")]
+pub mod platform {
+    use super::find_binary;
+    use std::io::Error;
+    use std::path::PathBuf;
+    use winreg::RegKey;
+    use winreg::enums::*;
+
+    pub fn firefox_default_path() -> Option<PathBuf> {
+        let opt_path = firefox_registry_path().unwrap_or(None);
+        if let Some(path) = opt_path {
+            if path.exists() {
+                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 = try!(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 = try!(mozilla_org.get_value("CurrentVersion"));
+            let mozilla = try!(subtree.open_subkey_with_flags("Mozilla", KEY_READ));
+            for key_res in mozilla.enum_keys() {
+                let key = try!(key_res);
+                let section_data = try!(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: Result<String, _> = bin_subtree.get_value("PathToExe");
+                            if let Ok(path) = path {
+                                return Ok(Some(PathBuf::from(path)))
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        Ok(None)
+    }
+
+    pub fn arg_prefix_char(c: char) -> bool {
+        c == '/' || c == '-'
+    }
+}
+
+#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
+pub mod platform {
+    use std::path::PathBuf;
+
+    pub fn firefox_default_path() -> Option<PathBuf> {
+        None
+    }
+
+    pub fn arg_prefix_char(c: char) -> bool {
+        c == '-'
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{parse_arg_name, is_profile_arg};
+
+    fn parse(arg: &str, name: Option<&str>) {
+        let result = parse_arg_name(arg);
+        assert_eq!(result, name);
+    }
+
+    #[test]
+    fn test_parse_arg_name() {
+        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"));
+    }
+
+    #[cfg(target_os = "windows")]
+    #[test]
+    fn test_parse_arg_name_windows() {
+        parse("/profile", Some("profile"));
+    }
+
+    #[cfg(not(target_os = "windows"))]
+    #[test]
+    fn test_parse_arg_name_non_windows() {
+        parse("/profile", None);
+    }
+
+    #[test]
+    fn test_is_profile_arg() {
+        assert!(is_profile_arg("--profile"));
+        assert!(is_profile_arg("-p"));
+        assert!(is_profile_arg("-PROFILEMANAGER"));
+        assert!(is_profile_arg("-ProfileMANAGER"));
+        assert!(!is_profile_arg("-- profile"));
+        assert!(!is_profile_arg("-profiled"));
+        assert!(!is_profile_arg("-p1"));
+        assert!(is_profile_arg("-p test"));
+        assert!(is_profile_arg("-profile /foo"));
+    }
+}