mod temp_dir;
mod browser_utils;
mod browser_config;
mod browser_builder;
use log::error;
use std::sync::Arc;
use serde_json::json;
use std::process::Child;
use tokio::sync::OnceCell;
use temp_dir::CustomTempDir;
use anyhow::{Context, Result};
use browser_config::BrowserConfig;
use crate::tab::Tab;
use crate::CaptureOptions;
use crate::transport::Transport;
use crate::general_utils::next_id;
use crate::transport_actor::TransportResponse;
use crate::browser::browser_builder::BrowserBuilder;
static mut BROWSER: OnceCell<Arc<Browser>> = OnceCell::const_new();
#[derive(Debug)]
struct Process(pub Child, pub CustomTempDir);
#[derive(Debug)]
pub struct Browser {
transport: Arc<Transport>,
process: Process,
is_closed: bool,
}
unsafe impl Send for Browser {}
unsafe impl Sync for Browser {}
impl Browser {
pub async fn new() -> Result<Self> {
BrowserBuilder::new().build().await
}
pub async fn new_with_head() -> Result<Self> {
BrowserBuilder::new()
.headless(false)
.build()
.await
}
async fn create_browser(config: BrowserConfig) -> Result<Self> {
let mut child = browser_utils::spawn_chrome_process(&config)?;
let ws_url = browser_utils::get_websocket_url(
child.stderr.take().context("Failed to get stderr")?
).await?;
Ok(Self {
transport: Arc::new(Transport::new(&ws_url).await?),
process: Process(child, config.temp_dir),
is_closed: false,
})
}
pub async fn new_tab(&self) -> Result<Tab> {
Tab::new(self.transport.clone()).await
}
pub async fn close_init_tab(&self) -> Result<()> {
let TransportResponse::Response(res) = self.transport.send(json!({
"id": next_id(),
"method": "Target.getTargets",
"params": {}
})).await? else { panic!() };
let target_id = res
.result["targetInfos"]
.as_array()
.unwrap()
.iter()
.find(|info| {
info["type"].as_str().unwrap() == "page"
}).unwrap()["targetId"]
.as_str()
.unwrap();
self.transport.send(json!({
"id": next_id(),
"method": "Target.closeTarget",
"params": {
"targetId": target_id
}
})).await?;
Ok(())
}
pub async fn capture_html(&self, html: &str, selector: &str) -> Result<String> {
let tab = self.new_tab().await?;
tab.set_content(html).await?;
let element = tab.find_element(selector).await?;
let base64 = element.screenshot().await?;
tab.close().await?;
Ok(base64)
}
pub async fn capture_html_with_options(
&self,
html: &str,
selector: &str,
options: CaptureOptions,
) -> Result<String> {
let tab = self.new_tab().await?;
tab.set_content(html).await?;
let element = tab.find_element(selector).await?;
let base64 = if options.raw_png {
element.raw_screenshot().await?
} else {
element.screenshot().await?
};
tab.close().await?;
Ok(base64)
}
pub fn close(&mut self) -> Result<()> {
if self.is_closed {
return Ok(());
}
Arc::get_mut(&mut self.transport)
.unwrap()
.shutdown()?;
self.process.0
.kill()
.and_then(|_| self.process.0.wait())
.context("Failed to kill the browser process")?;
self.process.1
.cleanup()?;
self.is_closed = true;
Ok(())
}
}
impl Browser {
pub async fn instance() -> Arc<Browser> {
unsafe {
let browser = BROWSER
.get_or_init(|| async {
let browser = Browser::new().await.unwrap();
browser.close_init_tab().await.unwrap();
Arc::new(browser)
})
.await;
browser.clone()
}
}
pub fn close_instance() -> Option<()> {
unsafe {
Arc::get_mut(&mut BROWSER.take()?)?.close().ok()
}
}
}
impl Drop for Browser {
fn drop(&mut self) {
if !self.is_closed {
if let Err(e) = self.close() {
error!("Error closing browser: {:?}", e);
}
}
}
}