firefox_webdriver/browser/
window.rs

1//! Browser window management and control.
2//!
3//! Each [`Window`] owns:
4//! - One Firefox process (child process)
5//! - Reference to shared ConnectionPool
6//! - One profile directory (temporary or persistent)
7//!
8//! # Example
9//!
10//! ```no_run
11//! use firefox_webdriver::Driver;
12//!
13//! # async fn example() -> firefox_webdriver::Result<()> {
14//! let driver = Driver::builder()
15//!     .binary("/usr/bin/firefox")
16//!     .extension("./extension")
17//!     .build()
18//!     .await?;
19//!
20//! let window = driver.window()
21//!     .headless()
22//!     .window_size(1920, 1080)
23//!     .spawn()
24//!     .await?;
25//!
26//! let tab = window.tab();
27//! tab.goto("https://example.com").await?;
28//!
29//! window.close().await?;
30//! # Ok(())
31//! # }
32//! ```
33
34// ============================================================================
35// Imports
36// ============================================================================
37
38use std::fmt;
39use std::path::PathBuf;
40use std::sync::Arc;
41
42use parking_lot::Mutex;
43use rustc_hash::FxHashMap;
44use serde_json::Value;
45use tokio::process::Child;
46use tracing::{debug, info};
47use uuid::Uuid;
48
49use crate::driver::{Driver, FirefoxOptions, Profile};
50use crate::error::{Error, Result};
51use crate::identifiers::{FrameId, SessionId, TabId};
52use crate::protocol::{
53    BrowsingContextCommand, Command, ProxyCommand, Request, Response, SessionCommand,
54};
55use crate::transport::ConnectionPool;
56
57use super::Tab;
58use super::proxy::ProxyConfig;
59
60// ============================================================================
61// ProcessGuard
62// ============================================================================
63
64/// Guards a child process and ensures it is killed when dropped.
65struct ProcessGuard {
66    /// The child process handle.
67    child: Option<Child>,
68    /// Process ID for logging.
69    pid: u32,
70}
71
72impl ProcessGuard {
73    /// Creates a new process guard.
74    fn new(child: Child) -> Self {
75        let pid = child.id().unwrap_or(0);
76        debug!(pid, "Process guard created");
77        Self {
78            child: Some(child),
79            pid,
80        }
81    }
82
83    /// Kills the process and waits for it to exit.
84    async fn kill(&mut self) -> Result<()> {
85        if let Some(mut child) = self.child.take() {
86            debug!(pid = self.pid, "Killing Firefox process");
87            if let Err(e) = child.kill().await {
88                debug!(pid = self.pid, error = %e, "Failed to kill process");
89            }
90            if let Err(e) = child.wait().await {
91                debug!(pid = self.pid, error = %e, "Failed to wait for process");
92            }
93            info!(pid = self.pid, "Process terminated");
94        }
95        Ok(())
96    }
97
98    /// Returns the process ID.
99    #[inline]
100    fn pid(&self) -> u32 {
101        self.pid
102    }
103}
104
105impl Drop for ProcessGuard {
106    fn drop(&mut self) {
107        if let Some(mut child) = self.child.take()
108            && let Err(e) = child.start_kill()
109        {
110            debug!(pid = self.pid, error = %e, "Failed to send kill signal in Drop");
111        }
112    }
113}
114
115// ============================================================================
116// Types
117// ============================================================================
118
119/// Internal shared state for a window.
120pub(crate) struct WindowInner {
121    /// Unique identifier for this window.
122    pub uuid: Uuid,
123    /// Session ID.
124    pub session_id: SessionId,
125    /// Protected process handle.
126    process: Mutex<ProcessGuard>,
127    /// Connection pool (shared with Driver and other Windows).
128    pub pool: Arc<ConnectionPool>,
129    /// Profile directory.
130    #[allow(dead_code)]
131    profile: Profile,
132    /// All tabs in this window.
133    tabs: Mutex<FxHashMap<TabId, Tab>>,
134    /// The initial tab created when Firefox opens.
135    pub initial_tab_id: TabId,
136}
137
138// ============================================================================
139// Window
140// ============================================================================
141
142/// A handle to a Firefox browser window.
143///
144/// The window owns a Firefox process and profile, and holds a reference
145/// to the shared ConnectionPool for WebSocket communication.
146/// When dropped, the process is automatically killed.
147///
148/// # Example
149///
150/// ```no_run
151/// # use firefox_webdriver::Driver;
152/// # async fn example() -> firefox_webdriver::Result<()> {
153/// # let driver = Driver::builder().binary("/usr/bin/firefox").extension("./ext").build().await?;
154/// let window = driver.window().headless().spawn().await?;
155///
156/// // Get the initial tab
157/// let tab = window.tab();
158///
159/// // Create a new tab
160/// let new_tab = window.new_tab().await?;
161///
162/// // Close the window
163/// window.close().await?;
164/// # Ok(())
165/// # }
166/// ```
167#[derive(Clone)]
168pub struct Window {
169    /// Shared inner state.
170    pub(crate) inner: Arc<WindowInner>,
171}
172
173// ============================================================================
174// Window - Display
175// ============================================================================
176
177impl fmt::Debug for Window {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        f.debug_struct("Window")
180            .field("uuid", &self.inner.uuid)
181            .field("session_id", &self.inner.session_id)
182            .field("port", &self.inner.pool.port())
183            .finish_non_exhaustive()
184    }
185}
186
187// ============================================================================
188// Window - Constructor
189// ============================================================================
190
191impl Window {
192    /// Creates a new window handle.
193    pub(crate) fn new(
194        pool: Arc<ConnectionPool>,
195        process: Child,
196        profile: Profile,
197        session_id: SessionId,
198        initial_tab_id: TabId,
199    ) -> Self {
200        let uuid = Uuid::new_v4();
201        let initial_tab = Tab::new(initial_tab_id, FrameId::main(), session_id, None);
202        let mut tabs = FxHashMap::default();
203        tabs.insert(initial_tab_id, initial_tab);
204
205        debug!(
206            uuid = %uuid,
207            session_id = %session_id,
208            tab_id = %initial_tab_id,
209            port = pool.port(),
210            "Window created"
211        );
212
213        Self {
214            inner: Arc::new(WindowInner {
215                uuid,
216                session_id,
217                process: Mutex::new(ProcessGuard::new(process)),
218                pool,
219                profile,
220                tabs: Mutex::new(tabs),
221                initial_tab_id,
222            }),
223        }
224    }
225}
226
227// ============================================================================
228// Window - Accessors
229// ============================================================================
230
231impl Window {
232    /// Returns the session ID.
233    #[inline]
234    #[must_use]
235    pub fn session_id(&self) -> SessionId {
236        self.inner.session_id
237    }
238
239    /// Returns the Rust-side unique UUID.
240    #[inline]
241    #[must_use]
242    pub fn uuid(&self) -> &Uuid {
243        &self.inner.uuid
244    }
245
246    /// Returns the WebSocket port (shared across all windows).
247    #[inline]
248    #[must_use]
249    pub fn port(&self) -> u16 {
250        self.inner.pool.port()
251    }
252
253    /// Returns the Firefox process ID.
254    #[inline]
255    #[must_use]
256    pub fn pid(&self) -> u32 {
257        self.inner.process.lock().pid()
258    }
259}
260
261// ============================================================================
262// Window - Lifecycle
263// ============================================================================
264
265impl Window {
266    /// Closes the window and kills the Firefox process.
267    ///
268    /// # Errors
269    ///
270    /// Returns an error if the process cannot be killed.
271    #[allow(clippy::await_holding_lock)]
272    pub async fn close(&self) -> Result<()> {
273        debug!(uuid = %self.inner.uuid, "Closing window");
274
275        // Remove from pool first
276        self.inner.pool.remove(self.inner.session_id);
277
278        // Kill process
279        let mut guard = self.inner.process.lock();
280        guard.kill().await?;
281
282        info!(uuid = %self.inner.uuid, "Window closed");
283        Ok(())
284    }
285}
286
287// ============================================================================
288// Window - Tab Management
289// ============================================================================
290
291impl Window {
292    /// Returns the initial tab for this window.
293    #[must_use]
294    pub fn tab(&self) -> Tab {
295        Tab::new(
296            self.inner.initial_tab_id,
297            FrameId::main(),
298            self.inner.session_id,
299            Some(self.clone()),
300        )
301    }
302
303    /// Creates a new tab in this window.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if tab creation fails.
308    pub async fn new_tab(&self) -> Result<Tab> {
309        let command = Command::BrowsingContext(BrowsingContextCommand::NewTab);
310        let response = self.send_command(command).await?;
311
312        let tab_id_u32 = response
313            .result
314            .as_ref()
315            .and_then(|v| v.get("tabId"))
316            .and_then(|v| v.as_u64())
317            .ok_or_else(|| Error::protocol("Expected tabId in NewTab response"))?;
318
319        let new_tab_id = TabId::new(tab_id_u32 as u32)
320            .ok_or_else(|| Error::protocol("Invalid tabId in NewTab response"))?;
321
322        let tab = Tab::new(
323            new_tab_id,
324            FrameId::main(),
325            self.inner.session_id,
326            Some(self.clone()),
327        );
328
329        self.inner.tabs.lock().insert(new_tab_id, tab.clone());
330        debug!(session_id = %self.inner.session_id, tab_id = %new_tab_id, "New tab created");
331        Ok(tab)
332    }
333
334    /// Returns the number of tabs in this window.
335    #[inline]
336    #[must_use]
337    pub fn tab_count(&self) -> usize {
338        self.inner.tabs.lock().len()
339    }
340
341    /// Steals logs from extension (returns and clears).
342    ///
343    /// Useful for debugging extension issues.
344    pub async fn steal_logs(&self) -> Result<Vec<Value>> {
345        let command = Command::Session(SessionCommand::StealLogs);
346        let response = self.send_command(command).await?;
347        let logs = response
348            .result
349            .as_ref()
350            .and_then(|v| v.get("logs"))
351            .and_then(|v| v.as_array())
352            .cloned()
353            .unwrap_or_default();
354        Ok(logs)
355    }
356}
357
358// ============================================================================
359// Window - Proxy
360// ============================================================================
361
362impl Window {
363    /// Sets a proxy for all tabs in this window.
364    ///
365    /// Window-level proxy applies to all tabs unless overridden by tab-level proxy.
366    ///
367    /// # Example
368    ///
369    /// ```ignore
370    /// use firefox_webdriver::ProxyConfig;
371    ///
372    /// // HTTP proxy for all tabs
373    /// window.set_proxy(ProxyConfig::http("proxy.example.com", 8080)).await?;
374    ///
375    /// // SOCKS5 proxy with auth
376    /// window.set_proxy(
377    ///     ProxyConfig::socks5("proxy.example.com", 1080)
378    ///         .with_credentials("user", "pass")
379    ///         .with_proxy_dns(true)
380    /// ).await?;
381    /// ```
382    pub async fn set_proxy(&self, config: ProxyConfig) -> Result<()> {
383        debug!(
384            session_id = %self.inner.session_id,
385            proxy_type = %config.proxy_type.as_str(),
386            host = %config.host,
387            port = config.port,
388            "Setting window proxy"
389        );
390
391        let command = Command::Proxy(ProxyCommand::SetWindowProxy {
392            proxy_type: config.proxy_type.as_str().to_string(),
393            host: config.host,
394            port: config.port,
395            username: config.username,
396            password: config.password,
397            proxy_dns: config.proxy_dns,
398        });
399
400        self.send_command(command).await?;
401        Ok(())
402    }
403
404    /// Clears the proxy for this window.
405    ///
406    /// After clearing, all tabs use direct connection (unless they have tab-level proxy).
407    pub async fn clear_proxy(&self) -> Result<()> {
408        debug!(session_id = %self.inner.session_id, "Clearing window proxy");
409        let command = Command::Proxy(ProxyCommand::ClearWindowProxy);
410        self.send_command(command).await?;
411        Ok(())
412    }
413}
414
415// ============================================================================
416// Window - Internal
417// ============================================================================
418
419impl Window {
420    /// Sends a command via the connection pool and waits for the response.
421    pub(crate) async fn send_command(&self, command: Command) -> Result<Response> {
422        let request = Request::new(self.inner.initial_tab_id, FrameId::main(), command);
423        self.inner.pool.send(self.inner.session_id, request).await
424    }
425}
426
427// ============================================================================
428// WindowBuilder
429// ============================================================================
430
431/// Builder for spawning browser windows.
432///
433/// # Example
434///
435/// ```no_run
436/// # use firefox_webdriver::Driver;
437/// # async fn example() -> firefox_webdriver::Result<()> {
438/// # let driver = Driver::builder().binary("/usr/bin/firefox").extension("./ext").build().await?;
439/// let window = driver.window()
440///     .headless()
441///     .window_size(1920, 1080)
442///     .profile("./my_profile")
443///     .spawn()
444///     .await?;
445/// # Ok(())
446/// # }
447/// ```
448pub struct WindowBuilder<'a> {
449    /// Reference to the driver.
450    driver: &'a Driver,
451    /// Firefox launch options.
452    options: FirefoxOptions,
453    /// Optional custom profile path.
454    profile: Option<PathBuf>,
455}
456
457// ============================================================================
458// WindowBuilder - Implementation
459// ============================================================================
460
461impl<'a> WindowBuilder<'a> {
462    /// Creates a new window builder.
463    pub(crate) fn new(driver: &'a Driver) -> Self {
464        Self {
465            driver,
466            options: FirefoxOptions::new(),
467            profile: None,
468        }
469    }
470
471    /// Enables headless mode.
472    ///
473    /// Firefox runs without a visible window.
474    #[must_use]
475    pub fn headless(mut self) -> Self {
476        self.options = self.options.with_headless();
477        self
478    }
479
480    /// Sets the window size.
481    ///
482    /// # Arguments
483    ///
484    /// * `width` - Window width in pixels
485    /// * `height` - Window height in pixels
486    #[must_use]
487    pub fn window_size(mut self, width: u32, height: u32) -> Self {
488        self.options = self.options.with_window_size(width, height);
489        self
490    }
491
492    /// Uses a custom profile directory.
493    ///
494    /// # Arguments
495    ///
496    /// * `path` - Path to profile directory
497    #[must_use]
498    pub fn profile(mut self, path: impl Into<PathBuf>) -> Self {
499        self.profile = Some(path.into());
500        self
501    }
502
503    /// Spawns the window.
504    ///
505    /// # Errors
506    ///
507    /// Returns an error if window creation fails.
508    pub async fn spawn(self) -> Result<Window> {
509        self.driver.spawn_window(self.options, self.profile).await
510    }
511}
512
513// ============================================================================
514// Tests
515// ============================================================================
516
517#[cfg(test)]
518mod tests {
519    use super::Window;
520
521    #[test]
522    fn test_window_is_clone() {
523        fn assert_clone<T: Clone>() {}
524        assert_clone::<Window>();
525    }
526
527    #[test]
528    fn test_window_is_debug() {
529        fn assert_debug<T: std::fmt::Debug>() {}
530        assert_debug::<Window>();
531    }
532}