feat: installation offline complete, HTTPS registry, 8Go WSL, v0.3.18

- Wizard: installation 100% offline (WSL bundle, Podman MSI, machine image, docker-compose)
- Wizard: suppression de wsl --install --no-distribution
- Wizard: .wslconfig avec 8Go RAM / 4 CPU
- Wizard: operations asynchrones pour eviter le freeze UI
- Wizard: detection automatique de podman.exe
- Wizard: version 0.1.1
- Agent: passage en v0.3.18
- Serveur: registry PrestaShop en HTTPS via gitea.alfrednobel.edudeploy.com
- Caddy: config gitea.alfrednobel.edudeploy.com
- Docs: mise a jour SUIVI_INSTALLER.md, README.md, seed.ts
This commit is contained in:
EduBox Dev
2026-07-02 22:52:28 +00:00
parent 3c519629d2
commit fc61404271
15 changed files with 1094 additions and 62 deletions
@@ -1,6 +1,9 @@
using System;
using System.Diagnostics;
using System.Management;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace StudioE5.SetupWizard;
@@ -19,13 +22,20 @@ 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: IsWSLInstalled(),
VirtualEnvironmentInstalled: virtualEnvironmentInstalled,
PodmanInstalled: IsPodmanInstalled(),
PodmanMachineReady: IsPodmanMachineReady()
PodmanMachineReady: podmanMachineReady
);
}
@@ -70,7 +80,143 @@ public static class PrerequisiteChecker
}
}
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 linfo utile est affichée
// (par exemple si aucune distribution nest 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
{
@@ -79,12 +225,23 @@ public static class PrerequisiteChecker
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = 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();
return process.ExitCode == 0;
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
{
@@ -92,16 +249,110 @@ public static class PrerequisiteChecker
}
}
private static bool IsPodmanInstalled()
private static bool WslListIndicatesWsl2()
{
try
{
var psi = new ProcessStartInfo("podman.exe", "--version")
var psi = new ProcessStartInfo("wsl.exe", "--list --verbose")
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = 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;
@@ -116,14 +367,20 @@ public static class PrerequisiteChecker
private static bool IsPodmanMachineReady()
{
var podmanPath = GetPodmanExePath();
if (string.IsNullOrEmpty(podmanPath))
return false;
try
{
var psi = new ProcessStartInfo("podman.exe", "machine list --format json")
var psi = new ProcessStartInfo(podmanPath, "machine list --format json")
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using var process = Process.Start(psi);
if (process == null) return false;