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}