using System; using System.Diagnostics; using System.Management; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; namespace StudioE5.SetupWizard; public record PrerequisiteResult( bool WindowsCompatible, ulong RamMB, ulong FreeDiskMB, bool VirtualEnvironmentInstalled, bool PodmanInstalled, bool PodmanMachineReady) { public bool AllReady => WindowsCompatible && RamMB >= 4096 && FreeDiskMB >= 5120 && VirtualEnvironmentInstalled && PodmanInstalled && PodmanMachineReady; } public static class PrerequisiteChecker { public static PrerequisiteResult Check() { var wsl2Ready = IsWSL2Ready(); var podmanMachineReady = IsPodmanMachineReady(); // Fallback : si la machine Podman est prête, WSL2 est nécessairement fonctionnel. // Cela contourne les problèmes de détection WSL liés à l'encodage ou au PATH. var virtualEnvironmentInstalled = wsl2Ready || podmanMachineReady; return new PrerequisiteResult( WindowsCompatible: IsWindowsCompatible(), RamMB: GetTotalPhysicalMemoryMB(), FreeDiskMB: GetFreeDiskSpaceMB("C:\\"), VirtualEnvironmentInstalled: virtualEnvironmentInstalled, PodmanInstalled: IsPodmanInstalled(), PodmanMachineReady: podmanMachineReady ); } private static bool IsWindowsCompatible() { var os = Environment.OSVersion; if (os.Platform != PlatformID.Win32NT) return false; // Windows 10 version 2004 (build 19041) or Windows 11. return Environment.OSVersion.Version.Build >= 19041; } private static ulong GetTotalPhysicalMemoryMB() { try { using var searcher = new ManagementObjectSearcher("SELECT TotalVisibleMemorySize FROM Win32_OperatingSystem"); foreach (ManagementObject obj in searcher.Get()) { var kb = Convert.ToUInt64(obj["TotalVisibleMemorySize"]); return kb / 1024; } } catch { // ignored } return 0; } private static ulong GetFreeDiskSpaceMB(string path) { try { var drive = new DriveInfo(Path.GetPathRoot(path) ?? path); return (ulong)(drive.AvailableFreeSpace / (1024 * 1024)); } catch { return 0; } } public static bool IsWSL2Ready() { // PowerShell gère mieux l'encodage de la sortie WSL que Process.Start en C#. if (IsWSL2ReadyViaPowerShell()) return true; // Fallback natif si PowerShell n'est pas disponible. return IsWSL2ReadyNative(); } private static bool IsWSL2ReadyViaPowerShell() { try { var tempFile = Path.GetTempFileName(); var script = "$status = & wsl.exe --status 2>&1; " + "$ready = ($status -match 'Version par d\\u00E9faut\\s*:\\s*2') -or " + "($status -match 'Default Version\\s*:\\s*2'); " + "$ready | Out-File -FilePath '" + tempFile + "' -Encoding utf8 -NoNewline"; var psi = new ProcessStartInfo("powershell.exe", $"-ExecutionPolicy Bypass -Command \"{script}\"") { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = Process.Start(psi); if (process == null) return false; process.WaitForExit(); if (!File.Exists(tempFile)) return false; var result = File.ReadAllText(tempFile).Trim(); File.Delete(tempFile); return result.Equals("True", StringComparison.OrdinalIgnoreCase); } catch { return false; } } private static bool IsWSL2ReadyNative() { try { // wsl --status est plus fiable que --version pour savoir si WSL2 est prêt. var psi = new ProcessStartInfo("wsl.exe", "--status") { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = Process.Start(psi); if (process == null) return false; var output = process.StandardOutput.ReadToEnd(); var error = process.StandardError.ReadToEnd(); process.WaitForExit(); // wsl --status peut retourner un code non nul même quand l’info utile est affichée // (par exemple si aucune distribution n’est installée). On parse quand même. var combined = output + "\n" + error; var normalized = combined .Replace('\u00A0', ' ') .Replace('\u202F', ' '); if (normalized.Contains("Version par défaut : 2", StringComparison.OrdinalIgnoreCase) || normalized.Contains("Default Version: 2", StringComparison.OrdinalIgnoreCase) || normalized.Contains("Version défaut : 2", StringComparison.OrdinalIgnoreCase)) { return true; } var defaultVersion = ParseWslDefaultVersion(combined); if (defaultVersion == 2) return true; // Si aucune version par défaut n'est trouvée, on tente les autres méthodes. return (defaultVersion == 0 && WslVersionIndicatesWsl2()) || WslListIndicatesWsl2(); } catch { return false; } } private static bool IsWSLInstalled() { return IsWSL2Ready(); } private static int ParseWslDefaultVersion(string text) { try { foreach (var rawLine in text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) { // Normalise les espaces insécables et les espaces multiples. var trimmed = rawLine .Replace('\u00A0', ' ') .Replace('\u202F', ' ') .Trim(); // Regex souple pour matcher : // - Default Version: 2 // - Version par défaut : 2 // - Version défaut:2 // etc. var match = Regex.Match( trimmed, @"(?i)(?:default\s+version|version\s+(?:par\s+)?d[eé]faut)\s*[:\-]?\s*(\d+)", RegexOptions.CultureInvariant); if (match.Success && int.TryParse(match.Groups[1].Value, out var version)) return version; } } catch { // ignored } return 0; } private static bool WslVersionIndicatesWsl2() { try { var psi = new ProcessStartInfo("wsl.exe", "--version") { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = Process.Start(psi); if (process == null) return false; var output = process.StandardOutput.ReadToEnd(); var error = process.StandardError.ReadToEnd(); process.WaitForExit(); if (process.ExitCode != 0) return false; var combined = output + "\n" + error; // Si la sortie mentionne explicitement WSL 2 ou un noyau 5.10+, on considère WSL2 prêt. return combined.Contains("WSL version: 2", StringComparison.OrdinalIgnoreCase) || combined.Contains("WSL version: 2.0", StringComparison.OrdinalIgnoreCase) || combined.Contains("Kernel version: 5.10", StringComparison.OrdinalIgnoreCase); } catch { return false; } } private static bool WslListIndicatesWsl2() { try { var psi = new ProcessStartInfo("wsl.exe", "--list --verbose") { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = Process.Start(psi); if (process == null) return false; var output = process.StandardOutput.ReadToEnd(); var error = process.StandardError.ReadToEnd(); process.WaitForExit(); var combined = output + "\n" + error; // Si au moins une distribution est en version 2, WSL2 est fonctionnel. foreach (var line in combined.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) { var parts = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2 && parts[^1] == "2") return true; } return false; } catch { return false; } } public static string? GetPodmanExePath() { // 1. Chercher dans le PATH actuel du processus. try { var psi = new ProcessStartInfo("where.exe", "podman.exe") { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = Process.Start(psi); if (process != null) { var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); if (process.ExitCode == 0) { var firstLine = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); if (!string.IsNullOrWhiteSpace(firstLine) && File.Exists(firstLine)) return firstLine; } } } catch { // ignored } // 2. Chercher dans les emplacements d'installation connus. var candidates = new[] { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "podman", "podman.exe"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "RedHat", "Podman", "podman.exe"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Podman", "podman.exe"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "RedHat", "Podman", "podman.exe"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Podman", "podman.exe"), }; foreach (var candidate in candidates) { if (File.Exists(candidate)) return candidate; } return null; } private static bool IsPodmanInstalled() { var podmanPath = GetPodmanExePath(); if (string.IsNullOrEmpty(podmanPath)) return false; try { var psi = new ProcessStartInfo(podmanPath, "--version") { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = Process.Start(psi); if (process == null) return false; process.WaitForExit(); return process.ExitCode == 0; } catch { return false; } } private static bool IsPodmanMachineReady() { var podmanPath = GetPodmanExePath(); if (string.IsNullOrEmpty(podmanPath)) return false; try { var psi = new ProcessStartInfo(podmanPath, "machine list --format json") { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = Process.Start(psi); if (process == null) return false; var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); if (process.ExitCode != 0) return false; // Very permissive check: if podman machine list returns any JSON, we consider it ready. return output.TrimStart().StartsWith("[") || output.TrimStart().StartsWith("{"); } catch { return false; } } }