Skip to main content

cdpkit/
lib.rs

1mod error;
2mod inner;
3mod listeners;
4mod types;
5
6// Generated CDP protocol definitions
7#[allow(clippy::all)]
8#[allow(deprecated)]
9pub mod protocol;
10
11pub use error::CdpError;
12pub use types::Method;
13
14// Re-export all CDP domains
15pub use protocol::*;
16
17use inner::CDPInner;
18use std::sync::Arc;
19
20/// Chrome DevTools Protocol client
21pub struct CDP {
22    pub(crate) inner: Arc<CDPInner>,
23}
24
25impl CDP {
26    /// Connect to Chrome by host and port (most common usage)
27    ///
28    /// Automatically discovers the WebSocket URL from Chrome's debugging endpoint.
29    ///
30    /// # Arguments
31    /// * `host` - The host and port (e.g., "localhost:9222" or "http://localhost:9222")
32    ///
33    /// # Example
34    /// ```no_run
35    /// # use cdpkit::CDP;
36    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
37    /// // Connect to default Chrome debugging port
38    /// let cdp = CDP::connect("localhost:9222").await?;
39    /// # Ok(())
40    /// # }
41    /// ```
42    pub async fn connect(host: &str) -> Result<Self, CdpError> {
43        // If it's a WebSocket URL, connect directly
44        if host.starts_with("ws://") || host.starts_with("wss://") {
45            return Self::connect_ws(host).await;
46        }
47
48        // Otherwise, auto-discover the WebSocket URL
49        let ws_url = discover_ws_url(host).await?;
50        Self::connect_ws(&ws_url).await
51    }
52
53    /// Connect directly using a WebSocket URL (advanced usage)
54    ///
55    /// Use this when you already have the full WebSocket URL.
56    ///
57    /// # Example
58    /// ```no_run
59    /// # use cdpkit::CDP;
60    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
61    /// let cdp = CDP::connect_ws("ws://localhost:9222/devtools/browser/...").await?;
62    /// # Ok(())
63    /// # }
64    /// ```
65    pub async fn connect_ws(url: &str) -> Result<Self, CdpError> {
66        let inner = CDPInner::connect(url).await?;
67        Ok(Self { inner })
68    }
69
70    /// Send a method
71    pub async fn send<C: Method>(
72        &self,
73        cmd: C,
74        session_id: Option<&str>,
75    ) -> Result<C::Response, CdpError> {
76        self.inner.send_command(cmd, session_id).await
77    }
78
79    /// Send a raw CDP command with a dynamic method name and JSON params.
80    ///
81    /// This is useful for sending arbitrary CDP commands that don't have
82    /// a corresponding typed `Method` implementation.
83    ///
84    /// # Arguments
85    /// * `method` - The CDP method name (e.g., "Page.navigate")
86    /// * `params` - The method parameters as a JSON value
87    /// * `session_id` - Optional CDP session ID for targeting a specific session
88    ///
89    /// # Example
90    /// ```no_run
91    /// # use cdpkit::CDP;
92    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
93    /// let cdp = CDP::connect("localhost:9222").await?;
94    /// let result = cdp.send_raw("Page.navigate", serde_json::json!({"url": "https://example.com"}), None).await?;
95    /// # Ok(())
96    /// # }
97    /// ```
98    pub async fn send_raw(
99        &self,
100        method: &str,
101        params: serde_json::Value,
102        session_id: Option<&str>,
103    ) -> Result<serde_json::Value, CdpError> {
104        self.inner.send_raw(method, params, session_id).await
105    }
106
107    /// Subscribe to CDP events by event name.
108    ///
109    /// Returns a stream of deserialized event objects.
110    ///
111    /// # Arguments
112    /// * `event_name` - The CDP event name (e.g., "Network.requestWillBeSent")
113    pub fn event_stream<T>(
114        &self,
115        event_name: &str,
116    ) -> std::pin::Pin<Box<dyn futures::Stream<Item = T> + Send>>
117    where
118        T: serde::de::DeserializeOwned + Send + 'static,
119    {
120        self.inner.event_stream(event_name)
121    }
122
123    /// Get the CDP protocol version this library was built with
124    pub fn version() -> &'static str {
125        CDP_VERSION
126    }
127}
128
129/// Discover WebSocket URL from Chrome's remote debugging endpoint
130async fn discover_ws_url(host: &str) -> Result<String, CdpError> {
131    use serde_json::Value;
132
133    // Normalize host to include http:// if not present
134    let base_url = if host.starts_with("http://") || host.starts_with("https://") {
135        host.to_string()
136    } else {
137        format!("http://{}", host)
138    };
139
140    // Fetch the JSON endpoint
141    let url = format!("{}/json/version", base_url);
142    let response = reqwest::get(&url)
143        .await
144        .map_err(|e| CdpError::ConnectionFailed(e.to_string()))?;
145
146    let json: Value = response
147        .json()
148        .await
149        .map_err(|e| CdpError::ConnectionFailed(e.to_string()))?;
150
151    // Extract webSocketDebuggerUrl
152    json.get("webSocketDebuggerUrl")
153        .and_then(|v| v.as_str())
154        .map(|s| s.to_string())
155        .ok_or_else(|| CdpError::ConnectionFailed("No webSocketDebuggerUrl found".to_string()))
156}