use std::path::{Path, PathBuf}; use inotify::{Inotify, WatchMask}; use mozprofile::preferences::Pref; use mozprofile::profile::Profile; use mozrunner::runner::{ FirefoxProcess, FirefoxRunner, Runner, RunnerError, RunnerProcess, }; use tokio_stream::StreamExt; use tracing::{debug, error, trace, warn}; #[derive(Debug)] pub enum BrowserError { Io(std::io::Error), Runner(RunnerError), } impl std::fmt::Display for BrowserError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Io(e) => write!(f, "{}", e), Self::Runner(e) => write!(f, "{}", e), } } } impl std::error::Error for BrowserError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Io(e) => Some(e), Self::Runner(e) => Some(e), } } } impl From for BrowserError { fn from(e: std::io::Error) -> Self { Self::Io(e) } } impl From for BrowserError { fn from(e: RunnerError) -> Self { Self::Runner(e) } } pub struct Browser { profile: PathBuf, process: FirefoxProcess, } impl Browser { pub fn launch() -> Result { Self::launch_with_binary(None) } pub fn launch_with_binary( binary: Option<&Path>, ) -> Result { let mut profile = Profile::new(None)?; match profile.user_prefs() { Ok(prefs) => { prefs.insert("marionette.port", Pref::new(0)); } Err(e) => { error!("Could not get profile user prefs: {}", e); } } let binary = match binary { Some(b) => b, None => Path::new("firefox"), }; let profile_path = profile.path.clone(); let mut runner = FirefoxRunner::new(binary, Some(profile)); runner.arg("--marionette"); let process = runner.start()?; Ok(Self { profile: profile_path, process, }) } pub fn marionette_port(&self) -> Option { let mut path = self.profile.clone(); path.push("MarionetteActivePort"); let port = match std::fs::read_to_string(path) { Ok(v) => match v.parse() { Ok(v) => v, Err(e) => { warn!("Failed to parse Marionette port: {}", e); return None; } }, Err(e) => { warn!("Could not read active Marionette port: {}", e); return None; } }; Some(port) } pub async fn wait_ready(&self) -> Result<(), std::io::Error> { if self.marionette_port().is_some() { debug!("Marionette port has already been assigned"); return Ok(()); } let mut inotify = Inotify::init()?; debug!("Starting inotify watch for {}", self.profile.display()); inotify.add_watch(self.profile.clone(), WatchMask::CREATE)?; let mut buffer = [0; 1024]; let mut stream = inotify.event_stream(&mut buffer)?; while let Some(evt) = stream.next().await { trace!("inotify event: {:?}", evt); if let Some(name) = evt?.name { if name == "MarionetteActivePort" { debug!("Marionette port has been assigned"); break; } } } Ok(()) } } impl Drop for Browser { fn drop(&mut self) { debug!("Stopping browser process"); if let Err(e) = self.process.wait(std::time::Duration::from_millis(500)) { error!("Error stopping browser process: {}", e); } } }