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