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//! ```no_run
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;
60
61type EventHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>>;
62type EventHandler = Arc<dyn Fn(Value) -> EventHandlerFuture + Send + Sync + 'static>;
63type CloseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>>;
64type CloseHandler = Arc<dyn Fn() -> CloseHandlerFuture + Send + Sync + 'static>;
65
66/// A Chrome DevTools Protocol session for a page or browser context.
67///
68/// CDPSession is only available in Chromium-based browsers.
69///
70/// See: <https://playwright.dev/docs/api/class-cdpsession>
71#[derive(Clone)]
72pub struct CDPSession {
73 base: ChannelOwnerImpl,
74 event_handlers: Arc<Mutex<HashMap<String, Vec<EventHandler>>>>,
75 close_handlers: Arc<Mutex<Vec<CloseHandler>>>,
76}
77
78impl CDPSession {
79 /// Creates a new CDPSession from protocol initialization.
80 ///
81 /// Called by the object factory when the server sends a `__create__` message.
82 pub fn new(
83 parent: ParentOrConnection,
84 type_name: String,
85 guid: Arc<str>,
86 initializer: Value,
87 ) -> Result<Self> {
88 Ok(Self {
89 base: ChannelOwnerImpl::new(parent, type_name, guid, initializer),
90 event_handlers: Arc::new(Mutex::new(HashMap::new())),
91 close_handlers: Arc::new(Mutex::new(Vec::new())),
92 })
93 }
94
95 /// Send a CDP command and return the result.
96 ///
97 /// # Arguments
98 ///
99 /// * `method` - The CDP method name (e.g., `"Runtime.evaluate"`)
100 /// * `params` - Optional JSON parameters for the method
101 ///
102 /// # Errors
103 ///
104 /// Returns error if:
105 /// - The session has been detached
106 /// - The CDP method fails
107 /// - Communication with browser process fails
108 ///
109 /// See: <https://playwright.dev/docs/api/class-cdpsession#cdp-session-send>
110 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), method = %method))]
111 pub async fn send(&self, method: &str, params: Option<Value>) -> Result<Value> {
112 let params = serde_json::json!({
113 "method": method,
114 "params": params.unwrap_or(serde_json::json!({})),
115 });
116 self.channel().send("send", params).await
117 }
118
119 /// Register a handler for a CDP event by method name. Multiple handlers
120 /// may be registered for the same method; they fire in registration
121 /// order. The Playwright server forwards every CDP event the
122 /// underlying session emits — no Playwright-side subscription is
123 /// needed, but you typically must enable the CDP domain itself first
124 /// (e.g. `session.send("Network.enable", None).await?` before
125 /// expecting `Network.requestWillBeSent`).
126 ///
127 /// See: <https://playwright.dev/docs/api/class-cdpsession#cdp-session-on>
128 pub fn on<F, Fut>(&self, method: impl Into<String>, handler: F)
129 where
130 F: Fn(Value) -> Fut + Send + Sync + 'static,
131 Fut: Future<Output = Result<()>> + Send + 'static,
132 {
133 let h: EventHandler =
134 Arc::new(move |v: Value| -> EventHandlerFuture { Box::pin(handler(v)) });
135 self.event_handlers
136 .lock()
137 .entry(method.into())
138 .or_default()
139 .push(h);
140 }
141
142 /// Register a handler for the `close` event, fired when the session
143 /// is detached (parent target closes, browser closes, or
144 /// [`detach`](Self::detach) is called).
145 ///
146 /// See: <https://playwright.dev/docs/api/class-cdpsession#cdp-session-event-close>
147 pub fn on_close<F, Fut>(&self, handler: F)
148 where
149 F: Fn() -> Fut + Send + Sync + 'static,
150 Fut: Future<Output = Result<()>> + Send + 'static,
151 {
152 let h: CloseHandler = Arc::new(move || -> CloseHandlerFuture { Box::pin(handler()) });
153 self.close_handlers.lock().push(h);
154 }
155
156 /// Detach the CDP session from the target.
157 ///
158 /// After detaching, the session can no longer be used to send commands.
159 ///
160 /// # Errors
161 ///
162 /// Returns error if:
163 /// - The session has already been detached
164 /// - Communication with browser process fails
165 ///
166 /// See: <https://playwright.dev/docs/api/class-cdpsession#cdp-session-detach>
167 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
168 pub async fn detach(&self) -> Result<()> {
169 self.channel()
170 .send_no_result("detach", serde_json::json!({}))
171 .await
172 }
173}
174
175impl ChannelOwner for CDPSession {
176 fn guid(&self) -> &str {
177 self.base.guid()
178 }
179
180 fn type_name(&self) -> &str {
181 self.base.type_name()
182 }
183
184 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
185 self.base.parent()
186 }
187
188 fn connection(&self) -> Arc<dyn ConnectionLike> {
189 self.base.connection()
190 }
191
192 fn initializer(&self) -> &Value {
193 self.base.initializer()
194 }
195
196 fn channel(&self) -> &Channel {
197 self.base.channel()
198 }
199
200 fn dispose(&self, reason: DisposeReason) {
201 self.base.dispose(reason)
202 }
203
204 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
205 self.base.adopt(child)
206 }
207
208 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
209 self.base.add_child(guid, child)
210 }
211
212 fn remove_child(&self, guid: &str) {
213 self.base.remove_child(guid)
214 }
215
216 fn on_event(&self, method: &str, params: Value) {
217 match method {
218 "event" => {
219 let cdp_method = params
220 .get("method")
221 .and_then(|v| v.as_str())
222 .map(|s| s.to_string());
223 let cdp_params = params.get("params").cloned().unwrap_or(Value::Null);
224 if let Some(cdp_method) = cdp_method {
225 let handlers = self
226 .event_handlers
227 .lock()
228 .get(&cdp_method)
229 .cloned()
230 .unwrap_or_default();
231 for h in handlers {
232 let p = cdp_params.clone();
233 crate::task::spawn(async move {
234 if let Err(e) = h(p).await {
235 tracing::warn!("CDPSession event handler error: {}", e);
236 }
237 });
238 }
239 }
240 }
241 "close" => {
242 let handlers = self.close_handlers.lock().clone();
243 for h in handlers {
244 crate::task::spawn(async move {
245 if let Err(e) = h().await {
246 tracing::warn!("CDPSession close handler error: {}", e);
247 }
248 });
249 }
250 }
251 _ => {}
252 }
253 self.base.on_event(method, params);
254 }
255
256 fn was_collected(&self) -> bool {
257 self.base.was_collected()
258 }
259
260 fn as_any(&self) -> &dyn Any {
261 self
262 }
263}
264
265impl std::fmt::Debug for CDPSession {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 f.debug_struct("CDPSession")
268 .field("guid", &self.guid())
269 .finish()
270 }
271}