//! Implémentation de ServiceInstance. //! //! Ce module contient l'implémentation complète de [`ServiceInstance`], //! qui représente une instance active d'un service UPnP. //! //! # Composants principaux //! //! - [`ServiceInstance`] : Structure principale contenant l'état du service //! - [`event_sub_handler`] : Handler Axum pour les abonnements aux événements //! - [`control_handler`] : Handler Axum pour les appels SOAP //! //! # Gestion des événements //! //! L'instance gère automatiquement : //! - Les souscriptions aux événements (SUBSCRIBE/UNSUBSCRIBE) //! - L'envoi d'événements initiaux aux nouveaux abonnés //! - Les notifications périodiques des changements d'état //! - Le séquençage des messages par abonné //! //! # Architecture //! //! ```text //! ServiceInstance //! ├── Variables d'état (StateVarInstanceSet) //! ├── Actions (ActionInstanceSet) //! ├── Abonnés (HashMap) //! ├── Buffer de changements (Mutex) //! └── Séquences (Mutex>) //! ``` use axum::{ body::Body, extract::{Request, State}, http::{HeaderMap, StatusCode}, response::{IntoResponse, Response}, }; use bevy_reflect::Reflect; use std::{ collections::HashMap, future::Future, pin::Pin, sync::{Arc, Mutex, RwLock}, time::Duration, }; use tokio::time; use tracing::{debug, error, info, warn}; use xmltree::{Element, EmitterConfig, XMLNode}; use quick_xml::escape::escape; use crate::{ UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance, actions::{ActionInstance, ActionInstanceSet}, devices::DeviceInstance, services::{Service, ServiceError}, state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable}, }; /// Méthodes HTTP pour les événements UPnP. pub const METHOD_SUBSCRIBE: &str = "SUBSCRIBE"; pub const METHOD_UNSUBSCRIBE: &str = "UNSUBSCRIBE"; /// Instance de service UPnP. /// /// Représente une instance concrète d'un service UPnP, attachée à un device. /// Gère l'exécution des actions, les notifications d'événements et les abonnements. /// /// # Fonctionnalités /// /// - Exécution d'actions via SOAP /// - Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE) /// - Notifications automatiques des changements d'état /// - Génération de la description SCPD /// /// # Cycle de vie /// /// 1. Création via [`Service::create_instance`](crate::UpnpModel::create_instance) /// 2. Enregistrement des URLs avec [`register_urls`](Self::register_urls) /// 3. Démarrage du notifier avec [`start_notifier`](Self::start_notifier) /// /// # Examples /// /// ```rust,ignore /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// # use pmoserver::Server; /// # use std::time::Duration; /// # #[tokio::main] /// # async fn main() { /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// /// // Enregistrer les endpoints /// let mut server = Server::new("test", "http://localhost:8080", 8080); /// instance.register_urls(&mut server).await.unwrap(); /// /// // Démarrer les notifications /// let _handle = instance.start_notifier(Duration::from_secs(5)); /// # } /// ``` #[derive(Clone)] pub struct ServiceInstance { /// Métadonnées de l'objet object: UpnpObjectType, /// Référence vers le modèle model: Arc, /// Identifiant du service identifier: String, /// Device parent (optionnel) - utilisé via interior mutability device: Arc>>>, /// Variables d'état instanciées statevariables: StateVarInstanceSet, /// Actions instanciées actions: ActionInstanceSet, /// Abonnés aux événements (SID -> Callback URL) subscribers: Arc>>, /// Buffer des changements en attente de notification (nom de variable -> valeur réflexive) changed_buffer: Arc>>>, /// Compteurs de séquence par abonné seqid: Arc>>, } impl std::fmt::Debug for ServiceInstance { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ServiceInstance") .field("object", &self.object) .field("identifier", &self.identifier) .field("device", &self.device) .field("statevariables", &self.statevariables) .field("actions", &self.actions) .finish() } } impl UpnpTyped for ServiceInstance { fn as_upnp_object_type(&self) -> &UpnpObjectType { &self.object } } impl UpnpInstance for ServiceInstance { type Model = Service; fn new(model: &Service) -> Self { // Phase 1 : Créer les instances de variables d'état let mut statevariables = StateVarInstanceSet::new(); for v in model.variables() { if let Err(e) = statevariables.insert(Arc::new(StateVarInstance::new(&*v))) { error!("Failed to insert state variable: {:?}", e); } } // Phase 2 : Créer les instances d'actions avec validation let mut actions = ActionInstanceSet::new(); for a in model.actions() { // Vérifier que toutes les variables référencées existent let mut missing_vars = Vec::new(); for arg in a.arguments().all() { let related_var_name = arg.state_variable().get_name(); if statevariables.get_by_name(related_var_name).is_none() { missing_vars.push(related_var_name.to_string()); } } if !missing_vars.is_empty() { error!( "Action '{}' references missing state variables: {:?}", a.get_name(), missing_vars ); continue; } // Créer l'instance d'action let action_instance = Arc::new(ActionInstance::new(&*a)); // ✅ Phase 3 : ACTIVER le binding des arguments aux variables d'instance for arg_instance in action_instance.arguments_set().all() { let var_name = arg_instance.get_model().state_variable().get_name(); if let Some(var_instance) = statevariables.get_by_name(var_name) { // ✅ Activer cette ligne (déjà présente dans ArgumentInstance) arg_instance.bind_variable(var_instance); } } if let Err(e) = actions.insert(action_instance) { error!("Failed to insert action '{}': {:?}", a.get_name(), e); } } Self { object: UpnpObjectType { name: model.name().to_string(), object_type: "ServiceInstance".to_string(), }, model: Arc::new(model.clone()), identifier: model.identifier().to_string(), device: Arc::new(RwLock::new(None)), statevariables, actions, subscribers: Arc::new(RwLock::new(HashMap::new())), changed_buffer: Arc::new(Mutex::new(HashMap::new())), seqid: Arc::new(Mutex::new(HashMap::new())), } } } impl UpnpTypedInstance for ServiceInstance { fn get_model(&self) -> &Self::Model { &self.model } } impl UpnpObject for ServiceInstance { fn to_xml_element(&self) -> Element { let mut elem = Element::new("service"); let mut service_type = Element::new("serviceType"); service_type .children .push(XMLNode::Text(self.service_type())); elem.children.push(XMLNode::Element(service_type)); let mut service_id = Element::new("serviceId"); service_id.children.push(XMLNode::Text(self.service_id())); elem.children.push(XMLNode::Element(service_id)); let mut scpd_url = Element::new("SCPDURL"); scpd_url.children.push(XMLNode::Text(self.scpd_route())); elem.children.push(XMLNode::Element(scpd_url)); let mut control_url = Element::new("controlURL"); control_url .children .push(XMLNode::Text(self.control_route())); elem.children.push(XMLNode::Element(control_url)); let mut event_sub_url = Element::new("eventSubURL"); event_sub_url .children .push(XMLNode::Text(self.event_route())); elem.children.push(XMLNode::Element(event_sub_url)); elem } } impl ServiceInstance { /// Enregistre cette instance de service auprès de toutes ses variables. /// /// Cette méthode doit être appelée APRÈS la création de l'Arc /// pour permettre aux variables de notifier le service lors de leurs changements. /// /// # Arguments /// /// * `self_arc` - Arc pointant vers cette instance /// /// # Examples /// /// ```rust,ignore /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// # use std::sync::Arc; /// let service = Service::new("AVTransport".to_string()); /// let instance = Arc::new(service.create_instance()); /// instance.register_with_variables(&instance); /// ``` pub fn register_with_variables(self: &Arc) { let weak_self = Arc::downgrade(self); for var in self.statevariables.all() { var.register_service(weak_self.clone()); } } /// Retourne l'identifiant du service. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// assert_eq!(instance.identifier(), "AVTransport"); /// ``` pub fn identifier(&self) -> &str { &self.identifier } /// Retourne le type de service UPnP. /// /// Format: `urn:schemas-upnp-org:service:{name}:{version}` /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// assert_eq!(instance.service_type(), "urn:schemas-upnp-org:service:AVTransport:1"); /// ``` pub fn service_type(&self) -> String { self.model.service_type() } /// Retourne l'ID de service UPnP. /// /// Format: `urn:upnp-org:serviceId:{identifier}` /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// assert_eq!(instance.service_id(), "urn:upnp-org:serviceId:AVTransport"); /// ``` pub fn service_id(&self) -> String { format!("urn:upnp-org:serviceId:{}", self.identifier) } /// Récupère une variable d'état par son nom. /// /// # Arguments /// /// * `name` - Nom de la variable d'état /// /// # Returns /// /// `Some(Arc)` si la variable existe, `None` sinon. /// /// # Examples /// /// ```rust,no_run /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// # let service = Service::new("AVTransport".to_string()); /// # let instance = service.create_instance(); /// if let Some(var) = instance.get_variable("TransportState") { /// println!("Value: {}", var.value()); /// } /// ``` pub fn get_variable(&self, name: &str) -> Option> { self.statevariables.get_by_name(name) } /// Récupère une action par son nom. /// /// # Arguments /// /// * `name` - Nom de l'action /// /// # Returns /// /// `Some(Arc)` si l'action existe, `None` sinon. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::{UpnpModel, UpnpTyped}; /// # let service = Service::new("AVTransport".to_string()); /// # let instance = service.create_instance(); /// if let Some(action) = instance.get_action("Play") { /// println!("Action found: {}", action.get_name()); /// } /// ``` pub fn get_action(&self, name: &str) -> Option> { self.actions.get_by_name(name) } /// Définit le device parent pour ce service. /// /// Cette méthode doit être appelée après la création du service instance /// pour établir la relation avec le device parent. /// /// # Arguments /// /// * `device` - Le device parent /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::devices::Device; /// # use pmoupnp::UpnpModel; /// # use std::sync::Arc; /// # let service = Service::new("AVTransport".to_string()); /// # let device = Device::new("MediaRenderer".to_string(), "urn:schemas-upnp-org:device:MediaRenderer:1".to_string(), "My MediaRenderer".to_string()); /// let service_instance = service.create_instance(); /// let device_instance = device.create_instance(); /// service_instance.set_device(device_instance); /// ``` pub fn set_device(&self, device: Arc) { let mut dev = self.device.write().unwrap(); *dev = Some(device); } /// Retourne la route du service (chemin relatif). /// /// # Returns /// /// Chemin relatif incluant le device parent si présent. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// assert_eq!(instance.route(), "/service/AVTransport"); /// ``` pub fn route(&self) -> String { let device = self.device.read().unwrap(); match device.as_ref() { Some(device) => format!("{}/service/{}", device.route(), self.get_name()), None => format!("/service/{}", self.get_name()), } } /// Retourne la route de contrôle SOAP. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// assert_eq!(instance.control_route(), "/service/AVTransport/control"); /// ``` pub fn control_route(&self) -> String { format!("{}/control", self.route()) } /// Retourne la route de souscription aux événements. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// assert_eq!(instance.event_route(), "/service/AVTransport/event"); /// ``` pub fn event_route(&self) -> String { format!("{}/event", self.route()) } /// Retourne la route de la description SCPD. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// assert_eq!(instance.scpd_route(), "/service/AVTransport/desc.xml"); /// ``` pub fn scpd_route(&self) -> String { format!("{}/desc.xml", self.route()) } /// Retourne l'USN (Unique Service Name). /// /// L'USN combine l'UUID du device parent et le type de service UPnP. /// /// # Examples /// /// ```rust,no_run /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// let usn = instance.usn(); /// // Format: uuid:{device-uuid}::urn:schemas-upnp-org:service:AVTransport:1 /// ``` pub fn usn(&self) -> String { let device = self.device.read().unwrap(); match device.as_ref() { Some(device) => format!("uuid:{}::urn:{}", device.udn(), self.service_type()), None => format!("uuid::urn:{}", self.service_type()), } } /// Retourne une référence vers l'ensemble des variables d'état. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// let vars = instance.statevariables(); /// println!("Variables count: {}", vars.all().len()); /// ``` pub fn statevariables(&self) -> &StateVarInstanceSet { &self.statevariables } /// Retourne une référence vers l'ensemble des actions. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// let actions = instance.actions(); /// println!("Actions count: {}", actions.all().len()); /// ``` pub fn actions(&self) -> &ActionInstanceSet { &self.actions } /// Retourne une action par son nom. /// /// # Arguments /// /// * `name` - Nom de l'action /// /// # Returns /// /// `Some(Arc)` si trouvée, `None` sinon. pub fn action(&self, name: &str) -> Option> { self.actions.get_by_name(name) } /// Enregistre les routes UPnP dans le serveur. /// /// # Errors /// /// Retourne une erreur si l'enregistrement des routes échoue. pub async fn register_urls(&self, server: &mut pmoserver::Server) -> Result<(), ServiceError> { let device = self.device.read().unwrap(); let device_name = device .as_ref() .map(|d| d.get_name().clone()) .unwrap_or_else(|| "unknown".to_string()); let server_url = device .as_ref() .map(|d| d.base_url().to_string()) .unwrap_or_default(); drop(device); info!( "✅ Service description for {}:{} available at : {}{}", device_name, self.get_name(), server_url, self.scpd_route(), ); // Handler SCPD let instance_scpd = self.clone(); server .add_handler(&self.scpd_route(), move || { let instance = instance_scpd.clone(); async move { instance.scpd_handler().await } }) .await; // Handler control let instance_control = Arc::new(self.clone()); server .add_post_handler_with_state(&self.control_route(), control_handler, instance_control) .await; // Handler événements let instance_event = self.clone(); server .add_handler_with_state(&self.event_route(), event_sub_handler, instance_event) .await; Ok(()) } /// Génère l'élément XML SCPD (Service Control Protocol Description). /// /// Cette méthode crée un élément XML conforme à la spécification UPnP décrivant /// le service, ses actions et ses variables d'état. /// /// # Returns /// /// Un élément `xmltree::Element` représentant le document SCPD. /// /// # Examples /// /// ```rust /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// let scpd = instance.scpd_element(); /// assert_eq!(scpd.name, "scpd"); /// ``` pub fn scpd_element(&self) -> Element { let mut elem = Element::new("scpd"); elem.attributes.insert( "xmlns".to_string(), "urn:schemas-upnp-org:service-1-0".to_string(), ); // specVersion let mut spec = Element::new("specVersion"); let mut major = Element::new("major"); major.children.push(XMLNode::Text("1".to_string())); spec.children.push(XMLNode::Element(major)); let mut minor = Element::new("minor"); minor.children.push(XMLNode::Text("0".to_string())); spec.children.push(XMLNode::Element(minor)); elem.children.push(XMLNode::Element(spec)); // actionList if !self.actions.all().is_empty() { elem.children .push(XMLNode::Element(self.actions.to_xml_element())); } // serviceStateTable if !self.statevariables.all().is_empty() { elem.children .push(XMLNode::Element(self.statevariables.to_xml_element())); } elem } /// Handler HTTP pour la description SCPD. /// /// Génère et retourne le document XML SCPD décrivant le service. /// Cette méthode est appelée lorsqu'un client accède à l'URL SCPD du service. /// /// # Returns /// /// Une réponse HTTP 200 avec le XML SCPD, ou 500 en cas d'erreur de sérialisation. /// /// # Format de réponse /// /// - Content-Type: `text/xml; charset="utf-8"` /// - Body: Document SCPD formaté avec indentation async fn scpd_handler(&self) -> Response { info!("📋 SCPD requested for service {}", self.get_name()); let elem = self.scpd_element(); let config = EmitterConfig::new() .perform_indent(true) .indent_string(" "); let mut xml_output = Vec::new(); if let Err(e) = elem.write_with_config(&mut xml_output, config) { error!("❌ Failed to serialize SCPD XML: {}", e); return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } let xml = String::from_utf8_lossy(&xml_output).to_string(); debug!("✅ SCPD generated for {} ({} bytes)", self.get_name(), xml.len()); ( StatusCode::OK, [( axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"", )], xml, ) .into_response() } /// Ajoute un abonné aux événements. /// /// # Arguments /// /// * `sid` - Identifiant de la souscription (SID) /// * `callback` - URL de callback pour les notifications /// /// # Examples /// /// ```rust,no_run /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// # #[tokio::main] /// # async fn main() { /// # let service = Service::new("AVTransport".to_string()); /// # let instance = service.create_instance(); /// instance.add_subscriber( /// "uuid:12345".to_string(), /// "".to_string() /// ).await; /// # } /// ``` pub async fn add_subscriber(&self, sid: String, callback: String) { let mut subscribers = self.subscribers.write().unwrap(); subscribers.insert(sid, callback); } /// Renouvelle un abonnement existant. /// /// # Arguments /// /// * `sid` - Identifiant de la souscription (SID) /// * `timeout` - Nouvelle durée de validité (format "Second-{n}") /// /// # Examples /// /// ```rust,no_run /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// # #[tokio::main] /// # async fn main() { /// # let service = Service::new("AVTransport".to_string()); /// # let instance = service.create_instance(); /// instance.renew_subscriber("uuid:12345", "Second-1800").await; /// # } /// ``` pub async fn renew_subscriber(&self, sid: &str, timeout: &str) { info!("♻️ Renewed SID {} for timeout {}", sid, timeout); } /// Supprime un abonné. /// /// # Arguments /// /// * `sid` - Identifiant de la souscription (SID) à supprimer /// /// # Examples /// /// ```rust,no_run /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// # #[tokio::main] /// # async fn main() { /// # let service = Service::new("AVTransport".to_string()); /// # let instance = service.create_instance(); /// instance.remove_subscriber("uuid:12345").await; /// # } /// ``` pub async fn remove_subscriber(&self, sid: &str) { let mut subscribers = self.subscribers.write().unwrap(); subscribers.remove(sid); } /// Envoie l'événement initial à un nouvel abonné. /// /// Lorsqu'un client s'abonne aux événements, cette méthode lui envoie /// immédiatement les valeurs actuelles de toutes les variables d'état /// qui envoient des notifications. /// /// # Arguments /// /// * `sid` - Identifiant de la souscription (SID) /// /// # Examples /// /// ```rust,no_run /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// # #[tokio::main] /// # async fn main() { /// # let service = Service::new("AVTransport".to_string()); /// # let instance = service.create_instance(); /// instance.send_initial_event("uuid:12345".to_string()).await; /// # } /// ``` pub async fn send_initial_event(&self, sid: String) { let callback = { let subscribers = self.subscribers.read().unwrap(); subscribers.get(&sid).cloned() }; if let Some(callback) = callback { let mut changed = HashMap::new(); for sv in self.statevariables.all() { if sv.is_sending_notification() { changed.insert(sv.get_name().to_string(), sv.value().to_string()); } } if changed.is_empty() { return; } tokio::spawn(async move { let callback = callback.trim().trim_matches(|c| c == '<' || c == '>'); let mut body = r#""#.to_string(); for (name, val) in changed { body.push_str(&format!( "<{0}>{1}", name, val )); } body.push_str(""); let client = reqwest::Client::new(); match client .request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback) .header("Content-Type", r#"text/xml; charset="utf-8"#) .header("NT", "upnp:event") .header("NTS", "upnp:propchange") .header("SID", &sid) .header("SEQ", "0") .body(body) .send() .await { Ok(resp) => { info!( "✅ Initial event sent to {}, status={}", callback, resp.status() ); } Err(e) => { error!("Failed to send initial event to {}: {}", callback, e); } } }); } } /// Marque un changement de variable à notifier ultérieurement. /// /// Les changements sont mis en buffer et seront envoyés lors du prochain /// appel à [`notify_subscribers`](Self::notify_subscribers). /// /// # Arguments /// /// * `name` - Nom de la variable d'état modifiée /// * `value` - Nouvelle valeur de la variable /// /// # Examples /// /// ```rust,ignore /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); /// let value = Arc::new("PLAYING".to_string()) as Arc; /// instance.event_to_be_sent("TransportState".to_string(), value); /// ``` pub fn event_to_be_sent(&self, name: String, value: Arc) { let mut buffer = self.changed_buffer.lock().unwrap(); buffer.insert(name, value); } /// Récupère le prochain numéro de séquence pour un abonné. /// /// Chaque notification envoyée à un abonné doit avoir un numéro de séquence /// unique et croissant. /// /// # Arguments /// /// * `sid` - Identifiant de la souscription (SID) /// /// # Returns /// /// Le prochain numéro de séquence sous forme de chaîne. fn next_seq(&self, sid: &str) -> String { let mut seqid = self.seqid.lock().unwrap(); let counter = seqid.entry(sid.to_string()).or_insert(0); *counter += 1; counter.to_string() } /// Notifie tous les abonnés des changements en attente. /// /// Cette méthode envoie les changements bufferisés à tous les abonnés actuels /// via des requêtes HTTP NOTIFY. Les changements sont envoyés de manière /// asynchrone dans des tâches séparées. /// /// # Examples /// /// ```rust,no_run /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// # #[tokio::main] /// # async fn main() { /// # let service = Service::new("AVTransport".to_string()); /// # let instance = service.create_instance(); /// # use std::sync::Arc; /// # use bevy_reflect::Reflect; /// let value = Arc::new("PLAYING".to_string()) as Arc; /// instance.event_to_be_sent("TransportState".to_string(), value); /// instance.notify_subscribers().await; /// # } /// ``` pub async fn notify_subscribers(&self) { let subscribers_copy = { let subscribers = self.subscribers.read().unwrap(); if subscribers.is_empty() { return; } subscribers.clone() }; let changed = { let mut buffer = self.changed_buffer.lock().unwrap(); if buffer.is_empty() { return; } std::mem::take(&mut *buffer) }; for (sid, callback) in subscribers_copy { let changed_clone = changed.clone(); let seq = self.next_seq(&sid); tokio::spawn(async move { let callback = callback.trim().trim_matches(|c| c == '<' || c == '>'); let mut body = r#""#.to_string(); for (name, val) in changed_clone { // Convertir la valeur Reflect en String let val_str = Self::reflect_to_string(&*val); body.push_str(&format!( "<{0}>{1}", name, val_str )); } body.push_str(""); let client = reqwest::Client::new(); match client .request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback) .header("Content-Type", r#"text/xml; charset="utf-8"#) .header("NT", "upnp:event") .header("NTS", "upnp:propchange") .header("SID", &sid) .header("SEQ", seq) .body(body) .send() .await { Ok(_) => { info!("✅ Notified subscriber {} of changes", callback); } Err(e) => { error!("Failed to notify subscriber {}: {}", callback, e); } } }); } } /// Convertit une valeur Reflect en String pour la notification UPnP. /// /// Cette fonction gère plusieurs cas : /// - Types primitifs : formatage direct /// - Structures serde (pmodidl, etc.) : sérialisation XML /// - Autres types : fallback sur Debug /// /// Le résultat est déjà échappé XML-safe selon les normes UPnP. fn reflect_to_string(value: &dyn Reflect) -> String { use std::any::Any; use bevy_reflect::ReflectRef; // Essayer de downcaster vers des types primitifs courants if let Some(v) = value.as_any().downcast_ref::() { // Échapper les caractères XML spéciaux return escape(v).to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return v.to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return v.to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return v.to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return v.to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return v.to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return v.to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return v.to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return v.to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return if *v { "1" } else { "0" }.to_string(); } else if let Some(v) = value.as_any().downcast_ref::() { return escape(&v.to_string()).to_string(); } // Pour les structures complexes, essayer de sérialiser avec bevy_reflect match value.reflect_ref() { ReflectRef::Struct(s) => { // Construire un XML simple pour la struct Self::serialize_struct_to_xml(s) } ReflectRef::TupleStruct(ts) => { // Pour les tuple structs, essayer d'extraire la valeur si c'est un wrapper if ts.field_len() == 1 { if let Some(inner) = ts.field(0) { // Convertir PartialReflect en Reflect si possible if let Some(reflect_val) = inner.try_as_reflect() { Self::reflect_to_string(reflect_val) } else { format!("{:?}", value) } } else { format!("{:?}", value) } } else { format!("{:?}", value) } } ReflectRef::Enum(e) => { // Pour les enums, formater comme "Variant(value)" let variant_name = e.variant_name(); if e.field_len() == 1 { if let Some(field) = e.field_at(0) { // Convertir PartialReflect en Reflect si possible if let Some(reflect_val) = field.try_as_reflect() { format!("{}", Self::reflect_to_string(reflect_val)) } else { variant_name.to_string() } } else { variant_name.to_string() } } else { variant_name.to_string() } } _ => { // Fallback: utiliser Debug et échapper let debug_str = format!("{:?}", value); escape(&debug_str).to_string() } } } /// Sérialise une structure Reflect en XML simple. fn serialize_struct_to_xml(s: &dyn bevy_reflect::Struct) -> String { use std::fmt::Write; use bevy_reflect::TypeInfo; let mut xml = String::new(); // Commencer par ouvrir la balise avec le nom du type let type_name = s.get_represented_type_info() .and_then(|ti| { if let TypeInfo::Struct(si) = ti { Some(si.type_path_table().short_path()) } else { None } }) .unwrap_or("struct"); let _ = write!(&mut xml, "<{}>", type_name); // Ajouter chaque champ for i in 0..s.field_len() { if let Some(field_name) = s.name_at(i) { if let Some(field_value) = s.field_at(i) { // Convertir PartialReflect en Reflect si possible if let Some(reflect_val) = field_value.try_as_reflect() { let value_str = Self::reflect_to_string(reflect_val); let _ = write!(&mut xml, "<{}>{}", field_name, value_str, field_name); } } } } let _ = write!(&mut xml, "", type_name); xml } /// Démarre le notifier périodique. /// /// # Arguments /// /// * `interval` - Intervalle entre les notifications /// /// # Returns /// /// Un handle vers la tâche tokio du notifier. pub fn start_notifier(&self, interval: Duration) -> tokio::task::JoinHandle<()> { let instance = self.clone(); tokio::spawn(async move { let mut ticker = time::interval(interval); info!("✅ Starting notifier every {:?}", interval); loop { ticker.tick().await; instance.notify_subscribers().await; } }) } } /// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE). /// /// Gère les requêtes HTTP SUBSCRIBE et UNSUBSCRIBE selon la spécification /// UPnP Device Architecture. /// /// # Opérations supportées /// /// - `SUBSCRIBE` sans SID : Nouvelle souscription /// - `SUBSCRIBE` avec SID : Renouvellement d'abonnement /// - `UNSUBSCRIBE` : Annulation d'abonnement /// /// # Arguments /// /// * `instance` - L'instance du service /// * `headers` - En-têtes HTTP de la requête /// * `req` - La requête HTTP complète /// /// # Returns /// /// Une réponse HTTP avec le SID et le timeout pour SUBSCRIBE, /// ou une simple confirmation pour UNSUBSCRIBE. async fn event_sub_handler( State(instance): State, headers: HeaderMap, req: Request, ) -> Response { info!("📡 Event Subscription request for {}", instance.get_name()); let method = req.method().as_str(); let sid = headers .get("SID") .and_then(|v| v.to_str().ok()) .unwrap_or(""); let timeout = headers .get("Timeout") .and_then(|v| v.to_str().ok()) .unwrap_or(""); let callback = headers .get("Callback") .and_then(|v| v.to_str().ok()) .unwrap_or(""); match method { METHOD_SUBSCRIBE => { let (response_sid, response_timeout) = if sid.is_empty() { // Nouvelle souscription let new_sid = format!("uuid:{}", uuid::Uuid::new_v4()); if !callback.is_empty() { instance .add_subscriber(new_sid.clone(), callback.to_string()) .await; } let timeout_val = if timeout.is_empty() { "Second-1800" } else { timeout }; info!( "🔒 New subscription: SID={}, Callback={}, Timeout={}", new_sid, callback, timeout_val ); let sid_clone = new_sid.clone(); let instance_clone = instance.clone(); tokio::spawn(async move { instance_clone.send_initial_event(sid_clone).await; }); (new_sid, timeout_val.to_string()) } else { // Renouvellement instance.renew_subscriber(sid, timeout).await; info!("♻️ Renew subscription: SID={}, Timeout={}", sid, timeout); (sid.to_string(), timeout.to_string()) }; ( StatusCode::OK, [ ( axum::http::header::HeaderName::from_static("sid"), axum::http::HeaderValue::from_str(&response_sid).unwrap(), ), ( axum::http::header::HeaderName::from_static("timeout"), axum::http::HeaderValue::from_str(&response_timeout).unwrap(), ), ], ) .into_response() } METHOD_UNSUBSCRIBE => { if !sid.is_empty() { instance.remove_subscriber(sid).await; info!("❌ Unsubscribe SID={}", sid); } StatusCode::OK.into_response() } _ => { warn!("Unsupported EventSub method: {}", method); StatusCode::METHOD_NOT_ALLOWED.into_response() } } } /// Handler Axum pour le contrôle SOAP. /// /// Gère les requêtes de contrôle SOAP pour invoquer des actions sur le service. /// /// # Arguments /// /// * `instance` - L'instance du service (Arc-wrapped) /// * `body` - Corps de la requête SOAP /// /// # Returns /// /// Une réponse SOAP avec le résultat de l'action, ou un SOAP fault en cas d'erreur. /// /// # Erreurs /// /// Retourne un SOAP fault dans les cas suivants : /// - Parsing SOAP invalide /// - Action non trouvée /// - Arguments invalides /// - Échec de l'exécution de l'action async fn control_handler(State(instance): State>, body: String) -> Response { use crate::{ soap::{parse_soap_action, build_soap_response, build_soap_fault, error_codes}, variable_types::{StateValue, UpnpVarType}, UpnpTypedInstance, }; use std::collections::HashMap; use tracing::debug; info!("📡 Control request for {}", instance.get_name()); // Parser le SOAP pour extraire l'action et ses arguments let soap_action = match parse_soap_action(body.as_bytes()) { Ok(action) => action, Err(e) => { error!("❌ Failed to parse SOAP: {:?}", e); let fault_xml = build_soap_fault( "s:Client", "Invalid SOAP request", Some(error_codes::INVALID_ACTION), Some("The SOAP request could not be parsed") ).unwrap_or_else(|_| String::from("s:ServerInternal Error")); return ( StatusCode::INTERNAL_SERVER_ERROR, [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], fault_xml, ).into_response(); } }; debug!("🎬 Received SOAP action: {}", soap_action.name); debug!("🎬 SOAP arguments: {:?}", soap_action.args); // Trouver l'action correspondante dans l'instance let action_instance = match instance.action(&soap_action.name) { Some(action_inst) => action_inst, None => { error!("❌ Action not found: {}", soap_action.name); let fault_xml = build_soap_fault( "s:Client", "Invalid Action", Some(error_codes::INVALID_ACTION), Some(&format!("Action '{}' not found", soap_action.name)) ).unwrap_or_else(|_| String::from("s:ServerInternal Error")); return ( StatusCode::INTERNAL_SERVER_ERROR, [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], fault_xml, ).into_response(); } }; // Convertir les arguments SOAP (String) en ActionData (StateValue) // D'abord, initialiser tous les arguments IN avec leurs valeurs par défaut let mut action_data = HashMap::new(); for arg_inst in action_instance.arguments_set().all() { let arg_model = arg_inst.as_ref().get_model(); if arg_model.is_in() { if let Some(var_inst) = arg_inst.get_variable_instance() { // Utiliser la valeur par défaut de la variable let default_value = var_inst.value(); action_data.insert(arg_inst.get_name().to_string(), default_value); } } } // Puis, écraser avec les valeurs fournies dans le SOAP for (arg_name, arg_value) in soap_action.args { debug!("🔍 Processing SOAP arg: {} = '{}'", arg_name, arg_value); // Trouver l'argument correspondant pour obtenir son type if let Some(arg_inst) = action_instance.argument(&arg_name) { if let Some(var_inst) = arg_inst.get_variable_instance() { let var_model = var_inst.as_ref().get_model(); // Parser la valeur selon le type de la variable match StateValue::from_string(&arg_value, &var_model.as_state_var_type()) { Ok(value) => { debug!("✅ Parsed {} = {:?}", arg_name, value); action_data.insert(arg_name, value); } Err(e) => { error!("❌ Failed to parse argument '{}': {:?}", arg_name, e); let fault_xml = build_soap_fault( "s:Client", "Invalid Arguments", Some(error_codes::ARGUMENT_VALUE_INVALID), Some(&format!("Invalid value for argument '{}'", arg_name)) ).unwrap_or_else(|_| String::from("s:ServerInternal Error")); return ( StatusCode::BAD_REQUEST, [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], fault_xml, ).into_response(); } } } } } let action_data = Arc::new(action_data); // Exécuter l'action match action_instance.run(action_data).await { Ok(output_data) => { // Convertir les StateValue en String pour SOAP let mut soap_values = HashMap::new(); for (key, value) in output_data.iter() { soap_values.insert(key.clone(), value.to_string()); } // Construire la réponse SOAP let response_xml = build_soap_response( &instance.service_type(), &soap_action.name, soap_values ).unwrap_or_else(|_| { build_soap_fault( "s:Server", "Action Failed", Some(error_codes::ACTION_FAILED), Some("Failed to build SOAP response") ).unwrap_or_else(|_| String::from("s:ServerInternal Error")) }); ( StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], response_xml, ).into_response() } Err(e) => { error!("❌ Action execution failed: {:?}", e); let fault_xml = build_soap_fault( "s:Server", "Action Failed", Some(error_codes::ACTION_FAILED), Some(&format!("Action execution failed: {:?}", e)) ).unwrap_or_else(|_| String::from("s:ServerInternal Error")); ( StatusCode::INTERNAL_SERVER_ERROR, [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], fault_xml, ).into_response() } } } #[cfg(test)] mod tests { use super::*; use crate::services::Service; use bevy_reflect::Reflect; #[test] fn test_reflect_to_string_primitives() { // Test des types primitifs assert_eq!(ServiceInstance::reflect_to_string(&42i32), "42"); assert_eq!(ServiceInstance::reflect_to_string(&3.14f64), "3.14"); assert_eq!(ServiceInstance::reflect_to_string(&true), "1"); assert_eq!(ServiceInstance::reflect_to_string(&false), "0"); assert_eq!(ServiceInstance::reflect_to_string(&'a'), "a"); } #[test] fn test_reflect_to_string_xml_escaping() { // Test de l'échappement XML let test_str = "Test & \"quotes\"".to_string(); let result = ServiceInstance::reflect_to_string(&test_str); // Vérifier que les caractères sont échappés assert!(result.contains("<")); assert!(result.contains(">")); assert!(result.contains("&")); assert!(result.contains(""")); } #[test] fn test_reflect_to_string_struct() { #[derive(Debug, Clone, Reflect)] struct TestStruct { name: String, value: i32, } let test = TestStruct { name: "Test".to_string(), value: 42, }; let result = ServiceInstance::reflect_to_string(&test); // Vérifier que c'est du XML assert!(result.starts_with("<")); assert!(result.ends_with(">")); assert!(result.contains("name")); assert!(result.contains("value")); assert!(result.contains("Test")); assert!(result.contains("42")); println!("Serialized struct: {}", result); } #[test] fn test_reflect_to_string_nested_struct() { #[derive(Debug, Clone, Reflect)] struct Address { street: String, city: String, } #[derive(Debug, Clone, Reflect)] struct Person { name: String, age: u32, address: Address, } let person = Person { name: "John ".to_string(), // Test XML escaping age: 30, address: Address { street: "123 Main St & Ave".to_string(), city: "Springfield".to_string(), }, }; let result = ServiceInstance::reflect_to_string(&person); // Vérifier la structure XML assert!(result.contains("")); assert!(result.contains("")); assert!(result.contains("name")); assert!(result.contains("age")); assert!(result.contains("address")); // Vérifier l'échappement XML dans les valeurs imbriquées assert!(result.contains("<")); assert!(result.contains(">")); assert!(result.contains("&")); println!("Nested struct XML: {}", result); } #[test] fn test_service_instance_creation() { let service = Service::new("AVTransport".to_string()); let instance = ServiceInstance::new(&service); assert_eq!(instance.get_name(), "AVTransport"); assert_eq!(instance.identifier(), "AVTransport"); } #[test] fn test_service_urls() { let service = Service::new("AVTransport".to_string()); let instance = ServiceInstance::new(&service); assert_eq!(instance.route(), "/service/AVTransport"); assert_eq!(instance.control_route(), "/service/AVTransport/control"); assert_eq!(instance.event_route(), "/service/AVTransport/event"); assert_eq!(instance.scpd_route(), "/service/AVTransport/desc.xml"); } #[test] fn test_service_type() { let mut service = Service::new("AVTransport".to_string()); service.set_version(2).unwrap(); let instance = ServiceInstance::new(&service); assert_eq!( instance.service_type(), "urn:schemas-upnp-org:service:AVTransport:2" ); } }