tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

runner.rs (16836B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 use mozprofile::prefreader::PrefReaderError;
      6 use mozprofile::profile::Profile;
      7 use std::collections::HashMap;
      8 use std::ffi::{OsStr, OsString};
      9 use std::io;
     10 use std::path::{Path, PathBuf};
     11 use std::process;
     12 use std::process::{Child, Command, Stdio};
     13 use std::thread;
     14 use std::time;
     15 use thiserror::Error;
     16 
     17 use crate::firefox_args::Arg;
     18 
     19 pub trait Runner {
     20    type Process;
     21 
     22    fn arg<S>(&mut self, arg: S) -> &mut Self
     23    where
     24        S: AsRef<OsStr>;
     25 
     26    fn args<I, S>(&mut self, args: I) -> &mut Self
     27    where
     28        I: IntoIterator<Item = S>,
     29        S: AsRef<OsStr>;
     30 
     31    fn env<K, V>(&mut self, key: K, value: V) -> &mut Self
     32    where
     33        K: AsRef<OsStr>,
     34        V: AsRef<OsStr>;
     35 
     36    fn envs<I, K, V>(&mut self, envs: I) -> &mut Self
     37    where
     38        I: IntoIterator<Item = (K, V)>,
     39        K: AsRef<OsStr>,
     40        V: AsRef<OsStr>;
     41 
     42    fn stdout<T>(&mut self, stdout: T) -> &mut Self
     43    where
     44        T: Into<Stdio>;
     45 
     46    fn stderr<T>(&mut self, stderr: T) -> &mut Self
     47    where
     48        T: Into<Stdio>;
     49 
     50    fn start(self) -> Result<Self::Process, RunnerError>;
     51 }
     52 
     53 pub trait RunnerProcess {
     54    /// Attempts to collect the exit status of the process if it has already exited.
     55    ///
     56    /// This function will not block the calling thread and will only advisorily check to see if
     57    /// the child process has exited or not.  If the process has exited then on Unix the process ID
     58    /// is reaped.  This function is guaranteed to repeatedly return a successful exit status so
     59    /// long as the child has already exited.
     60    ///
     61    /// If the process has exited, then `Ok(Some(status))` is returned.  If the exit status is not
     62    /// available at this time then `Ok(None)` is returned.  If an error occurs, then that error is
     63    /// returned.
     64    fn try_wait(&mut self) -> io::Result<Option<process::ExitStatus>>;
     65 
     66    /// Waits for the process to exit completely, killing it if it does not stop within `timeout`,
     67    /// and returns the status that it exited with.
     68    ///
     69    /// Firefox' integrated background monitor observes long running threads during shutdown and
     70    /// kills these after 63 seconds.  If the process fails to exit within the duration of
     71    /// `timeout`, it is forcefully killed.
     72    ///
     73    /// This function will continue to have the same return value after it has been called at least
     74    /// once.
     75    fn wait(&mut self, timeout: time::Duration) -> io::Result<process::ExitStatus>;
     76 
     77    /// Determine if the process is still running.
     78    fn running(&mut self) -> bool;
     79 
     80    /// Forces the process to exit and returns the exit status.  This is
     81    /// equivalent to sending a SIGKILL on Unix platforms.
     82    fn kill(&mut self) -> io::Result<process::ExitStatus>;
     83 }
     84 
     85 #[derive(Debug, Error)]
     86 pub enum RunnerError {
     87    #[error("IO Error: {0}")]
     88    Io(#[from] io::Error),
     89    #[error("PrefReader Error: {0}")]
     90    PrefReader(#[from] PrefReaderError),
     91 }
     92 
     93 #[derive(Debug)]
     94 pub struct FirefoxProcess {
     95    process: Child,
     96    // The profile field is not directly used, but it is kept to avoid its
     97    // Drop removing the (temporary) profile directory.
     98    #[allow(dead_code)]
     99    profile: Option<Profile>,
    100 }
    101 
    102 impl RunnerProcess for FirefoxProcess {
    103    fn try_wait(&mut self) -> io::Result<Option<process::ExitStatus>> {
    104        self.process.try_wait()
    105    }
    106 
    107    fn wait(&mut self, timeout: time::Duration) -> io::Result<process::ExitStatus> {
    108        let start = time::Instant::now();
    109        loop {
    110            match self.try_wait() {
    111                // child has already exited, reap its exit code
    112                Ok(Some(status)) => return Ok(status),
    113 
    114                // child still running and timeout elapsed, kill it
    115                Ok(None) if start.elapsed() >= timeout => return self.kill(),
    116 
    117                // child still running, let's give it more time
    118                Ok(None) => thread::sleep(time::Duration::from_millis(100)),
    119 
    120                Err(e) => return Err(e),
    121            }
    122        }
    123    }
    124 
    125    fn running(&mut self) -> bool {
    126        self.try_wait().unwrap().is_none()
    127    }
    128 
    129    fn kill(&mut self) -> io::Result<process::ExitStatus> {
    130        match self.try_wait() {
    131            // child has already exited, reap its exit code
    132            Ok(Some(status)) => Ok(status),
    133 
    134            // child still running, kill it
    135            Ok(None) => {
    136                debug!("Killing process {}", self.process.id());
    137                self.process.kill()?;
    138                self.process.wait()
    139            }
    140 
    141            Err(e) => Err(e),
    142        }
    143    }
    144 }
    145 
    146 #[derive(Debug)]
    147 pub struct FirefoxRunner {
    148    path: PathBuf,
    149    profile: Option<Profile>,
    150    args: Vec<OsString>,
    151    envs: HashMap<OsString, OsString>,
    152    stdout: Option<Stdio>,
    153    stderr: Option<Stdio>,
    154 }
    155 
    156 impl FirefoxRunner {
    157    /// Initialize Firefox process runner.
    158    ///
    159    /// On macOS, `path` can optionally point to an application bundle,
    160    /// i.e. _/Applications/Firefox.app_, as well as to an executable program
    161    /// such as _/Applications/Firefox.app/Content/MacOS/firefox_.
    162    pub fn new(path: &Path, profile: Option<Profile>) -> FirefoxRunner {
    163        FirefoxRunner {
    164            path: path.to_path_buf(),
    165            envs: HashMap::new(),
    166            profile,
    167            args: vec![],
    168            stdout: None,
    169            stderr: None,
    170        }
    171    }
    172 }
    173 
    174 impl Runner for FirefoxRunner {
    175    type Process = FirefoxProcess;
    176 
    177    fn arg<S>(&mut self, arg: S) -> &mut FirefoxRunner
    178    where
    179        S: AsRef<OsStr>,
    180    {
    181        self.args.push((&arg).into());
    182        self
    183    }
    184 
    185    fn args<I, S>(&mut self, args: I) -> &mut FirefoxRunner
    186    where
    187        I: IntoIterator<Item = S>,
    188        S: AsRef<OsStr>,
    189    {
    190        for arg in args {
    191            self.args.push((&arg).into());
    192        }
    193        self
    194    }
    195 
    196    fn env<K, V>(&mut self, key: K, value: V) -> &mut FirefoxRunner
    197    where
    198        K: AsRef<OsStr>,
    199        V: AsRef<OsStr>,
    200    {
    201        self.envs.insert((&key).into(), (&value).into());
    202        self
    203    }
    204 
    205    fn envs<I, K, V>(&mut self, envs: I) -> &mut FirefoxRunner
    206    where
    207        I: IntoIterator<Item = (K, V)>,
    208        K: AsRef<OsStr>,
    209        V: AsRef<OsStr>,
    210    {
    211        for (key, value) in envs {
    212            self.envs.insert((&key).into(), (&value).into());
    213        }
    214        self
    215    }
    216 
    217    fn stdout<T>(&mut self, stdout: T) -> &mut Self
    218    where
    219        T: Into<Stdio>,
    220    {
    221        self.stdout = Some(stdout.into());
    222        self
    223    }
    224 
    225    fn stderr<T>(&mut self, stderr: T) -> &mut Self
    226    where
    227        T: Into<Stdio>,
    228    {
    229        self.stderr = Some(stderr.into());
    230        self
    231    }
    232 
    233    fn start(mut self) -> Result<FirefoxProcess, RunnerError> {
    234        if let Some(ref mut profile) = self.profile {
    235            profile.user_prefs()?.write()?;
    236        }
    237 
    238        let stdout = self.stdout.unwrap_or_else(Stdio::inherit);
    239        let stderr = self.stderr.unwrap_or_else(Stdio::inherit);
    240 
    241        let binary_path = platform::resolve_binary_path(&mut self.path);
    242        let mut cmd = Command::new(binary_path);
    243        cmd.args(&self.args[..])
    244            .envs(&self.envs)
    245            .stdout(stdout)
    246            .stderr(stderr);
    247 
    248        let mut seen_foreground = false;
    249        let mut seen_no_remote = false;
    250        let mut seen_profile = false;
    251        for arg in self.args.iter() {
    252            match arg.into() {
    253                Arg::Foreground => seen_foreground = true,
    254                Arg::NoRemote => seen_no_remote = true,
    255                Arg::Profile | Arg::NamedProfile | Arg::ProfileManager => seen_profile = true,
    256                Arg::Marionette
    257                | Arg::None
    258                | Arg::Other(_)
    259                | Arg::RemoteAllowHosts
    260                | Arg::RemoteAllowOrigins
    261                | Arg::RemoteDebuggingPort => {}
    262            }
    263        }
    264        // -foreground is only supported on Mac, and shouldn't be passed
    265        // to Firefox on other platforms (bug 1720502).
    266        if cfg!(target_os = "macos") && !seen_foreground {
    267            cmd.arg("-foreground");
    268        }
    269        if !seen_no_remote {
    270            cmd.arg("-no-remote");
    271        }
    272        if let Some(ref profile) = self.profile {
    273            if !seen_profile {
    274                cmd.arg("-profile").arg(&profile.path);
    275            }
    276        }
    277 
    278        info!("Running command: {:?}", cmd);
    279        let process = cmd.spawn()?;
    280        Ok(FirefoxProcess {
    281            process,
    282            profile: self.profile,
    283        })
    284    }
    285 }
    286 
    287 #[cfg(all(not(target_os = "macos"), unix))]
    288 pub mod platform {
    289    use crate::path::find_binary;
    290    use std::path::PathBuf;
    291 
    292    pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf {
    293        path
    294    }
    295 
    296    fn running_as_snap() -> bool {
    297        std::env::var("SNAP_INSTANCE_NAME")
    298            .or_else(|_| {
    299                // Compatibility for snapd <= 2.35
    300                std::env::var("SNAP_NAME")
    301            })
    302            .map(|name| !name.is_empty())
    303            .unwrap_or(false)
    304    }
    305 
    306    /// Searches the system path for `firefox`.
    307    pub fn firefox_default_path() -> Option<PathBuf> {
    308        if running_as_snap() {
    309            return Some(PathBuf::from(
    310                "/snap/firefox/current/usr/lib/firefox/firefox",
    311            ));
    312        }
    313        find_binary("firefox")
    314    }
    315 
    316    pub fn arg_prefix_char(c: char) -> bool {
    317        c == '-'
    318    }
    319 
    320    #[cfg(test)]
    321    mod tests {
    322        use crate::firefox_default_path;
    323        use std::env;
    324        use std::ops::Drop;
    325        use std::path::PathBuf;
    326 
    327        static SNAP_KEY: &str = "SNAP_INSTANCE_NAME";
    328        static SNAP_LEGACY_KEY: &str = "SNAP_NAME";
    329 
    330        struct SnapEnvironment {
    331            initial_environment: (Option<String>, Option<String>),
    332        }
    333 
    334        impl SnapEnvironment {
    335            fn new() -> SnapEnvironment {
    336                SnapEnvironment {
    337                    initial_environment: (env::var(SNAP_KEY).ok(), env::var(SNAP_LEGACY_KEY).ok()),
    338                }
    339            }
    340 
    341            fn set(&self, value: Option<String>, legacy_value: Option<String>) {
    342                fn set_env(key: &str, value: Option<String>) {
    343                    match value {
    344                        Some(value) => env::set_var(key, value),
    345                        None => env::remove_var(key),
    346                    }
    347                }
    348                set_env(SNAP_KEY, value);
    349                set_env(SNAP_LEGACY_KEY, legacy_value);
    350            }
    351        }
    352 
    353        impl Drop for SnapEnvironment {
    354            fn drop(&mut self) {
    355                self.set(
    356                    self.initial_environment.0.clone(),
    357                    self.initial_environment.1.clone(),
    358                )
    359            }
    360        }
    361 
    362        #[test]
    363        fn test_default_path() {
    364            let snap_path = Some(PathBuf::from(
    365                "/snap/firefox/current/usr/lib/firefox/firefox",
    366            ));
    367 
    368            let snap_env = SnapEnvironment::new();
    369 
    370            snap_env.set(None, None);
    371            assert_ne!(firefox_default_path(), snap_path);
    372 
    373            snap_env.set(Some("value".into()), None);
    374            assert_eq!(firefox_default_path(), snap_path);
    375 
    376            snap_env.set(None, Some("value".into()));
    377            assert_eq!(firefox_default_path(), snap_path);
    378        }
    379    }
    380 }
    381 
    382 #[cfg(target_os = "macos")]
    383 pub mod platform {
    384    use crate::path::{find_binary, is_app_bundle, is_binary};
    385    use plist::Value;
    386    use std::path::PathBuf;
    387 
    388    /// Searches for the binary file inside the path passed as parameter.
    389    /// If the binary is not found, the path remains unaltered.
    390    /// Else, it gets updated by the new binary path.
    391    pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf {
    392        if path.as_path().is_dir() {
    393            let mut info_plist = path.clone();
    394            info_plist.push("Contents");
    395            info_plist.push("Info.plist");
    396            if let Ok(plist) = Value::from_file(&info_plist) {
    397                if let Some(dict) = plist.as_dictionary() {
    398                    if let Some(Value::String(s)) = dict.get("CFBundleExecutable") {
    399                        path.push("Contents");
    400                        path.push("MacOS");
    401                        path.push(s);
    402                    }
    403                }
    404            }
    405        }
    406        path
    407    }
    408 
    409    /// Searches the system path for `firefox`, then looks for
    410    /// `Applications/Firefox.app/Contents/MacOS/firefox` as well
    411    /// as `Applications/Firefox Nightly.app/Contents/MacOS/firefox`
    412    /// and `Applications/Firefox Developer Edition.app/Contents/MacOS/firefox`
    413    /// under both `/` (system root) and the user home directory.
    414    pub fn firefox_default_path() -> Option<PathBuf> {
    415        if let Some(path) = find_binary("firefox") {
    416            return Some(path);
    417        }
    418 
    419        let home = dirs::home_dir();
    420        for &(prefix_home, trial_path) in [
    421            (false, "/Applications/Firefox.app"),
    422            (true, "Applications/Firefox.app"),
    423            (false, "/Applications/Firefox Developer Edition.app"),
    424            (true, "Applications/Firefox Developer Edition.app"),
    425            (false, "/Applications/Firefox Nightly.app"),
    426            (true, "Applications/Firefox Nightly.app"),
    427        ]
    428        .iter()
    429        {
    430            let path = match (home.as_ref(), prefix_home) {
    431                (Some(home_dir), true) => home_dir.join(trial_path),
    432                (None, true) => continue,
    433                (_, false) => PathBuf::from(trial_path),
    434            };
    435 
    436            if is_binary(&path) || is_app_bundle(&path) {
    437                return Some(path);
    438            }
    439        }
    440 
    441        None
    442    }
    443 
    444    pub fn arg_prefix_char(c: char) -> bool {
    445        c == '-'
    446    }
    447 }
    448 
    449 #[cfg(target_os = "windows")]
    450 pub mod platform {
    451    use crate::path::{find_binary, is_binary};
    452    use std::io::Error;
    453    use std::path::PathBuf;
    454    use winreg::enums::*;
    455    use winreg::RegKey;
    456 
    457    pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf {
    458        path
    459    }
    460 
    461    /// Searches the Windows registry, then the system path for `firefox.exe`.
    462    ///
    463    /// It _does not_ currently check the `HKEY_CURRENT_USER` tree.
    464    pub fn firefox_default_path() -> Option<PathBuf> {
    465        if let Ok(Some(path)) = firefox_registry_path() {
    466            if is_binary(&path) {
    467                return Some(path);
    468            }
    469        };
    470        find_binary("firefox.exe")
    471    }
    472 
    473    fn firefox_registry_path() -> Result<Option<PathBuf>, Error> {
    474        let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
    475        for subtree_key in ["SOFTWARE", "SOFTWARE\\WOW6432Node"].iter() {
    476            let subtree = hklm.open_subkey_with_flags(subtree_key, KEY_READ)?;
    477            let mozilla_org = match subtree.open_subkey_with_flags("mozilla.org\\Mozilla", KEY_READ)
    478            {
    479                Ok(val) => val,
    480                Err(_) => continue,
    481            };
    482            let current_version: String = mozilla_org.get_value("CurrentVersion")?;
    483            let mozilla = subtree.open_subkey_with_flags("Mozilla", KEY_READ)?;
    484            for key_res in mozilla.enum_keys() {
    485                let key = key_res?;
    486                let section_data = mozilla.open_subkey_with_flags(&key, KEY_READ)?;
    487                let version: Result<String, _> = section_data.get_value("GeckoVer");
    488                if let Ok(ver) = version {
    489                    if ver == current_version {
    490                        let mut bin_key = key.to_owned();
    491                        bin_key.push_str("\\bin");
    492                        if let Ok(bin_subtree) = mozilla.open_subkey_with_flags(bin_key, KEY_READ) {
    493                            let path_to_exe: Result<String, _> = bin_subtree.get_value("PathToExe");
    494                            if let Ok(path_to_exe) = path_to_exe {
    495                                let path = PathBuf::from(path_to_exe);
    496                                if is_binary(&path) {
    497                                    return Ok(Some(path));
    498                                }
    499                            }
    500                        }
    501                    }
    502                }
    503            }
    504        }
    505        Ok(None)
    506    }
    507 
    508    pub fn arg_prefix_char(c: char) -> bool {
    509        c == '/' || c == '-'
    510    }
    511 }
    512 
    513 #[cfg(not(any(unix, target_os = "windows")))]
    514 pub mod platform {
    515    use std::path::PathBuf;
    516 
    517    /// Returns an unaltered path for all operating systems other than macOS.
    518    pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf {
    519        path
    520    }
    521 
    522    /// Returns `None` for all other operating systems than Linux, macOS, and
    523    /// Windows.
    524    pub fn firefox_default_path() -> Option<PathBuf> {
    525        None
    526    }
    527 
    528    pub fn arg_prefix_char(c: char) -> bool {
    529        c == '-'
    530    }
    531 }