Skip to main content

xcelerate_core/
browser.rs

1use 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/// Configuration for the Browser instance.
14#[derive(uniffi::Record)]
15pub struct BrowserConfig {
16    /// Whether to run the browser in headless mode.
17    pub headless: bool,
18    /// Whether to apply stealth patches to the binary.
19    pub stealth: bool,
20    /// Whether to run the browser as a detached process.
21    pub detached: bool,
22    /// Optional path to the browser executable.
23    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/// Represents a browser instance (e.g., Chrome or Edge).
38#[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        // Create a temporary user data directory and KEEP IT
59        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        // Convert path to OsString for Command
70        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            // Also set a standard user agent to avoid "Headless" in the string if it persists
83            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            // We still use tokio::process::Command for non-detached to get the Child
94            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        // 1. Wait for the HTTP server to respond and give us the URL
102        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        // 2. Now connect to the WebSocket URL we found
128        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        // Spawn the handler task internally in Rust
137        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        // 1. Create target with about:blank so we can inject scripts before loading the real URL
150        let target = self.client.execute(browser_protocol::target::CreateTargetParams {
151            url: "about:blank".into(),
152            ..Default::default()
153        }).await?;
154
155        // 2. Attach to target
156        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        // 3. Inject stealth payload if enabled
168        if self._stealth {
169            page.add_script_to_evaluate_on_new_document(CDC_PAYLOAD.to_string()).await?;
170            // We also need to enable the Page domain for some events to fire correctly
171            self.client.execute_with_session(
172                Some(&page.session_id),
173                browser_protocol::page::EnableParams { ..Default::default() }
174            ).await?;
175        }
176
177        // 4. Finally navigate to the actual URL
178        page.navigate(url).await?;
179
180        Ok(page)
181    }
182
183    /// Returns the browser version information.
184    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    /// Closes the browser and kills the process.
190    pub async fn close(&self) -> XcelerateResult<()> {
191        // Try to close gracefully via CDP first
192        let _ = self.client.execute(browser_protocol::browser::CloseParams { ..Default::default() }).await;
193        
194        // Kill the process if it's still running
195        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    // Common Windows installation paths
205    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", // Fallback to Edge
209    ];
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}