Skip to main content

playwright_cdp/
browser.rs

1//! `Browser` — top-level entry point. Launches/connects to Chromium over CDP.
2
3use crate::browser_context::BrowserContext;
4use crate::browser_process::{resolve_ws_endpoint, BrowserProcess};
5use crate::cdp::connection::CdpConnection;
6use crate::cdp::session::CdpSession;
7use crate::error::Result;
8use crate::options::{ConnectOverCdpOptions, LaunchOptions};
9use crate::page::Page;
10use parking_lot::Mutex;
11use serde_json::json;
12use std::sync::Arc;
13
14/// A connected Chromium browser driven over CDP.
15///
16/// Created via [`Browser::launch`] or [`Browser::connect_over_cdp`]. Cheaply
17/// cloneable (shares one connection).
18#[derive(Clone)]
19pub struct Browser {
20    inner: Arc<BrowserInner>,
21}
22
23struct BrowserInner {
24    conn: Arc<CdpConnection>,
25    browser_session: CdpSession,
26    process: Mutex<Option<BrowserProcess>>,
27    contexts: Mutex<Vec<BrowserContext>>,
28}
29
30impl Browser {
31    /// Spawn a local Chromium-based browser per `opts`.
32    pub async fn launch(opts: LaunchOptions) -> Result<Browser> {
33        let process = BrowserProcess::launch(&opts).await?;
34        let ws_url = process.ws_url().to_string();
35        let (conn, browser_session) = connect(&ws_url, None).await?;
36        Ok(Self {
37            inner: Arc::new(BrowserInner {
38                conn,
39                browser_session,
40                process: Mutex::new(Some(process)),
41                contexts: Mutex::new(Vec::new()),
42            }),
43        })
44    }
45
46    /// Attach to an already-running Chromium given an `http://host:port` or
47    /// `ws://...` endpoint.
48    pub async fn connect_over_cdp(
49        endpoint: &str,
50        opts: Option<ConnectOverCdpOptions>,
51    ) -> Result<Browser> {
52        let ws_url = resolve_ws_endpoint(endpoint).await?;
53        let headers = opts.and_then(|o| o.headers);
54        let (conn, browser_session) = connect(&ws_url, headers).await?;
55        Ok(Self {
56            inner: Arc::new(BrowserInner {
57                conn,
58                browser_session,
59                process: Mutex::new(None),
60                contexts: Mutex::new(Vec::new()),
61            }),
62        })
63    }
64
65    /// The browser-level CDP connection (advanced/power-user access).
66    pub fn connection(&self) -> &Arc<CdpConnection> {
67        &self.inner.conn
68    }
69
70    /// A browser-level [`CdpSession`] (no `sessionId`). Equivalent to
71    /// Playwright's `browser.new_browser_cdp_session()`.
72    pub fn new_browser_cdp_session(&self) -> CdpSession {
73        CdpSession::browser(self.inner.conn.clone())
74    }
75
76    /// Create a new isolated browser context (incognito-style).
77    pub async fn new_context(&self) -> Result<BrowserContext> {
78        BrowserContext::create(self.clone(), None).await
79    }
80
81    /// Open a page in the default browser context.
82    pub async fn new_page(&self) -> Result<Page> {
83        let ctx = BrowserContext::default_for(self.clone());
84        ctx.new_page().await
85    }
86
87    /// Browser version string (e.g. "HeadlessChrome/...").
88    pub async fn version(&self) -> Result<String> {
89        let v = self
90            .inner
91            .browser_session
92            .send("Browser.getVersion", json!({}))
93            .await?;
94        Ok(v.get("product")
95            .and_then(|x| x.as_str())
96            .unwrap_or("unknown")
97            .to_string())
98    }
99
100    /// Whether the underlying connection is still open.
101    pub fn is_connected(&self) -> bool {
102        // The dispatch task holds an Arc to the connection; we consider it
103        // connected as long as a writer is reachable. A precise signal comes
104        // from `on_disconnected`.
105        true
106    }
107
108    /// The browser engine type this browser was launched as (always Chromium here).
109    pub fn browser_type(&self) -> crate::browser_type::BrowserType {
110        crate::browser_type::BrowserType::chromium()
111    }
112
113    /// All browser contexts created on this browser.
114    pub fn contexts(&self) -> Vec<BrowserContext> {
115        self.inner.contexts.lock().clone()
116    }
117
118    /// Register a handler invoked when the connection drops.
119    pub fn on_disconnected<F, Fut>(&self, handler: F)
120    where
121        F: FnOnce() -> Fut + Send + 'static,
122        Fut: std::future::Future<Output = ()> + Send + 'static,
123    {
124        let mut rx = self.inner.conn.subscribe(None);
125        tokio::spawn(async move {
126            loop {
127                match rx.recv().await {
128                    Err(tokio::sync::broadcast::error::RecvError::Closed) => {
129                        handler().await;
130                        return;
131                    }
132                    // Lagged or ok: keep waiting for the close.
133                    _ => {}
134                }
135            }
136        });
137    }
138
139    /// Close the browser and kill the owned process (if we launched it).
140    pub async fn close(&self) -> Result<()> {
141        // Best-effort graceful close.
142        let _ = self
143            .inner
144            .browser_session
145            .send("Browser.close", json!({}))
146            .await;
147        if let Some(mut process) = self.inner.process.lock().take() {
148            let _ = process.kill().await;
149        }
150        let _ = self.inner.conn.close().await;
151        Ok(())
152    }
153
154    /// (Internal) the browser-level session, used by contexts/pages.
155    pub(crate) fn browser_session(&self) -> &CdpSession {
156        &self.inner.browser_session
157    }
158
159    /// (Internal) register a context so `contexts()` reports it.
160    pub(crate) fn track_context(&self, ctx: BrowserContext) {
161        self.inner.contexts.lock().push(ctx);
162    }
163}
164
165impl Drop for BrowserInner {
166    fn drop(&mut self) {
167        // BrowserProcess::Drop kills the child; nothing async to do here.
168    }
169}
170
171/// Connect a [`CdpConnection`] + browser session, enabling flattened auto-attach
172/// so popups show up as new sessions on the same socket.
173async fn connect(
174    ws_url: &str,
175    headers: Option<std::collections::HashMap<String, String>>,
176) -> Result<(Arc<CdpConnection>, CdpSession)> {
177    let (conn, _browser_rx) = CdpConnection::connect(ws_url, headers).await?;
178    let session = CdpSession::browser(conn.clone());
179    // Flattened auto-attach: child-target traffic rides the same socket tagged
180    // with sessionId. waitForDebuggerOnStart=false so we don't pause popups.
181    let _ = session
182        .send(
183            "Target.setAutoAttach",
184            json!({"autoAttach": true, "waitForDebuggerOnStart": false, "flatten": true}),
185        )
186        .await;
187    Ok((conn, session))
188}