use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use chromiumoxide::browser::{Browser as CdpBrowser, BrowserConfig};
use futures::StreamExt;
use crate::browser::browser_process::BrowserCloser;
use crate::browser::chromiumoxide_adapter::ChromiumoxidePage;
use crate::browser::launcher::{Browser, LaunchOptions, LaunchResult};
use crate::browser::node_bridge::NodeBridgePage;
use crate::browser::profile_directory::{create_temporary_user_data_dir, remove_user_data_dir};
use crate::core::engine::{EngineAdapter, EngineType};
use crate::downloads::attach_downloads;
use crate::fingerprint::apply::{apply_fingerprint, ApplyOptions};
enum EngineBrowser {
Chromiumoxide(Arc<ChromiumoxidePage>),
NodeBridge(Arc<NodeBridgePage>),
}
impl EngineBrowser {
fn page(&self) -> Arc<dyn EngineAdapter> {
match self {
Self::Chromiumoxide(page) => page.clone(),
Self::NodeBridge(page) => page.clone(),
}
}
async fn close(&self) {
let _ = match self {
Self::Chromiumoxide(page) => page.close().await,
Self::NodeBridge(page) => page.close().await,
};
}
}
struct EngineCloser {
browser: EngineBrowser,
user_data_dir: PathBuf,
temporary_profile: bool,
closed: tokio::sync::OnceCell<()>,
}
#[async_trait]
impl BrowserCloser for EngineCloser {
async fn close(&self) -> Result<()> {
self.closed
.get_or_try_init(|| async {
self.browser.close().await;
if self.temporary_profile {
remove_user_data_dir(&self.user_data_dir).await?;
}
Ok::<(), anyhow::Error>(())
})
.await
.map(|_| ())
}
}
pub(crate) async fn launch_with_engine(options: &LaunchOptions) -> Result<LaunchResult> {
let args = options.all_chrome_args()?;
let env = options.browser_env()?;
let temporary_profile = options.user_data_dir.is_none();
let user_data_dir = match &options.user_data_dir {
Some(user_data_dir) => {
std::fs::create_dir_all(user_data_dir)?;
user_data_dir.clone()
}
None => create_temporary_user_data_dir(None)?,
};
let launched = match options.engine {
EngineType::Chromiumoxide => {
launch_chromiumoxide(options, &args, env.as_ref(), &user_data_dir).await
}
EngineType::Playwright | EngineType::Puppeteer => {
launch_node_bridge(options, &args, env.as_ref(), &user_data_dir).await
}
EngineType::Fantoccini => Err(anyhow::anyhow!(
"fantoccini engine launch is not yet implemented"
)),
};
let (browser, downloads) = match launched {
Ok(launched) => launched,
Err(error) => {
if temporary_profile {
let _ = remove_user_data_dir(&user_data_dir).await;
}
return Err(error);
}
};
let page = browser.page();
let closer = Arc::new(EngineCloser {
browser,
user_data_dir: user_data_dir.clone(),
temporary_profile,
closed: tokio::sync::OnceCell::new(),
});
Ok(LaunchResult::attached(
Browser {
engine: options.engine,
user_data_dir,
headless: options.headless,
},
page,
downloads,
)
.launched_by_engine(
args,
temporary_profile,
options.executable_path.clone(),
closer,
))
}
type Launched = (
EngineBrowser,
Option<Arc<crate::downloads::DownloadManager>>,
);
async fn launch_node_bridge(
options: &LaunchOptions,
args: &[String],
env: Option<&HashMap<String, String>>,
user_data_dir: &Path,
) -> Result<Launched> {
let page = NodeBridgePage::launch(options, args, env, user_data_dir).await?;
Ok((EngineBrowser::NodeBridge(Arc::new(page)), None))
}
async fn launch_chromiumoxide(
options: &LaunchOptions,
args: &[String],
env: Option<&HashMap<String, String>>,
user_data_dir: &Path,
) -> Result<Launched> {
let builder = BrowserConfig::builder();
let builder = if options.headless {
builder.new_headless_mode()
} else {
builder.with_head()
};
let mut builder = builder.user_data_dir(user_data_dir).args(args.to_vec());
if options.ignore_all_default_args || !options.all_ignored_default_args().is_empty() {
builder = builder.disable_default_args();
}
if !options.sandbox {
builder = builder.no_sandbox();
}
if let Some(ref executable_path) = options.executable_path {
builder = builder.chrome_executable(executable_path);
}
if let Some(timeout) = options.launch_timeout {
builder = builder.launch_timeout(timeout);
}
if let Some(env) = env {
builder = builder.envs(env.clone());
}
let config = builder
.build()
.map_err(|e| anyhow::anyhow!("failed to build browser config: {}", e))?;
let (browser, mut handler) = CdpBrowser::launch(config)
.await
.map_err(|e| anyhow::anyhow!("failed to launch chromium: {}", e))?;
let handler_task = tokio::spawn(async move {
while let Some(event) = handler.next().await {
if let Err(err) = event {
tracing::debug!(error = %err, "chromiumoxide handler event error");
}
}
});
let page = match browser.new_page("about:blank").await {
Ok(page) => page,
Err(error) => {
let mut browser = browser;
let _ = browser.close().await;
let _ = browser.wait().await;
handler_task.abort();
return Err(anyhow::anyhow!("failed to open initial page: {error}"));
}
};
let adapter = Arc::new(ChromiumoxidePage::new(
page,
browser,
handler_task,
user_data_dir.to_path_buf(),
));
match set_up_page(options, &adapter).await {
Ok(downloads) => Ok((EngineBrowser::Chromiumoxide(adapter), downloads)),
Err(error) => {
let _ = adapter.close().await;
Err(error)
}
}
}
async fn set_up_page(
options: &LaunchOptions,
adapter: &ChromiumoxidePage,
) -> Result<Option<Arc<crate::downloads::DownloadManager>>> {
if let Some(ref profile) = options.fingerprint {
apply_fingerprint(adapter, profile, ApplyOptions::default()).await?;
if options.verbose {
tracing::info!("Fingerprint profile applied");
}
}
let downloads = attach_downloads(options.engine, adapter, options.downloads.clone())
.await
.map_err(|error| anyhow::anyhow!("{error}"))?;
if let Some(ref color_scheme) = options.color_scheme {
if let Err(error) = adapter.set_color_scheme(Some(color_scheme)).await {
if options.verbose {
tracing::warn!(%error, "could not set color scheme");
}
}
}
if !options.headless {
if let Err(error) = adapter.bring_to_front().await {
if options.verbose {
tracing::debug!(%error, "bring_to_front failed");
}
}
}
Ok(downloads)
}