mod connector;
mod context_builder;
mod launcher;
mod process;
use std::process::Child;
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
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, StorageState, StorageStateSource};
use crate::error::BrowserError;
pub use connector::ConnectOverCdpBuilder;
pub use context_builder::NewContextBuilder;
pub use launcher::{BrowserBuilder, UserDataDir};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub struct Browser {
connection: Arc<CdpConnection>,
process: Option<Mutex<Child>>,
owned: bool,
_temp_user_data_dir: Option<TempDir>,
}
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,
_temp_user_data_dir: None,
})
}
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,
_temp_user_data_dir: None,
}
}
pub(crate) fn from_launch(
connection: CdpConnection,
process: Child,
temp_user_data_dir: Option<TempDir>,
) -> Self {
Self {
connection: Arc::new(connection),
process: Some(Mutex::new(process)),
owned: true,
_temp_user_data_dir: temp_user_data_dir,
}
}
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 create_params = match &options.proxy {
Some(proxy) => CreateBrowserContextParams {
dispose_on_detach: None,
proxy_server: Some(proxy.server.clone()),
proxy_bypass_list: proxy.bypass.clone(),
},
None => CreateBrowserContextParams::default(),
};
let result: CreateBrowserContextResult = self
.connection
.send_command("Target.createBrowserContext", Some(create_params), 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_mutex) = self.process {
let mut child = process_mutex.lock().await;
process::kill_and_reap_async(&mut child).await;
}
Ok(())
}
pub fn connection(&self) -> &Arc<CdpConnection> {
&self.connection
}
pub fn is_owned(&self) -> bool {
self.owned
}
}
impl Drop for Browser {
fn drop(&mut self) {
if self.owned {
if let Some(ref process_mutex) = self.process {
if let Ok(mut guard) = process_mutex.try_lock() {
process::kill_and_reap_sync(&mut guard, 10, Duration::from_millis(10));
}
}
}
}
}