firefox_webdriver/driver/
core.rs

1//! Firefox WebDriver coordinator and factory.
2//!
3//! The [`Driver`] struct acts as the central coordinator for browser automation.
4//! It manages the lifecycle of browser windows.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use firefox_webdriver::Driver;
10//!
11//! # async fn example() -> firefox_webdriver::Result<()> {
12//! let driver = Driver::builder()
13//!     .binary("/usr/bin/firefox")
14//!     .extension("./extension")
15//!     .build()
16//!     .await?;
17//!
18//! let window = driver.window().headless().spawn().await?;
19//! # Ok(())
20//! # }
21//! ```
22
23// ============================================================================
24// Imports
25// ============================================================================
26
27use std::fmt;
28use std::path::PathBuf;
29use std::process::Stdio;
30use std::sync::Arc;
31
32use parking_lot::Mutex;
33use rustc_hash::FxHashMap;
34use tokio::process::{Child, Command};
35use tracing::{debug, info};
36
37use crate::browser::{Window, WindowBuilder};
38use crate::error::{Error, Result};
39use crate::identifiers::{SessionId, TabId};
40use crate::transport::ConnectionPool;
41
42use super::assets;
43use super::builder::DriverBuilder;
44use super::options::FirefoxOptions;
45use super::profile::{ExtensionSource, Profile};
46
47// ============================================================================
48// Types
49// ============================================================================
50
51/// Internal shared state for the driver.
52pub(crate) struct DriverInner {
53    /// Path to the Firefox binary executable.
54    pub binary: PathBuf,
55
56    /// Extension source for WebDriver functionality.
57    pub extension: ExtensionSource,
58
59    /// Connection pool for multiplexed WebSocket connections.
60    pub pool: Arc<ConnectionPool>,
61
62    /// Active windows tracked by their internal UUID.
63    pub windows: Mutex<FxHashMap<uuid::Uuid, Window>>,
64}
65
66// ============================================================================
67// Driver
68// ============================================================================
69
70/// Firefox WebDriver coordinator.
71///
72/// The driver is responsible for:
73/// - Spawning Firefox processes with custom profiles
74/// - Managing WebSocket server lifecycle
75/// - Tracking active browser windows
76///
77/// # Examples
78///
79/// ```no_run
80/// use firefox_webdriver::Driver;
81///
82/// # async fn example() -> firefox_webdriver::Result<()> {
83/// let driver = Driver::builder()
84///     .binary("/usr/bin/firefox")
85///     .extension("./extension")
86///     .build()
87///     .await?;
88///
89/// let window = driver.window().headless().spawn().await?;
90/// # Ok(())
91/// # }
92/// ```
93#[derive(Clone)]
94pub struct Driver {
95    /// Shared inner state.
96    pub(crate) inner: Arc<DriverInner>,
97}
98
99// ============================================================================
100// Driver - Display
101// ============================================================================
102
103impl fmt::Debug for Driver {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        f.debug_struct("Driver")
106            .field("binary", &self.inner.binary)
107            .field("window_count", &self.window_count())
108            .finish_non_exhaustive()
109    }
110}
111
112// ============================================================================
113// Driver - Public API
114// ============================================================================
115
116impl Driver {
117    /// Creates a configuration builder for the driver.
118    ///
119    /// # Example
120    ///
121    /// ```no_run
122    /// use firefox_webdriver::Driver;
123    ///
124    /// # async fn example() -> firefox_webdriver::Result<()> {
125    /// let driver = Driver::builder()
126    ///     .binary("/usr/bin/firefox")
127    ///     .extension("./extension")
128    ///     .build()
129    ///     .await?;
130    /// # Ok(())
131    /// # }
132    /// ```
133    #[inline]
134    #[must_use]
135    pub fn builder() -> DriverBuilder {
136        DriverBuilder::new()
137    }
138
139    /// Creates a window builder for spawning new browser windows.
140    ///
141    /// # Example
142    ///
143    /// ```no_run
144    /// # use firefox_webdriver::Driver;
145    /// # async fn example(driver: &Driver) -> firefox_webdriver::Result<()> {
146    /// let window = driver.window()
147    ///     .headless()
148    ///     .window_size(1920, 1080)
149    ///     .spawn()
150    ///     .await?;
151    /// # Ok(())
152    /// # }
153    /// ```
154    #[inline]
155    #[must_use]
156    pub fn window(&self) -> WindowBuilder<'_> {
157        WindowBuilder::new(self)
158    }
159
160    /// Returns the number of active windows currently tracked.
161    #[inline]
162    #[must_use]
163    pub fn window_count(&self) -> usize {
164        self.inner.windows.lock().len()
165    }
166
167    /// Closes all active windows and shuts down the driver.
168    ///
169    /// # Errors
170    ///
171    /// Returns an error if any window fails to close.
172    pub async fn close(&self) -> Result<()> {
173        let windows: Vec<Window> = {
174            let mut map = self.inner.windows.lock();
175            map.drain().map(|(_, w)| w).collect()
176        };
177
178        info!(count = windows.len(), "Shutting down all windows");
179
180        for window in windows {
181            if let Err(e) = window.close().await {
182                debug!(error = %e, "Error closing window during shutdown");
183            }
184        }
185
186        // Shutdown the connection pool
187        self.inner.pool.shutdown().await;
188
189        Ok(())
190    }
191
192    /// Returns the WebSocket port used by the connection pool.
193    #[inline]
194    #[must_use]
195    pub fn port(&self) -> u16 {
196        self.inner.pool.port()
197    }
198}
199
200// ============================================================================
201// Driver - Internal API
202// ============================================================================
203
204impl Driver {
205    /// Creates a new driver instance.
206    ///
207    /// # Arguments
208    ///
209    /// * `binary` - Path to Firefox binary
210    /// * `extension` - Extension source for WebDriver
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if initialization fails.
215    pub(crate) async fn new(binary: PathBuf, extension: ExtensionSource) -> Result<Self> {
216        // Create connection pool (binds WebSocket server)
217        let pool = ConnectionPool::new().await?;
218
219        let inner = Arc::new(DriverInner {
220            binary,
221            extension,
222            pool,
223            windows: Mutex::new(FxHashMap::default()),
224        });
225
226        info!(
227            port = inner.pool.port(),
228            "Driver initialized with WebSocket server"
229        );
230
231        Ok(Self { inner })
232    }
233
234    /// Spawns a new Firefox window with the specified configuration.
235    ///
236    /// # Arguments
237    ///
238    /// * `options` - Firefox launch options
239    /// * `custom_profile` - Optional custom profile path
240    ///
241    /// # Errors
242    ///
243    /// Returns an error if:
244    /// - Profile creation fails
245    /// - Extension installation fails
246    /// - Firefox process fails to spawn
247    /// - Extension fails to connect
248    pub(crate) async fn spawn_window(
249        &self,
250        options: FirefoxOptions,
251        custom_profile: Option<PathBuf>,
252    ) -> Result<Window> {
253        // Create profile
254        let profile = self.prepare_profile(custom_profile)?;
255
256        // Install extension
257        profile.install_extension(&self.inner.extension)?;
258        debug!("Installed WebDriver extension");
259
260        // Write preferences
261        let prefs = Profile::default_prefs();
262        profile.write_prefs(&prefs)?;
263        debug!(pref_count = prefs.len(), "Wrote profile preferences");
264
265        // Generate session ID BEFORE launching Firefox
266        let session_id = SessionId::next();
267
268        // Use pool's ws_url (same for all windows)
269        let ws_url = self.inner.pool.ws_url();
270        let data_uri = assets::build_init_data_uri(&ws_url, &session_id);
271        debug!(session_id = %session_id, url = %ws_url, "Using shared WebSocket server");
272
273        // Spawn Firefox process
274        let child = self.spawn_firefox_process(&profile, &options, &data_uri)?;
275        let pid = child.id();
276        info!(pid, session_id = %session_id, "Firefox process spawned");
277
278        // Wait for this specific session to connect via pool
279        let ready_data = self.inner.pool.wait_for_session(session_id).await?;
280        debug!(session_id = %session_id, "Session connected via pool");
281
282        // Extract tab ID from ready message
283        let tab_id = TabId::new(ready_data.tab_id)
284            .ok_or_else(|| Error::protocol("Invalid tab_id in READY message"))?;
285        debug!(session_id = %session_id, tab_id = %tab_id, "Browser IDs assigned");
286
287        // Create window with pool reference
288        let window = Window::new(
289            Arc::clone(&self.inner.pool),
290            child,
291            profile,
292            session_id,
293            tab_id,
294        );
295
296        // Track window
297        self.inner
298            .windows
299            .lock()
300            .insert(*window.uuid(), window.clone());
301
302        info!(
303            session_id = %session_id,
304            window_count = self.window_count(),
305            "Window spawned successfully"
306        );
307
308        Ok(window)
309    }
310
311    /// Prepares a Firefox profile for the window.
312    ///
313    /// # Arguments
314    ///
315    /// * `custom_profile` - Optional path to existing profile
316    ///
317    /// # Errors
318    ///
319    /// Returns an error if profile creation fails.
320    fn prepare_profile(&self, custom_profile: Option<PathBuf>) -> Result<Profile> {
321        match custom_profile {
322            Some(path) => {
323                debug!(path = %path.display(), "Using custom profile");
324                Profile::from_path(path)
325            }
326            None => {
327                debug!("Creating temporary profile");
328                Profile::new_temp()
329            }
330        }
331    }
332
333    /// Spawns the Firefox process with the given configuration.
334    ///
335    /// # Arguments
336    ///
337    /// * `profile` - Firefox profile to use
338    /// * `options` - Firefox launch options
339    /// * `data_uri` - Initial page data URI
340    ///
341    /// # Errors
342    ///
343    /// Returns an error if the process fails to spawn.
344    fn spawn_firefox_process(
345        &self,
346        profile: &Profile,
347        options: &FirefoxOptions,
348        data_uri: &str,
349    ) -> Result<Child> {
350        let mut cmd = Command::new(&self.inner.binary);
351
352        // Profile arguments
353        cmd.arg("--profile")
354            .arg(profile.path())
355            .arg("--no-remote")
356            .arg("--new-instance");
357
358        // User-specified options
359        cmd.args(options.to_args());
360
361        // Initial page
362        cmd.arg(data_uri);
363
364        // Suppress stdio
365        cmd.stdin(Stdio::null())
366            .stdout(Stdio::null())
367            .stderr(Stdio::null());
368
369        cmd.spawn().map_err(Error::process_launch_failed)
370    }
371}
372
373// ============================================================================
374// Tests
375// ============================================================================
376
377#[cfg(test)]
378mod tests {
379    use super::Driver;
380
381    #[test]
382    fn test_builder_returns_driver_builder() {
383        let _builder = Driver::builder();
384    }
385
386    #[test]
387    fn test_driver_is_clone() {
388        fn assert_clone<T: Clone>() {}
389        assert_clone::<Driver>();
390    }
391
392    #[test]
393    fn test_driver_is_debug() {
394        fn assert_debug<T: std::fmt::Debug>() {}
395        assert_debug::<Driver>();
396    }
397}