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}