From adb6555aa68a893ba79a79dcee745d2acba3bd43 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 6 Oct 2025 20:24:04 +0200 Subject: [PATCH] Revue de code et refactoring --- .DS_Store | Bin 0 -> 8196 bytes Cargo.lock | 81 ++++++- PMOMusic/Cargo.toml | 2 +- PMOMusic/src/main.rs | 77 ++----- pmoapp/Cargo.toml | 8 + pmoapp/src/lib.rs | 64 +++++- pmoapp/src/pmoserver_impl.rs | 57 +++++ pmoserver/Cargo.toml | 4 - pmoserver/src/lib.rs | 17 +- pmoserver/src/logs/mod.rs | 58 +++++ pmoserver/src/server.rs | 42 ++++ pmoserver/src/upnp_impl.rs | 94 -------- pmoupnp/Cargo.toml | 1 + pmoupnp/src/devices/device_instance.rs | 43 +++- pmoupnp/src/devices/device_methods.rs | 18 +- pmoupnp/src/lib.rs | 6 +- pmoupnp/src/server.rs | 274 ++--------------------- pmoupnp/src/services/service_instance.rs | 2 +- pmoutils/Cargo.toml | 3 +- pmoutils/src/lib.rs | 34 ++- 20 files changed, 445 insertions(+), 440 deletions(-) create mode 100644 .DS_Store create mode 100644 pmoapp/src/pmoserver_impl.rs delete mode 100644 pmoserver/src/upnp_impl.rs diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1f4aa30355072a8c3a778ef5a052a4de54b88f51 GIT binary patch literal 8196 zcmeHM&2G~`5FRIKeh5W50O|!{iEA2K`hyUckhVfdL`4mP1E7$^rbKn@D0Z4kQL7yJ zc?VvB8{$cL7f$fau2a^V1|cC;73^Bx+1THVcfXlgcU&T}YC~t0Xo-mOI9Zm8=+0<- zpU;_=im%*+w4hJaqalUVr4BV`(B^_+z%XDKFbo(53$QjyvhY#r!C2>_YFWzjH39Uyeky5C_rmP;USk;P43`?rxzRhsR zYAh#GN_DcNI$80Pt++z5#M7Z?3piO-DNSz}FbvExz_ojcD%3~q9wh4b4t^h!O9Kkq zqp;uhf;O;U+kLMaMk_1dtW-L)kj>=^xnk~B<83f%M6GBT)>?xX{Of5Dc zN6pgJt-z03UEduDg|6cQ^747tcY;wZ82U~qbQ|(Up^z^$OH1SN>e_1AUfb9_F5BaK zcUH>w`sVuaaUp;0*6sUy%>%FJ2Os3ptay8%MQy1c|74wqje+k4{ywm5Cr2&va8xLw zBZ?k*K__N`H<<@GD?dcTh&8tU5uUITjfqRN1uh1(kC>={kpN$K4m3VKYkVYiA`x9) zQpdWX>11K|H0m_$cTjwy@SP%u+8TzvtokGmjEmR~G3yY~yszP5Gl5S7kF_OHDUJu# z>SfqG0v|nC_AqY3b{{i$l6oRSJ}EQKQFp@i8nBYhbQrZP!(UKU0B_wWqOFwGTgX}Uptx>)23==cIHZLHlV)8>koJV?n00;nX)!q;0z(P>bUvY=S!`fQ4d!T$Mg+&W z>{um{znuGqcYQowUn|=icYm|py@MCf=5BZIaq|}wr|Z(vdg_TGJmSxI8c~tpDIfc7 zh!E!33pHLjK~>KcXiu=|AW>W-U{6mFj*Nsx28k3RCi-e(EaDO`V2BKh(6fuq%w`cf zCrapKftS0MJF|Op*r?bAU;`Yp%i?fRk!j>U4sG#FmWmSYx!_$|&Tpv*xikO!{{>%# znVw<5Fz`1FSefm{b`2J$e?05PxYo9DzQ)OoaT6(p3OZ4aLyB@7^7J2u7~6odPBoSj aDRBku+&=`E_rFr$$g("/app").await; + // Ajouter la webapp via le trait WebAppExt + info!("📡 Registering Web application..."); + server.add_webapp_with_redirect::("/app").await; - // Routes de logging - server - .add_handler_with_state("/log-sse", log_sse, log_state.clone()) - .await; - server - .add_handler_with_state("/log-dump", log_dump, log_state.clone()) - .await; - - server.add_redirect("/", "/app").await; - - // Créer et enregistrer le MediaRenderer - info!("🎵 Creating MediaRenderer instance..."); - let renderer_instance = MEDIA_RENDERER.create_instance(); - - // Créer et ajouter les instances de services - for service in MEDIA_RENDERER.services().iter() { - let service_instance = service.create_instance(); - renderer_instance - .add_service(service_instance) - .expect("Failed to add service to renderer"); - } - - info!("📡 Registering MediaRenderer routes..."); - renderer_instance - .register_urls(&mut server) + info!("📡 Registering MediaRenderer..."); + let renderer_instance = server.register_device(MEDIA_RENDERER.clone()) .await .expect("Failed to register MediaRenderer routes"); @@ -76,25 +45,9 @@ async fn main() { let mut ssdp_server = SsdpServer::new(); ssdp_server.start().expect("Failed to start SSDP server"); - // Créer le device SSDP pour le MediaRenderer - let location = format!("{}{}", - renderer_instance.base_url(), - renderer_instance.description_route() - ); - - let mut ssdp_device = SsdpDevice::new( - renderer_instance.udn().to_string(), - MEDIA_RENDERER.device_type(), - location, - format!("Linux/5.0 UPnP/1.1 PMOMusic/1.0"), - ); - - // Ajouter les types de notification pour chaque service - for service in renderer_instance.services() { - ssdp_device.add_notification_type(service.service_type()); - } - - // Enregistrer le device et envoyer les annonces SSDP + // Créer et enregistrer le device SSDP pour le MediaRenderer + let ssdp_device = renderer_instance + .to_ssdp_device("PMOMusic", "1.0"); ssdp_server.add_device(ssdp_device); info!("✅ SSDP announcements sent for MediaRenderer"); diff --git a/pmoapp/Cargo.toml b/pmoapp/Cargo.toml index 894dea15..8b18b4e2 100644 --- a/pmoapp/Cargo.toml +++ b/pmoapp/Cargo.toml @@ -5,3 +5,11 @@ edition = "2021" [dependencies] rust-embed = "8.5.0" + +[dependencies.pmoserver] +path = "../pmoserver" +optional = true + +[features] +default = [] +pmoserver = ["dep:pmoserver"] diff --git a/pmoapp/src/lib.rs b/pmoapp/src/lib.rs index 763181ae..d7e1a37f 100644 --- a/pmoapp/src/lib.rs +++ b/pmoapp/src/lib.rs @@ -239,6 +239,8 @@ //! - [Vite Documentation](https://vitejs.dev/) use rust_embed::RustEmbed; +use std::future::Future; +use std::pin::Pin; /// Structure représentant l'application web embarquée. /// @@ -248,16 +250,72 @@ use rust_embed::RustEmbed; /// ## Exemple /// /// ```rust,no_run -/// use pmoapp::Webapp; +/// use pmoapp::{Webapp, WebAppExt}; /// use pmoserver::ServerBuilder; /// /// # async fn example() { /// let mut server = ServerBuilder::new("MyApp").build(); /// -/// // Ajouter la webapp comme SPA sur le chemin /app -/// server.add_spa::("/app").await; +/// // Ajouter la webapp via le trait WebAppExt +/// server.add_webapp::("/app").await; /// # } /// ``` #[derive(RustEmbed, Clone)] #[folder = "webapp/dist"] pub struct Webapp; + +/// Trait pour étendre un serveur HTTP avec des fonctionnalités webapp. +/// +/// Ce trait permet à `pmoapp` d'ajouter des méthodes d'extension sur des types +/// de serveurs externes (comme `pmoserver::Server`) sans que ces crates dépendent de `pmoapp`. +/// +/// # Architecture +/// +/// Similaire au pattern utilisé par `pmoupnp` pour `UpnpServer`, ce trait permet +/// une extension propre et découplée : +/// +/// - `pmoserver` définit un serveur HTTP générique +/// - `pmoapp` étend ce serveur avec des méthodes webapp via ce trait +/// - Le serveur n'a pas besoin de connaître `pmoapp` +/// +/// # Exemple d'implémentation +/// +/// ```ignore +/// impl WebAppExt for pmoserver::Server { +/// fn add_webapp(&mut self, path: &str) -> ... { +/// // Délègue à la méthode interne add_spa +/// self.add_spa::(path) +/// } +/// } +/// ``` +pub trait WebAppExt { + /// Ajoute une Single Page Application au serveur. + /// + /// # Arguments + /// + /// * `path` - Le chemin où monter la webapp (ex: "/app") + /// + /// # Type Parameter + /// + /// * `W` - Type RustEmbed contenant les fichiers de la webapp + fn add_webapp(&mut self, path: &str) -> Pin + Send + '_>> + where + W: RustEmbed + Clone + Send + Sync + 'static; + + /// Ajoute une webapp avec une redirection automatique depuis la racine. + /// + /// # Arguments + /// + /// * `path` - Le chemin où monter la webapp (ex: "/app") + /// + /// # Type Parameter + /// + /// * `W` - Type RustEmbed contenant les fichiers de la webapp + fn add_webapp_with_redirect(&mut self, path: &str) -> Pin + Send + '_>> + where + W: RustEmbed + Clone + Send + Sync + 'static; +} + +// Implémentation du trait pour pmoserver::Server (feature-gated) +#[cfg(feature = "pmoserver")] +mod pmoserver_impl; diff --git a/pmoapp/src/pmoserver_impl.rs b/pmoapp/src/pmoserver_impl.rs new file mode 100644 index 00000000..d5e7db7f --- /dev/null +++ b/pmoapp/src/pmoserver_impl.rs @@ -0,0 +1,57 @@ +//! Implémentation du trait WebAppExt pour le serveur pmoserver +//! +//! Ce module enrichit `pmoserver::Server` avec les fonctionnalités webapp en +//! implémentant le trait [`WebAppExt`](crate::WebAppExt). Cette implémentation +//! permet d'enregistrer facilement des webapps embarquées sur le serveur. +//! +//! ## Architecture +//! +//! `pmoapp` étend `pmoserver::Server` sans que `pmoserver` connaisse `pmoapp`. +//! C'est le pattern d'extension : `pmoapp` ajoute des fonctionnalités à un type +//! externe via un trait, similaire au pattern utilisé par `pmoupnp` pour `UpnpServer`. +//! +//! ## Exemple d'utilisation +//! +//! ```rust,no_run +//! use pmoapp::{Webapp, WebAppExt}; +//! use pmoserver::ServerBuilder; +//! +//! # async fn example() { +//! let mut server = ServerBuilder::new("MyApp").build(); +//! +//! // Le trait WebAppExt est automatiquement disponible +//! server.add_webapp::("/app").await; +//! +//! // Ou avec redirection +//! server.add_webapp_with_redirect::("/app").await; +//! # } +//! ``` + +use crate::WebAppExt; +use pmoserver::Server; +use rust_embed::RustEmbed; +use std::future::Future; +use std::pin::Pin; + +impl WebAppExt for Server { + fn add_webapp(&mut self, path: &str) -> Pin + Send + '_>> + where + W: RustEmbed + Clone + Send + Sync + 'static, + { + let path = path.to_string(); + Box::pin(async move { + self.add_spa::(&path).await; + }) + } + + fn add_webapp_with_redirect(&mut self, path: &str) -> Pin + Send + '_>> + where + W: RustEmbed + Clone + Send + Sync + 'static, + { + let path = path.to_string(); + Box::pin(async move { + self.add_spa::(&path).await; + self.add_redirect("/", &path).await; + }) + } +} diff --git a/pmoserver/Cargo.toml b/pmoserver/Cargo.toml index d5d74ddd..c2388bb8 100644 --- a/pmoserver/Cargo.toml +++ b/pmoserver/Cargo.toml @@ -21,7 +21,3 @@ axum-embed = "0.1.0" rust-embed = "8.7.2" utoipa = { version = "5.4.0", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } - -[dependencies.pmoupnp] -path = "../pmoupnp" -default-features = false diff --git a/pmoserver/src/lib.rs b/pmoserver/src/lib.rs index 8125fa06..9cea71a4 100644 --- a/pmoserver/src/lib.rs +++ b/pmoserver/src/lib.rs @@ -20,7 +20,6 @@ //! //! - [`server`] : Implémentation du serveur principal et du builder //! - [`logs`] : Système de logs SSE pour monitoring en temps réel -//! - `upnp_impl` : Implémentation du trait `pmoupnp::UpnpServer` (privé) //! //! ## Exemple d'utilisation //! @@ -53,27 +52,25 @@ //! //! ## Intégration UPnP //! -//! Le serveur implémente automatiquement le trait `pmoupnp::UpnpServer`, permettant -//! de connecter des devices UPnP : +//! Le serveur peut être étendu avec UPnP via le trait `pmoupnp::UpnpServer`. +//! L'implémentation est fournie par `pmoupnp` (feature `pmoserver`), permettant +//! de connecter des devices UPnP sans que `pmoserver` dépende de `pmoupnp` : //! //! ```rust,no_run -//! use pmoupnp::{UpnpServer, mediarenderer::device::MEDIA_RENDERER}; -//! use pmoupnp::devices::DeviceInstance; +//! use pmoupnp::{UpnpServer, mediarenderer::MEDIA_RENDERER}; //! use pmoserver::ServerBuilder; -//! use std::sync::Arc; //! //! # async fn example() { //! let mut server = ServerBuilder::new("MediaRenderer").build(); -//! let device = Arc::new(DeviceInstance::new(&MEDIA_RENDERER)); +//! let device = MEDIA_RENDERER.create_instance(); //! -//! // Le device enregistre automatiquement ses routes +//! // Le trait UpnpServer est automatiquement disponible (implémenté dans pmoupnp) //! device.register_urls(&mut server).await; //! # } //! ``` pub mod server; pub mod logs; -mod upnp_impl; pub use server::{Server, ServerBuilder, ServerInfo}; -pub use logs::{LogState, SseLayer, log_sse, log_dump}; +pub use logs::{LogState, SseLayer, log_sse, log_dump, init_logging, LoggingOptions}; diff --git a/pmoserver/src/logs/mod.rs b/pmoserver/src/logs/mod.rs index 6ff83ece..1d05e274 100644 --- a/pmoserver/src/logs/mod.rs +++ b/pmoserver/src/logs/mod.rs @@ -19,6 +19,7 @@ use axum::{ }; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; +use tracing_subscriber::{Registry, layer::SubscriberExt}; /// Représente une entrée de log #[derive(Debug, Clone, Serialize)] @@ -157,3 +158,60 @@ fn filter_entry(entry: &LogEntry, q: &LogQuery) -> bool { allowed } + +/// Options d'initialisation du système de logging +#[derive(Debug, Clone)] +pub struct LoggingOptions { + /// Capacité du buffer circulaire (nombre d'entrées conservées) + pub buffer_capacity: usize, + /// Activer la sortie vers stderr/stdout + pub enable_console: bool, +} + +impl Default for LoggingOptions { + fn default() -> Self { + Self { + buffer_capacity: 1000, + enable_console: true, + } + } +} + +/// Initialise le système de logging avec SSE et optionnellement la console +/// +/// # Arguments +/// * `options` - Options de configuration du logging +/// +/// # Retourne +/// Le `LogState` qui peut être utilisé pour ajouter les routes de logging au serveur +/// +/// # Exemple +/// ```rust,no_run +/// use pmoserver::logs::{init_logging, LoggingOptions}; +/// +/// let log_state = init_logging(LoggingOptions { +/// buffer_capacity: 1000, +/// enable_console: true, +/// }); +/// ``` +pub fn init_logging(options: LoggingOptions) -> LogState { + let log_state = LogState::new(options.buffer_capacity); + + let subscriber = Registry::default().with(SseLayer::new(log_state.clone())); + + if options.enable_console { + let subscriber = subscriber.with( + tracing_subscriber::fmt::layer() + .with_target(true) + .with_level(true) + .with_ansi(true), + ); + tracing::subscriber::set_global_default(subscriber) + .expect("Failed to set global default subscriber"); + } else { + tracing::subscriber::set_global_default(subscriber) + .expect("Failed to set global default subscriber"); + } + + log_state +} diff --git a/pmoserver/src/server.rs b/pmoserver/src/server.rs index 0f621ce9..703cfa17 100644 --- a/pmoserver/src/server.rs +++ b/pmoserver/src/server.rs @@ -25,6 +25,7 @@ use std::{net::SocketAddr, sync::Arc}; use tokio::{signal, sync::RwLock, task::JoinHandle}; use tracing::info; use utoipa_swagger_ui::SwaggerUi; +use crate::logs::{LogState, LoggingOptions, init_logging, log_sse, log_dump}; /// Info serveur sérialisable #[derive(Clone, Serialize, utoipa::ToSchema)] @@ -45,6 +46,7 @@ pub struct Server { router: Arc>, api_router: Arc>>, join_handle: Option>, + log_state: Option, } impl Server { @@ -70,6 +72,7 @@ impl Server { router: Arc::new(RwLock::new(Router::new())), api_router: Arc::new(RwLock::new(None)), join_handle: None, + log_state: None, } } @@ -508,6 +511,45 @@ impl Server { http_port: self.http_port, } } + + /// Initialise le système de logging et enregistre les routes de logs + /// + /// Cette méthode configure le système de tracing avec SSE et optionnellement la console, + /// puis enregistre automatiquement les routes `/log-sse` et `/log-dump`. + /// + /// # Arguments + /// + /// * `options` - Options de configuration du logging + /// + /// # Exemple + /// + /// ```rust,no_run + /// # use pmoserver::{ServerBuilder, logs::LoggingOptions}; + /// # #[tokio::main] + /// # async fn main() { + /// let mut server = ServerBuilder::new_configured().build(); + /// + /// // Initialiser les logs avec console + /// server.init_logging(LoggingOptions::default()).await; + /// + /// // Ou sans console + /// server.init_logging(LoggingOptions { + /// buffer_capacity: 1000, + /// enable_console: false, + /// }).await; + /// + /// server.start().await; + /// # } + /// ``` + pub async fn init_logging(&mut self, options: LoggingOptions) { + let log_state = init_logging(options); + + // Enregistrer automatiquement les routes de logging + self.add_handler_with_state("/log-sse", log_sse, log_state.clone()).await; + self.add_handler_with_state("/log-dump", log_dump, log_state.clone()).await; + + self.log_state = Some(log_state); + } } /// Builder pattern diff --git a/pmoserver/src/upnp_impl.rs b/pmoserver/src/upnp_impl.rs deleted file mode 100644 index d877c108..00000000 --- a/pmoserver/src/upnp_impl.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Implémentation du trait UpnpServer pour le serveur pmoserver -//! -//! Ce module fournit l'implémentation du trait [`pmoupnp::UpnpServer`] pour -//! le [`Server`](crate::server::Server) de pmoserver, permettant aux devices -//! et services UPnP d'enregistrer automatiquement leurs endpoints HTTP. -//! -//! ## Architecture -//! -//! L'implémentation fait le pont entre : -//! - Les pointeurs de fonction du trait `UpnpServer` (agnostiques du framework web) -//! - Les handlers Axum (spécifiques à l'implémentation `pmoserver`) -//! -//! Chaque méthode du trait crée un wrapper qui : -//! 1. Convertit les pointeurs de fonction en closures compatibles Axum -//! 2. Délègue l'enregistrement aux méthodes internes du `Server` -//! 3. Retourne une future qui se résout une fois le handler enregistré -//! -//! ## Exemple d'utilisation -//! -//! ```rust,no_run -//! use pmoupnp::{UpnpServer, mediarenderer::device::MEDIA_RENDERER}; -//! use pmoupnp::devices::DeviceInstance; -//! use pmoserver::ServerBuilder; -//! use std::sync::Arc; -//! -//! # async fn example() { -//! let mut server = ServerBuilder::new("MyRenderer").build(); -//! let device = Arc::new(DeviceInstance::new(&MEDIA_RENDERER)); -//! -//! // Le trait UpnpServer est automatiquement disponible -//! device.register_urls(&mut server).await; -//! # } -//! ``` - -use crate::server::Server; -use pmoupnp::{UpnpServer, server::{Response, HeaderMap, Request}}; -use std::future::Future; -use std::pin::Pin; -use axum::extract::State; - -impl UpnpServer for Server { - fn add_handler(&mut self, path: &str, handler: F) -> Pin + Send + '_>> - where - F: Fn() -> Fut + Send + Sync + 'static + Clone, - Fut: Future + Send + 'static, - { - let path = path.to_string(); - Box::pin(async move { - Self::add_handler(self, &path, handler).await; - }) - } - - fn add_post_handler_with_state( - &mut self, - path: &str, - handler: fn(State, String) -> Pin + Send>>, - state: S, - ) -> Pin + Send + '_>> - where - S: Clone + Send + Sync + 'static, - { - let path = path.to_string(); - - // Créer un wrapper qui convertit le fn pointer en handler Axum - let wrapper = move |State(s): State, body: String| -> Pin + Send>> { - handler(State(s), body) - }; - - Box::pin(async move { - Self::add_post_handler_with_state(self, &path, wrapper, state).await; - }) - } - - fn add_handler_with_state( - &mut self, - path: &str, - handler: fn(State, HeaderMap, Request) -> Pin + Send>>, - state: S, - ) -> Pin + Send + '_>> - where - S: Clone + Send + Sync + 'static, - { - let path = path.to_string(); - - // Créer un wrapper qui convertit le fn pointer en handler Axum - let wrapper = move |State(s): State, headers: HeaderMap, req: Request| -> Pin + Send>> { - handler(State(s), headers, req) - }; - - Box::pin(async move { - Self::add_handler_with_state(self, &path, wrapper, state).await; - }) - } -} diff --git a/pmoupnp/Cargo.toml b/pmoupnp/Cargo.toml index 3d550251..b6b761d3 100644 --- a/pmoupnp/Cargo.toml +++ b/pmoupnp/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" pmoconfig = { path = "../pmoconfig" } pmodidl = { path = "../pmodidl"} pmoutils = { path = "../pmoutils" } +pmoserver = { path = "../pmoserver" } url = "2.5.7" uuid = "1.18.1" diff --git a/pmoupnp/src/devices/device_instance.rs b/pmoupnp/src/devices/device_instance.rs index 1aa2f6c4..8a9695ad 100644 --- a/pmoupnp/src/devices/device_instance.rs +++ b/pmoupnp/src/devices/device_instance.rs @@ -251,7 +251,7 @@ impl DeviceInstance { } /// Enregistre toutes les URLs du device et de ses services dans le serveur. - pub fn register_urls<'a, S: crate::UpnpServer + ?Sized>(&'a self, server: &'a mut S) -> std::pin::Pin> + 'a>> { + pub fn register_urls<'a>(&'a self, server: &'a mut pmoserver::Server) -> std::pin::Pin> + 'a>> { Box::pin(async move { info!( "✅ Device description for {} available at: {}{}", @@ -330,4 +330,45 @@ impl DeviceInstance { xml, ).into_response() } + + /// Crée un SsdpDevice configuré pour ce device UPnP. + /// + /// Cette méthode simplifie la création d'un device SSDP en configurant automatiquement : + /// - L'UDN du device + /// - Le type de device + /// - La location (URL de description) + /// - Le serveur (User-Agent avec OS/version détecté automatiquement) + /// - Les types de notification pour tous les services + /// + /// # Arguments + /// + /// * `app_name` - Nom de l'application (ex: "PMOMusic") + /// * `app_version` - Version de l'application (ex: "1.0") + /// + /// # Exemple + /// + /// ```ignore + /// let renderer_instance = MEDIA_RENDERER.create_instance(); + /// let ssdp_device = renderer_instance.to_ssdp_device("PMOMusic", "1.0"); + /// ssdp_server.add_device(ssdp_device); + /// ``` + pub fn to_ssdp_device(&self, app_name: &str, app_version: &str) -> crate::ssdp::SsdpDevice { + let location = format!("{}{}", self.base_url(), self.description_route()); + let os_string = pmoutils::get_os_string(); + let server_string = format!("{} UPnP/1.1 {}/{}", os_string, app_name, app_version); + + let mut ssdp_device = crate::ssdp::SsdpDevice::new( + self.udn().to_string(), + self.model.device_type(), + location, + server_string, + ); + + // Ajouter les types de notification pour chaque service + for service in self.services() { + ssdp_device.add_notification_type(service.service_type()); + } + + ssdp_device + } } diff --git a/pmoupnp/src/devices/device_methods.rs b/pmoupnp/src/devices/device_methods.rs index 96fd9b3b..cf4b76e8 100644 --- a/pmoupnp/src/devices/device_methods.rs +++ b/pmoupnp/src/devices/device_methods.rs @@ -1,10 +1,11 @@ //! Implémentation des traits UPnP pour Device. +use std::sync::Arc; use xmltree::{Element, XMLNode}; use crate::{ devices::{Device, DeviceInstance}, - UpnpObject, UpnpModel, + UpnpObject, UpnpModel, UpnpInstance, }; impl UpnpObject for Device { @@ -115,4 +116,19 @@ impl UpnpObject for Device { impl UpnpModel for Device { type Instance = DeviceInstance; + + /// Crée une instance du device avec ses services déjà instanciés. + /// + /// Les services sont créés dans DeviceInstance::new(), cette méthode + /// établit uniquement les liens bidirectionnels parent-enfant. + fn create_instance(&self) -> Arc { + let instance = Arc::new(DeviceInstance::new(self)); + + // Établir le lien parent pour chaque service + for service in instance.services() { + service.set_device(Arc::clone(&instance)); + } + + instance + } } diff --git a/pmoupnp/src/lib.rs b/pmoupnp/src/lib.rs index a23d1cbc..a5cd62d9 100644 --- a/pmoupnp/src/lib.rs +++ b/pmoupnp/src/lib.rs @@ -1,10 +1,10 @@ mod object_trait; mod object_set; +mod server; pub mod actions; pub mod devices; pub mod mediarenderer; -pub mod server; pub mod services; pub mod soap; pub mod ssdp; @@ -12,8 +12,7 @@ pub mod state_variables; pub mod value_ranges; pub mod variable_types; -// Re-exports -pub use server::UpnpServer; + use std::{collections::HashMap, sync::Arc}; @@ -21,6 +20,7 @@ use std::{collections::HashMap, sync::Arc}; use std::sync::RwLock; pub use crate::object_trait::*; +pub use crate::server::UpnpServer; #[derive(Debug, Clone)] pub struct UpnpObjectType { diff --git a/pmoupnp/src/server.rs b/pmoupnp/src/server.rs index 007c8342..6b5a288e 100644 --- a/pmoupnp/src/server.rs +++ b/pmoupnp/src/server.rs @@ -1,262 +1,22 @@ -//! Trait pour les serveurs UPnP -//! -//! Ce module définit le trait [`UpnpServer`] qui permet de connecter -//! des devices UPnP à n'importe quelle implémentation de serveur web. -//! -//! ## Architecture -//! -//! Le trait `UpnpServer` définit une interface minimale permettant aux devices -//! et services UPnP d'enregistrer leurs endpoints HTTP sans dépendre d'une -//! implémentation de serveur spécifique. -//! -//! ## Séparation des responsabilités -//! -//! - **pmoupnp** : Définit le trait `UpnpServer` et l'utilise via des contraintes génériques -//! - **pmoserver** : Fournit une implémentation concrète basée sur Axum -//! - **Autres crates** : Peuvent fournir leurs propres implémentations (actix-web, warp, etc.) -//! -//! ## Exemple d'utilisation -//! -//! ```rust,no_run -//! use pmoupnp::{UpnpServer, devices::{Device, DeviceInstance}}; -//! use std::sync::Arc; -//! -//! # async fn example(mut server: S) { -//! // Créer un device -//! let device = Device::new( -//! "MyDevice".to_string(), -//! "MyDeviceType".to_string(), -//! "Friendly Name".to_string(), -//! ); -//! let device_instance = Arc::new(DeviceInstance::new(&device)); -//! -//! // Le device enregistre automatiquement ses routes UPnP -//! device_instance.register_urls(&mut server).await; -//! # } -//! ``` -//! -//! ## Implémentation -//! -//! Pour implémenter ce trait, votre serveur doit fournir trois méthodes -//! pour enregistrer des handlers HTTP asynchrones : -//! -//! ```rust,no_run -//! use pmoupnp::UpnpServer; -//! use std::future::Future; -//! use std::pin::Pin; -//! -//! struct MyServer { -//! // votre implémentation -//! } -//! -//! impl UpnpServer for MyServer { -//! fn add_handler(&mut self, path: &str, handler: F) -//! -> Pin + Send + '_>> -//! where -//! F: Fn() -> Fut + Send + Sync + 'static + Clone, -//! Fut: Future + Send + 'static, -//! { -//! // Enregistrer le handler pour GET requests -//! # todo!() -//! } -//! -//! fn add_post_handler_with_state( -//! &mut self, -//! path: &str, -//! handler: fn(axum::extract::State, String) -//! -> Pin + Send>>, -//! state: S, -//! ) -> Pin + Send + '_>> -//! where -//! S: Clone + Send + Sync + 'static, -//! { -//! // Enregistrer le handler pour POST avec body -//! # todo!() -//! } -//! -//! fn add_handler_with_state( -//! &mut self, -//! path: &str, -//! handler: fn(axum::extract::State, -//! pmoupnp::server::HeaderMap, -//! pmoupnp::server::Request) -//! -> Pin + Send>>, -//! state: S, -//! ) -> Pin + Send + '_>> -//! where -//! S: Clone + Send + Sync + 'static, -//! { -//! // Enregistrer le handler avec accès complet à la requête -//! # todo!() -//! } -//! } -//! ``` +use std::sync::Arc; -use std::future::Future; -use std::pin::Pin; +use pmoserver::Server; -/// Type alias pour la réponse HTTP (basé sur Axum). -/// -/// Utilisé pour éviter une dépendance directe sur axum dans les signatures de trait, -/// tout en restant compatible avec les types Axum. -pub type Response = axum::response::Response; +use crate::devices::errors::DeviceError; +use crate::devices::{Device, DeviceInstance}; +use crate::UpnpModel; -/// Type alias pour les en-têtes HTTP (basé sur Axum). -pub type HeaderMap = axum::http::HeaderMap; +pub trait UpnpServer { + async fn register_device(&mut self, device: Arc) -> Result,DeviceError> ; -/// Type alias pour la requête HTTP (basé sur Axum). -pub type Request = axum::extract::Request; - -/// Trait pour les serveurs compatibles UPnP. -/// -/// Ce trait définit l'interface minimale qu'un serveur web doit implémenter -/// pour supporter l'enregistrement automatique des endpoints UPnP par les -/// [`DeviceInstance`](crate::devices::DeviceInstance) et -/// [`ServiceInstance`](crate::services::ServiceInstance). -/// -/// ## Contraintes -/// -/// - `Send + Sync` : Le serveur doit être partageable entre threads -/// -/// ## Méthodes -/// -/// Les trois méthodes permettent d'enregistrer différents types de handlers : -/// -/// 1. **`add_handler`** : Handler GET simple sans état -/// 2. **`add_post_handler_with_state`** : Handler POST avec état et body texte (pour SOAP) -/// 3. **`add_handler_with_state`** : Handler générique avec accès complet (pour SUBSCRIBE/UNSUBSCRIBE) -/// -/// ## Implémentations -/// -/// - **pmoserver::Server** : Implémentation basée sur Axum (fournie par la crate `pmoserver`) -pub trait UpnpServer: Send + Sync { - /// Ajoute un handler GET pour un chemin donné. - /// - /// Utilisé principalement pour servir les descripteurs XML des devices et services. - /// - /// # Arguments - /// - /// * `path` - Le chemin HTTP (ex: `/device/MediaRenderer/description.xml`) - /// * `handler` - Une closure asynchrone qui génère la réponse - /// - /// # Retour - /// - /// Une future qui se résout quand le handler est enregistré. - /// - /// # Exemple - /// - /// ```rust,no_run - /// use pmoupnp::UpnpServer; - /// use axum::response::IntoResponse; - /// - /// # async fn example(mut server: S) { - /// server.add_handler("/description.xml", || async { - /// "".into_response() - /// }).await; - /// # } - /// ``` - fn add_handler(&mut self, path: &str, handler: F) -> Pin + Send + '_>> - where - F: Fn() -> Fut + Send + Sync + 'static + Clone, - Fut: Future + Send + 'static; - - /// Ajoute un handler POST avec état pour un chemin donné. - /// - /// Utilisé pour les endpoints de contrôle SOAP des services UPnP. - /// - /// # Arguments - /// - /// * `path` - Le chemin HTTP (ex: `/service/AVTransport/control`) - /// * `handler` - Un pointeur de fonction qui traite la requête SOAP - /// * `state` - L'état partagé (typiquement une `ServiceInstance`) - /// - /// # Retour - /// - /// Une future qui se résout quand le handler est enregistré. - /// - /// # Exemple - /// - /// ```rust,no_run - /// use pmoupnp::{UpnpServer, server::Response}; - /// use axum::extract::State; - /// use std::pin::Pin; - /// use std::future::Future; - /// - /// fn soap_handler( - /// State(service): State, - /// body: String, - /// ) -> Pin + Send>> { - /// Box::pin(async move { - /// // Traiter la requête SOAP - /// axum::response::Response::default() - /// }) - /// } - /// - /// # async fn example(mut server: S) { - /// server.add_post_handler_with_state( - /// "/control", - /// soap_handler, - /// "ServiceName".to_string(), - /// ).await; - /// # } - /// ``` - fn add_post_handler_with_state( - &mut self, - path: &str, - handler: fn(axum::extract::State, String) -> Pin + Send>>, - state: S, - ) -> Pin + Send + '_>> - where - S: Clone + Send + Sync + 'static; - - /// Ajoute un handler avec état et accès complet à la requête. - /// - /// Utilisé pour les endpoints d'événements (SUBSCRIBE/UNSUBSCRIBE) qui nécessitent - /// un accès aux en-têtes HTTP et à la méthode HTTP. - /// - /// # Arguments - /// - /// * `path` - Le chemin HTTP (ex: `/service/AVTransport/event`) - /// * `handler` - Un pointeur de fonction avec accès complet à la requête - /// * `state` - L'état partagé (typiquement une `ServiceInstance`) - /// - /// # Retour - /// - /// Une future qui se résout quand le handler est enregistré. - /// - /// # Exemple - /// - /// ```rust,no_run - /// use pmoupnp::{UpnpServer, server::{Response, HeaderMap, Request}}; - /// use axum::extract::State; - /// use std::pin::Pin; - /// use std::future::Future; - /// - /// fn event_handler( - /// State(service): State, - /// headers: HeaderMap, - /// req: Request, - /// ) -> Pin + Send>> { - /// Box::pin(async move { - /// // Traiter SUBSCRIBE/UNSUBSCRIBE - /// axum::response::Response::default() - /// }) - /// } - /// - /// # async fn example(mut server: S) { - /// server.add_handler_with_state( - /// "/event", - /// event_handler, - /// "ServiceName".to_string(), - /// ).await; - /// # } - /// ``` - fn add_handler_with_state( - &mut self, - path: &str, - handler: fn(axum::extract::State, HeaderMap, Request) -> Pin + Send>>, - state: S, - ) -> Pin + Send + '_>> - where - S: Clone + Send + Sync + 'static; } + +impl UpnpServer for Server { + async fn register_device(&mut self, device: Arc) -> Result,DeviceError> { + let di = device.create_instance(); + + di.register_urls(self).await?; + + Ok(di) + } +} \ No newline at end of file diff --git a/pmoupnp/src/services/service_instance.rs b/pmoupnp/src/services/service_instance.rs index de9dbae1..76796619 100644 --- a/pmoupnp/src/services/service_instance.rs +++ b/pmoupnp/src/services/service_instance.rs @@ -304,7 +304,7 @@ impl ServiceInstance { /// # Errors /// /// Retourne une erreur si l'enregistrement des routes échoue. - pub async fn register_urls(&self, server: &mut S) -> Result<(), ServiceError> { + 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(); diff --git a/pmoutils/Cargo.toml b/pmoutils/Cargo.toml index 439ebc85..e971e145 100644 --- a/pmoutils/Cargo.toml +++ b/pmoutils/Cargo.toml @@ -4,4 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] -get_if_addrs = "0.5.3" \ No newline at end of file +get_if_addrs = "0.5.3" +os_info = "3.8" \ No newline at end of file diff --git a/pmoutils/src/lib.rs b/pmoutils/src/lib.rs index e578b603..68b03512 100644 --- a/pmoutils/src/lib.rs +++ b/pmoutils/src/lib.rs @@ -17,4 +17,36 @@ /// ``` mod ip_utils; -pub use ip_utils::guess_local_ip; \ No newline at end of file +pub use ip_utils::guess_local_ip; + +/// Retourne une chaîne décrivant le système d'exploitation et sa version. +/// +/// Utilise la crate `os_info` pour obtenir de manière portable et fiable +/// les informations sur le système d'exploitation courant. +/// +/// # Format +/// - macOS: "macOS/15.1" ou "Mac OS/10.15.7" +/// - Linux: "Linux/6.5.0" ou "Ubuntu/22.04" +/// - Windows: "Windows/10.0.19045" +/// - Autre: "{OS}/Unknown" +/// +/// # Exemples +/// +/// ``` +/// use pmoutils::get_os_string; +/// +/// let os = get_os_string(); +/// println!("OS: {}", os); // Ex: "Linux/6.5.0" +/// ``` +pub fn get_os_string() -> String { + let info = os_info::get(); + let os_type = format!("{:?}", info.os_type()); + + // Obtenir la version si disponible + let version = info.version(); + if version != &os_info::Version::Unknown { + format!("{}/{}", os_type, version) + } else { + format!("{}/Unknown", os_type) + } +} \ No newline at end of file