From 5b8642a70b339599f45042024fe8458c1c6dcf97 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 1 Mar 2026 21:43:00 +0100 Subject: [PATCH] Migrate pmoutils to registry and refactor dependencies Migrate pmoutils crate to registry version 0.1.2 and update all dependencies to use the registry version instead of local path references. Remove pmoutils from Cargo.lock and Cargo.toml files where it was previously used as a local path dependency. Move ToXmlElement trait to pmodidl crate and update all usages to import from pmodidl instead of pmoutils. Fix parameter order in find_process_using_port function call. --- Cargo.lock | 7 +- Cargo.toml | 1 - pmoconfig/Cargo.toml | 2 +- pmocontrol/Cargo.toml | 2 +- .../src/music_renderer/musicrenderer.rs | 2 +- pmodidl/Cargo.toml | 1 - pmodidl/src/lib.rs | 21 +- pmomediaserver/Cargo.toml | 2 +- pmomediaserver/src/content_handler.rs | 2 +- pmoupnp/Cargo.toml | 2 +- pmoupnp/src/upnp_server.rs | 2 +- pmoutils/Cargo.toml | 13 - pmoutils/Cargo.toml.backup | 13 - pmoutils/src/ip_utils.rs | 273 ------------------ pmoutils/src/lib.rs | 76 ----- pmoutils/src/process.rs | 88 ------ pmowebrenderer/Cargo.toml | 2 +- pmowebrenderer/src/handlers.rs | 2 +- 18 files changed, 31 insertions(+), 480 deletions(-) delete mode 100644 pmoutils/Cargo.toml delete mode 100644 pmoutils/Cargo.toml.backup delete mode 100644 pmoutils/src/ip_utils.rs delete mode 100644 pmoutils/src/lib.rs delete mode 100644 pmoutils/src/process.rs diff --git a/Cargo.lock b/Cargo.lock index fe7c03f8..e75089a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4047,7 +4047,6 @@ version = "0.1.0" dependencies = [ "bevy_reflect", "bevy_reflect_derive", - "pmoutils", "quick-xml", "serde", "utoipa", @@ -4362,15 +4361,15 @@ dependencies = [ [[package]] name = "pmoutils" -version = "0.1.0" +version = "0.1.2" +source = "sparse+https://gargoton.petite-maison-orange.fr/api/packages/pmo/cargo/" +checksum = "9ed68fb06ffdd1a30fc7792d883dcd6d61ad7705c3a744f54a0a5bcc341fc65e" dependencies = [ "get_if_addrs", "netstat2", "os_info", - "quick-xml", "sysinfo", "users", - "xmltree 0.10.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fd97a8da..135dbd7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ "pmowebrenderer", "pmomediaserver", "pmoconfig", - "pmoutils", "pmodidl", "pmoserver", "pmoapp", diff --git a/pmoconfig/Cargo.toml b/pmoconfig/Cargo.toml index 81f85369..3a3790ad 100644 --- a/pmoconfig/Cargo.toml +++ b/pmoconfig/Cargo.toml @@ -5,7 +5,7 @@ version = "0.1.0" edition = "2021" [dependencies] -pmoutils ={ path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } serde = { workspace = true } serde_yaml = { workspace = true } diff --git a/pmocontrol/Cargo.toml b/pmocontrol/Cargo.toml index e91fb791..7b062861 100644 --- a/pmocontrol/Cargo.toml +++ b/pmocontrol/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] pmoupnp = { path = "../pmoupnp" } pmodidl = { path = "../pmodidl" } -pmoutils = { path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } quick-xml = { workspace = true } thiserror = { workspace = true } ureq = "3.1.4" diff --git a/pmocontrol/src/music_renderer/musicrenderer.rs b/pmocontrol/src/music_renderer/musicrenderer.rs index efc929ed..0dfa758b 100644 --- a/pmocontrol/src/music_renderer/musicrenderer.rs +++ b/pmocontrol/src/music_renderer/musicrenderer.rs @@ -1562,7 +1562,7 @@ pub(crate) fn build_didl_lite_metadata( protocol_info: &str, ) -> String { use pmodidl::{DIDLLite, Item, Resource}; - use pmoutils::ToXmlElement; + use pmodidl::ToXmlElement; // Construire l'Item DIDL avec toutes les métadonnées let item = Item { diff --git a/pmodidl/Cargo.toml b/pmodidl/Cargo.toml index baf5a6bc..99f0ca59 100644 --- a/pmodidl/Cargo.toml +++ b/pmodidl/Cargo.toml @@ -10,5 +10,4 @@ utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } quick-xml = { workspace = true } bevy_reflect = "0.17.1" bevy_reflect_derive = "0.17.1" -pmoutils = { path = "../pmoutils" } xmltree = "0.10" diff --git a/pmodidl/src/lib.rs b/pmodidl/src/lib.rs index 3779add2..0f6af2c6 100644 --- a/pmodidl/src/lib.rs +++ b/pmodidl/src/lib.rs @@ -3,14 +3,31 @@ //! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA. use bevy_reflect::Reflect; -use pmoutils::ToXmlElement; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::HashSet; use std::fmt::Write; use std::io::Cursor; use std::time::SystemTime; -use xmltree::{Element, XMLNode}; +use xmltree::{Element, EmitterConfig, XMLNode}; + +/// Trait générique pour obtenir un élément XML (xmltree::Element). +pub trait ToXmlElement { + /// Convertit l'objet en élément XML. + fn to_xml_element(&self) -> Element; + + /// Sérialise en chaîne XML formatée. + fn to_xml(&self) -> String { + let elem = self.to_xml_element(); + let config = EmitterConfig::new() + .perform_indent(true) + .indent_string(" "); + let mut buf = Vec::new(); + elem.write_with_config(&mut buf, config) + .expect("Failed to write XML"); + String::from_utf8(buf).expect("Invalid UTF-8") + } +} // ============= Couche d'abstraction générique ============= diff --git a/pmomediaserver/Cargo.toml b/pmomediaserver/Cargo.toml index 27f46c4b..72dc6a97 100644 --- a/pmomediaserver/Cargo.toml +++ b/pmomediaserver/Cargo.toml @@ -18,7 +18,7 @@ quick-xml = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -pmoutils = { path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } # Optional dependencies axum = { version = "0.8", optional = true } diff --git a/pmomediaserver/src/content_handler.rs b/pmomediaserver/src/content_handler.rs index 652d241f..80ebffec 100644 --- a/pmomediaserver/src/content_handler.rs +++ b/pmomediaserver/src/content_handler.rs @@ -13,7 +13,7 @@ use pmodidl::{Container, DIDLLite}; use pmosource::api::{get_source as get_source_from_registry, list_all_sources}; use pmosource::{BrowseResult, MusicSource, MusicSourceError}; -use pmoutils::ToXmlElement; +use pmodidl::ToXmlElement; use std::collections::HashSet; use std::sync::Arc; diff --git a/pmoupnp/Cargo.toml b/pmoupnp/Cargo.toml index ab545720..844d6dd6 100644 --- a/pmoupnp/Cargo.toml +++ b/pmoupnp/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] pmoconfig = { path = "../pmoconfig" } pmodidl = { path = "../pmodidl"} -pmoutils = { path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } pmoserver = { path = "../pmoserver" } pmocovers = { path = "../pmocovers", features = ["pmoserver"] } pmoaudiocache = { path = "../pmoaudiocache", features = ["pmoserver"] } diff --git a/pmoupnp/src/upnp_server.rs b/pmoupnp/src/upnp_server.rs index 467755b8..8baeb70e 100644 --- a/pmoupnp/src/upnp_server.rs +++ b/pmoupnp/src/upnp_server.rs @@ -430,7 +430,7 @@ impl UpnpServerExt for Server { let kind = e.kind(); if kind == std::io::ErrorKind::AddrInUse { let port = crate::ssdp::SSDP_PORT; - if let Some(process) = find_process_using_port(port, TransportProtocol::Udp) { + if let Some(process) = find_process_using_port(TransportProtocol::Udp, port) { error!( "❌ SSDP initialization failed: port {} is already in use by \ PID {} ({}) owned by {}: {}", diff --git a/pmoutils/Cargo.toml b/pmoutils/Cargo.toml deleted file mode 100644 index dc3e22e3..00000000 --- a/pmoutils/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pmoutils" -version = "0.1.0" -edition = "2024" - -[dependencies] -get_if_addrs = "0.5.3" -os_info = "3.8" -netstat2 = "0.11" -sysinfo = "0.30" -users = "0.11" -quick-xml = { workspace = true } -xmltree = "0.10" diff --git a/pmoutils/Cargo.toml.backup b/pmoutils/Cargo.toml.backup deleted file mode 100644 index 49e86a59..00000000 --- a/pmoutils/Cargo.toml.backup +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pmoutils" -version = "0.1.0" -edition = "2024" - -[dependencies] -get_if_addrs = "0.5.3" -os_info = "3.8" -netstat2 = "0.11" -sysinfo = "0.30" -users = "0.11" -quick-xml = "0.38.3" -xmltree = "0.10" diff --git a/pmoutils/src/ip_utils.rs b/pmoutils/src/ip_utils.rs deleted file mode 100644 index c8ab423f..00000000 --- a/pmoutils/src/ip_utils.rs +++ /dev/null @@ -1,273 +0,0 @@ -use get_if_addrs::get_if_addrs; -use std::net::UdpSocket; - -/// Devine l'adresse IP locale de la machine. -/// -/// Cette fonction tente de déterminer l'adresse IP locale en créant une connexion UDP -/// vers un serveur DNS public (8.8.8.8). Cette technique permet d'identifier l'interface -/// réseau qui serait utilisée pour communiquer avec Internet. -/// -/// # Fonctionnement -/// -/// 1. Crée un socket UDP lié à `0.0.0.0:0` (n'importe quelle interface, port aléatoire) -/// 2. Tente une connexion (non effective pour UDP) vers `8.8.8.8:80` -/// 3. Récupère l'adresse IP locale du socket -/// 4. En cas d'échec à n'importe quelle étape, retourne `127.0.0.1` -/// -/// # Returns -/// -/// Retourne l'adresse IP locale sous forme de `String`, ou `"127.0.0.1"` en cas d'erreur. -/// -/// # Examples -/// -/// ``` -/// use pmoutils::guess_local_ip; -/// -/// let ip = guess_local_ip(); -/// println!("IP locale détectée: {}", ip); -/// // Affiche par exemple: "IP locale détectée: 192.168.1.42" -/// ``` -/// -/// # Note -/// -/// Cette méthode ne crée pas de véritable connexion réseau (UDP est sans connexion), -/// elle demande simplement au système d'exploitation quelle interface serait utilisée -/// pour joindre l'adresse cible. -pub fn guess_local_ip() -> String { - match UdpSocket::bind("0.0.0.0:0") { - Ok(socket) => { - if socket.connect("8.8.8.8:80").is_ok() { - if let Ok(local_addr) = socket.local_addr() { - return local_addr.ip().to_string(); - } - } - "127.0.0.1".to_string() - } - Err(_) => "127.0.0.1".to_string(), - } -} - -/// Liste toutes les adresses IP non-loopback des interfaces réseau. -/// -/// Parcourt toutes les interfaces réseau de la machine et collecte leurs adresses IPv4, -/// en excluant les adresses de loopback (127.0.0.1). -/// -/// # Returns -/// -/// Retourne une `HashMap` où : -/// - **Clé** : nom de l'interface réseau (ex: `"eth0"`, `"wlan0"`, `"en0"`) -/// - **Valeur** : vecteur des adresses IP (format String) associées à cette interface -/// -/// En cas d'erreur lors de la récupération des interfaces, retourne une HashMap -/// contenant une entrée `"error"` avec un message d'erreur. -/// -/// # Examples -/// -/// ``` -/// use pmoutils::ip_utils::list_all_ips; -/// -/// let ips = list_all_ips(); -/// for (interface, addresses) in ips { -/// println!("Interface {}: {:?}", interface, addresses); -/// } -/// // Affiche par exemple: -/// // Interface eth0: ["192.168.1.42"] -/// // Interface wlan0: ["10.0.0.15"] -/// ``` -/// -/// # Note -/// -/// - Seules les adresses IPv4 sont retournées -/// - Les adresses de loopback (127.x.x.x) sont filtrées -/// - Les adresses IPv6 sont ignorées -pub fn list_all_ips() -> std::collections::HashMap> { - let mut result = std::collections::HashMap::new(); - - if let Ok(interfaces) = get_if_addrs() { - for iface in interfaces { - let ip = iface.ip(); - if ip.is_loopback() { - continue; - } - if ip.is_ipv4() { - result - .entry(iface.name) - .or_insert_with(Vec::new) - .push(ip.to_string()); - } - } - } else { - result.insert( - "error".to_string(), - vec!["Failed to get interfaces".to_string()], - ); - } - - result -} - -#[cfg(test)] -mod tests { - use super::*; - use std::net::IpAddr; - - #[test] - fn test_guess_local_ip_returns_valid_ip() { - let ip = guess_local_ip(); - - // Vérifie que le résultat est parsable comme une IP - assert!( - ip.parse::().is_ok(), - "Should return a valid IP address" - ); - } - - #[test] - fn test_guess_local_ip_not_empty() { - let ip = guess_local_ip(); - - assert!(!ip.is_empty(), "IP should not be empty"); - } - - #[test] - fn test_guess_local_ip_is_ipv4() { - let ip = guess_local_ip(); - - if let Ok(parsed_ip) = ip.parse::() { - assert!(parsed_ip.is_ipv4(), "Should return an IPv4 address"); - } - } - - #[test] - fn test_guess_local_ip_fallback_is_localhost() { - // Ce test vérifie que si aucune IP n'est trouvée, on retourne 127.0.0.1 - // (difficile à tester sans mocker, mais on vérifie la cohérence) - let ip = guess_local_ip(); - let parsed = ip.parse::().unwrap(); - - // L'IP doit être soit locale (127.0.0.1) soit une IP privée valide - assert!( - parsed.is_loopback() || is_private_ip(&ip), - "IP should be either loopback or private" - ); - } - - #[test] - fn test_list_all_ips_no_loopback() { - let ips = list_all_ips(); - - // Vérifie qu'aucune adresse de loopback n'est présente - for (_, addresses) in ips.iter() { - for addr in addresses { - if let Ok(parsed_ip) = addr.parse::() { - assert!( - !parsed_ip.is_loopback(), - "Loopback addresses should be filtered out" - ); - } - } - } - } - - #[test] - fn test_list_all_ips_only_ipv4() { - let ips = list_all_ips(); - - // Vérifie que seules des adresses IPv4 sont retournées - for (iface_name, addresses) in ips.iter() { - if iface_name == "error" { - continue; // Skip error entries - } - - for addr in addresses { - if let Ok(parsed_ip) = addr.parse::() { - assert!( - parsed_ip.is_ipv4(), - "Only IPv4 addresses should be returned" - ); - } - } - } - } - - #[test] - fn test_list_all_ips_valid_format() { - let ips = list_all_ips(); - - // Vérifie que toutes les IPs sont dans un format valide - for (iface_name, addresses) in ips.iter() { - if iface_name == "error" { - continue; - } - - for addr in addresses { - assert!( - addr.parse::().is_ok(), - "Each IP should be in valid format: {}", - addr - ); - } - } - } - - #[test] - fn test_list_all_ips_interface_names_not_empty() { - let ips = list_all_ips(); - - // Vérifie que les noms d'interface ne sont pas vides - for (iface_name, _) in ips.iter() { - assert!( - !iface_name.is_empty(), - "Interface names should not be empty" - ); - } - } - - #[test] - fn test_list_all_ips_no_duplicate_ips_per_interface() { - let ips = list_all_ips(); - - // Vérifie qu'il n'y a pas de doublons par interface - for (iface_name, addresses) in ips.iter() { - if iface_name == "error" { - continue; - } - - let unique_addresses: std::collections::HashSet<_> = addresses.iter().collect(); - assert_eq!( - addresses.len(), - unique_addresses.len(), - "No duplicate IPs should exist for interface {}", - iface_name - ); - } - } - - // Fonction helper pour les tests - fn is_private_ip(ip_str: &str) -> bool { - if let Ok(ip) = ip_str.parse::() { - match ip { - IpAddr::V4(ipv4) => { - let octets = ipv4.octets(); - // Plages privées: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 - octets[0] == 10 - || (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31) - || (octets[0] == 192 && octets[1] == 168) - } - IpAddr::V6(_) => false, - } - } else { - false - } - } - - #[test] - fn test_helper_is_private_ip() { - // Tests pour la fonction helper - assert!(is_private_ip("10.0.0.1")); - assert!(is_private_ip("172.16.0.1")); - assert!(is_private_ip("192.168.1.1")); - assert!(!is_private_ip("8.8.8.8")); - assert!(!is_private_ip("127.0.0.1")); // loopback n'est pas "privé" au sens réseau local - } -} diff --git a/pmoutils/src/lib.rs b/pmoutils/src/lib.rs deleted file mode 100644 index 0e0a0279..00000000 --- a/pmoutils/src/lib.rs +++ /dev/null @@ -1,76 +0,0 @@ -/// Utilitaires pour la gestion des adresses IP réseau. -/// -/// Ce module fournit des fonctions pour détecter et lister les adresses IP -/// des interfaces réseau locales de la machine. -/// -/// # Fonctions principales -/// -/// - [`guess_local_ip`] : Devine l'adresse IP locale utilisée pour les connexions sortantes -/// -/// # Examples -/// -/// ``` -/// use pmoutils::guess_local_ip; -/// -/// let ip = guess_local_ip(); -/// println!("Adresse IP locale: {}", ip); -/// ``` -pub mod ip_utils; - -pub use ip_utils::guess_local_ip; -pub mod process; -pub use process::{ProcessPortInfo, TransportProtocol, find_process_using_port}; -use xmltree::{Element, EmitterConfig}; - -/// 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) - } -} - -/// Trait générique pour obtenir un élément XML (xmltree::Element). -/// -/// Aligné sur la signature utilisée dans pmoupnp (UpnpObject::to_xml_element), -/// afin de pouvoir factoriser la sérialisation XML entre crates. -pub trait ToXmlElement { - /// Convertit l'objet en élément XML. - fn to_xml_element(&self) -> Element; - - /// Sérialise en chaîne XML formatée. - fn to_xml(&self) -> String { - let elem = self.to_xml_element(); - let config = EmitterConfig::new() - .perform_indent(true) - .indent_string(" "); - let mut buf = Vec::new(); - elem.write_with_config(&mut buf, config) - .expect("Failed to write XML"); - String::from_utf8(buf).expect("Invalid UTF-8") - } -} diff --git a/pmoutils/src/process.rs b/pmoutils/src/process.rs deleted file mode 100644 index 37b46f75..00000000 --- a/pmoutils/src/process.rs +++ /dev/null @@ -1,88 +0,0 @@ -use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo, get_sockets_info}; -use sysinfo::{Pid, System}; - -/// Informations sur un processus utilisant un port réseau. -#[derive(Debug, Clone)] -pub struct ProcessPortInfo { - pub pid: u32, - pub process_name: String, - pub owner: String, - pub port: u16, -} - -/// Protocole de transport utilisé pour la recherche. -#[derive(Debug, Clone, Copy)] -pub enum TransportProtocol { - Tcp, - Udp, -} - -/// Tente de trouver le processus qui écoute sur `port` pour le protocole donné. -/// -/// Retourne `Some(ProcessPortInfo)` si un processus a pu être identifié, sinon `None`. -pub fn find_process_using_port(port: u16, protocol: TransportProtocol) -> Option { - let proto_flag = match protocol { - TransportProtocol::Tcp => ProtocolFlags::TCP, - TransportProtocol::Udp => ProtocolFlags::UDP, - }; - - let sockets = get_sockets_info( - AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6, - proto_flag, - ) - .ok()?; - - // Préparer l'inspection des processus. - let mut system = System::new_all(); - system.refresh_all(); - - for socket in sockets { - match socket.protocol_socket_info { - ProtocolSocketInfo::Tcp(ref tcp_info) - if matches!(protocol, TransportProtocol::Tcp) && tcp_info.local_port == port => - { - if let Some(info) = - build_process_info(&mut system, port, socket.associated_pids.first()) - { - return Some(info); - } - } - ProtocolSocketInfo::Udp(ref udp_info) - if matches!(protocol, TransportProtocol::Udp) && udp_info.local_port == port => - { - if let Some(info) = - build_process_info(&mut system, port, socket.associated_pids.first()) - { - return Some(info); - } - } - _ => continue, - } - } - - None -} - -fn build_process_info( - system: &mut System, - port: u16, - pid_opt: Option<&u32>, -) -> Option { - let pid = *pid_opt?; - let process = system.process(Pid::from_u32(pid))?; - let process_name = process.name().to_string(); - - let owner = process - .user_id() - .and_then(|uid| { - users::get_user_by_uid(**uid).map(|user| user.name().to_string_lossy().into_owned()) - }) - .unwrap_or_else(|| "unknown".to_string()); - - Some(ProcessPortInfo { - pid, - process_name, - owner, - port, - }) -} diff --git a/pmowebrenderer/Cargo.toml b/pmowebrenderer/Cargo.toml index 6809822b..71df801d 100644 --- a/pmowebrenderer/Cargo.toml +++ b/pmowebrenderer/Cargo.toml @@ -40,7 +40,7 @@ thiserror = { workspace = true } tracing = { workspace = true } pmodidl = { path = "../pmodidl" } -pmoutils = { path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } [features] default = [] diff --git a/pmowebrenderer/src/handlers.rs b/pmowebrenderer/src/handlers.rs index 48fd08d2..c29d8152 100644 --- a/pmowebrenderer/src/handlers.rs +++ b/pmowebrenderer/src/handlers.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use pmodidl::DIDLLite; use pmoupnp::actions::{ActionData, ActionError, ActionHandler, get_value}; use pmoupnp::{get, set}; -use pmoutils::ToXmlElement; +use pmodidl::ToXmlElement; use crate::messages::PlaybackState; use crate::pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time, upnp_time_to_seconds};