1use 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 fn try_wait(&mut self) -> io::Result<Option<process::ExitStatus>>;
65
66 fn wait(&mut self, timeout: time::Duration) -> io::Result<process::ExitStatus>;
76
77 fn running(&mut self) -> bool;
79
80 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 #[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 Ok(Some(status)) => return Ok(status),
113
114 Ok(None) if start.elapsed() >= timeout => return self.kill(),
116
117 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 Ok(Some(status)) => Ok(status),
133
134 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 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 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 std::env::var("SNAP_NAME")
301 })
302 .map(|name| !name.is_empty())
303 .unwrap_or(false)
304 }
305
306 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 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 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 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 pub fn resolve_binary_path(path: &mut PathBuf) -> &PathBuf {
515 path
516 }
517
518 pub fn firefox_default_path() -> Option<PathBuf> {
521 None
522 }
523
524 pub fn arg_prefix_char(c: char) -> bool {
525 c == '-'
526 }
527}