use crate::connection::CdpClient;
use crate::error::{XcelerateResult, XcelerateError};
use crate::page::Page;
use std::sync::Arc;
use tokio_tungstenite::connect_async;
use tokio::sync::mpsc;
use std::process::Stdio;
use tokio::process::Command;
use std::path::PathBuf;
use std::time::Duration;
pub struct BrowserConfig {
pub headless: bool,
pub executable_path: Option<PathBuf>,
}
impl BrowserConfig {
pub fn builder() -> BrowserConfigBuilder {
BrowserConfigBuilder::default()
}
}
pub struct BrowserConfigBuilder {
headless: bool,
executable_path: Option<PathBuf>,
}
impl Default for BrowserConfigBuilder {
fn default() -> Self {
Self {
headless: true,
executable_path: None,
}
}
}
impl BrowserConfigBuilder {
pub fn headless(mut self, headless: bool) -> Self {
self.headless = headless;
self
}
pub fn executable_path(mut self, path: impl Into<PathBuf>) -> Self {
self.executable_path = Some(path.into());
self
}
pub fn build(self) -> XcelerateResult<BrowserConfig> {
Ok(BrowserConfig {
headless: self.headless,
executable_path: self.executable_path,
})
}
}
pub struct Browser {
pub(crate) client: Arc<CdpClient>,
_process: Option<tokio::process::Child>,
_user_data_dir: Option<tempfile::TempDir>, }
impl Browser {
pub async fn launch(config: BrowserConfig) -> XcelerateResult<(Self, crate::connection::CdpHandler)> {
let exe = config.executable_path.or_else(find_chrome_executable).ok_or_else(|| {
XcelerateError::NotFound("Chrome executable not found. Please specify executable_path.".into())
})?;
let user_data_dir = tempfile::tempdir().map_err(|_| XcelerateError::InternalError)?;
let port = 9222;
eprintln!("[LAUNCHER] Found executable: {:?}", exe);
let mut cmd = Command::new(exe);
cmd.arg(format!("--remote-debugging-port={}", port))
.arg("--remote-debugging-address=127.0.0.1") .arg(format!("--user-data-dir={}", user_data_dir.path().display()))
.arg("--no-first-run")
.arg("--no-default-browser-check")
.arg("--remote-allow-origins=*")
.stdout(Stdio::null())
.stderr(Stdio::null());
if config.headless {
cmd.arg("--headless");
}
eprintln!("[LAUNCHER] Spawning process...");
let child = cmd.spawn().map_err(|e| XcelerateError::NotFound(format!("Failed to start Chrome: {}", e)))?;
let version_url = format!("http://127.0.0.1:{}/json/version", port);
eprintln!("[LAUNCHER] Waiting for browser to respond at {}...", version_url);
let mut attempts = 0;
let ws_url = loop {
match reqwest::get(&version_url).await {
Ok(resp) => {
let json: serde_json::Value = resp.json().await.map_err(|_| XcelerateError::InternalError)?;
if let Some(ws_url) = json["webSocketDebuggerUrl"].as_str() {
break ws_url.to_string();
}
}
Err(_) => {
attempts += 1;
if attempts % 10 == 0 {
eprintln!("[LAUNCHER] Attempt {}: Still waiting for browser to start...", attempts);
}
if attempts > 100 {
return Err(XcelerateError::NotFound("Timed out waiting for Chrome HTTP server".into()));
}
tokio::time::sleep(Duration::from_millis(150)).await;
}
}
};
eprintln!("[LAUNCHER] Connecting to WebSocket: {}...", ws_url);
let (ws, _) = connect_async(&ws_url).await?;
eprintln!("[LAUNCHER] Debugger connected successfully!");
let (tx, rx) = mpsc::unbounded_channel();
let (handler, _event_rx) = crate::connection::CdpHandler::new(ws, rx);
let client = Arc::new(CdpClient::new(tx, handler.event_tx.clone()));
Ok((Self {
client,
_process: Some(child),
_user_data_dir: Some(user_data_dir),
}, handler))
}
pub async fn new_page(&self, url: impl Into<String>) -> XcelerateResult<Page> {
let target = self.client.execute(browser_protocol::target::CreateTargetParams {
url: url.into(),
..Default::default()
}).await?;
let session = self.client.execute(browser_protocol::target::AttachToTargetParams {
targetId: target.targetId,
flatten: Some(true),
..Default::default()
}).await?;
Ok(Page {
client: Arc::clone(&self.client),
session_id: session.sessionId,
})
}
pub async fn version(&self) -> XcelerateResult<browser_protocol::browser::GetVersionReturns> {
self.client.execute(browser_protocol::browser::GetVersionParams { ..Default::default() }).await
}
pub async fn close(&mut self) -> XcelerateResult<()> {
let _ = self.client.execute(browser_protocol::browser::CloseParams { ..Default::default() }).await;
if let Some(mut child) = self._process.take() {
let _ = child.kill().await;
}
Ok(())
}
pub async fn targets(&self) -> XcelerateResult<Vec<browser_protocol::target::TargetInfo>> {
let res = self.client.execute(browser_protocol::target::GetTargetsParams { ..Default::default() }).await?;
Ok(res.targetInfos)
}
}
fn find_chrome_executable() -> Option<PathBuf> {
let paths = [
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", ];
for path in paths {
let pb = PathBuf::from(path);
if pb.exists() {
return Some(pb);
}
}
None
}