1use 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 #[arg(long)]
15 pub detailed: bool,
16
17 #[arg(long)]
19 pub installation: bool,
20
21 #[arg(long)]
23 pub devices: bool,
24
25 #[arg(long)]
27 pub features: bool,
28
29 #[arg(long)]
31 pub diagnostics: bool,
32
33 #[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_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 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 diagnostics.push(check_torsh_installation().await);
223
224 diagnostics.push(check_dependencies().await);
226
227 diagnostics.extend(check_device_availability().await);
229
230 diagnostics.push(check_configuration(config).await);
232
233 diagnostics.push(check_permissions(config).await);
235
236 diagnostics.push(check_disk_space(config).await);
238
239 output::print_table("Diagnostic Results", &diagnostics, output_format)?;
240
241 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(), git_commit: "abc123def".to_string(), 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 #[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 let mut total_size = 0u64;
358 if let Ok(metadata) = tokio::fs::metadata(¤t_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 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 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 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 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 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 let cache_dir = &config.general.cache_dir;
580
581 let disks = Disks::new_with_refreshed_list();
583
584 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 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 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}