xcelerate_core/
browser.rs1use crate::connection::CdpClient;
2use crate::error::{XcelerateResult, XcelerateError};
3use crate::page::Page;
4use std::sync::Arc;
5use tokio_tungstenite::connect_async;
6use tokio::sync::mpsc;
7use std::process::Stdio;
8use std::path::PathBuf;
9use std::time::Duration;
10
11const CDC_PAYLOAD: &str = include_str!("cdc_payload.js");
12
13#[derive(uniffi::Record)]
15pub struct BrowserConfig {
16 pub headless: bool,
18 pub stealth: bool,
20 pub detached: bool,
22 pub executable_path: Option<String>,
24}
25
26impl Default for BrowserConfig {
27 fn default() -> Self {
28 Self {
29 headless: true,
30 stealth: true,
31 detached: true,
32 executable_path: None,
33 }
34 }
35}
36
37#[derive(uniffi::Object)]
39pub struct Browser {
40 pub(crate) client: Arc<CdpClient>,
41 _process: tokio::sync::Mutex<Option<tokio::process::Child>>,
42 _process_guard: Option<crate::stealth::process::ProcessGuard>,
43 _user_data_dir: Option<tempfile::TempDir>,
44 _stealth: bool,
45}
46
47#[uniffi::export(async_runtime = "tokio")]
48impl Browser {
49 #[uniffi::constructor]
50 pub async fn launch(config: BrowserConfig) -> XcelerateResult<Arc<Self>> {
51 let exe = match config.executable_path {
52 Some(p) => Some(PathBuf::from(p)),
53 None => find_chrome_executable(),
54 }.ok_or_else(|| {
55 XcelerateError::NotFound("Chrome executable not found. Please specify executable_path.".into())
56 })?;
57
58 let user_data_dir = tempfile::tempdir().map_err(|_| XcelerateError::InternalError)?;
60 let port = 9222;
61
62 if config.stealth {
63 eprintln!("[LAUNCHER] Applying stealth patches to binary...");
64 crate::stealth::patcher::BinaryPatcher::patch_binary(&exe)?;
65 }
66
67 eprintln!("[LAUNCHER] Found executable: {:?}", exe);
68
69 let mut cmd = std::process::Command::new(&exe);
71 cmd.arg(format!("--remote-debugging-port={}", port))
72 .arg("--remote-debugging-address=127.0.0.1")
73 .arg(format!("--user-data-dir={}", user_data_dir.path().display()))
74 .arg("--no-first-run")
75 .arg("--no-default-browser-check")
76 .arg("--remote-allow-origins=*")
77 .stdout(Stdio::null())
78 .stderr(Stdio::null());
79
80 if config.headless {
81 cmd.arg("--headless=new");
82 cmd.arg("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36");
84 }
85
86 let (child, guard) = if config.detached {
87 eprintln!("[LAUNCHER] Spawning detached process...");
88 let pid = crate::stealth::process::spawn_detached(cmd)?;
89 let guard = crate::stealth::process::ProcessGuard { pid, auto_kill: false };
90 (None, Some(guard))
91 } else {
92 eprintln!("[LAUNCHER] Spawning managed process...");
93 let mut t_cmd = tokio::process::Command::from(cmd);
95 let child = t_cmd.spawn().map_err(|e| XcelerateError::NotFound(format!("Failed to start Chrome: {}", e)))?;
96 let pid = child.id().ok_or(XcelerateError::InternalError)?;
97 let guard = crate::stealth::process::ProcessGuard { pid, auto_kill: true };
98 (Some(child), Some(guard))
99 };
100
101 let version_url = format!("http://127.0.0.1:{}/json/version", port);
103 eprintln!("[LAUNCHER] Waiting for browser to respond at {}...", version_url);
104
105 let mut attempts = 0;
106 let ws_url = loop {
107 match reqwest::get(&version_url).await {
108 Ok(resp) => {
109 let json: serde_json::Value = resp.json().await.map_err(|_| XcelerateError::InternalError)?;
110 if let Some(ws_url) = json["webSocketDebuggerUrl"].as_str() {
111 break ws_url.to_string();
112 }
113 }
114 Err(_) => {
115 attempts += 1;
116 if attempts % 10 == 0 {
117 eprintln!("[LAUNCHER] Attempt {}: Still waiting for browser to start...", attempts);
118 }
119 if attempts > 100 {
120 return Err(XcelerateError::NotFound("Timed out waiting for Chrome HTTP server".into()));
121 }
122 tokio::time::sleep(Duration::from_millis(150)).await;
123 }
124 }
125 };
126
127 eprintln!("[LAUNCHER] Connecting to WebSocket: {}...", ws_url);
129 let (ws, _) = connect_async(&ws_url).await?;
130 eprintln!("[LAUNCHER] Debugger connected successfully!");
131
132 let (tx, rx) = mpsc::unbounded_channel();
133 let (handler, _event_rx) = crate::connection::CdpHandler::new(ws, rx);
134 let client = Arc::new(CdpClient::new(tx, handler.event_tx.clone()));
135
136 tokio::spawn(handler.run());
138
139 Ok(Arc::new(Self {
140 client,
141 _process: tokio::sync::Mutex::new(child),
142 _process_guard: guard,
143 _user_data_dir: Some(user_data_dir),
144 _stealth: config.stealth,
145 }))
146 }
147
148 pub async fn new_page(self: Arc<Self>, url: String) -> XcelerateResult<Arc<Page>> {
149 let target = self.client.execute(browser_protocol::target::CreateTargetParams {
151 url: "about:blank".into(),
152 ..Default::default()
153 }).await?;
154
155 let session = self.client.execute(browser_protocol::target::AttachToTargetParams {
157 targetId: target.targetId,
158 flatten: Some(true),
159 ..Default::default()
160 }).await?;
161
162 let page = Arc::new(Page {
163 client: Arc::clone(&self.client),
164 session_id: session.sessionId,
165 });
166
167 if self._stealth {
169 page.add_script_to_evaluate_on_new_document(CDC_PAYLOAD.to_string()).await?;
170 self.client.execute_with_session(
172 Some(&page.session_id),
173 browser_protocol::page::EnableParams { ..Default::default() }
174 ).await?;
175 }
176
177 page.navigate(url).await?;
179
180 Ok(page)
181 }
182
183 pub async fn version(&self) -> XcelerateResult<String> {
185 let res = self.client.execute(browser_protocol::browser::GetVersionParams { ..Default::default() }).await?;
186 Ok(format!("{} (Protocol {})", res.product, res.protocolVersion))
187 }
188
189 pub async fn close(&self) -> XcelerateResult<()> {
191 let _ = self.client.execute(browser_protocol::browser::CloseParams { ..Default::default() }).await;
193
194 let mut lock = self._process.lock().await;
196 if let Some(mut child) = lock.take() {
197 let _ = child.kill().await;
198 }
199 Ok(())
200 }
201}
202
203fn find_chrome_executable() -> Option<PathBuf> {
204 let paths = [
206 r"C:\Program Files\Google\Chrome\Application\chrome.exe",
207 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
208 r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", ];
210
211 for path in paths {
212 let pb = PathBuf::from(path);
213 if pb.exists() {
214 return Some(pb);
215 }
216 }
217 None
218}