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.
This commit is contained in:
2026-03-01 21:43:00 +01:00
parent f9cd3063fb
commit 5b8642a70b
18 changed files with 31 additions and 480 deletions

7
Cargo.lock generated
View File

@@ -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]]

View File

@@ -7,7 +7,6 @@ members = [
"pmowebrenderer",
"pmomediaserver",
"pmoconfig",
"pmoutils",
"pmodidl",
"pmoserver",
"pmoapp",

View File

@@ -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 }

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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 =============

View File

@@ -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 }

View File

@@ -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;

View File

@@ -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"] }

View File

@@ -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 {}: {}",

View File

@@ -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"

View File

@@ -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"

View File

@@ -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<String, Vec<String>> {
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::<IpAddr>().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::<IpAddr>() {
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::<IpAddr>().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::<IpAddr>() {
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::<IpAddr>() {
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::<IpAddr>().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::<IpAddr>() {
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
}
}

View File

@@ -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")
}
}

View File

@@ -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<ProcessPortInfo> {
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<ProcessPortInfo> {
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,
})
}

View File

@@ -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 = []

View File

@@ -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};