Skip to main content

playwright_rs/protocol/
cdp_session.rs

1// Copyright 2026 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// CDPSession — Chrome DevTools Protocol session object
5//
6// Architecture Reference:
7// - Python: playwright-python/playwright/_impl/_cdp_session.py
8// - JavaScript: playwright/packages/playwright-core/src/client/cdpSession.ts
9// - Docs: https://playwright.dev/docs/api/class-cdpsession
10
11//! CDPSession — Chrome DevTools Protocol session
12//!
13//! Provides access to the Chrome DevTools Protocol for Chromium-based browsers.
14//! CDPSession is created via [`BrowserContext::new_cdp_session`](crate::protocol::BrowserContext::new_cdp_session).
15//!
16//! # Example
17//!
18//! ```ignore
19//! use playwright_rs::protocol::Playwright;
20//!
21//! #[tokio::main]
22//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
23//!     let playwright = Playwright::launch().await?;
24//!     let browser = playwright.chromium().launch().await?;
25//!     let context = browser.new_context().await?;
26//!     let page = context.new_page().await?;
27//!
28//!     // Create a CDP session for the page
29//!     let session = context.new_cdp_session(&page).await?;
30//!
31//!     // Send a CDP command
32//!     let result = session
33//!         .send("Runtime.evaluate", Some(serde_json::json!({ "expression": "1+1" })))
34//!         .await?;
35//!
36//!     println!("Result: {:?}", result);
37//!
38//!     session.detach().await?;
39//!     context.close().await?;
40//!     browser.close().await?;
41//!     Ok(())
42//! }
43//! ```
44//!
45//! See: <https://playwright.dev/docs/api/class-cdpsession>
46
47use crate::error::Result;
48use crate::server::channel::Channel;
49use crate::server::channel_owner::{
50    ChannelOwner, ChannelOwnerImpl, DisposeReason, ParentOrConnection,
51};
52use crate::server::connection::ConnectionLike;
53use parking_lot::Mutex;
54use serde_json::Value;
55use std::any::Any;
56use std::collections::HashMap;
57use std::future::Future;
58use std::pin::Pin;
59use std::sync::Arc;
60use tracing::Instrument;
61
62type EventHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>>;
63type EventHandler = Arc<dyn Fn(Value) -> EventHandlerFuture + Send + Sync + 'static>;
64type CloseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>>;
65type CloseHandler = Arc<dyn Fn() -> CloseHandlerFuture + Send + Sync + 'static>;
66
67/// A Chrome DevTools Protocol session for a page or browser context.
68///
69/// CDPSession is only available in Chromium-based browsers.
70///
71/// See: <https://playwright.dev/docs/api/class-cdpsession>
72#[derive(Clone)]
73pub struct CDPSession {
74    base: ChannelOwnerImpl,
75    event_handlers: Arc<Mutex<HashMap<String, Vec<EventHandler>>>>,
76    close_handlers: Arc<Mutex<Vec<CloseHandler>>>,
77}
78
79impl CDPSession {
80    /// Creates a new CDPSession from protocol initialization.
81    ///
82    /// Called by the object factory when the server sends a `__create__` message.
83    pub fn new(
84        parent: ParentOrConnection,
85        type_name: String,
86        guid: Arc<str>,
87        initializer: Value,
88    ) -> Result<Self> {
89        Ok(Self {
90            base: ChannelOwnerImpl::new(parent, type_name, guid, initializer),
91            event_handlers: Arc::new(Mutex::new(HashMap::new())),
92            close_handlers: Arc::new(Mutex::new(Vec::new())),
93        })
94    }
95
96    /// Send a CDP command and return the result.
97    ///
98    /// # Arguments
99    ///
100    /// * `method` - The CDP method name (e.g., `"Runtime.evaluate"`)
101    /// * `params` - Optional JSON parameters for the method
102    ///
103    /// # Errors
104    ///
105    /// Returns error if:
106    /// - The session has been detached
107    /// - The CDP method fails
108    /// - Communication with browser process fails
109    ///
110    /// See: <https://playwright.dev/docs/api/class-cdpsession#cdp-session-send>
111    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), method = %method))]
112    pub async fn send(&self, method: &str, params: Option<Value>) -> Result<Value> {
113        let params = serde_json::json!({
114            "method": method,
115            "params": params.unwrap_or(serde_json::json!({})),
116        });
117        self.channel().send("send", params).await
118    }
119
120    /// Register a handler for a CDP event by method name. Multiple handlers
121    /// may be registered for the same method; they fire in registration
122    /// order. The Playwright server forwards every CDP event the
123    /// underlying session emits — no Playwright-side subscription is
124    /// needed, but you typically must enable the CDP domain itself first
125    /// (e.g. `session.send("Network.enable", None).await?` before
126    /// expecting `Network.requestWillBeSent`).
127    ///
128    /// See: <https://playwright.dev/docs/api/class-cdpsession#cdp-session-on>
129    pub fn on<F, Fut>(&self, method: impl Into<String>, handler: F)
130    where
131        F: Fn(Value) -> Fut + Send + Sync + 'static,
132        Fut: Future<Output = Result<()>> + Send + 'static,
133    {
134        let h: EventHandler =
135            Arc::new(move |v: Value| -> EventHandlerFuture { Box::pin(handler(v)) });
136        self.event_handlers
137            .lock()
138            .entry(method.into())
139            .or_default()
140            .push(h);
141    }
142
143    /// Register a handler for the `close` event, fired when the session
144    /// is detached (parent target closes, browser closes, or
145    /// [`detach`](Self::detach) is called).
146    ///
147    /// See: <https://playwright.dev/docs/api/class-cdpsession#cdp-session-event-close>
148    pub fn on_close<F, Fut>(&self, handler: F)
149    where
150        F: Fn() -> Fut + Send + Sync + 'static,
151        Fut: Future<Output = Result<()>> + Send + 'static,
152    {
153        let h: CloseHandler = Arc::new(move || -> CloseHandlerFuture { Box::pin(handler()) });
154        self.close_handlers.lock().push(h);
155    }
156
157    /// Detach the CDP session from the target.
158    ///
159    /// After detaching, the session can no longer be used to send commands.
160    ///
161    /// # Errors
162    ///
163    /// Returns error if:
164    /// - The session has already been detached
165    /// - Communication with browser process fails
166    ///
167    /// See: <https://playwright.dev/docs/api/class-cdpsession#cdp-session-detach>
168    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
169    pub async fn detach(&self) -> Result<()> {
170        self.channel()
171            .send_no_result("detach", serde_json::json!({}))
172            .await
173    }
174}
175
176impl ChannelOwner for CDPSession {
177    fn guid(&self) -> &str {
178        self.base.guid()
179    }
180
181    fn type_name(&self) -> &str {
182        self.base.type_name()
183    }
184
185    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
186        self.base.parent()
187    }
188
189    fn connection(&self) -> Arc<dyn ConnectionLike> {
190        self.base.connection()
191    }
192
193    fn initializer(&self) -> &Value {
194        self.base.initializer()
195    }
196
197    fn channel(&self) -> &Channel {
198        self.base.channel()
199    }
200
201    fn dispose(&self, reason: DisposeReason) {
202        self.base.dispose(reason)
203    }
204
205    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
206        self.base.adopt(child)
207    }
208
209    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
210        self.base.add_child(guid, child)
211    }
212
213    fn remove_child(&self, guid: &str) {
214        self.base.remove_child(guid)
215    }
216
217    fn on_event(&self, method: &str, params: Value) {
218        match method {
219            "event" => {
220                let cdp_method = params
221                    .get("method")
222                    .and_then(|v| v.as_str())
223                    .map(|s| s.to_string());
224                let cdp_params = params.get("params").cloned().unwrap_or(Value::Null);
225                if let Some(cdp_method) = cdp_method {
226                    let handlers = self
227                        .event_handlers
228                        .lock()
229                        .get(&cdp_method)
230                        .cloned()
231                        .unwrap_or_default();
232                    for h in handlers {
233                        let p = cdp_params.clone();
234                        tokio::spawn(
235                            async move {
236                                if let Err(e) = h(p).await {
237                                    tracing::warn!("CDPSession event handler error: {}", e);
238                                }
239                            }
240                            .in_current_span(),
241                        );
242                    }
243                }
244            }
245            "close" => {
246                let handlers = self.close_handlers.lock().clone();
247                for h in handlers {
248                    tokio::spawn(
249                        async move {
250                            if let Err(e) = h().await {
251                                tracing::warn!("CDPSession close handler error: {}", e);
252                            }
253                        }
254                        .in_current_span(),
255                    );
256                }
257            }
258            _ => {}
259        }
260        self.base.on_event(method, params);
261    }
262
263    fn was_collected(&self) -> bool {
264        self.base.was_collected()
265    }
266
267    fn as_any(&self) -> &dyn Any {
268        self
269    }
270}
271
272impl std::fmt::Debug for CDPSession {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        f.debug_struct("CDPSession")
275            .field("guid", &self.guid())
276            .finish()
277    }
278}