viewpoint_core/browser/
mod.rs

1//! Browser launching and management.
2
3mod launcher;
4
5use std::process::Child;
6use std::sync::Arc;
7use std::time::Duration;
8
9use viewpoint_cdp::protocol::target::{CreateBrowserContextParams, CreateBrowserContextResult};
10use viewpoint_cdp::CdpConnection;
11use tokio::sync::Mutex;
12
13use crate::context::BrowserContext;
14use crate::error::BrowserError;
15
16pub use launcher::BrowserBuilder;
17
18/// Default timeout for browser operations.
19#[allow(dead_code)]
20const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
21
22/// A browser instance connected via CDP.
23#[derive(Debug)]
24pub struct Browser {
25    /// CDP connection to the browser.
26    connection: Arc<CdpConnection>,
27    /// Browser process (only present if we launched it).
28    process: Option<Mutex<Child>>,
29    /// Whether the browser was launched by us (vs connected to).
30    owned: bool,
31}
32
33impl Browser {
34    /// Create a browser builder for launching a new browser.
35    ///
36    /// # Example
37    ///
38    /// ```no_run
39    /// use viewpoint_core::Browser;
40    ///
41    /// # async fn example() -> Result<(), viewpoint_core::CoreError> {
42    /// let browser = Browser::launch()
43    ///     .headless(true)
44    ///     .launch()
45    ///     .await?;
46    /// # Ok(())
47    /// # }
48    /// ```
49    pub fn launch() -> BrowserBuilder {
50        BrowserBuilder::new()
51    }
52
53    /// Connect to an already-running browser via WebSocket URL.
54    ///
55    /// # Example
56    ///
57    /// ```no_run
58    /// use viewpoint_core::Browser;
59    ///
60    /// # async fn example() -> Result<(), viewpoint_core::CoreError> {
61    /// let browser = Browser::connect("ws://localhost:9222/devtools/browser/...").await?;
62    /// # Ok(())
63    /// # }
64    /// ```
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the connection fails.
69    pub async fn connect(ws_url: &str) -> Result<Self, BrowserError> {
70        let connection = CdpConnection::connect(ws_url).await?;
71
72        Ok(Self {
73            connection: Arc::new(connection),
74            process: None,
75            owned: false,
76        })
77    }
78
79    /// Create a browser from an existing connection and process.
80    pub(crate) fn from_connection_and_process(
81        connection: CdpConnection,
82        process: Child,
83    ) -> Self {
84        Self {
85            connection: Arc::new(connection),
86            process: Some(Mutex::new(process)),
87            owned: true,
88        }
89    }
90
91    /// Create a new isolated browser context.
92    ///
93    /// Browser contexts are isolated environments within the browser,
94    /// similar to incognito windows. They have their own cookies,
95    /// cache, and storage.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if context creation fails.
100    pub async fn new_context(&self) -> Result<BrowserContext, BrowserError> {
101        let result: CreateBrowserContextResult = self
102            .connection
103            .send_command(
104                "Target.createBrowserContext",
105                Some(CreateBrowserContextParams::default()),
106                None,
107            )
108            .await?;
109
110        Ok(BrowserContext::new(
111            self.connection.clone(),
112            result.browser_context_id,
113        ))
114    }
115
116    /// Close the browser.
117    ///
118    /// If this browser was launched by us, the process will be terminated.
119    /// If it was connected to, only the WebSocket connection is closed.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if closing fails.
124    pub async fn close(&self) -> Result<(), BrowserError> {
125        // If we own the process, terminate it
126        if let Some(ref process) = self.process {
127            let mut child = process.lock().await;
128            let _ = child.kill();
129        }
130
131        Ok(())
132    }
133
134    /// Get a reference to the CDP connection.
135    pub fn connection(&self) -> &Arc<CdpConnection> {
136        &self.connection
137    }
138
139    /// Check if this browser was launched by us.
140    pub fn is_owned(&self) -> bool {
141        self.owned
142    }
143}
144
145impl Drop for Browser {
146    fn drop(&mut self) {
147        // Try to kill the process if we own it
148        if self.owned {
149            if let Some(ref process) = self.process {
150                // We can't await in drop, so we try to kill synchronously
151                if let Ok(mut guard) = process.try_lock() {
152                    let _ = guard.kill();
153                }
154            }
155        }
156    }
157}