Skip to main content

rustenium/browsers/firefox/
browser.rs

1use 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/// How Firefox is launched and managed.
15#[derive(Debug, Clone, Default)]
16pub enum FirefoxLaunchMode {
17    /// Rustenium starts Firefox and connects to its BiDi WebSocket directly (default).
18    #[default]
19    SpawnAndAttach,
20    /// Connect to an existing Firefox instance on the specified remote debugging port.
21    Remote(u16),
22}
23
24/// Configuration for Firefox browser.
25#[derive(Debug, Clone, Default)]
26pub struct FirefoxConfig {
27    /// Host (default: localhost).
28    pub host: Option<String>,
29
30    /// Firefox capabilities for configuring browser behavior.
31    pub capabilities: FirefoxCapabilities,
32
33    /// Optional proxy configuration.
34    pub proxy: Option<ProxyConfiguration>,
35
36    /// How Firefox is launched and managed.
37    pub launch_mode: FirefoxLaunchMode,
38
39    /// Remote debugging port for Firefox BiDi WebSocket. Auto-assigned if None.
40    pub remote_debugging_port: Option<u16>,
41
42    /// Path to Firefox executable.
43    /// Defaults to auto-downloaded Firefox if not specified.
44    pub firefox_executable_path: Option<String>,
45
46    /// Firefox profile directory. If not specified, uses a temporary directory.
47    pub profile_dir: Option<String>,
48
49    /// Additional Firefox command-line arguments.
50    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                // Wait for Firefox to start
137                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        // Firefox exposes BiDi WebSocket directly — connect the same way as Chrome
151        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            // No driver process — Firefox handles BiDi natively
166            #[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 the stored process (best-effort; may be stale if Firefox respawned)
244        drop(self.firefox_process.take());
245        // Kill by port to catch the actual Firefox process regardless of PID
246        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}