1use std::collections::HashMap;
2
3use std::path::{Path, PathBuf};
4use serde::{Deserialize, Serialize};
5use crate::platform::{PlatformInfo, PlatformError, OsType};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct DiagnosticInfo {
10 pub platform: PlatformDiagnostics,
12
13 pub network: NetworkDiagnostics,
15
16 pub database: DatabaseDiagnostics,
18
19 pub filesystem: FilesystemDiagnostics,
21
22 pub configuration: ConfigurationDiagnostics,
24
25 pub system: SystemDiagnostics,
27
28 pub timestamp: chrono::DateTime<chrono::Utc>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct PlatformDiagnostics {
34 pub os_type: String,
35 pub os_version: String,
36 pub architecture: String,
37 pub hostname: String,
38 pub capabilities: PlatformCapabilitiesDiag,
39 pub platform_specific: HashMap<String, String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PlatformCapabilitiesDiag {
44 pub case_sensitive_fs: bool,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct NetworkDiagnostics {
49 pub interfaces: Vec<NetworkInterfaceDiag>,
50 pub primary_interface: Option<String>,
51 pub multicast_support: bool,
52 pub port_availability: HashMap<u16, bool>,
53 pub connectivity_tests: HashMap<String, bool>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct NetworkInterfaceDiag {
58 pub name: String,
59 pub ip_address: String,
60 pub is_loopback: bool,
61 pub is_up: bool,
62 pub supports_multicast: bool,
63 pub interface_type: String,
64 pub mtu: Option<u32>,
65 pub speed: Option<u64>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub enum FirewallStatus {
70 Enabled,
71 Disabled,
72 Unknown,
73 Configured { rules: Vec<String> },
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DatabaseDiagnostics {
78 pub database_path: Option<PathBuf>,
79 pub database_exists: bool,
80 pub database_size: Option<u64>,
81 pub database_accessible: bool,
82 pub schema_version: Option<u32>,
83 pub media_file_count: Option<u64>,
84 pub last_scan_time: Option<chrono::DateTime<chrono::Utc>>,
85 pub integrity_status: DatabaseIntegrityStatus,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub enum DatabaseIntegrityStatus {
90 Healthy,
91 Corrupted { details: String },
92 Unknown,
93 NotChecked,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct FilesystemDiagnostics {
98 pub monitored_directories: Vec<DirectoryDiag>,
99 pub config_directory: DirectoryDiag,
100 pub cache_directory: DirectoryDiag,
101 pub log_directory: DirectoryDiag,
102 pub temp_directory: DirectoryDiag,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct DirectoryDiag {
107 pub path: PathBuf,
108 pub exists: bool,
109 pub accessible: bool,
110 pub readable: bool,
111 pub writable: bool,
112 pub file_count: Option<u64>,
113 pub total_size: Option<u64>,
114 pub free_space: Option<u64>,
115 pub permissions: Option<String>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ConfigurationDiagnostics {
120 pub config_file_path: Option<PathBuf>,
121 pub config_file_exists: bool,
122 pub config_file_valid: bool,
123 pub config_errors: Vec<String>,
124 pub hot_reload_enabled: bool,
125 pub default_values_used: Vec<String>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct SystemDiagnostics {
130 pub uptime: Option<u64>,
131 pub memory_total: Option<u64>,
132 pub memory_available: Option<u64>,
133 pub cpu_count: Option<u32>,
134 pub load_average: Option<f64>,
135 pub disk_usage: HashMap<String, DiskUsage>,
136 pub process_info: ProcessInfo,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct DiskUsage {
141 pub total: u64,
142 pub used: u64,
143 pub available: u64,
144 pub percentage: f64,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ProcessInfo {
149 pub pid: u32,
150 pub memory_usage: Option<u64>,
151 pub cpu_usage: Option<f64>,
152 pub thread_count: Option<u32>,
153 pub file_descriptors: Option<u32>,
154}
155
156impl DiagnosticInfo {
157 pub async fn collect() -> Result<Self, PlatformError> {
159 tracing::info!("Collecting diagnostic information...");
160
161 let platform_info = PlatformInfo::detect().await?;
162
163 Ok(DiagnosticInfo {
164 platform: Self::collect_platform_diagnostics(&platform_info).await?,
165 network: Self::collect_network_diagnostics(&platform_info).await?,
166 database: Self::collect_database_diagnostics().await?,
167 filesystem: Self::collect_filesystem_diagnostics().await?,
168 configuration: Self::collect_configuration_diagnostics().await?,
169 system: Self::collect_system_diagnostics().await?,
170 timestamp: chrono::Utc::now(),
171 })
172 }
173
174 async fn collect_platform_diagnostics(platform_info: &PlatformInfo) -> Result<PlatformDiagnostics, PlatformError> {
176 let hostname = hostname::get()
177 .map(|h| h.to_string_lossy().to_string())
178 .unwrap_or_else(|_| "unknown".to_string());
179
180 let capabilities = PlatformCapabilitiesDiag {
181 case_sensitive_fs: platform_info.capabilities.case_sensitive_fs,
182 };
183
184 Ok(PlatformDiagnostics {
185 os_type: platform_info.os_type.display_name().to_string(),
186 os_version: platform_info.version.clone(),
187 architecture: std::env::consts::ARCH.to_string(),
188 hostname,
189 capabilities,
190 platform_specific: platform_info.metadata.clone(),
191 })
192 }
193
194 async fn collect_network_diagnostics(platform_info: &PlatformInfo) -> Result<NetworkDiagnostics, PlatformError> {
196 let interfaces: Vec<NetworkInterfaceDiag> = platform_info.network_interfaces
197 .iter()
198 .map(|iface| NetworkInterfaceDiag {
199 name: iface.name.clone(),
200 ip_address: iface.ip_address.to_string(),
201 is_loopback: iface.is_loopback,
202 is_up: iface.is_up,
203 supports_multicast: iface.supports_multicast,
204 interface_type: format!("{:?}", iface.interface_type),
205 mtu: None, speed: None, })
208 .collect();
209
210 let primary_interface = platform_info.get_primary_interface()
211 .map(|iface| iface.name.clone());
212
213 let multicast_support = true; let mut port_availability = HashMap::new();
217 for port in &[1900, 8080, 8081, 8082] {
218 port_availability.insert(*port, Self::test_port_availability(*port).await);
219 }
220
221 let mut connectivity_tests = HashMap::new();
223 connectivity_tests.insert("localhost".to_string(), Self::test_connectivity("127.0.0.1", 80).await);
224 connectivity_tests.insert("internet".to_string(), Self::test_connectivity("8.8.8.8", 53).await);
225
226
227 Ok(NetworkDiagnostics {
228 interfaces,
229 primary_interface,
230 multicast_support,
231 port_availability,
232 connectivity_tests,
233 })
234 }
235
236 async fn test_port_availability(port: u16) -> bool {
238 use tokio::net::TcpListener;
239
240 (TcpListener::bind(format!("127.0.0.1:{}", port)).await).is_ok()
241 }
242
243 async fn test_connectivity(host: &str, port: u16) -> bool {
245 use tokio::net::TcpStream;
246 use tokio::time::{timeout, Duration};
247
248 matches!(timeout(Duration::from_secs(5), TcpStream::connect(format!("{}:{}", host, port))).await, Ok(Ok(_)))
249 }
250
251
252async fn collect_database_diagnostics() -> Result<DatabaseDiagnostics, PlatformError> {
253 Ok(DatabaseDiagnostics {
255 database_path: None,
256 database_exists: true,
257 database_size: None,
258 database_accessible: false,
259 schema_version: None,
260 media_file_count: None,
261 last_scan_time: None,
262 integrity_status: DatabaseIntegrityStatus::NotChecked,
263 })
264 }
265
266 async fn collect_filesystem_diagnostics() -> Result<FilesystemDiagnostics, PlatformError> {
268 Ok(FilesystemDiagnostics {
270 monitored_directories: vec![],
271 config_directory: Self::diagnose_directory(&PathBuf::from(".")).await,
272 cache_directory: Self::diagnose_directory(&PathBuf::from(".")).await,
273 log_directory: Self::diagnose_directory(&PathBuf::from(".")).await,
274 temp_directory: Self::diagnose_directory(&std::env::temp_dir()).await,
275 })
276 }
277
278 async fn diagnose_directory(path: &PathBuf) -> DirectoryDiag {
280 let exists = path.exists();
281 let accessible = path.is_dir();
282 let readable = path.metadata().map(|m| !m.permissions().readonly()).unwrap_or(false);
283 let writable = std::fs::OpenOptions::new()
284 .write(true)
285 .create(true)
286 .truncate(false)
287 .open(path.join(".test_write"))
288 .and_then(|_| std::fs::remove_file(path.join(".test_write")))
289 .is_ok();
290
291 let (file_count, total_size) = if accessible {
292 Self::count_directory_contents(path).await
293 } else {
294 (None, None)
295 };
296
297 let free_space = Self::get_free_space(path).await;
298
299 DirectoryDiag {
300 path: path.clone(),
301 exists,
302 accessible,
303 readable,
304 writable,
305 file_count,
306 total_size,
307 free_space,
308 permissions: None, }
310 }
311
312 async fn count_directory_contents(path: &PathBuf) -> (Option<u64>, Option<u64>) {
314 match std::fs::read_dir(path) {
315 Ok(entries) => {
316 let mut count = 0;
317 let mut total_size = 0;
318
319 for entry in entries.flatten() {
320 count += 1;
321 if let Ok(metadata) = entry.metadata() {
322 total_size += metadata.len();
323 }
324 }
325
326 (Some(count), Some(total_size))
327 }
328 Err(_) => (None, None),
329 }
330 }
331
332 async fn get_free_space(_path: &Path) -> Option<u64> {
334 None
336 }
337
338 async fn collect_configuration_diagnostics() -> Result<ConfigurationDiagnostics, PlatformError> {
340 Ok(ConfigurationDiagnostics {
342 config_file_path: None,
343 config_file_exists: false,
344 config_file_valid: false,
345 config_errors: vec![],
346 hot_reload_enabled: false,
347 default_values_used: vec![],
348 })
349 }
350
351 async fn collect_system_diagnostics() -> Result<SystemDiagnostics, PlatformError> {
353 let process_info = ProcessInfo {
354 pid: std::process::id(),
355 memory_usage: None, cpu_usage: None, thread_count: None, file_descriptors: None, };
360
361 Ok(SystemDiagnostics {
362 uptime: None, memory_total: None, memory_available: None, cpu_count: std::thread::available_parallelism()
366 .map(|n| n.get() as u32)
367 .ok(),
368 load_average: None, disk_usage: HashMap::new(), process_info,
371 })
372 }
373
374 pub fn log_startup_diagnostics(&self) {
376 tracing::info!("=== VuIO Startup Diagnostics ===");
377 tracing::info!("Platform: {} {} ({})",
378 self.platform.os_type,
379 self.platform.os_version,
380 self.platform.architecture
381 );
382 tracing::info!("Hostname: {}", self.platform.hostname);
383
384 tracing::info!("Platform Capabilities:");
386 tracing::info!(" - Case-sensitive FS: {}", self.platform.capabilities.case_sensitive_fs);
387
388 tracing::info!("Network Configuration:");
390 tracing::info!(" - Interfaces found: {}", self.network.interfaces.len());
391 if let Some(primary) = &self.network.primary_interface {
392 tracing::info!(" - Primary interface: {}", primary);
393 }
394 tracing::info!(" - Multicast support: {}", self.network.multicast_support);
395
396 tracing::info!("Port Availability:");
398 for (port, available) in &self.network.port_availability {
399 let status = if *available { "Available" } else { "In use" };
400 tracing::info!(" - Port {}: {}", port, status);
401 }
402
403 tracing::info!("Connectivity Tests:");
405 for (test, result) in &self.network.connectivity_tests {
406 let status = if *result { "Success" } else { "Failed" };
407 tracing::info!(" - {}: {}", test, status);
408 }
409
410 tracing::info!("Database Status:");
412 tracing::info!(" - Database exists: {}", self.database.database_exists);
413 tracing::info!(" - Database accessible: {}", self.database.database_accessible);
414 tracing::info!(" - Integrity status: {:?}", self.database.integrity_status);
415 if let Some(count) = self.database.media_file_count {
416 tracing::info!(" - Media files: {}", count);
417 }
418
419 tracing::info!("Filesystem Status:");
421 tracing::info!(" - Config directory: {} (accessible: {})",
422 self.filesystem.config_directory.path.display(),
423 self.filesystem.config_directory.accessible
424 );
425 tracing::info!(" - Monitored directories: {}", self.filesystem.monitored_directories.len());
426
427 tracing::info!("Configuration Status:");
429 tracing::info!(" - Config file exists: {}", self.configuration.config_file_exists);
430 tracing::info!(" - Config file valid: {}", self.configuration.config_file_valid);
431 tracing::info!(" - Hot reload enabled: {}", self.configuration.hot_reload_enabled);
432 if !self.configuration.config_errors.is_empty() {
433 tracing::warn!(" - Configuration errors: {:?}", self.configuration.config_errors);
434 }
435
436 tracing::info!("System Information:");
438 tracing::info!(" - Process ID: {}", self.system.process_info.pid);
439 if let Some(cpu_count) = self.system.cpu_count {
440 tracing::info!(" - CPU cores: {}", cpu_count);
441 }
442
443 tracing::info!("=== End Diagnostics ===");
444 }
445
446 pub fn log_debug_diagnostics(&self) {
448 tracing::debug!("=== Detailed Diagnostic Information ===");
449
450 tracing::debug!("Network Interfaces:");
452 for iface in &self.network.interfaces {
453 tracing::debug!(" - {} ({}): {} [{}] up={} multicast={}",
454 iface.name,
455 iface.interface_type,
456 iface.ip_address,
457 if iface.is_loopback { "loopback" } else { "physical" },
458 iface.is_up,
459 iface.supports_multicast
460 );
461 }
462
463 tracing::debug!("Platform Metadata:");
465 for (key, value) in &self.platform.platform_specific {
466 tracing::debug!(" - {}: {}", key, value);
467 }
468
469 tracing::debug!("Directory Details:");
471 for dir in &self.filesystem.monitored_directories {
472 tracing::debug!(" - {}: exists={} readable={} writable={} files={:?}",
473 dir.path.display(),
474 dir.exists,
475 dir.readable,
476 dir.writable,
477 dir.file_count
478 );
479 }
480
481 tracing::debug!("=== End Detailed Diagnostics ===");
482 }
483
484 pub fn to_json(&self) -> Result<String, serde_json::Error> {
486 serde_json::to_string_pretty(self)
487 }
488
489 pub async fn save_to_file(&self, path: &PathBuf) -> Result<(), std::io::Error> {
491 let json = self.to_json().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
492 tokio::fs::write(path, json).await
493 }
494}
495
496pub struct StartupDiagnostics;
498
499impl StartupDiagnostics {
500 pub async fn perform_startup_checks() -> Result<(), PlatformError> {
502 tracing::info!("Performing startup diagnostic checks...");
503
504 Self::check_platform_compatibility().await?;
506
507 Self::check_network_requirements().await?;
509
510 Self::check_filesystem_requirements().await?;
512
513 Self::check_system_resources().await?;
515
516 tracing::info!("All startup diagnostic checks passed");
517 Ok(())
518 }
519
520 async fn check_platform_compatibility() -> Result<(), PlatformError> {
521 let platform_info = PlatformInfo::detect().await?;
522
523 match platform_info.os_type {
525 OsType::Windows | OsType::MacOS | OsType::Linux | OsType::Bsd => {
526 tracing::info!("Platform {} is supported", platform_info.os_type.display_name());
527 }
528 }
529
530
531 Ok(())
532 }
533
534 async fn check_network_requirements() -> Result<(), PlatformError> {
535 let platform_info = PlatformInfo::detect().await?;
536
537 let usable_interfaces: Vec<_> = platform_info.network_interfaces
539 .iter()
540 .filter(|iface| !iface.is_loopback && iface.is_up)
541 .collect();
542
543 if usable_interfaces.is_empty() {
544 tracing::error!("No usable network interfaces found");
545 return Err(PlatformError::NetworkConfig(
546 "No active network interfaces available for DLNA service".to_string()
547 ));
548 }
549
550 tracing::info!("Found {} usable network interface(s)", usable_interfaces.len());
551
552 if !DiagnosticInfo::test_port_availability(1900).await {
554 tracing::warn!("Port 1900 is not available - will use alternative port");
555 }
556
557 Ok(())
558 }
559
560 async fn check_filesystem_requirements() -> Result<(), PlatformError> {
561 let temp_dir = std::env::temp_dir();
563 let test_dir = temp_dir.join("vuio_startup_test");
564
565 match std::fs::create_dir_all(&test_dir) {
566 Ok(_) => {
567 tracing::debug!("Filesystem write test passed");
568 let _ = std::fs::remove_dir_all(&test_dir);
569 }
570 Err(e) => {
571 tracing::error!("Filesystem write test failed: {}", e);
572 return Err(PlatformError::FileSystemAccess(
573 format!("Cannot create directories: {}", e)
574 ));
575 }
576 }
577
578 Ok(())
579 }
580
581 async fn check_system_resources() -> Result<(), PlatformError> {
582 tracing::debug!("System resource checks passed");
586 Ok(())
587 }
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593
594 #[tokio::test]
595 async fn test_diagnostic_collection() {
596 let diagnostics = DiagnosticInfo::collect().await;
597 assert!(diagnostics.is_ok());
598
599 let diag = diagnostics.unwrap();
600 assert!(!diag.platform.os_type.is_empty());
601 assert!(!diag.platform.hostname.is_empty());
602 }
603
604 #[tokio::test]
605 async fn test_port_availability() {
606 let available = DiagnosticInfo::test_port_availability(0).await; assert!(available);
609 }
610
611 #[tokio::test]
612 async fn test_startup_diagnostics() {
613 let result = StartupDiagnostics::perform_startup_checks().await;
614 match result {
616 Ok(_) => tracing::info!("Startup diagnostics passed"),
617 Err(e) => tracing::warn!("Startup diagnostics failed: {}", e),
618 }
619 }
620
621 #[test]
622 fn test_diagnostic_serialization() {
623 let diag = DiagnosticInfo {
624 platform: PlatformDiagnostics {
625 os_type: "Linux".to_string(),
626 os_version: "5.4.0".to_string(),
627 architecture: "x86_64".to_string(),
628 hostname: "test-host".to_string(),
629 capabilities: PlatformCapabilitiesDiag {
630 case_sensitive_fs: true,
631 },
632 platform_specific: HashMap::new(),
633 },
634 network: NetworkDiagnostics {
635 interfaces: vec![],
636 primary_interface: None,
637 multicast_support: true,
638 port_availability: HashMap::new(),
639 connectivity_tests: HashMap::new(),
640 },
641 database: DatabaseDiagnostics {
642 database_path: None,
643 database_exists: false,
644 database_size: None,
645 database_accessible: false,
646 schema_version: None,
647 media_file_count: None,
648 last_scan_time: None,
649 integrity_status: DatabaseIntegrityStatus::NotChecked,
650 },
651 filesystem: FilesystemDiagnostics {
652 monitored_directories: vec![],
653 config_directory: DirectoryDiag {
654 path: PathBuf::from("/tmp"),
655 exists: true,
656 accessible: true,
657 readable: true,
658 writable: true,
659 file_count: None,
660 total_size: None,
661 free_space: None,
662 permissions: None,
663 },
664 cache_directory: DirectoryDiag {
665 path: PathBuf::from("/tmp"),
666 exists: true,
667 accessible: true,
668 readable: true,
669 writable: true,
670 file_count: None,
671 total_size: None,
672 free_space: None,
673 permissions: None,
674 },
675 log_directory: DirectoryDiag {
676 path: PathBuf::from("/tmp"),
677 exists: true,
678 accessible: true,
679 readable: true,
680 writable: true,
681 file_count: None,
682 total_size: None,
683 free_space: None,
684 permissions: None,
685 },
686 temp_directory: DirectoryDiag {
687 path: PathBuf::from("/tmp"),
688 exists: true,
689 accessible: true,
690 readable: true,
691 writable: true,
692 file_count: None,
693 total_size: None,
694 free_space: None,
695 permissions: None,
696 },
697 },
698 configuration: ConfigurationDiagnostics {
699 config_file_path: None,
700 config_file_exists: false,
701 config_file_valid: false,
702 config_errors: vec![],
703 hot_reload_enabled: false,
704 default_values_used: vec![],
705 },
706 system: SystemDiagnostics {
707 uptime: None,
708 memory_total: None,
709 memory_available: None,
710 cpu_count: Some(4),
711 load_average: None,
712 disk_usage: HashMap::new(),
713 process_info: ProcessInfo {
714 pid: 1234,
715 memory_usage: None,
716 cpu_usage: None,
717 thread_count: None,
718 file_descriptors: None,
719 },
720 },
721 timestamp: chrono::Utc::now(),
722 };
723
724 let json = diag.to_json();
725 assert!(json.is_ok());
726 assert!(json.unwrap().contains("Linux"));
727 }
728}