mozrunner/
runner.rs

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
5use mozprofile::prefreader::PrefReaderError;
6use mozprofile::profile::Profile;
7use std::collections::HashMap;
8use std::ffi::{OsStr, OsString};
9use std::io;
10use std::path::{Path, PathBuf};
11use std::process;
12use std::process::{Child, Command, Stdio};
13use std::thread;
14use std::time;
15use thiserror::Error;
16
17use crate::firefox_args::Arg;
18
19pub 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
53pub 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)]
86pub 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)]
94pub 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
102impl 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)]
147pub 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
156impl 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
174impl 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))]
288pub 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("/snap/firefox/current/firefox.launcher"));
310        }
311        find_binary("firefox")
312    }
313
314    pub fn arg_prefix_char(c: char) -> bool {
315        c == '-'
316    }
317
318    #[cfg(test)]
319    mod tests {
320        use crate::firefox_default_path;
321        use std::env;
322        use std::ops::Drop;
323        use std::path::PathBuf;
324
325        static SNAP_KEY: &str = "SNAP_INSTANCE_NAME";
326        static SNAP_LEGACY_KEY: &str = "SNAP_NAME";
327
328        struct SnapEnvironment {
329            initial_environment: (Option<String>, Option<String>),
330        }
331
332        impl SnapEnvironment {
333            fn new() -> SnapEnvironment {
334                SnapEnvironment {
335                    initial_environment: (env::var(SNAP_KEY).ok(), env::var(SNAP_LEGACY_KEY).ok()),
336                }
337            }
338
339            fn set(&self, value: Option<String>, legacy_value: Option<String>) {
340                fn set_env(key: &str, value: Option<String>) {
341                    match value {
342                        Some(value) => env::set_var(key, value),
343                        None => env::remove_var(key),
344                    }
345                }
346                set_env(SNAP_KEY, value);
347                set_env(SNAP_LEGACY_KEY, legacy_value);
348            }
349        }
350
351        impl Drop for SnapEnvironment {
352            fn drop(&mut self) {
353                self.set(
354                    self.initial_environment.0.clone(),
355                    self.initial_environment.1.clone(),
356                )
357            }
358        }
359
360        #[test]
361        fn test_default_path() {
362            let snap_path = Some(PathBuf::from("/snap/firefox/current/firefox.launcher"));
363
364            let snap_env = SnapEnvironment::new();
365
366            snap_env.set(None, None);
367            assert_ne!(firefox_default_path(), snap_path);
368
369            snap_env.set(Some("value".into()), None);
370            assert_eq!(firefox_default_path(), snap_path);
371
372            snap_env.set(None, Some("value".into()));
373            assert_eq!(firefox_default_path(), snap_path);
374        }
375    }
376}
377
378#[cfg(target_os = "macos")]
379pub mod platform {
380    use crate::path::{find_binary, is_app_bundle, is_binary};
381    use plist::Value;
382    use std::path::PathBuf;
383
384    /// Searches for the binary file inside the path passed as parameter.
385    /// If the binary is not found, the path remains unaltered.
386    /// Else, it gets updated by the new binary path.
387    pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf {
388        if path.as_path().is_dir() {
389            let mut info_plist = path.clone();
390            info_plist.push("Contents");
391            info_plist.push("Info.plist");
392            if let Ok(plist) = Value::from_file(&info_plist) {
393                if let Some(dict) = plist.as_dictionary() {
394                    if let Some(Value::String(s)) = dict.get("CFBundleExecutable") {
395                        path.push("Contents");
396                        path.push("MacOS");
397                        path.push(s);
398                    }
399                }
400            }
401        }
402        path
403    }
404
405    /// Searches the system path for `firefox`, then looks for
406    /// `Applications/Firefox.app/Contents/MacOS/firefox` as well
407    /// as `Applications/Firefox Nightly.app/Contents/MacOS/firefox`
408    /// and `Applications/Firefox Developer Edition.app/Contents/MacOS/firefox`
409    /// under both `/` (system root) and the user home directory.
410    pub fn firefox_default_path() -> Option<PathBuf> {
411        if let Some(path) = find_binary("firefox") {
412            return Some(path);
413        }
414
415        let home = dirs::home_dir();
416        for &(prefix_home, trial_path) in [
417            (false, "/Applications/Firefox.app"),
418            (true, "Applications/Firefox.app"),
419            (false, "/Applications/Firefox Developer Edition.app"),
420            (true, "Applications/Firefox Developer Edition.app"),
421            (false, "/Applications/Firefox Nightly.app"),
422            (true, "Applications/Firefox Nightly.app"),
423        ]
424        .iter()
425        {
426            let path = match (home.as_ref(), prefix_home) {
427                (Some(home_dir), true) => home_dir.join(trial_path),
428                (None, true) => continue,
429                (_, false) => PathBuf::from(trial_path),
430            };
431
432            if is_binary(&path) || is_app_bundle(&path) {
433                return Some(path);
434            }
435        }
436
437        None
438    }
439
440    pub fn arg_prefix_char(c: char) -> bool {
441        c == '-'
442    }
443}
444
445#[cfg(target_os = "windows")]
446pub mod platform {
447    use crate::path::{find_binary, is_binary};
448    use std::io::Error;
449    use std::path::PathBuf;
450    use winreg::enums::*;
451    use winreg::RegKey;
452
453    pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf {
454        path
455    }
456
457    /// Searches the Windows registry, then the system path for `firefox.exe`.
458    ///
459    /// It _does not_ currently check the `HKEY_CURRENT_USER` tree.
460    pub fn firefox_default_path() -> Option<PathBuf> {
461        if let Ok(Some(path)) = firefox_registry_path() {
462            if is_binary(&path) {
463                return Some(path);
464            }
465        };
466        find_binary("firefox.exe")
467    }
468
469    fn firefox_registry_path() -> Result<Option<PathBuf>, Error> {
470        let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
471        for subtree_key in ["SOFTWARE", "SOFTWARE\\WOW6432Node"].iter() {
472            let subtree = hklm.open_subkey_with_flags(subtree_key, KEY_READ)?;
473            let mozilla_org = match subtree.open_subkey_with_flags("mozilla.org\\Mozilla", KEY_READ)
474            {
475                Ok(val) => val,
476                Err(_) => continue,
477            };
478            let current_version: String = mozilla_org.get_value("CurrentVersion")?;
479            let mozilla = subtree.open_subkey_with_flags("Mozilla", KEY_READ)?;
480            for key_res in mozilla.enum_keys() {
481                let key = key_res?;
482                let section_data = mozilla.open_subkey_with_flags(&key, KEY_READ)?;
483                let version: Result<String, _> = section_data.get_value("GeckoVer");
484                if let Ok(ver) = version {
485                    if ver == current_version {
486                        let mut bin_key = key.to_owned();
487                        bin_key.push_str("\\bin");
488                        if let Ok(bin_subtree) = mozilla.open_subkey_with_flags(bin_key, KEY_READ) {
489                            let path_to_exe: Result<String, _> = bin_subtree.get_value("PathToExe");
490                            if let Ok(path_to_exe) = path_to_exe {
491                                let path = PathBuf::from(path_to_exe);
492                                if is_binary(&path) {
493                                    return Ok(Some(path));
494                                }
495                            }
496                        }
497                    }
498                }
499            }
500        }
501        Ok(None)
502    }
503
504    pub fn arg_prefix_char(c: char) -> bool {
505        c == '/' || c == '-'
506    }
507}
508
509#[cfg(not(any(unix, target_os = "windows")))]
510pub mod platform {
511    use std::path::PathBuf;
512
513    /// Returns an unaltered path for all operating systems other than macOS.
514    pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf {
515        path
516    }
517
518    /// Returns `None` for all other operating systems than Linux, macOS, and
519    /// Windows.
520    pub fn firefox_default_path() -> Option<PathBuf> {
521        None
522    }
523
524    pub fn arg_prefix_char(c: char) -> bool {
525        c == '-'
526    }
527}