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 }