use std::time::Duration; use tracing::{debug, error, info, warn}; use crate::browser::{Browser, BrowserError}; use crate::config::Configuration; use crate::marionette::error::ConnectionError; use crate::marionette::Marionette; use crate::mqtt::{Message, MqttClient, MqttPublisher}; #[derive(Debug)] pub enum SessionError { Browser(BrowserError), Io(std::io::Error), Connection(ConnectionError), InvalidState(String), } impl From for SessionError { fn from(e: BrowserError) -> Self { Self::Browser(e) } } impl From for SessionError { fn from(e: std::io::Error) -> Self { Self::Io(e) } } impl From for SessionError { fn from(e: ConnectionError) -> Self { Self::Connection(e) } } impl std::fmt::Display for SessionError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Browser(e) => write!(f, "Error launching browser: {}", e), Self::Io(e) => write!(f, "I/O error: {}", e), Self::Connection(e) => write!(f, "Connection error: {}", e), Self::InvalidState(e) => write!(f, "Invalid state: {}", e), } } } impl std::error::Error for SessionError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Browser(e) => Some(e), Self::Io(e) => Some(e), Self::Connection(e) => Some(e), Self::InvalidState(_) => None, } } } pub struct Session { config: Configuration, browser: Browser, marionette: Marionette, } impl Session { pub async fn begin(config: Configuration) -> Result { debug!("Launching Firefox"); let browser = Browser::launch()?; browser.wait_ready().await?; debug!("Firefox Marionette is now ready"); let Some(port) = browser.marionette_port() else { return Err(SessionError::InvalidState("No active Marionette port".into())); }; debug!("Connecting to Firefox Marionette on port {}", port); let mut marionette = Marionette::connect(("127.0.0.1", port)).await?; info!("Successfully connected to Firefox Marionette"); let ses = marionette.new_session().await?; debug!("Started Marionette session {}", ses.session_id); Ok(Self { config, browser, marionette, }) } pub async fn run(mut self) { let mut client = MqttClient::new(&self.config).unwrap(); loop { if let Err(e) = client.connect().await { warn!("Failed to connect to MQTT server: {}", e); tokio::time::sleep(Duration::from_secs(1)).await; continue; } if let Err(e) = client.subscribe().await { warn!("Error subscribing to MQTT topics: {}", e); tokio::time::sleep(Duration::from_secs(1)).await; continue; } break; } let handler = MessageHandler { marionette: &mut self.marionette, }; client.run(handler).await; } } pub struct MessageHandler<'a> { marionette: &'a mut Marionette, } #[async_trait::async_trait] impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> { async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message) { let url = msg.payload_str(); let parts: Vec<&str> = msg.topic().split('/').rev().collect(); let screen = match parts.get(1) { Some(&"") | None => { warn!("Invalid navigate request: no screen"); return; } Some(s) => s, }; debug!("Handling navigate request: {}", url); info!("Navigate screen {} to {}", screen, url); if let Err(e) = self.marionette.navigate(url.to_string()).await { error!("Failed to navigate: {}", e); } if let Err(e) = publisher.publish_url(screen, &url).await { error!("Failed to publish title: {}", e); } match self.marionette.get_title().await { Ok(t) => { if let Err(e) = publisher.publish_title(screen, &t).await { error!("Failed to publish title: {}", e); } } Err(e) => error!("Error getting title: {}", e), } } }