mod connector;
mod launcher;
use std::process::Child;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tracing::info;
use viewpoint_cdp::CdpConnection;
use viewpoint_cdp::protocol::target_domain::{
CreateBrowserContextParams, CreateBrowserContextResult, GetBrowserContextsResult,
};
use crate::context::{
BrowserContext, ContextOptions, ContextOptionsBuilder, StorageState, StorageStateSource,
};
use crate::devices::DeviceDescriptor;
use crate::error::BrowserError;
pub use connector::ConnectOverCdpBuilder;
pub use launcher::BrowserBuilder;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub struct Browser {
connection: Arc<CdpConnection>,
process: Option<Mutex<Child>>,
owned: bool,
}
impl Browser {
pub fn launch() -> BrowserBuilder {
BrowserBuilder::new()
}
pub async fn connect(ws_url: &str) -> Result<Self, BrowserError> {
let connection = CdpConnection::connect(ws_url).await?;
Ok(Self {
connection: Arc::new(connection),
process: None,
owned: false,
})
}
pub fn connect_over_cdp(endpoint_url: impl Into<String>) -> ConnectOverCdpBuilder {
ConnectOverCdpBuilder::new(endpoint_url)
}
pub async fn contexts(&self) -> Result<Vec<BrowserContext>, BrowserError> {
info!("Getting browser contexts");
let result: GetBrowserContextsResult = self
.connection
.send_command("Target.getBrowserContexts", None::<()>, None)
.await?;
let mut contexts = Vec::new();
contexts.push(BrowserContext::from_existing(
self.connection.clone(),
String::new(), ));
for context_id in result.browser_context_ids {
if !context_id.is_empty() {
contexts.push(BrowserContext::from_existing(
self.connection.clone(),
context_id,
));
}
}
info!(count = contexts.len(), "Found browser contexts");
Ok(contexts)
}
pub(crate) fn from_connection_and_process(connection: CdpConnection, process: Child) -> Self {
Self {
connection: Arc::new(connection),
process: Some(Mutex::new(process)),
owned: true,
}
}
pub async fn new_context(&self) -> Result<BrowserContext, BrowserError> {
let result: CreateBrowserContextResult = self
.connection
.send_command(
"Target.createBrowserContext",
Some(CreateBrowserContextParams::default()),
None,
)
.await?;
Ok(BrowserContext::new(
self.connection.clone(),
result.browser_context_id,
))
}
pub fn new_context_builder(&self) -> NewContextBuilder<'_> {
NewContextBuilder::new(self)
}
pub async fn new_context_with_options(
&self,
options: ContextOptions,
) -> Result<BrowserContext, BrowserError> {
let storage_state = match &options.storage_state {
Some(StorageStateSource::Path(path)) => {
Some(StorageState::load(path).await.map_err(|e| {
BrowserError::LaunchFailed(format!("Failed to load storage state: {e}"))
})?)
}
Some(StorageStateSource::State(state)) => Some(state.clone()),
None => None,
};
let result: CreateBrowserContextResult = self
.connection
.send_command(
"Target.createBrowserContext",
Some(CreateBrowserContextParams::default()),
None,
)
.await?;
let context = BrowserContext::with_options(
self.connection.clone(),
result.browser_context_id,
options,
);
context.apply_options().await?;
if let Some(state) = storage_state {
context.add_cookies(state.cookies.clone()).await?;
let local_storage_script = state.to_local_storage_init_script();
if !local_storage_script.is_empty() {
context.add_init_script(&local_storage_script).await?;
}
let indexed_db_script = state.to_indexed_db_init_script();
if !indexed_db_script.is_empty() {
context.add_init_script(&indexed_db_script).await?;
}
}
Ok(context)
}
pub async fn close(&self) -> Result<(), BrowserError> {
if let Some(ref process) = self.process {
let mut child = process.lock().await;
let _ = child.kill();
}
Ok(())
}
pub fn connection(&self) -> &Arc<CdpConnection> {
&self.connection
}
pub fn is_owned(&self) -> bool {
self.owned
}
}
#[derive(Debug)]
pub struct NewContextBuilder<'a> {
browser: &'a Browser,
builder: ContextOptionsBuilder,
}
impl<'a> NewContextBuilder<'a> {
fn new(browser: &'a Browser) -> Self {
Self {
browser,
builder: ContextOptionsBuilder::new(),
}
}
#[must_use]
pub fn storage_state_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.builder = self.builder.storage_state_path(path);
self
}
#[must_use]
pub fn storage_state(mut self, state: StorageState) -> Self {
self.builder = self.builder.storage_state(state);
self
}
#[must_use]
pub fn geolocation(mut self, latitude: f64, longitude: f64) -> Self {
self.builder = self.builder.geolocation(latitude, longitude);
self
}
#[must_use]
pub fn geolocation_with_accuracy(
mut self,
latitude: f64,
longitude: f64,
accuracy: f64,
) -> Self {
self.builder = self
.builder
.geolocation_with_accuracy(latitude, longitude, accuracy);
self
}
#[must_use]
pub fn permissions(mut self, permissions: Vec<crate::context::Permission>) -> Self {
self.builder = self.builder.permissions(permissions);
self
}
#[must_use]
pub fn http_credentials(
mut self,
username: impl Into<String>,
password: impl Into<String>,
) -> Self {
self.builder = self.builder.http_credentials(username, password);
self
}
#[must_use]
pub fn extra_http_headers(
mut self,
headers: std::collections::HashMap<String, String>,
) -> Self {
self.builder = self.builder.extra_http_headers(headers);
self
}
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.builder = self.builder.header(name, value);
self
}
#[must_use]
pub fn offline(mut self, offline: bool) -> Self {
self.builder = self.builder.offline(offline);
self
}
#[must_use]
pub fn default_timeout(mut self, timeout: Duration) -> Self {
self.builder = self.builder.default_timeout(timeout);
self
}
#[must_use]
pub fn default_navigation_timeout(mut self, timeout: Duration) -> Self {
self.builder = self.builder.default_navigation_timeout(timeout);
self
}
#[must_use]
pub fn has_touch(mut self, has_touch: bool) -> Self {
self.builder = self.builder.has_touch(has_touch);
self
}
#[must_use]
pub fn locale(mut self, locale: impl Into<String>) -> Self {
self.builder = self.builder.locale(locale);
self
}
#[must_use]
pub fn timezone_id(mut self, timezone_id: impl Into<String>) -> Self {
self.builder = self.builder.timezone_id(timezone_id);
self
}
#[must_use]
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.builder = self.builder.user_agent(user_agent);
self
}
#[must_use]
pub fn viewport(mut self, width: i32, height: i32) -> Self {
self.builder = self.builder.viewport(width, height);
self
}
#[must_use]
pub fn color_scheme(mut self, color_scheme: crate::context::ColorScheme) -> Self {
self.builder = self.builder.color_scheme(color_scheme);
self
}
#[must_use]
pub fn reduced_motion(mut self, reduced_motion: crate::context::ReducedMotion) -> Self {
self.builder = self.builder.reduced_motion(reduced_motion);
self
}
#[must_use]
pub fn forced_colors(mut self, forced_colors: crate::context::ForcedColors) -> Self {
self.builder = self.builder.forced_colors(forced_colors);
self
}
#[must_use]
pub fn device_scale_factor(mut self, scale_factor: f64) -> Self {
self.builder = self.builder.device_scale_factor(scale_factor);
self
}
#[must_use]
pub fn is_mobile(mut self, is_mobile: bool) -> Self {
self.builder = self.builder.is_mobile(is_mobile);
self
}
#[must_use]
pub fn device(mut self, device: DeviceDescriptor) -> Self {
self.builder = self.builder.device(device);
self
}
#[must_use]
pub fn record_video(mut self, options: crate::page::VideoOptions) -> Self {
self.builder = self.builder.record_video(options);
self
}
pub async fn build(self) -> Result<BrowserContext, BrowserError> {
self.browser
.new_context_with_options(self.builder.build())
.await
}
}
impl Drop for Browser {
fn drop(&mut self) {
if self.owned {
if let Some(ref process) = self.process {
if let Ok(mut guard) = process.try_lock() {
let _ = guard.kill();
}
}
}
}
}