rustenium/browsers/firefox/
browser.rs1use super::capabilities::FirefoxCapabilities;
2use crate::browsers::BidiBrowser;
3use crate::conduit::bidi::drivers::BidiDriver;
4use crate::error::bidi::BrowserCloseError;
5use crate::nodes::FirefoxNode;
6use rustenium_bidi_definitions::browsing_context::types::{BrowsingContext, Locator};
7use rustenium_bidi_definitions::script::types::NodeRemoteValue;
8use rustenium_bidi_definitions::session::types::ProxyConfiguration;
9use rustenium_core::find_free_port;
10use rustenium_core::process::Process;
11use rustenium_core::transport::{ConnectionTransportConfig, WebsocketConnectionTransport};
12use std::sync::{Arc, Mutex};
13
14#[derive(Debug, Clone, Default)]
16pub enum FirefoxLaunchMode {
17 #[default]
19 SpawnAndAttach,
20 Remote(u16),
22}
23
24#[derive(Debug, Clone, Default)]
26pub struct FirefoxConfig {
27 pub host: Option<String>,
29
30 pub capabilities: FirefoxCapabilities,
32
33 pub proxy: Option<ProxyConfiguration>,
35
36 pub launch_mode: FirefoxLaunchMode,
38
39 pub remote_debugging_port: Option<u16>,
41
42 pub firefox_executable_path: Option<String>,
45
46 pub profile_dir: Option<String>,
48
49 pub browser_flags: Option<Vec<String>>,
51}
52
53pub struct FirefoxBrowser {
54 config: FirefoxConfig,
55 driver: Option<BidiDriver<WebsocketConnectionTransport>>,
56 firefox_process: Option<Process>,
57}
58
59impl std::fmt::Debug for FirefoxBrowser {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("FirefoxBrowser")
62 .field("config", &self.config)
63 .field("firefox_process", &self.firefox_process)
64 .finish_non_exhaustive()
65 }
66}
67
68impl FirefoxBrowser {
69 pub async fn new(mut config: FirefoxConfig) -> FirefoxBrowser {
70 let host = config.host.clone().unwrap_or(String::from("localhost"));
71 let firefox_port = match &config.launch_mode {
72 FirefoxLaunchMode::Remote(port) => *port,
73 FirefoxLaunchMode::SpawnAndAttach => config
74 .remote_debugging_port
75 .unwrap_or_else(|| find_free_port().unwrap()),
76 };
77 config.remote_debugging_port = Some(firefox_port);
78
79 let firefox_process = Self::init_firefox(&mut config, firefox_port).await;
80
81 let ct_config = ConnectionTransportConfig {
82 host,
83 port: firefox_port,
84 ..ConnectionTransportConfig::default()
85 };
86
87 let driver = Self::init_bidi(&mut config, &ct_config).await;
88
89 FirefoxBrowser {
90 config,
91 driver: Some(driver),
92 firefox_process,
93 }
94 }
95
96 async fn init_firefox(config: &mut FirefoxConfig, firefox_port: u16) -> Option<Process> {
97 match &config.launch_mode {
98 FirefoxLaunchMode::Remote(_) => None,
99 FirefoxLaunchMode::SpawnAndAttach => {
100 let firefox_exe = config.firefox_executable_path.clone().unwrap_or_else(|| {
101 crate::downloader::ensure_firefox()
102 .to_string_lossy()
103 .into_owned()
104 });
105
106 let profile_dir = config.profile_dir.clone().unwrap_or_else(|| {
107 std::env::temp_dir()
108 .join(format!("rustenium-firefox-{}", firefox_port))
109 .display()
110 .to_string()
111 });
112
113 let _ = std::fs::create_dir_all(&profile_dir);
114
115 let mut firefox_args = vec![
116 format!("--remote-debugging-port={}", firefox_port),
117 "--profile".to_string(),
118 profile_dir,
119 "--no-remote".to_string(),
120 ];
121
122 if let Some(ref flags) = config.browser_flags {
123 firefox_args.extend(flags.iter().cloned());
124 }
125
126 if let Some(proxy) = config.proxy.clone() {
127 config.capabilities.base_capabilities.proxy = Some(proxy);
128 }
129
130 let firefox_proc = Process::create_with_env(
131 firefox_exe,
132 firefox_args,
133 [("MOZ_LAUNCHER_PROCESS".to_string(), "0".to_string())],
134 );
135
136 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
138
139 Some(firefox_proc)
140 }
141 }
142 }
143
144 async fn init_bidi(
145 config: &mut FirefoxConfig,
146 ct_config: &ConnectionTransportConfig,
147 ) -> BidiDriver<WebsocketConnectionTransport> {
148 let capabilities = config.capabilities.clone().build();
149
150 let session = rustenium_core::BidiSession::<WebsocketConnectionTransport>::new(
152 ct_config,
153 capabilities,
154 )
155 .await;
156
157 let session = Arc::new(tokio::sync::Mutex::new(session));
158
159 let mut driver = BidiDriver::new(
160 String::from("firefox"),
161 vec![],
162 session,
163 0,
164 Arc::new(Mutex::new(Vec::new())),
165 #[cfg(unix)]
167 Process::create("echo", vec!["hello".to_string()]),
168 #[cfg(windows)]
169 Process::create(
170 "cmd",
171 vec!["/C".to_string(), "echo".to_string(), "hello".to_string()],
172 ),
173 );
174 driver.listen_to_context_creation().await.unwrap();
175 driver
176 }
177
178 pub async fn connect_bidi(&mut self) {
179 if self.driver.is_some() {
180 return;
181 }
182 let host = self
183 .config
184 .host
185 .clone()
186 .unwrap_or(String::from("localhost"));
187 let port = self.config.remote_debugging_port.unwrap();
188 let ct_config = ConnectionTransportConfig {
189 host,
190 port,
191 ..ConnectionTransportConfig::default()
192 };
193 self.driver = Some(Self::init_bidi(&mut self.config, &ct_config).await);
194 }
195
196 pub fn get_config(&self) -> &FirefoxConfig {
197 &self.config
198 }
199
200 pub fn get_browser_process(&self) -> &Option<Process> {
201 &self.firefox_process
202 }
203}
204
205impl BidiBrowser for FirefoxBrowser {
206 type Transport = WebsocketConnectionTransport;
207 type BrowserNode = FirefoxNode<WebsocketConnectionTransport>;
208
209 fn driver(&self) -> &BidiDriver<WebsocketConnectionTransport> {
210 self.driver
211 .as_ref()
212 .expect("BiDi driver is not initialized.")
213 }
214
215 fn driver_mut(&mut self) -> &mut BidiDriver<WebsocketConnectionTransport> {
216 self.driver
217 .as_mut()
218 .expect("BiDi driver is not initialized.")
219 }
220
221 fn build_node(
222 &self,
223 raw_node: NodeRemoteValue,
224 locator: Locator,
225 context: BrowsingContext,
226 ) -> FirefoxNode<WebsocketConnectionTransport> {
227 let driver = self.driver();
228 FirefoxNode::from_bidi(
229 raw_node,
230 locator,
231 driver.session.clone(),
232 context,
233 driver.mouse.clone(),
234 driver.keyboard.clone(),
235 )
236 }
237
238 async fn close(mut self) -> Result<(), BrowserCloseError> {
239 tracing::debug!("Closing FirefoxBrowser");
240 if let Some(ref mut driver) = self.driver {
241 driver.end_session().await?;
242 }
243 drop(self.firefox_process.take());
245 if let Some(port) = self.config.remote_debugging_port {
247 rustenium_core::process::kill_process_on_port(port);
248 }
249 tracing::debug!("FirefoxBrowser closed");
250 Ok(())
251 }
252}
253
254pub async fn firefox(config: Option<FirefoxConfig>) -> FirefoxBrowser {
255 FirefoxBrowser::new(config.unwrap_or_default()).await
256}