1use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(tag = "status", rename_all = "snake_case")]
27pub enum WslStatus {
28 NotInstalled,
30 Available {
32 distro: String,
34 version: u8,
36 },
37 Error {
39 message: String,
41 },
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct WslHealthResult {
47 pub check: String,
49 pub success: bool,
51 pub message: String,
53}
54
55pub fn detect_wsl2() -> WslStatus {
79 #[cfg(target_os = "windows")]
80 {
81 detect_wsl2_windows()
82 }
83
84 #[cfg(not(target_os = "windows"))]
85 {
86 detect_wsl2_non_windows()
87 }
88}
89
90pub fn provisioning_guidance(status: &WslStatus) -> String {
96 match status {
97 WslStatus::NotInstalled => {
98 let mut guidance = String::new();
99 guidance.push_str("WSL2 is not installed on this system.\n\n");
100 guidance.push_str("To install WSL2:\n");
101 guidance.push_str(" 1. Open PowerShell as Administrator\n");
102 guidance.push_str(" 2. Run: wsl --install\n");
103 guidance.push_str(" 3. Restart your computer when prompted\n");
104 guidance.push_str(" 4. Set up a default Linux distribution (Ubuntu recommended)\n");
105 guidance.push_str(" 5. Install ROCm inside WSL2 following AMD's WSL2 guide\n\n");
106 guidance.push_str("After installation, click 'Re-check' to verify WSL2 setup.");
107 guidance
108 }
109 WslStatus::Available { distro, .. } => {
110 format!(
111 "WSL2 is available with distribution '{}'.\n\n\
112 Next steps:\n\
113 1. Ensure ROCm is installed inside the WSL2 distribution\n\
114 2. Verify GPU access with 'rocm-smi' inside WSL2\n\
115 3. The installer will use WSL2 for Linux-only components",
116 distro
117 )
118 }
119 WslStatus::Error { message: msg } => {
120 format!(
121 "WSL2 detection encountered an error: {}\n\n\
122 Troubleshooting:\n\
123 1. Ensure Windows is updated to the latest version\n\
124 2. Check that 'Windows Subsystem for Linux' is enabled\n\
125 3. Try running 'wsl --install' from an Administrator PowerShell",
126 msg
127 )
128 }
129 }
130}
131
132pub fn check_wsl_health(status: &WslStatus) -> Vec<WslHealthResult> {
141 match status {
142 WslStatus::NotInstalled => vec![
143 WslHealthResult {
144 check: "distro_running".to_string(),
145 success: false,
146 message: "WSL2 is not installed".to_string(),
147 },
148 WslHealthResult {
149 check: "rocm_accessible".to_string(),
150 success: false,
151 message: "WSL2 is not installed".to_string(),
152 },
153 WslHealthResult {
154 check: "gpu_nodes".to_string(),
155 success: false,
156 message: "WSL2 is not installed".to_string(),
157 },
158 ],
159 WslStatus::Available { distro, .. } => check_wsl_health_available(distro),
160 WslStatus::Error { message: msg } => vec![
161 WslHealthResult {
162 check: "distro_running".to_string(),
163 success: false,
164 message: format!("Detection error: {}", msg),
165 },
166 WslHealthResult {
167 check: "rocm_accessible".to_string(),
168 success: false,
169 message: format!("Detection error: {}", msg),
170 },
171 WslHealthResult {
172 check: "gpu_nodes".to_string(),
173 success: false,
174 message: format!("Detection error: {}", msg),
175 },
176 ],
177 }
178}
179
180#[cfg(target_os = "windows")]
186fn detect_wsl2_windows() -> WslStatus {
187 use std::process::Command;
188
189 let output = match Command::new("wsl").args(["--list", "--verbose"]).output() {
190 Ok(o) => o,
191 Err(e) => {
192 return WslStatus::Error {
193 message: format!("Failed to execute wsl: {}", e),
194 }
195 }
196 };
197
198 if !output.status.success() {
199 return WslStatus::NotInstalled;
200 }
201
202 let stdout = String::from_utf8_lossy(&output.stdout);
203 parse_wsl_list_output(&stdout)
204}
205
206#[cfg(any(target_os = "windows", test))]
208fn parse_wsl_list_output(output: &str) -> WslStatus {
209 for line in output.lines().skip(1) {
210 let line = line.trim();
212 if line.is_empty() {
213 continue;
214 }
215
216 let parts: Vec<&str> = line.split_whitespace().collect();
220 if parts.len() < 3 {
221 continue;
222 }
223
224 let version_str = parts[parts.len() - 1]; let state_str = parts[parts.len() - 2]; let name = if parts[0] == "*" {
229 if parts.len() < 4 {
231 continue;
232 }
233 parts[1]
234 } else if parts[0].starts_with('*') {
235 parts[0].trim_start_matches('*')
237 } else {
238 parts[0]
239 };
240
241 let _ = state_str; if let Ok(version) = version_str.parse::<u8>() {
246 if version == 2 && !name.is_empty() {
247 return WslStatus::Available {
248 distro: name.to_string(),
249 version: 2,
250 };
251 }
252 }
253 }
254
255 WslStatus::NotInstalled
257}
258
259#[cfg(not(target_os = "windows"))]
263fn detect_wsl2_non_windows() -> WslStatus {
264 if is_running_in_wsl() {
266 let distro = detect_wsl_distro_name();
267 return WslStatus::Available {
268 distro: distro.unwrap_or_else(|| "unknown".to_string()),
269 version: 2,
270 };
271 }
272
273 WslStatus::NotInstalled
274}
275
276#[cfg(not(target_os = "windows"))]
278fn is_running_in_wsl() -> bool {
279 if let Ok(version) = std::fs::read_to_string("/proc/version") {
281 let version_lower = version.to_lowercase();
282 return version_lower.contains("microsoft") || version_lower.contains("wsl");
283 }
284 false
285}
286
287#[cfg(not(target_os = "windows"))]
289fn detect_wsl_distro_name() -> Option<String> {
290 if let Ok(name) = std::env::var("WSL_DISTRO_NAME") {
292 if !name.is_empty() {
293 return Some(name);
294 }
295 }
296
297 if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
299 for line in content.lines() {
300 if let Some(name) = line.strip_prefix("NAME=") {
301 let name = name.trim().trim_matches('"').trim_matches('\'');
302 if !name.is_empty() {
303 return Some(name.to_string());
304 }
305 }
306 }
307 }
308
309 None
310}
311
312fn check_wsl_health_available(distro: &str) -> Vec<WslHealthResult> {
314 let distro_result = check_distro_running(distro);
315 let rocm_result = check_rocm_accessible();
316 let gpu_result = check_gpu_nodes();
317
318 vec![distro_result, rocm_result, gpu_result]
319}
320
321fn check_distro_running(distro: &str) -> WslHealthResult {
323 #[cfg(target_os = "windows")]
324 {
325 use std::process::Command;
326
327 let output = match Command::new("wsl").args(["--list", "--running"]).output() {
328 Ok(o) => o,
329 Err(e) => {
330 return WslHealthResult {
331 check: "distro_running".to_string(),
332 success: false,
333 message: format!("Failed to check running distros: {}", e),
334 }
335 }
336 };
337
338 let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
339 let running = stdout.contains(&distro.to_lowercase());
340
341 WslHealthResult {
342 check: "distro_running".to_string(),
343 success: running,
344 message: if running {
345 format!("{} is running", distro)
346 } else {
347 format!("{} is not currently running", distro)
348 },
349 }
350 }
351
352 #[cfg(not(target_os = "windows"))]
353 {
354 WslHealthResult {
356 check: "distro_running".to_string(),
357 success: true,
358 message: format!("{} is running (native execution)", distro),
359 }
360 }
361}
362
363fn check_rocm_accessible() -> WslHealthResult {
365 #[cfg(target_os = "windows")]
366 {
367 use std::process::Command;
368
369 let output = match Command::new("wsl")
370 .args(["--", "rocm-smi", "--version"])
371 .output()
372 {
373 Ok(o) => o,
374 Err(e) => {
375 return WslHealthResult {
376 check: "rocm_accessible".to_string(),
377 success: false,
378 message: format!("Failed to check ROCm in WSL: {}", e),
379 }
380 }
381 };
382
383 let success = output.status.success();
384 WslHealthResult {
385 check: "rocm_accessible".to_string(),
386 success,
387 message: if success {
388 "ROCm is accessible inside WSL2".to_string()
389 } else {
390 "ROCm is not accessible inside WSL2".to_string()
391 },
392 }
393 }
394
395 #[cfg(not(target_os = "windows"))]
396 {
397 let rocm_accessible = std::path::Path::new("/opt/rocm/bin/rocm-smi").exists()
399 || std::process::Command::new("rocm-smi")
400 .arg("--version")
401 .output()
402 .map(|o| o.status.success())
403 .unwrap_or(false);
404
405 WslHealthResult {
406 check: "rocm_accessible".to_string(),
407 success: rocm_accessible,
408 message: if rocm_accessible {
409 "ROCm is accessible".to_string()
410 } else {
411 "ROCm is not accessible".to_string()
412 },
413 }
414 }
415}
416
417fn check_gpu_nodes() -> WslHealthResult {
419 #[cfg(target_os = "windows")]
420 {
421 use std::process::Command;
422
423 let output = match Command::new("wsl").args(["--", "ls", "/dev/kfd"]).output() {
424 Ok(o) => o,
425 Err(e) => {
426 return WslHealthResult {
427 check: "gpu_nodes".to_string(),
428 success: false,
429 message: format!("Failed to check GPU nodes in WSL: {}", e),
430 }
431 }
432 };
433
434 let success = output.status.success();
435 WslHealthResult {
436 check: "gpu_nodes".to_string(),
437 success,
438 message: if success {
439 "GPU device nodes are present in WSL2".to_string()
440 } else {
441 "GPU device nodes are not present in WSL2".to_string()
442 },
443 }
444 }
445
446 #[cfg(not(target_os = "windows"))]
447 {
448 let kfd_exists = std::path::Path::new("/dev/kfd").exists();
450 let dri_exists = std::path::Path::new("/dev/dri").exists();
451 let gpu_nodes_present = kfd_exists || dri_exists;
452
453 WslHealthResult {
454 check: "gpu_nodes".to_string(),
455 success: gpu_nodes_present,
456 message: if gpu_nodes_present {
457 "GPU device nodes are present".to_string()
458 } else {
459 "GPU device nodes are not present".to_string()
460 },
461 }
462 }
463}
464
465#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
476 fn test_detect_wsl2_returns_valid_status() {
477 let status = detect_wsl2();
478 match &status {
480 WslStatus::NotInstalled => {}
481 WslStatus::Available { distro, version } => {
482 assert!(!distro.is_empty());
483 assert_eq!(*version, 2);
484 }
485 WslStatus::Error { message: msg } => {
486 assert!(!msg.is_empty());
487 }
488 }
489 }
490
491 #[test]
492 fn test_detect_wsl2_completes_quickly() {
493 let start = std::time::Instant::now();
494 let _ = detect_wsl2();
495 let elapsed = start.elapsed();
496 assert!(elapsed.as_secs() < 5, "Detection took {:?}", elapsed);
497 }
498
499 #[test]
502 fn test_guidance_not_installed() {
503 let g = provisioning_guidance(&WslStatus::NotInstalled);
504 assert!(g.to_lowercase().contains("wsl"));
505 assert!(g.to_lowercase().contains("install"));
506 }
507
508 #[test]
509 fn test_guidance_available() {
510 let s = WslStatus::Available {
511 distro: "Ubuntu".to_string(),
512 version: 2,
513 };
514 let g = provisioning_guidance(&s);
515 assert!(g.contains("Ubuntu"));
516 }
517
518 #[test]
519 fn test_guidance_error() {
520 let s = WslStatus::Error {
521 message: "test".to_string(),
522 };
523 let g = provisioning_guidance(&s);
524 assert!(!g.is_empty());
525 }
526
527 #[test]
530 fn test_health_checks_not_installed_count() {
531 let results = check_wsl_health(&WslStatus::NotInstalled);
532 assert_eq!(results.len(), 3);
533 }
534
535 #[test]
536 fn test_health_checks_not_installed_all_fail() {
537 let results = check_wsl_health(&WslStatus::NotInstalled);
538 for r in &results {
539 assert!(!r.success, "{} should fail when not installed", r.check);
540 }
541 }
542
543 #[test]
544 fn test_health_checks_independent() {
545 let results = check_wsl_health(&WslStatus::NotInstalled);
546 let names: std::collections::HashSet<&str> =
547 results.iter().map(|r| r.check.as_str()).collect();
548 assert_eq!(names.len(), 3);
549 }
550
551 #[test]
552 fn test_health_check_names() {
553 let results = check_wsl_health(&WslStatus::NotInstalled);
554 let names: Vec<&str> = results.iter().map(|r| r.check.as_str()).collect();
555 assert!(names.contains(&"distro_running"));
556 assert!(names.contains(&"rocm_accessible"));
557 assert!(names.contains(&"gpu_nodes"));
558 }
559
560 #[test]
563 fn test_wsl_status_serde_not_installed() {
564 let s = WslStatus::NotInstalled;
565 let json = serde_json::to_string(&s).unwrap();
566 let back: WslStatus = serde_json::from_str(&json).unwrap();
567 assert_eq!(s, back);
568 }
569
570 #[test]
571 fn test_wsl_status_serde_available() {
572 let s = WslStatus::Available {
573 distro: "Ubuntu-22.04".to_string(),
574 version: 2,
575 };
576 let json = serde_json::to_string(&s).unwrap();
577 let back: WslStatus = serde_json::from_str(&json).unwrap();
578 assert_eq!(s, back);
579 }
580
581 #[test]
582 fn test_wsl_status_serde_error() {
583 let s = WslStatus::Error {
584 message: "test error".to_string(),
585 };
586 let json = serde_json::to_string(&s).unwrap();
587 let back: WslStatus = serde_json::from_str(&json).unwrap();
588 assert_eq!(s, back);
589 }
590
591 #[test]
592 fn test_wsl_health_result_serde() {
593 let r = WslHealthResult {
594 check: "distro_running".to_string(),
595 success: true,
596 message: "Running".to_string(),
597 };
598 let json = serde_json::to_string(&r).unwrap();
599 let back: WslHealthResult = serde_json::from_str(&json).unwrap();
600 assert_eq!(r, back);
601 }
602
603 #[test]
606 fn test_parse_wsl_list_with_v2_distro() {
607 let output = " NAME STATE VERSION\n\
608 * Ubuntu-22.04 Running 2\n\
609 Debian Stopped 1";
610 let status = parse_wsl_list_output(output);
611 match status {
612 WslStatus::Available { distro, version } => {
613 assert_eq!(distro, "Ubuntu-22.04");
614 assert_eq!(version, 2);
615 }
616 _ => panic!("Expected Available, got {:?}", status),
617 }
618 }
619
620 #[test]
621 fn test_parse_wsl_list_no_v2_distro() {
622 let output = " NAME STATE VERSION\n\
623 Debian Stopped 1";
624 let status = parse_wsl_list_output(output);
625 assert_eq!(status, WslStatus::NotInstalled);
626 }
627
628 #[test]
629 fn test_parse_wsl_list_empty() {
630 let output = "";
631 let status = parse_wsl_list_output(output);
632 assert_eq!(status, WslStatus::NotInstalled);
633 }
634
635 #[test]
636 #[cfg(not(target_os = "windows"))]
637 fn test_is_running_in_wsl() {
638 let _ = is_running_in_wsl();
640 }
641}