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 = get_free_port().ok_or(XcelerateError::InternalError)?;
61
62 let exe = if config.stealth {
63 crate::stealth::patcher::BinaryPatcher::patch_to_temp(&exe)?
64 } else {
65 exe
66 };
67
68
69 let mut cmd = std::process::Command::new(&exe);
71 setup_browser_args(&mut cmd, &user_data_dir, port, config.headless);
72
73 let (child, guard) = if config.detached {
74 let pid = crate::stealth::process::spawn_detached(cmd)?;
75 let guard = crate::stealth::process::ProcessGuard { pid, auto_kill: false };
76 (None, Some(guard))
77 } else {
78 let mut t_cmd = tokio::process::Command::from(cmd);
79 let child = t_cmd.spawn().map_err(|e| XcelerateError::NotFound(format!("Failed to start Chrome: {}", e)))?;
80 let pid = child.id().ok_or(XcelerateError::InternalError)?;
81 let guard = crate::stealth::process::ProcessGuard { pid, auto_kill: true };
82 (Some(child), Some(guard))
83 };
84
85 let ws_url = wait_for_ws_url(port).await?;
87
88 let (ws, _) = connect_async(&ws_url).await?;
89
90 let (tx, rx) = mpsc::unbounded_channel();
91 let (handler, _event_rx) = crate::connection::CdpHandler::new(ws, rx);
92 let client = Arc::new(CdpClient::new(tx, handler.event_tx.clone()));
93
94 tokio::spawn(handler.run());
95
96 Ok(Arc::new(Self {
97 client,
98 _process: tokio::sync::Mutex::new(child),
99 _process_guard: guard,
100 _user_data_dir: Some(user_data_dir),
101 _stealth: config.stealth,
102 }))
103 }
104
105 pub async fn new_page(self: Arc<Self>, url: String) -> XcelerateResult<Arc<Page>> {
106 let target = self.client.execute(browser_protocol::target::CreateTargetParams {
108 url: "about:blank".into(),
109 ..Default::default()
110 }).await?;
111
112 let session = self.client.execute(browser_protocol::target::AttachToTargetParams {
114 targetId: target.targetId,
115 flatten: Some(true),
116 }).await?;
117
118 let page = Arc::new(Page {
119 client: Arc::clone(&self.client),
120 session_id: session.sessionId,
121 });
122
123 if self._stealth {
125 page.add_script_to_evaluate_on_new_document(CDC_PAYLOAD.to_string()).await?;
126 self.client.execute_with_session(
128 Some(&page.session_id),
129 browser_protocol::page::EnableParams { ..Default::default() }
130 ).await?;
131 }
132
133 page.navigate(url).await?;
135
136 Ok(page)
137 }
138
139 pub async fn version(&self) -> XcelerateResult<String> {
141 let res = self.client.execute(browser_protocol::browser::GetVersionParams {}).await?;
142 Ok(format!("{} (Protocol {})", res.product, res.protocolVersion))
143 }
144
145 pub async fn close(&self) -> XcelerateResult<()> {
147 let _ = self.client.execute(browser_protocol::browser::CloseParams {}).await;
149
150 let mut lock = self._process.lock().await;
152 if let Some(mut child) = lock.take() {
153 let _ = child.kill().await;
154 }
155 Ok(())
156 }
157}
158
159fn find_chrome_executable() -> Option<PathBuf> {
160 let paths = if cfg!(windows) {
161 vec![
162 r"C:\Program Files\Google\Chrome\Application\chrome.exe",
163 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
164 r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
165 ]
166 } else if cfg!(target_os = "macos") {
167 vec![
168 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
169 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
170 ]
171 } else {
172 vec![
174 "/usr/bin/google-chrome",
175 "/usr/bin/chromium-browser",
176 "/usr/bin/chromium",
177 "/usr/bin/microsoft-edge-stable",
178 ]
179 };
180
181 for path in paths {
182 let pb = PathBuf::from(path);
183 if pb.exists() {
184 return Some(pb);
185 }
186 }
187 None
188}
189
190fn get_free_port() -> Option<u16> {
191 use std::net::TcpListener;
192 TcpListener::bind("127.0.0.1:0")
193 .and_then(|listener| listener.local_addr())
194 .map(|addr| addr.port())
195 .ok()
196}
197
198fn setup_browser_args(cmd: &mut std::process::Command, user_data_dir: &tempfile::TempDir, port: u16, headless: bool) {
199 cmd.arg(format!("--remote-debugging-port={}", port))
200 .arg("--remote-debugging-address=127.0.0.1")
201 .arg(format!("--user-data-dir={}", user_data_dir.path().display()))
202 .arg("--no-first-run")
203 .arg("--no-default-browser-check")
204 .arg("--remote-allow-origins=*")
205 .arg("--no-startup-window")
206 .stdout(Stdio::null())
207 .stderr(Stdio::null());
208
209 if headless {
210 cmd.arg("--headless=new");
211 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");
212 }
213}
214
215async fn wait_for_ws_url(port: u16) -> XcelerateResult<String> {
216 let version_url = format!("http://127.0.0.1:{}/json/version", port);
217
218 let mut attempts = 0;
219 loop {
220 match reqwest::get(&version_url).await {
221 Ok(resp) => {
222 let json: serde_json::Value = resp.json().await.map_err(|_| XcelerateError::InternalError)?;
223 if let Some(ws_url) = json["webSocketDebuggerUrl"].as_str() {
224 return Ok(ws_url.to_string());
225 }
226 }
227 Err(_) => {
228 attempts += 1;
229 if attempts > 100 {
230 return Err(XcelerateError::NotFound("Timed out waiting for Chrome HTTP server".into()));
231 }
232 tokio::time::sleep(Duration::from_millis(150)).await;
233 }
234 }
235 }
236}