Skip to main content

torsh_cli/commands/
info.rs

1//! System information and diagnostics commands
2
3use anyhow::Result;
4use clap::Args;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use crate::config::Config;
9use crate::utils::{display_banner, output, system};
10
11#[derive(Debug, Args)]
12pub struct InfoCommand {
13    /// Show detailed system information
14    #[arg(long)]
15    pub detailed: bool,
16
17    /// Show ToRSh installation information
18    #[arg(long)]
19    pub installation: bool,
20
21    /// Show available devices and capabilities
22    #[arg(long)]
23    pub devices: bool,
24
25    /// Show feature availability
26    #[arg(long)]
27    pub features: bool,
28
29    /// Run system diagnostics
30    #[arg(long)]
31    pub diagnostics: bool,
32
33    /// Show configuration information
34    #[arg(long)]
35    pub show_config: bool,
36}
37
38#[derive(Debug, Serialize, Deserialize)]
39pub struct SystemInformation {
40    pub torsh: TorshInfo,
41    pub system: system::SystemInfo,
42    pub devices: HashMap<String, serde_json::Value>,
43    pub features: FeatureInfo,
44    pub installation: InstallationInfo,
45}
46
47#[derive(Debug, Serialize, Deserialize)]
48pub struct TorshInfo {
49    pub version: String,
50    pub build_type: String,
51    pub build_date: String,
52    pub git_commit: String,
53    pub rust_version: String,
54    pub target_triple: String,
55}
56
57#[derive(Debug, Serialize, Deserialize)]
58pub struct FeatureInfo {
59    pub enabled_features: Vec<String>,
60    pub disabled_features: Vec<String>,
61    pub experimental_features: Vec<String>,
62}
63
64#[derive(Debug, Serialize, Deserialize)]
65pub struct InstallationInfo {
66    pub install_path: String,
67    pub config_path: String,
68    pub cache_path: String,
69    pub models_path: String,
70    pub size_on_disk: String,
71}
72
73#[derive(Debug, Serialize, Deserialize)]
74pub struct DiagnosticResult {
75    pub name: String,
76    pub status: DiagnosticStatus,
77    pub message: String,
78    pub details: Option<serde_json::Value>,
79}
80
81#[derive(Debug, Serialize, Deserialize)]
82pub enum DiagnosticStatus {
83    Pass,
84    Warning,
85    Fail,
86    Info,
87}
88
89pub async fn execute(args: InfoCommand, config: &Config, output_format: &str) -> Result<()> {
90    display_banner();
91
92    if !args.detailed
93        && !args.installation
94        && !args.devices
95        && !args.features
96        && !args.diagnostics
97        && !args.show_config
98    {
99        // Show basic system information by default
100        show_basic_info(output_format).await?;
101    } else {
102        if args.detailed || args.installation {
103            show_detailed_info(output_format).await?;
104        }
105
106        if args.devices {
107            show_device_info(output_format).await?;
108        }
109
110        if args.features {
111            show_feature_info(output_format).await?;
112        }
113
114        if args.show_config {
115            show_config_info(config, output_format).await?;
116        }
117
118        if args.diagnostics {
119            run_diagnostics(config, output_format).await?;
120        }
121    }
122
123    Ok(())
124}
125
126async fn show_basic_info(output_format: &str) -> Result<()> {
127    let torsh_info = get_torsh_info();
128    let system_info = system::get_system_info();
129
130    let basic_info = serde_json::json!({
131        "torsh_version": torsh_info.version,
132        "os": system_info.os,
133        "total_memory": system_info.total_memory,
134        "cpu_count": system_info.cpu_count,
135        "available_devices": get_available_devices_summary(),
136    });
137
138    output::print_table("ToRSh System Information", &basic_info, output_format)?;
139    Ok(())
140}
141
142async fn show_detailed_info(output_format: &str) -> Result<()> {
143    let system_info = SystemInformation {
144        torsh: get_torsh_info(),
145        system: system::get_system_info(),
146        devices: system::get_device_info(),
147        features: get_feature_info(),
148        installation: get_installation_info().await?,
149    };
150
151    output::print_table("Detailed System Information", &system_info, output_format)?;
152    Ok(())
153}
154
155async fn show_device_info(output_format: &str) -> Result<()> {
156    let device_info = system::get_device_info();
157    output::print_table("Available Devices", &device_info, output_format)?;
158
159    // Show device capabilities
160    for (device_name, info) in &device_info {
161        if let Some(available) = info.get("available").and_then(|v| v.as_bool()) {
162            if available {
163                output::print_success(&format!(
164                    "✓ {} device is available",
165                    device_name.to_uppercase()
166                ));
167            } else {
168                output::print_warning(&format!(
169                    "⚠ {} device is not available",
170                    device_name.to_uppercase()
171                ));
172            }
173        }
174    }
175
176    Ok(())
177}
178
179async fn show_feature_info(output_format: &str) -> Result<()> {
180    let feature_info = get_feature_info();
181    output::print_table("Feature Information", &feature_info, output_format)?;
182
183    output::print_info(&format!(
184        "Enabled features: {}",
185        feature_info.enabled_features.len()
186    ));
187    output::print_info(&format!(
188        "Disabled features: {}",
189        feature_info.disabled_features.len()
190    ));
191    if !feature_info.experimental_features.is_empty() {
192        output::print_warning(&format!(
193            "Experimental features: {}",
194            feature_info.experimental_features.len()
195        ));
196    }
197
198    Ok(())
199}
200
201async fn show_config_info(config: &Config, output_format: &str) -> Result<()> {
202    let config_summary = serde_json::json!({
203        "output_dir": config.general.output_dir,
204        "cache_dir": config.general.cache_dir,
205        "default_device": config.general.default_device,
206        "num_workers": config.general.num_workers,
207        "default_dtype": config.general.default_dtype,
208        "hub_endpoint": config.hub.api_endpoint,
209        "mixed_precision": config.training.mixed_precision,
210    });
211
212    output::print_table("Configuration", &config_summary, output_format)?;
213    Ok(())
214}
215
216async fn run_diagnostics(config: &Config, output_format: &str) -> Result<()> {
217    output::print_info("Running system diagnostics...");
218
219    let mut diagnostics = Vec::new();
220
221    // Check ToRSh installation
222    diagnostics.push(check_torsh_installation().await);
223
224    // Check dependencies
225    diagnostics.push(check_dependencies().await);
226
227    // Check device availability
228    diagnostics.extend(check_device_availability().await);
229
230    // Check configuration
231    diagnostics.push(check_configuration(config).await);
232
233    // Check permissions
234    diagnostics.push(check_permissions(config).await);
235
236    // Check disk space
237    diagnostics.push(check_disk_space(config).await);
238
239    output::print_table("Diagnostic Results", &diagnostics, output_format)?;
240
241    // Summary
242    let pass_count = diagnostics
243        .iter()
244        .filter(|d| matches!(d.status, DiagnosticStatus::Pass))
245        .count();
246    let warning_count = diagnostics
247        .iter()
248        .filter(|d| matches!(d.status, DiagnosticStatus::Warning))
249        .count();
250    let fail_count = diagnostics
251        .iter()
252        .filter(|d| matches!(d.status, DiagnosticStatus::Fail))
253        .count();
254
255    println!();
256    output::print_info(&format!(
257        "Diagnostic Summary: {} passed, {} warnings, {} failed",
258        pass_count, warning_count, fail_count
259    ));
260
261    if fail_count > 0 {
262        output::print_error("Some diagnostics failed. Please check the results above.");
263    } else if warning_count > 0 {
264        output::print_warning("Some diagnostics have warnings. Please review the results above.");
265    } else {
266        output::print_success("All diagnostics passed!");
267    }
268
269    Ok(())
270}
271
272fn get_torsh_info() -> TorshInfo {
273    TorshInfo {
274        version: env!("CARGO_PKG_VERSION").to_string(),
275        build_type: if cfg!(debug_assertions) {
276            "debug"
277        } else {
278            "release"
279        }
280        .to_string(),
281        build_date: "2024-01-15".to_string(), // This would be set during build
282        git_commit: "abc123def".to_string(),  // This would be set during build
283        rust_version: std::env::var("RUST_VERSION").unwrap_or_else(|_| "unknown".to_string()),
284        target_triple: format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS),
285    }
286}
287
288fn get_feature_info() -> FeatureInfo {
289    #[allow(unused_mut)]
290    let mut enabled_features = Vec::new();
291    #[allow(unused_mut)]
292    let mut disabled_features = Vec::new();
293    #[allow(unused_mut)]
294    let mut experimental_features = Vec::new();
295
296    // Check compiled features
297    #[cfg(feature = "nn")]
298    enabled_features.push("nn".to_string());
299    #[cfg(not(feature = "nn"))]
300    disabled_features.push("nn".to_string());
301
302    #[cfg(feature = "optim")]
303    enabled_features.push("optim".to_string());
304    #[cfg(not(feature = "optim"))]
305    disabled_features.push("optim".to_string());
306
307    #[cfg(feature = "data")]
308    enabled_features.push("data".to_string());
309    #[cfg(not(feature = "data"))]
310    disabled_features.push("data".to_string());
311
312    #[cfg(feature = "vision")]
313    enabled_features.push("vision".to_string());
314    #[cfg(not(feature = "vision"))]
315    disabled_features.push("vision".to_string());
316
317    #[cfg(feature = "text")]
318    enabled_features.push("text".to_string());
319    #[cfg(not(feature = "text"))]
320    disabled_features.push("text".to_string());
321
322    #[cfg(feature = "quantization")]
323    enabled_features.push("quantization".to_string());
324    #[cfg(not(feature = "quantization"))]
325    disabled_features.push("quantization".to_string());
326
327    #[cfg(feature = "jit")]
328    experimental_features.push("jit".to_string());
329
330    #[cfg(feature = "hub")]
331    enabled_features.push("hub".to_string());
332    #[cfg(not(feature = "hub"))]
333    disabled_features.push("hub".to_string());
334
335    FeatureInfo {
336        enabled_features,
337        disabled_features,
338        experimental_features,
339    }
340}
341
342async fn get_installation_info() -> Result<InstallationInfo> {
343    let current_exe = std::env::current_exe().unwrap_or_else(|_| "unknown".into());
344    let install_path = current_exe
345        .parent()
346        .unwrap_or_else(|| std::path::Path::new("unknown"));
347
348    let home_dir = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
349    let config_dir = dirs::config_dir().unwrap_or_else(|| home_dir.join(".config"));
350    let cache_dir = dirs::cache_dir().unwrap_or_else(|| home_dir.join(".cache"));
351
352    let torsh_config = config_dir.join("torsh");
353    let torsh_cache = cache_dir.join("torsh");
354    let torsh_models = torsh_cache.join("models");
355
356    // Calculate size on disk (simplified)
357    let mut total_size = 0u64;
358    if let Ok(metadata) = tokio::fs::metadata(&current_exe).await {
359        total_size += metadata.len();
360    }
361
362    Ok(InstallationInfo {
363        install_path: install_path.display().to_string(),
364        config_path: torsh_config.display().to_string(),
365        cache_path: torsh_cache.display().to_string(),
366        models_path: torsh_models.display().to_string(),
367        size_on_disk: crate::utils::fs::format_file_size(total_size),
368    })
369}
370
371fn get_available_devices_summary() -> HashMap<String, bool> {
372    let device_info = system::get_device_info();
373    let mut summary = HashMap::new();
374
375    for (device_name, info) in device_info {
376        if let Some(available) = info.get("available").and_then(|v| v.as_bool()) {
377            summary.insert(device_name, available);
378        }
379    }
380
381    summary
382}
383
384async fn check_torsh_installation() -> DiagnosticResult {
385    let current_exe = std::env::current_exe();
386
387    match current_exe {
388        Ok(exe_path) => {
389            if exe_path.exists() {
390                DiagnosticResult {
391                    name: "ToRSh Installation".to_string(),
392                    status: DiagnosticStatus::Pass,
393                    message: "ToRSh CLI is properly installed".to_string(),
394                    details: Some(serde_json::json!({
395                        "executable_path": exe_path.display().to_string()
396                    })),
397                }
398            } else {
399                DiagnosticResult {
400                    name: "ToRSh Installation".to_string(),
401                    status: DiagnosticStatus::Fail,
402                    message: "ToRSh executable not found".to_string(),
403                    details: None,
404                }
405            }
406        }
407        Err(e) => DiagnosticResult {
408            name: "ToRSh Installation".to_string(),
409            status: DiagnosticStatus::Fail,
410            message: format!("Cannot determine executable path: {}", e),
411            details: None,
412        },
413    }
414}
415
416async fn check_dependencies() -> DiagnosticResult {
417    let mut dependency_status = HashMap::new();
418    let mut issues = Vec::new();
419
420    // Check for Python (needed for some model conversions)
421    if let Ok(output) = std::process::Command::new("python3")
422        .arg("--version")
423        .output()
424    {
425        if output.status.success() {
426            let version = String::from_utf8_lossy(&output.stdout);
427            dependency_status.insert("python3", version.trim().to_string());
428        } else {
429            issues.push("Python3 not found");
430            dependency_status.insert("python3", "Not Available".to_string());
431        }
432    } else {
433        issues.push("Python3 not found");
434        dependency_status.insert("python3", "Not Available".to_string());
435    }
436
437    // Check for Git (needed for model hub operations)
438    if let Ok(output) = std::process::Command::new("git").arg("--version").output() {
439        if output.status.success() {
440            let version = String::from_utf8_lossy(&output.stdout);
441            dependency_status.insert("git", version.trim().to_string());
442        } else {
443            issues.push("Git not found");
444            dependency_status.insert("git", "Not Available".to_string());
445        }
446    } else {
447        issues.push("Git not found");
448        dependency_status.insert("git", "Not Available".to_string());
449    }
450
451    // Check for curl (needed for downloads)
452    if let Ok(output) = std::process::Command::new("curl").arg("--version").output() {
453        if output.status.success() {
454            dependency_status.insert("curl", "Available".to_string());
455        } else {
456            dependency_status.insert("curl", "Not Available".to_string());
457        }
458    } else {
459        dependency_status.insert("curl", "Not Available".to_string());
460    }
461
462    let status = if issues.is_empty() {
463        DiagnosticStatus::Pass
464    } else if issues.len() <= 2 {
465        DiagnosticStatus::Warning
466    } else {
467        DiagnosticStatus::Fail
468    };
469
470    let message = if issues.is_empty() {
471        "All external dependencies are available".to_string()
472    } else {
473        format!("Some dependencies missing: {}", issues.join(", "))
474    };
475
476    DiagnosticResult {
477        name: "External Dependencies".to_string(),
478        status,
479        message,
480        details: Some(serde_json::json!(dependency_status)),
481    }
482}
483
484async fn check_device_availability() -> Vec<DiagnosticResult> {
485    let mut results = Vec::new();
486    let device_info = system::get_device_info();
487
488    for (device_name, info) in device_info {
489        let available = info
490            .get("available")
491            .and_then(|v| v.as_bool())
492            .unwrap_or(false);
493
494        let status = if available {
495            DiagnosticStatus::Pass
496        } else {
497            DiagnosticStatus::Warning
498        };
499
500        let message = if available {
501            format!("{} device is available", device_name.to_uppercase())
502        } else {
503            format!("{} device is not available", device_name.to_uppercase())
504        };
505
506        results.push(DiagnosticResult {
507            name: format!("{} Device", device_name.to_uppercase()),
508            status,
509            message,
510            details: Some(info),
511        });
512    }
513
514    results
515}
516
517async fn check_configuration(config: &Config) -> DiagnosticResult {
518    // Check if configuration is valid
519    let mut issues = Vec::new();
520
521    if !config.general.cache_dir.exists() {
522        issues.push("Cache directory does not exist");
523    }
524
525    if config.general.num_workers == 0 {
526        issues.push("Number of workers is set to 0");
527    }
528
529    if issues.is_empty() {
530        DiagnosticResult {
531            name: "Configuration".to_string(),
532            status: DiagnosticStatus::Pass,
533            message: "Configuration is valid".to_string(),
534            details: None,
535        }
536    } else {
537        DiagnosticResult {
538            name: "Configuration".to_string(),
539            status: DiagnosticStatus::Warning,
540            message: format!("Configuration has {} issues", issues.len()),
541            details: Some(serde_json::json!({
542                "issues": issues
543            })),
544        }
545    }
546}
547
548async fn check_permissions(config: &Config) -> DiagnosticResult {
549    // Check write permissions for important directories
550    let test_file = config.general.cache_dir.join(".torsh_test");
551
552    match tokio::fs::write(&test_file, "test").await {
553        Ok(_) => {
554            let _ = tokio::fs::remove_file(&test_file).await;
555            DiagnosticResult {
556                name: "Permissions".to_string(),
557                status: DiagnosticStatus::Pass,
558                message: "Write permissions are available".to_string(),
559                details: None,
560            }
561        }
562        Err(e) => DiagnosticResult {
563            name: "Permissions".to_string(),
564            status: DiagnosticStatus::Fail,
565            message: format!("Cannot write to cache directory: {}", e),
566            details: Some(serde_json::json!({
567                "cache_dir": config.general.cache_dir.display().to_string(),
568                "error": e.to_string(),
569            })),
570        },
571    }
572}
573
574async fn check_disk_space(config: &Config) -> DiagnosticResult {
575    use byte_unit::Byte;
576    use sysinfo::Disks;
577
578    // Check disk space for cache directory
579    let cache_dir = &config.general.cache_dir;
580
581    // ✅ Pure Rust: Use sysinfo instead of libc::statvfs
582    let disks = Disks::new_with_refreshed_list();
583
584    // Find the disk containing the cache directory
585    let cache_path = cache_dir
586        .canonicalize()
587        .unwrap_or_else(|_| cache_dir.clone());
588    let mut target_disk = None;
589    let mut longest_mount_len = 0;
590
591    for disk in disks.list() {
592        let mount_point = disk.mount_point();
593        if cache_path.starts_with(mount_point) {
594            let mount_len = mount_point.as_os_str().len();
595            if mount_len > longest_mount_len {
596                target_disk = Some((
597                    disk.total_space(),
598                    disk.available_space(),
599                    mount_point.to_path_buf(),
600                ));
601                longest_mount_len = mount_len;
602            }
603        }
604    }
605
606    if let Some((total_bytes, available_bytes, mount_point)) = target_disk {
607        let used_bytes = total_bytes.saturating_sub(available_bytes);
608
609        let usage_percent = if total_bytes > 0 {
610            (used_bytes as f64 / total_bytes as f64) * 100.0
611        } else {
612            0.0
613        };
614
615        let status = if usage_percent > 90.0 {
616            DiagnosticStatus::Fail
617        } else if usage_percent > 80.0 {
618            DiagnosticStatus::Warning
619        } else {
620            DiagnosticStatus::Pass
621        };
622
623        let message = if usage_percent > 90.0 {
624            "Very low disk space available (>90% used)".to_string()
625        } else if usage_percent > 80.0 {
626            "Low disk space warning (>80% used)".to_string()
627        } else {
628            "Sufficient disk space available".to_string()
629        };
630
631        return DiagnosticResult {
632            name: "Disk Space".to_string(),
633            status,
634            message,
635            details: Some(serde_json::json!({
636                "cache_dir": cache_dir.display().to_string(),
637                "mount_point": mount_point.display().to_string(),
638                "total_space": Byte::from_u128(total_bytes as u128).unwrap_or_else(|| Byte::from_u128(0).expect("zero bytes should always be valid")).get_appropriate_unit(byte_unit::UnitType::Binary).to_string(),
639                "available_space": Byte::from_u128(available_bytes as u128).unwrap_or_else(|| Byte::from_u128(0).expect("zero bytes should always be valid")).get_appropriate_unit(byte_unit::UnitType::Binary).to_string(),
640                "used_space": Byte::from_u128(used_bytes as u128).unwrap_or_else(|| Byte::from_u128(0).expect("zero bytes should always be valid")).get_appropriate_unit(byte_unit::UnitType::Binary).to_string(),
641                "usage_percent": format!("{:.1}%", usage_percent),
642            })),
643        };
644    }
645
646    // Fallback if disk not found
647    DiagnosticResult {
648        name: "Disk Space".to_string(),
649        status: DiagnosticStatus::Warning,
650        message: "Could not determine disk space usage".to_string(),
651        details: Some(serde_json::json!({
652            "cache_dir": cache_dir.display().to_string(),
653            "note": "Could not find disk containing cache directory"
654        })),
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661
662    #[test]
663    fn test_torsh_info() {
664        let info = get_torsh_info();
665        assert!(!info.version.is_empty());
666        assert!(!info.target_triple.is_empty());
667    }
668
669    #[test]
670    fn test_feature_info() {
671        let features = get_feature_info();
672        // At least some features should be enabled by default
673        assert!(!features.enabled_features.is_empty() || !features.disabled_features.is_empty());
674    }
675
676    #[tokio::test]
677    async fn test_installation_info() {
678        let info = get_installation_info()
679            .await
680            .expect("operation should succeed");
681        assert!(!info.install_path.is_empty());
682    }
683}