browser_automation_cli/native/cdp/
oxide.rs1#![allow(missing_docs)]
3use std::path::PathBuf;
8use std::sync::Arc;
9use std::time::Duration;
10
11use chromiumoxide::browser::{Browser, BrowserConfig};
12use chromiumoxide::Handler as OxideHandler;
13use tokio::sync::Mutex;
14
15use super::chrome::{build_chrome_args, find_chrome, LaunchOptions};
16
17pub struct OxideLaunch {
19 pub browser: Browser,
20 pub handler: OxideHandler,
21 pub executable: Option<PathBuf>,
22 pub ws_url: String,
23 pub temp_user_data_dir: Option<PathBuf>,
25}
26
27pub async fn launch_with_oxide(options: &LaunchOptions) -> Result<OxideLaunch, String> {
32 let chrome_args = build_chrome_args(options)?;
33
34 let mut builder = BrowserConfig::builder();
35
36 let executable = options
37 .executable_path
38 .as_ref()
39 .map(PathBuf::from)
40 .or_else(find_chrome);
41
42 if let Some(ref exe) = executable {
43 builder = builder.chrome_executable(exe);
44 }
45
46 let has_extensions = options.extensions.as_ref().is_some_and(|e| !e.is_empty());
49 if !options.headless || has_extensions {
50 builder = builder.with_head();
51 }
52
53 if let Some(ref exts) = options.extensions {
55 if !exts.is_empty() {
56 builder = builder.extensions(exts.clone());
57 }
58 }
59
60 builder = builder.user_data_dir(chrome_args.user_data_dir.clone());
61
62 for a in &chrome_args.args {
65 if a.starts_with("--user-data-dir=") {
66 continue;
67 }
68 if a == "--headless=new" || a == "--headless" || a.starts_with("--headless=") {
69 continue;
71 }
72 if a.starts_with("--load-extension=") || a.starts_with("--disable-extensions-except=") {
74 continue;
75 }
76 builder = builder.arg(a.as_str());
77 }
78
79 let config = builder
80 .build()
81 .map_err(|e| format!("chromiumoxide BrowserConfig: {e}"))?;
82
83 let (browser, handler) = Browser::launch(config)
84 .await
85 .map_err(|e| format!("chromiumoxide Browser::launch: {e}"))?;
86
87 let ws_url = browser.websocket_address().clone();
88
89 Ok(OxideLaunch {
90 browser,
91 handler,
92 executable,
93 ws_url,
94 temp_user_data_dir: chrome_args.temp_user_data_dir,
95 })
96}
97
98pub async fn finalize_browser(browser: Arc<Mutex<Browser>>) -> Result<(), String> {
100 let mut browser = match Arc::try_unwrap(browser) {
101 Ok(m) => m.into_inner(),
102 Err(shared) => {
103 let mut guard = shared.lock().await;
104 if let Err(e) = guard.close().await {
105 let _ = guard.kill().await;
106 return Err(format!("chromiumoxide close: {e}"));
107 }
108 match tokio::time::timeout(Duration::from_secs(5), guard.wait()).await {
109 Ok(Ok(_)) => {}
110 Ok(Err(e)) => {
111 let _ = guard.kill().await;
112 return Err(format!("chromiumoxide wait: {e}"));
113 }
114 Err(_) => {
115 let _ = guard.kill().await;
116 }
117 }
118 return Ok(());
119 }
120 };
121
122 if let Err(e) = browser.close().await {
123 let _ = browser.kill().await;
124 return Err(format!("chromiumoxide close: {e}"));
125 }
126
127 match tokio::time::timeout(Duration::from_secs(5), browser.wait()).await {
128 Ok(Ok(_)) => {}
129 Ok(Err(e)) => {
130 let _ = browser.kill().await;
131 return Err(format!("chromiumoxide wait: {e}"));
132 }
133 Err(_) => {
134 let _ = browser.kill().await;
135 }
136 }
137
138 Ok(())
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn launch_options_default_is_headless_path() {
147 let o = LaunchOptions::default();
148 assert!(o.headless);
149 }
150
151 #[test]
152 fn build_args_feed_proxy_into_oxide_flag_list() {
153 let o = LaunchOptions {
154 proxy: Some("http://127.0.0.1:8080".to_string()),
155 ..Default::default()
156 };
157 let args = build_chrome_args(&o).unwrap();
158 assert!(args
159 .args
160 .iter()
161 .any(|a| a == "--proxy-server=http://127.0.0.1:8080"));
162 if let Some(ref dir) = args.temp_user_data_dir {
163 let _ = std::fs::remove_dir_all(dir);
164 }
165 }
166}