use std::fmt;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use tokio::process::{Child, Command};
use tracing::{debug, info};
use crate::browser::{Window, WindowBuilder};
use crate::error::{Error, Result};
use crate::identifiers::{SessionId, TabId};
use crate::transport::ConnectionPool;
use super::assets;
use super::builder::DriverBuilder;
use super::options::FirefoxOptions;
use super::profile::{ExtensionSource, Profile};
pub(crate) struct DriverInner {
pub binary: PathBuf,
pub extension: ExtensionSource,
pub pool: Arc<ConnectionPool>,
pub windows: Mutex<FxHashMap<uuid::Uuid, Window>>,
}
#[derive(Clone)]
pub struct Driver {
pub(crate) inner: Arc<DriverInner>,
}
impl fmt::Debug for Driver {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Driver")
.field("binary", &self.inner.binary)
.field("window_count", &self.window_count())
.finish_non_exhaustive()
}
}
impl Driver {
#[inline]
#[must_use]
pub fn builder() -> DriverBuilder {
DriverBuilder::new()
}
#[inline]
#[must_use]
pub fn window(&self) -> WindowBuilder<'_> {
WindowBuilder::new(self)
}
#[inline]
#[must_use]
pub fn window_count(&self) -> usize {
self.inner.windows.lock().len()
}
pub async fn close(&self) -> Result<()> {
let windows: Vec<Window> = {
let mut map = self.inner.windows.lock();
map.drain().map(|(_, w)| w).collect()
};
info!(count = windows.len(), "Shutting down all windows");
for window in windows {
if let Err(e) = window.close().await {
debug!(error = %e, "Error closing window during shutdown");
}
}
self.inner.pool.shutdown().await;
Ok(())
}
#[inline]
#[must_use]
pub fn port(&self) -> u16 {
self.inner.pool.port()
}
}
impl Driver {
pub(crate) async fn new(binary: PathBuf, extension: ExtensionSource) -> Result<Self> {
let pool = ConnectionPool::new().await?;
let inner = Arc::new(DriverInner {
binary,
extension,
pool,
windows: Mutex::new(FxHashMap::default()),
});
info!(
port = inner.pool.port(),
"Driver initialized with WebSocket server"
);
Ok(Self { inner })
}
pub(crate) async fn spawn_window(
&self,
options: FirefoxOptions,
custom_profile: Option<PathBuf>,
) -> Result<Window> {
let profile = self.prepare_profile(custom_profile)?;
profile.install_extension(&self.inner.extension)?;
debug!("Installed WebDriver extension");
let prefs = Profile::default_prefs();
profile.write_prefs(&prefs)?;
debug!(pref_count = prefs.len(), "Wrote profile preferences");
let session_id = SessionId::next();
let ws_url = self.inner.pool.ws_url();
let data_uri = assets::build_init_data_uri(ws_url, &session_id);
debug!(session_id = %session_id, url = %ws_url, "Using shared WebSocket server");
let child = self.spawn_firefox_process(&profile, &options, &data_uri)?;
let pid = child.id();
info!(pid, session_id = %session_id, "Firefox process spawned");
let ready_data = self.inner.pool.wait_for_session(session_id).await?;
debug!(session_id = %session_id, "Session connected via pool");
let tab_id = TabId::new(ready_data.tab_id)
.ok_or_else(|| Error::protocol("Invalid tab_id in READY message"))?;
debug!(session_id = %session_id, tab_id = %tab_id, "Browser IDs assigned");
let window = Window::new(
Arc::clone(&self.inner.pool),
child,
profile,
session_id,
tab_id,
);
self.inner
.windows
.lock()
.insert(*window.uuid(), window.clone());
info!(
session_id = %session_id,
window_count = self.window_count(),
"Window spawned successfully"
);
Ok(window)
}
fn prepare_profile(&self, custom_profile: Option<PathBuf>) -> Result<Profile> {
match custom_profile {
Some(path) => {
debug!(path = %path.display(), "Using custom profile");
Profile::from_path(path)
}
None => {
debug!("Creating temporary profile");
Profile::new_temp()
}
}
}
fn spawn_firefox_process(
&self,
profile: &Profile,
options: &FirefoxOptions,
data_uri: &str,
) -> Result<Child> {
let mut cmd = Command::new(&self.inner.binary);
cmd.arg("--profile")
.arg(profile.path())
.arg("--no-remote")
.arg("--new-instance");
cmd.args(options.to_args());
cmd.arg(data_uri);
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd.spawn().map_err(Error::process_launch_failed)
}
}
#[cfg(test)]
mod tests {
use super::Driver;
#[test]
fn test_builder_returns_driver_builder() {
let _builder = Driver::builder();
}
#[test]
fn test_driver_is_clone() {
fn assert_clone<T: Clone>() {}
assert_clone::<Driver>();
}
#[test]
fn test_driver_is_debug() {
fn assert_debug<T: std::fmt::Debug>() {}
assert_debug::<Driver>();
}
}