viewpoint_core/browser/
mod.rs1mod launcher;
4
5use std::process::Child;
6use std::sync::Arc;
7use std::time::Duration;
8
9use viewpoint_cdp::protocol::target_domain::{CreateBrowserContextParams, CreateBrowserContextResult};
10use viewpoint_cdp::CdpConnection;
11use tokio::sync::Mutex;
12
13use crate::context::{BrowserContext, ContextOptions, ContextOptionsBuilder, StorageState, StorageStateSource};
14use crate::devices::DeviceDescriptor;
15use crate::error::BrowserError;
16
17pub use launcher::BrowserBuilder;
18
19const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
21
22#[derive(Debug)]
24pub struct Browser {
25 connection: Arc<CdpConnection>,
27 process: Option<Mutex<Child>>,
29 owned: bool,
31}
32
33impl Browser {
34 pub fn launch() -> BrowserBuilder {
50 BrowserBuilder::new()
51 }
52
53 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 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 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 pub fn new_context_builder(&self) -> NewContextBuilder<'_> {
138 NewContextBuilder::new(self)
139 }
140
141 pub async fn new_context_with_options(
147 &self,
148 options: ContextOptions,
149 ) -> Result<BrowserContext, BrowserError> {
150 let storage_state = match &options.storage_state {
152 Some(StorageStateSource::Path(path)) => {
153 Some(StorageState::load(path).await.map_err(|e| {
154 BrowserError::LaunchFailed(format!("Failed to load storage state: {e}"))
155 })?)
156 }
157 Some(StorageStateSource::State(state)) => Some(state.clone()),
158 None => None,
159 };
160
161 let result: CreateBrowserContextResult = self
162 .connection
163 .send_command(
164 "Target.createBrowserContext",
165 Some(CreateBrowserContextParams::default()),
166 None,
167 )
168 .await?;
169
170 let context = BrowserContext::with_options(
171 self.connection.clone(),
172 result.browser_context_id,
173 options,
174 );
175
176 context.apply_options().await?;
178
179 if let Some(state) = storage_state {
181 context.add_cookies(state.cookies.clone()).await?;
183
184 let local_storage_script = state.to_local_storage_init_script();
186 if !local_storage_script.is_empty() {
187 context.add_init_script(&local_storage_script).await?;
188 }
189
190 let indexed_db_script = state.to_indexed_db_init_script();
192 if !indexed_db_script.is_empty() {
193 context.add_init_script(&indexed_db_script).await?;
194 }
195 }
196
197 Ok(context)
198 }
199
200 pub async fn close(&self) -> Result<(), BrowserError> {
209 if let Some(ref process) = self.process {
211 let mut child = process.lock().await;
212 let _ = child.kill();
213 }
214
215 Ok(())
216 }
217
218 pub fn connection(&self) -> &Arc<CdpConnection> {
220 &self.connection
221 }
222
223 pub fn is_owned(&self) -> bool {
225 self.owned
226 }
227}
228
229#[derive(Debug)]
231pub struct NewContextBuilder<'a> {
232 browser: &'a Browser,
233 builder: ContextOptionsBuilder,
234}
235
236impl<'a> NewContextBuilder<'a> {
237 fn new(browser: &'a Browser) -> Self {
238 Self {
239 browser,
240 builder: ContextOptionsBuilder::new(),
241 }
242 }
243
244 #[must_use]
246 pub fn storage_state_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
247 self.builder = self.builder.storage_state_path(path);
248 self
249 }
250
251 #[must_use]
253 pub fn storage_state(mut self, state: StorageState) -> Self {
254 self.builder = self.builder.storage_state(state);
255 self
256 }
257
258 #[must_use]
260 pub fn geolocation(mut self, latitude: f64, longitude: f64) -> Self {
261 self.builder = self.builder.geolocation(latitude, longitude);
262 self
263 }
264
265 #[must_use]
267 pub fn geolocation_with_accuracy(mut self, latitude: f64, longitude: f64, accuracy: f64) -> Self {
268 self.builder = self.builder.geolocation_with_accuracy(latitude, longitude, accuracy);
269 self
270 }
271
272 #[must_use]
274 pub fn permissions(mut self, permissions: Vec<crate::context::Permission>) -> Self {
275 self.builder = self.builder.permissions(permissions);
276 self
277 }
278
279 #[must_use]
281 pub fn http_credentials(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
282 self.builder = self.builder.http_credentials(username, password);
283 self
284 }
285
286 #[must_use]
288 pub fn extra_http_headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
289 self.builder = self.builder.extra_http_headers(headers);
290 self
291 }
292
293 #[must_use]
295 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
296 self.builder = self.builder.header(name, value);
297 self
298 }
299
300 #[must_use]
302 pub fn offline(mut self, offline: bool) -> Self {
303 self.builder = self.builder.offline(offline);
304 self
305 }
306
307 #[must_use]
309 pub fn default_timeout(mut self, timeout: Duration) -> Self {
310 self.builder = self.builder.default_timeout(timeout);
311 self
312 }
313
314 #[must_use]
316 pub fn default_navigation_timeout(mut self, timeout: Duration) -> Self {
317 self.builder = self.builder.default_navigation_timeout(timeout);
318 self
319 }
320
321 #[must_use]
323 pub fn has_touch(mut self, has_touch: bool) -> Self {
324 self.builder = self.builder.has_touch(has_touch);
325 self
326 }
327
328 #[must_use]
330 pub fn locale(mut self, locale: impl Into<String>) -> Self {
331 self.builder = self.builder.locale(locale);
332 self
333 }
334
335 #[must_use]
337 pub fn timezone_id(mut self, timezone_id: impl Into<String>) -> Self {
338 self.builder = self.builder.timezone_id(timezone_id);
339 self
340 }
341
342 #[must_use]
344 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
345 self.builder = self.builder.user_agent(user_agent);
346 self
347 }
348
349 #[must_use]
351 pub fn viewport(mut self, width: i32, height: i32) -> Self {
352 self.builder = self.builder.viewport(width, height);
353 self
354 }
355
356 #[must_use]
358 pub fn color_scheme(mut self, color_scheme: crate::context::ColorScheme) -> Self {
359 self.builder = self.builder.color_scheme(color_scheme);
360 self
361 }
362
363 #[must_use]
365 pub fn reduced_motion(mut self, reduced_motion: crate::context::ReducedMotion) -> Self {
366 self.builder = self.builder.reduced_motion(reduced_motion);
367 self
368 }
369
370 #[must_use]
372 pub fn forced_colors(mut self, forced_colors: crate::context::ForcedColors) -> Self {
373 self.builder = self.builder.forced_colors(forced_colors);
374 self
375 }
376
377 #[must_use]
379 pub fn device_scale_factor(mut self, scale_factor: f64) -> Self {
380 self.builder = self.builder.device_scale_factor(scale_factor);
381 self
382 }
383
384 #[must_use]
386 pub fn is_mobile(mut self, is_mobile: bool) -> Self {
387 self.builder = self.builder.is_mobile(is_mobile);
388 self
389 }
390
391 #[must_use]
412 pub fn device(mut self, device: DeviceDescriptor) -> Self {
413 self.builder = self.builder.device(device);
414 self
415 }
416
417 #[must_use]
433 pub fn record_video(mut self, options: crate::page::VideoOptions) -> Self {
434 self.builder = self.builder.record_video(options);
435 self
436 }
437
438 pub async fn build(self) -> Result<BrowserContext, BrowserError> {
444 self.browser.new_context_with_options(self.builder.build()).await
445 }
446}
447
448impl Drop for Browser {
449 fn drop(&mut self) {
450 if self.owned {
452 if let Some(ref process) = self.process {
453 if let Ok(mut guard) = process.try_lock() {
455 let _ = guard.kill();
456 }
457 }
458 }
459 }
460}