From fc61404271fa82266005d92ef2d1bafca2de51af Mon Sep 17 00:00:00 2001 From: EduBox Dev Date: Thu, 2 Jul 2026 22:52:28 +0000 Subject: [PATCH] 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 --- Caddyfile | 5 + agent/VERSION | 2 +- agent/installer/SUIVI_INSTALLER.md | 27 +- .../installer/setup-wizard/InstallerState.cs | 12 + agent/installer/setup-wizard/MainForm.cs | 523 ++++++++++++++++-- .../setup-wizard/PrerequisiteChecker.cs | 275 ++++++++- agent/installer/setup-wizard/README.md | 32 +- .../installer/setup-wizard/SUIVI_INSTALLER.md | 127 +++++ .../installer/setup-wizard/SetupWizard.csproj | 12 +- agent/installer/setup-wizard/VERSION | 1 + agent/installer/setup-wizard/app.manifest | 12 + agent/installer/setup-wizard/install-wsl2.ps1 | 120 ++++ agent/studioE5-agent-setup-wizard.zip | Bin 12552 -> 19138 bytes prestashop-image/README.md | 6 +- server/prisma/seed.ts | 2 +- 15 files changed, 1094 insertions(+), 62 deletions(-) create mode 100644 agent/installer/setup-wizard/SUIVI_INSTALLER.md create mode 100644 agent/installer/setup-wizard/VERSION create mode 100644 agent/installer/setup-wizard/app.manifest create mode 100644 agent/installer/setup-wizard/install-wsl2.ps1 diff --git a/Caddyfile b/Caddyfile index 114d52c..13f093c 100644 --- a/Caddyfile +++ b/Caddyfile @@ -26,6 +26,11 @@ headscale.studioe5.edudeploy.com:443 { reverse_proxy headscale:8080 } +gitea.alfrednobel.edudeploy.com { + tls admin@edudeploy.com + reverse_proxy 151.80.60.98:3001 +} + studioe5.edudeploy.com:443 { route /studioE5-agent* { file_server { diff --git a/agent/VERSION b/agent/VERSION index e5a9958..8355eaf 100644 --- a/agent/VERSION +++ b/agent/VERSION @@ -1 +1 @@ -0.3.17 +0.3.18 diff --git a/agent/installer/SUIVI_INSTALLER.md b/agent/installer/SUIVI_INSTALLER.md index 8256b17..5f11dc8 100644 --- a/agent/installer/SUIVI_INSTALLER.md +++ b/agent/installer/SUIVI_INSTALLER.md @@ -24,9 +24,20 @@ Fournir un **installateur professionnel Windows** pour studioE5 Agent, guidé pa - Wizard C# avec 7 étapes guidées. - Détection des prérequis : Windows, RAM, disque, WSL2, Podman. -- Installation WSL2 avec redémarrage + reprise automatique. +- Installation WSL2 en plusieurs étapes contrôlées avec suivi visuel : + 1. Activation des fonctionnalités Windows (`Microsoft-Windows-Subsystem-Linux` et `VirtualMachinePlatform`) avec gestion du code 3010 (redémarrage nécessaire). + 2. Installation du package WSL2 complet depuis le bundle Microsoft Store offline (`Microsoft.WSL_*.msixbundle`) via `Add-AppxPackage`. + 3. Mise à jour du noyau WSL2 depuis le MSI bundlé (`wsl_update_x64.msi`) ou, en dernier recours, via `wsl --update`. + 4. Définition de WSL2 comme version par défaut (`wsl --set-default-version 2`). + 5. L’étape `wsl --install --no-distribution` n’est plus utilisée : l’installation est entièrement offline grâce au bundle. + 6. Conversion des distributions WSL1 existantes vers WSL2 (`wsl --set-version 2`) si nécessaire. +- Reprise automatique après redémarrage grâce à `RunOnce` (y compris redémarrage post-installation WSL2). +- Versioning indépendant du wizard (fichier `setup-wizard/VERSION`, affiché dans la fenêtre et la page d’accueil). +- Amélioration de l’interface : fenêtre agrandie à 800×600, `AutoScaleMode = Dpi`, meilleur rendu sur petits écrans (11 pouces). +- `RunCommand` capture désormais stdout + stderr pour des diagnostics et fallbacks plus fiables. +- Détection des prérequis vérifie que WSL2 (et non WSL1) est installé via `wsl --status`. - Installation Podman via MSI bundlé. -- Configuration Podman (`machine init` + `machine start`). +- Configuration Podman (`machine init` + `machine start`), avec initialisation offline possible depuis une image locale (`podman-machine.*.wsl.tar.zst`). - Lancement du package Inno Setup agent. - Mode désinstallation complet. - Script Inno Setup de base pour l’agent. @@ -57,6 +68,10 @@ Dans `setup-wizard/Resources/` : ```text podman-installer-windows-amd64.msi studioE5-agent-setup.exe +Microsoft.WSL_2.7.10.0_x64_ARM64.msixbundle # package WSL2 complet (offline) +podman-machine.x86_64.wsl.tar.zst # image Podman machine pour WSL (offline) +docker-compose-windows-x86_64.exe # Docker Compose standalone (offline) +wsl_update_x64.msi # optionnel, fallback noyau WSL2 ``` ### Commande @@ -95,8 +110,14 @@ Le fichier généré se trouve dans `installer-output/`. ## Notes importantes - Le wizard doit être exécuté **en administrateur**. -- L’installation de WSL2 nécessite un **redémarrage** de l’ordinateur. +- L’installation de WSL2 nécessite un **redémarrage** de l’ordinateur après l’activation des fonctionnalités Windows. +- L’installation de WSL2 est découpée en étapes et chaque étape est enregistrée pour permettre la reprise après redémarrage. +- Podman nécessite WSL2 : le wizard vérifie donc que la version par défaut de WSL est 2. - Le MSI Podman officiel pour Windows est `podman-installer-windows-amd64.msi`. +- Le bundle WSL2 offline est disponible sur : `Microsoft.WSL_2.7.10.0_x64_ARM64.msixbundle`. +- L’image Podman machine offline est disponible sur : `podman-machine.x86_64.wsl.tar.zst`. +- Docker Compose standalone est disponible sur : `docker-compose-windows-x86_64.exe`. +- Le wizard crée automatiquement un fichier `.wslconfig` allouant **8 Go de RAM et 4 CPU** à WSL2, ce qui est nécessaire pour des applications lourdes comme PrestaShop. - Pour la désinstallation, le MSI Podman doit être présent dans `Resources/`. ## Liens utiles diff --git a/agent/installer/setup-wizard/InstallerState.cs b/agent/installer/setup-wizard/InstallerState.cs index ada7459..1ad1cb8 100644 --- a/agent/installer/setup-wizard/InstallerState.cs +++ b/agent/installer/setup-wizard/InstallerState.cs @@ -24,6 +24,18 @@ public class InstallerState [JsonPropertyName("virtualEnvironmentInstalled")] public bool VirtualEnvironmentInstalled { get; set; } + [JsonPropertyName("wslFeaturesEnabled")] + public bool WslFeaturesEnabled { get; set; } + + [JsonPropertyName("wslPackageInstalled")] + public bool WslPackageInstalled { get; set; } + + [JsonPropertyName("wslDefaultVersionSet")] + public bool WslDefaultVersionSet { get; set; } + + [JsonPropertyName("wslKernelUpdated")] + public bool WslKernelUpdated { get; set; } + [JsonPropertyName("podmanInstalled")] public bool PodmanInstalled { get; set; } diff --git a/agent/installer/setup-wizard/MainForm.cs b/agent/installer/setup-wizard/MainForm.cs index 128c5bd..6de2ee3 100644 --- a/agent/installer/setup-wizard/MainForm.cs +++ b/agent/installer/setup-wizard/MainForm.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using System.Diagnostics; using System.Reflection; +using System.Text; using Microsoft.Win32; namespace StudioE5.SetupWizard; @@ -13,6 +15,7 @@ public partial class MainForm : Form private readonly Button _btnCancel; private readonly Label _titleLabel; private readonly Label _subtitleLabel; + private readonly Label _statusLabel; private WizardStep _currentStep = WizardStep.Welcome; private PrerequisiteResult? _lastCheck; @@ -20,7 +23,8 @@ public partial class MainForm : Form public MainForm(bool startInUninstallMode = false) { _state = InstallerState.Load(); - Text = startInUninstallMode ? "Désinstallation studioE5" : "Installateur studioE5 Agent"; + var version = GetVersion(); + Text = startInUninstallMode ? $"Désinstallation studioE5 v{version}" : $"Installateur studioE5 Agent v{version}"; Size = new Size(700, 520); StartPosition = FormStartPosition.CenterScreen; FormBorderStyle = FormBorderStyle.FixedDialog; @@ -34,7 +38,7 @@ public partial class MainForm : Form Width = 640, Height = 32, Font = new Font("Segoe UI", 14, FontStyle.Bold), - Text = "Installateur studioE5 Agent" + Text = $"Installateur studioE5 Agent v{version}" }; _subtitleLabel = new Label @@ -47,12 +51,23 @@ public partial class MainForm : Form ForeColor = Color.Gray }; + _statusLabel = new Label + { + Left = 24, + Top = 76, + Width = 640, + Height = 24, + Font = new Font("Segoe UI", 9, FontStyle.Bold), + ForeColor = Color.DodgerBlue, + Visible = false + }; + _contentPanel = new Panel { Left = 24, - Top = 84, + Top = 100, Width = 640, - Height = 320, + Height = 304, BorderStyle = BorderStyle.None }; @@ -99,6 +114,7 @@ public partial class MainForm : Form Controls.Add(_titleLabel); Controls.Add(_subtitleLabel); + Controls.Add(_statusLabel); Controls.Add(_contentPanel); Controls.Add(separator); Controls.Add(_btnBack); @@ -116,18 +132,30 @@ public partial class MainForm : Form private void ResumeAfterReboot() { - // If we are resuming after a reboot, skip directly to the relevant step. - if (_state.VirtualEnvironmentInstalled && !_state.PodmanInstalled) + // Si aucune étape n'a été faite, c'est une nouvelle installation (pas une reprise). + if (IsFreshInstall()) + { + GoToStep(WizardStep.Welcome); + return; + } + + // Reprise après redémarrage : on saute à la première étape non terminée. + if (!_state.WslFeaturesEnabled || !_state.WslPackageInstalled || !_state.VirtualEnvironmentInstalled || !_state.WslDefaultVersionSet || !_state.WslKernelUpdated) + { + GoToStep(WizardStep.InstallVirtualEnvironment); + return; + } + if (!_state.PodmanInstalled) { GoToStep(WizardStep.InstallPodman); return; } - if (_state.PodmanInstalled && !_state.PodmanConfigured) + if (!_state.PodmanConfigured) { GoToStep(WizardStep.ConfigurePodman); return; } - if (_state.PodmanConfigured && !_state.AgentInstalled) + if (!_state.AgentInstalled) { GoToStep(WizardStep.InstallAgent); return; @@ -136,6 +164,19 @@ public partial class MainForm : Form GoToStep(WizardStep.Welcome); } + private bool IsFreshInstall() + { + return _state.Step == WizardStep.Welcome && + !_state.WslFeaturesEnabled && + !_state.WslPackageInstalled && + !_state.VirtualEnvironmentInstalled && + !_state.WslDefaultVersionSet && + !_state.WslKernelUpdated && + !_state.PodmanInstalled && + !_state.PodmanConfigured && + !_state.AgentInstalled; + } + private void GoBack() { switch (_currentStep) @@ -167,10 +208,14 @@ public partial class MainForm : Form GoToStep(WizardStep.Prerequisites); break; case WizardStep.Prerequisites: - if (_lastCheck?.VirtualEnvironmentInstalled == true) - GoToStep(WizardStep.InstallPodman); - else + if (_lastCheck?.VirtualEnvironmentInstalled != true) GoToStep(WizardStep.InstallVirtualEnvironment); + else if (_lastCheck?.PodmanInstalled != true) + GoToStep(WizardStep.InstallPodman); + else if (_lastCheck?.PodmanMachineReady != true) + GoToStep(WizardStep.ConfigurePodman); + else + GoToStep(WizardStep.InstallAgent); break; case WizardStep.InstallVirtualEnvironment: InstallVirtualEnvironment(); @@ -237,8 +282,9 @@ public partial class MainForm : Form private void ShowWelcome() { + var version = GetVersion(); _titleLabel.Text = "Bienvenue dans l'installateur studioE5"; - _subtitleLabel.Text = "Cet assistant va vous guider pour installer studioE5 Agent sur votre poste."; + _subtitleLabel.Text = $"Version {version}"; var label = new Label { @@ -246,7 +292,8 @@ public partial class MainForm : Form Top = 20, Width = _contentPanel.Width, Height = 200, - Text = "L'installation se déroule en plusieurs étapes :\n\n" + + Text = $"Version {version}\n\n" + + "L'installation se déroule en plusieurs étapes :\n\n" + "1. Vérification des prérequis\n" + "2. Installation de l'environnement virtuel (si nécessaire)\n" + "3. Installation de Podman\n" + @@ -306,46 +353,174 @@ public partial class MainForm : Form _contentPanel.Controls.Add(label); _btnBack.Visible = true; - _btnNext.Text = result.AllReady ? "Suivant >" : (result.VirtualEnvironmentInstalled ? "Installer Podman" : "Installer l'environnement virtuel"); + _btnNext.Text = GetPrerequisitesNextButtonText(result); _btnNext.Enabled = true; } + private string GetPrerequisitesNextButtonText(PrerequisiteResult result) + { + if (result.AllReady) + return "Suivant >"; + if (!result.VirtualEnvironmentInstalled) + return "Installer l'environnement virtuel"; + if (!result.PodmanInstalled) + return "Installer Podman"; + if (!result.PodmanMachineReady) + return "Configurer Podman"; + return "Suivant >"; + } + private void ShowInstallVirtualEnvironment() { _titleLabel.Text = "Installation de l'environnement virtuel"; _subtitleLabel.Text = "L'environnement virtuel permet de faire tourner Podman sur Windows."; + var progress = GetWslInstallationProgress(); + var label = new Label { Left = 0, Top = 20, Width = _contentPanel.Width, - Height = 120, + Height = 160, Text = "L'environnement virtuel (WSL2) n'est pas installé.\n\n" + - "Cliquez sur 'Installer' pour lancer l'installation.\n" + - "Un redémarrage de l'ordinateur sera nécessaire. L'installateur se relancera automatiquement.", + "L'assistant va effectuer les opérations suivantes :\n" + + "1. Activer les fonctionnalités Windows requises\n" + + "2. Mettre à jour le noyau WSL2\n" + + "3. Installer WSL2 si nécessaire\n" + + "4. Définir WSL2 comme version par défaut\n" + + "5. Vérifier que WSL2 est opérationnel\n\n" + + "Selon votre version de Windows, certaines étapes peuvent être implicites.\n" + + "Un redémarrage de l'ordinateur sera probablement nécessaire. L'installateur se relancera automatiquement.", Font = new Font("Segoe UI", 10) }; + var progressLabel = new Label + { + Left = 0, + Top = 190, + Width = _contentPanel.Width, + Height = 80, + Text = progress, + Font = new Font("Consolas", 9), + ForeColor = Color.DarkSlateGray + }; + _contentPanel.Controls.Add(label); + _contentPanel.Controls.Add(progressLabel); _btnBack.Visible = true; - _btnNext.Text = "Installer"; + _btnNext.Text = progress.Contains("Terminé") ? "Continuer" : "Installer"; _btnNext.Enabled = true; } - private void InstallVirtualEnvironment() + private string GetWslInstallationProgress() + { + var wsl2Operational = PrerequisiteChecker.IsWSL2Ready(); + + var steps = new List + { + $"[{( _state.WslFeaturesEnabled ? "x" : " " )}] Fonctionnalités Windows activées", + $"[{( _state.WslPackageInstalled ? "x" : " " )}] Package WSL2 installé", + $"[{( _state.WslKernelUpdated ? "x" : " " )}] Noyau WSL2 mis à jour", + $"[{( _state.VirtualEnvironmentInstalled ? "x" : " " )}] WSL2 installé", + $"[{( _state.WslDefaultVersionSet ? "x" : " " )}] WSL2 défini comme version par défaut", + $"[{( wsl2Operational ? "x" : " " )}] WSL2 opérationnel" + }; + + if (_state.WslFeaturesEnabled && _state.WslPackageInstalled && _state.WslKernelUpdated && _state.VirtualEnvironmentInstalled && _state.WslDefaultVersionSet && wsl2Operational) + steps.Add("\nTerminé. Cliquez sur Continuer."); + + return string.Join("\n", steps); + } + + private async void InstallVirtualEnvironment() { _btnNext.Enabled = false; _btnBack.Enabled = false; + UseWaitCursor = true; try { - RunCommand("wsl.exe", "--install --no-distribution", "Installation de l'environnement virtuel en cours..."); - _state.VirtualEnvironmentInstalled = true; - _state.Save(); - RegisterRunOnce(); - GoToStep(WizardStep.RestartRequired); + // Si WSL2 est déjà prêt, on marque toutes les étapes comme faites et on continue. + if (PrerequisiteChecker.IsWSL2Ready()) + { + MarkWslInstallationComplete(); + SetStatus(null); + GoToStep(WizardStep.InstallPodman); + return; + } + + // Étape 1 : activer les fonctionnalités Windows requises par WSL2. + if (!_state.WslFeaturesEnabled) + { + SetStatus("Activation des fonctionnalités Windows requises... Cela peut prendre plusieurs minutes."); + var rebootRequired = await Task.Run(() => + { + var r = false; + r |= EnableWindowsFeature("Microsoft-Windows-Subsystem-Linux"); + r |= EnableWindowsFeature("VirtualMachinePlatform"); + + // Fonctionnalités Hyper-V optionnelles mais utiles pour WSL2. + // Elles peuvent ne pas exister sur Windows Home : on ignore les erreurs. + TryEnableWindowsFeature("Microsoft-Hyper-V-All"); + TryEnableWindowsFeature("Microsoft-Hyper-V"); + TryEnableWindowsFeature("HypervisorPlatform"); + + return r; + }); + _state.WslFeaturesEnabled = true; + _state.Save(); + + if (rebootRequired) + { + // Au moins une fonctionnalité nécessite un redémarrage. + RegisterRunOnce(); + GoToStep(WizardStep.RestartRequired); + return; + } + } + + // Étape 2 : installer le package WSL2 complet offline si disponible. + // Le package Microsoft.WSL.msixbundle remplace l'installation via le Store + // et inclut le noyau WSL2. + if (!_state.WslPackageInstalled) + { + SetStatus("Installation du package WSL2..."); + var packageInstalled = await Task.Run(() => TryInstallWslAppxBundle()); + if (packageInstalled) + { + _state.WslPackageInstalled = true; + _state.WslKernelUpdated = true; // Le bundle inclut le noyau. + _state.Save(); + + if (CheckWsl2ReadyAndContinue()) return; + } + } + + // Étape 3 : installer/mettre à jour le noyau WSL2 si ce n'est pas déjà fait. + // Fallback sur le MSI noyau bundlé ou sur Windows Update (wsl --update). + if (!_state.WslKernelUpdated) + { + SetStatus("Mise à jour du noyau WSL2..."); + await Task.Run(() => TryInstallWslKernelMsi()); + _state.WslKernelUpdated = true; + _state.Save(); + + if (CheckWsl2ReadyAndContinue()) return; + } + + // Étape 4 : définir WSL2 comme version par défaut. + if (!_state.WslDefaultVersionSet) + { + SetStatus("Définition de WSL2 comme version par défaut..."); + await Task.Run(() => RunCommand("wsl.exe", "--set-default-version 2", "Définition de WSL2 comme version par défaut")); + _state.WslDefaultVersionSet = true; + _state.Save(); + } + + SetStatus(null); + GoToStep(WizardStep.InstallPodman); } catch (Exception ex) { @@ -355,13 +530,122 @@ public partial class MainForm : Form { _btnNext.Enabled = true; _btnBack.Enabled = true; + UseWaitCursor = false; + } + } + + private void MarkWslInstallationComplete() + { + _state.WslFeaturesEnabled = true; + _state.WslPackageInstalled = true; + _state.WslKernelUpdated = true; + _state.VirtualEnvironmentInstalled = true; + _state.WslDefaultVersionSet = true; + _state.Save(); + } + + private bool CheckWsl2ReadyAndContinue() + { + if (!PrerequisiteChecker.IsWSL2Ready()) + return false; + + MarkWslInstallationComplete(); + SetStatus(null); + GoToStep(WizardStep.InstallPodman); + return true; + } + + private bool TryInstallWslAppxBundle() + { + var resourcesDir = Path.Combine(AppContext.BaseDirectory, "Resources"); + if (!Directory.Exists(resourcesDir)) + return false; + + var bundlePath = Directory.EnumerateFiles(resourcesDir, "Microsoft.WSL_*.msixbundle").FirstOrDefault(); + if (string.IsNullOrEmpty(bundlePath)) + return false; + + SetStatus("Installation du package WSL2 depuis le bundle local..."); + var script = $"Add-AppxPackage -Path \"{bundlePath}\""; + RunCommand( + "powershell.exe", + $"-ExecutionPolicy Bypass -Command \"{script}\"", + "Installation du package WSL2 (Add-AppxPackage)"); + + return true; + } + + private void TryInstallWslKernelMsi() + { + var kernelMsiPath = Path.Combine(AppContext.BaseDirectory, "Resources", "wsl_update_x64.msi"); + + if (File.Exists(kernelMsiPath)) + { + // Mode offline : installation du noyau WSL2 depuis le MSI bundlé. + SetStatus("Installation du noyau WSL2 depuis le package local..."); + RunCommand("msiexec.exe", $"/i \"{kernelMsiPath}\" /qn /norestart", "Installation du noyau WSL2"); + return; + } + + // Mode online : mise à jour via Windows Update. + SetStatus("Téléchargement et mise à jour du noyau WSL2 (nécessite Internet)..."); + RunCommand("wsl.exe", "--update", "Mise à jour du noyau WSL2", additionalSuccessCodes: new[] { 3010 }); + } + + private void SetStatus(string? text) + { + if (string.IsNullOrEmpty(text)) + { + _statusLabel.Visible = false; + _statusLabel.Text = string.Empty; + } + else + { + _statusLabel.Text = text; + _statusLabel.Visible = true; + } + } + + private bool EnableWindowsFeature(string featureName) + { + // 3010 = succès, mais un redémarrage est nécessaire. + // L'activation des fonctionnalités Windows peut prendre plusieurs minutes + // (voire plus de 10 min sur certaines machines), on allonge donc le timeout. + var exitCode = RunCommand( + "dism.exe", + $"/online /enable-feature /featurename:{featureName} /all /norestart", + $"Activation de la fonctionnalité Windows {featureName}", + additionalSuccessCodes: new[] { 3010 }, + timeoutSeconds: 1200); + + return exitCode == 3010; + } + + private void TryEnableWindowsFeature(string featureName) + { + try + { + // 3010 = succès, mais un redémarrage est nécessaire. + var exitCode = RunCommand( + "dism.exe", + $"/online /enable-feature /featurename:{featureName} /all /norestart", + $"Activation de la fonctionnalité Windows {featureName}", + additionalSuccessCodes: new[] { 3010 }, + timeoutSeconds: 1200); + + // Si la fonctionnalité a été activée avec succès et nécessite un redémarrage, + // on ne force pas ici : les fonctionnalités obligatoires gèrent déjà le redémarrage. + } + catch + { + // Fonctionnalité optionnelle ou indisponible sur cette édition de Windows : on ignore. } } private void ShowRestartRequired() { _titleLabel.Text = "Redémarrage nécessaire"; - _subtitleLabel.Text = "L'environnement virtuel a été installé."; + _subtitleLabel.Text = "Les fonctionnalités Windows ont été activées."; var label = new Label { @@ -369,8 +653,8 @@ public partial class MainForm : Form Top = 20, Width = _contentPanel.Width, Height = 120, - Text = "Veuillez redémarrer votre ordinateur pour finaliser l'installation de l'environnement virtuel.\n\n" + - "L'installateur se relancera automatiquement après le redémarrage pour continuer l'installation.", + Text = "Veuillez redémarrer votre ordinateur pour finaliser l'activation des fonctionnalités Windows.\n\n" + + "L'installateur se relancera automatiquement après le redémarrage pour continuer l'installation de WSL2.", Font = new Font("Segoe UI", 10) }; @@ -405,10 +689,11 @@ public partial class MainForm : Form _btnNext.Enabled = true; } - private void InstallPodman() + private async void InstallPodman() { _btnNext.Enabled = false; _btnBack.Enabled = false; + UseWaitCursor = true; try { @@ -416,9 +701,17 @@ public partial class MainForm : Form if (!File.Exists(msiPath)) throw new FileNotFoundException("Le fichier podman-installer-windows-amd64.msi est introuvable. Vérifiez qu'il est bien inclus dans le package."); - RunCommand("msiexec.exe", $"/i \"{msiPath}\" /qn /norestart", "Installation de Podman en cours..."); + SetStatus("Installation de Podman en cours..."); + await Task.Run(() => RunCommand("msiexec.exe", $"/i \"{msiPath}\" /qn /norestart", "Installation de Podman en cours...")); _state.PodmanInstalled = true; _state.Save(); + + // Installation de Docker Compose standalone pour que l'agent studioE5 + // puisse utiliser la commande `docker-compose` avec Podman. + SetStatus("Installation de Docker Compose..."); + await Task.Run(() => InstallDockerCompose()); + + SetStatus(null); GoToStep(WizardStep.ConfigurePodman); } catch (Exception ex) @@ -429,6 +722,91 @@ public partial class MainForm : Form { _btnNext.Enabled = true; _btnBack.Enabled = true; + UseWaitCursor = false; + } + } + + private void InstallDockerCompose() + { + var composeSourcePath = Path.Combine(AppContext.BaseDirectory, "Resources", "docker-compose-windows-x86_64.exe"); + if (!File.Exists(composeSourcePath)) + { + // Docker Compose n'est pas bundlé : on ignore silencieusement. + // L'utilisateur devra l'installer manuellement. + return; + } + + // Cible : même dossier que podman.exe, qui est déjà dans le PATH. + var podmanPath = PrerequisiteChecker.GetPodmanExePath(); + if (string.IsNullOrEmpty(podmanPath)) + { + // Fallback : dossier dédié studioE5-agent. + var fallbackDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "studioE5-agent", + "bin"); + Directory.CreateDirectory(fallbackDir); + var fallbackPath = Path.Combine(fallbackDir, "docker-compose.exe"); + File.Copy(composeSourcePath, fallbackPath, overwrite: true); + AddToPath(fallbackDir); + return; + } + + var podmanDir = Path.GetDirectoryName(podmanPath); + if (!string.IsNullOrEmpty(podmanDir)) + { + var targetPath = Path.Combine(podmanDir, "docker-compose.exe"); + File.Copy(composeSourcePath, targetPath, overwrite: true); + } + } + + private static void AddToPath(string directory) + { + try + { + const string keyPath = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; + using var key = Registry.LocalMachine.OpenSubKey(keyPath, true); + if (key == null) return; + + var currentPath = key.GetValue("Path", "", RegistryValueOptions.DoNotExpandEnvironmentNames) as string ?? ""; + if (currentPath.Contains(directory, StringComparison.OrdinalIgnoreCase)) + return; + + var newPath = currentPath.TrimEnd(';') + ";" + directory; + key.SetValue("Path", newPath, RegistryValueKind.ExpandString); + } + catch + { + // ignored + } + } + + private void EnsureWslConfig() + { + try + { + var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var wslConfigPath = Path.Combine(userProfile, ".wslconfig"); + + const string desiredContent = "[wsl2]\nmemory=8GB\nprocessors=4\n"; + var currentContent = File.Exists(wslConfigPath) ? File.ReadAllText(wslConfigPath) : ""; + + var needsUpdate = !currentContent.Contains("memory=8GB", StringComparison.OrdinalIgnoreCase) || + !currentContent.Contains("processors=4", StringComparison.OrdinalIgnoreCase); + + if (needsUpdate) + { + File.WriteAllText(wslConfigPath, desiredContent); + } + + // Appliquer la configuration en arrêtant WSL. + // La nouvelle configuration sera prise en compte au prochain démarrage. + RunCommand("wsl.exe", "--shutdown", "Arrêt de WSL2 pour appliquer la configuration", timeoutSeconds: 60); + } + catch + { + // On ignore les erreurs : si WSL n'est pas encore installé, la configuration + // sera prise en compte au premier démarrage. } } @@ -455,17 +833,40 @@ public partial class MainForm : Form _btnNext.Enabled = true; } - private void ConfigurePodman() + private async void ConfigurePodman() { _btnNext.Enabled = false; _btnBack.Enabled = false; + UseWaitCursor = true; try { - RunCommand("podman.exe", "machine init", "Initialisation de la machine Podman..."); - RunCommand("podman.exe", "machine start", "Démarrage de la machine Podman..."); + var podmanPath = PrerequisiteChecker.GetPodmanExePath(); + if (string.IsNullOrEmpty(podmanPath)) + throw new FileNotFoundException("podman.exe est introuvable. Vérifiez que Podman est bien installé."); + + // Configurer WSL pour allouer suffisamment de ressources à la machine Podman. + // Par défaut WSL2 n'utilise que 2 Go de RAM, ce qui est insuffisant pour Prestashop. + SetStatus("Configuration des ressources WSL2..."); + await Task.Run(() => EnsureWslConfig()); + + var machineImagePath = GetPodmanMachineImagePath(); + if (!string.IsNullOrEmpty(machineImagePath)) + { + SetStatus("Initialisation de la machine Podman depuis l'image locale..."); + await Task.Run(() => RunCommand(podmanPath, $"machine init --image \"{machineImagePath}\"", "Initialisation de la machine Podman (offline)")); + } + else + { + SetStatus("Initialisation de la machine Podman (téléchargement depuis Internet)..."); + await Task.Run(() => RunCommand(podmanPath, "machine init", "Initialisation de la machine Podman")); + } + + SetStatus("Démarrage de la machine Podman..."); + await Task.Run(() => RunCommand(podmanPath, "machine start", "Démarrage de la machine Podman...")); _state.PodmanConfigured = true; _state.Save(); + SetStatus(null); GoToStep(WizardStep.InstallAgent); } catch (Exception ex) @@ -476,9 +877,19 @@ public partial class MainForm : Form { _btnNext.Enabled = true; _btnBack.Enabled = true; + UseWaitCursor = false; } } + private static string? GetPodmanMachineImagePath() + { + var resourcesDir = Path.Combine(AppContext.BaseDirectory, "Resources"); + if (!Directory.Exists(resourcesDir)) + return null; + + return Directory.EnumerateFiles(resourcesDir, "podman-machine.*.wsl.tar.zst").FirstOrDefault(); + } + private void ShowInstallAgent() { _titleLabel.Text = "Installation de studioE5 Agent"; @@ -631,26 +1042,62 @@ public partial class MainForm : Form } } - private void RunCommand(string fileName, string arguments, string description) + private int RunCommand(string fileName, string arguments, string description, int[]? additionalSuccessCodes = null, int timeoutSeconds = 300) { var psi = new ProcessStartInfo(fileName, arguments) { 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) throw new InvalidOperationException($"Impossible de démarrer {fileName}"); - process.WaitForExit(); - if (process.ExitCode != 0) + // Lire stdout/stderr en parallèle pour éviter le blocage du processus + // lorsque ses buffers de sortie sont pleins. + var stdoutTask = Task.Run(() => process.StandardOutput.ReadToEnd()); + var stderrTask = Task.Run(() => process.StandardError.ReadToEnd()); + + if (!process.WaitForExit(timeoutSeconds * 1000)) { - var error = process.StandardError.ReadToEnd(); - throw new InvalidOperationException($"{description} a échoué (code {process.ExitCode}) : {error}"); + try { process.Kill(); } catch { /* ignored */ } + throw new TimeoutException($"{description} a dépassé le délai de {timeoutSeconds} secondes."); } + + Task.WaitAll(stdoutTask, stderrTask); + + var exitCode = process.ExitCode; + if (exitCode != 0 && !(additionalSuccessCodes?.Contains(exitCode) ?? false)) + { + throw new InvalidOperationException($"{description} a échoué (code {exitCode}) : {stderrTask.Result}"); + } + + return exitCode; + } + + private static string GetVersion() + { + try + { + var versionPath = Path.Combine(AppContext.BaseDirectory, "VERSION"); + if (File.Exists(versionPath)) + { + var version = File.ReadAllText(versionPath).Trim(); + if (!string.IsNullOrWhiteSpace(version)) + return version; + } + } + catch + { + // ignored + } + + return "0.0.0"; } private void RegisterRunOnce() diff --git a/agent/installer/setup-wizard/PrerequisiteChecker.cs b/agent/installer/setup-wizard/PrerequisiteChecker.cs index 54f45c2..8b94fc7 100644 --- a/agent/installer/setup-wizard/PrerequisiteChecker.cs +++ b/agent/installer/setup-wizard/PrerequisiteChecker.cs @@ -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 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 { @@ -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; diff --git a/agent/installer/setup-wizard/README.md b/agent/installer/setup-wizard/README.md index 2f84b41..bdee858 100644 --- a/agent/installer/setup-wizard/README.md +++ b/agent/installer/setup-wizard/README.md @@ -29,9 +29,14 @@ setup-wizard/ ├── MainForm.cs ├── InstallerState.cs ├── PrerequisiteChecker.cs +├── app.manifest └── Resources/ - ├── podman-installer-windows-amd64.msi # MSI officiel Podman pour Windows - └── studioE5-agent-setup.exe # Package Inno Setup de l'agent + ├── podman-installer-windows-amd64.msi # MSI officiel Podman pour Windows + ├── studioE5-agent-setup.exe # Package Inno Setup de l'agent + ├── Microsoft.WSL_2.7.10.0_x64_ARM64.msixbundle # Package WSL2 complet (offline) + ├── podman-machine.x86_64.wsl.tar.zst # Image Podman machine pour WSL (offline) + ├── docker-compose-windows-x86_64.exe # Docker Compose standalone (offline) + └── wsl_update_x64.msi # Noyau WSL2 (optionnel, fallback) ``` ## Build @@ -58,9 +63,25 @@ bin\Release\net8.0-windows\win-x64\publish\StudioE5-SetupWizard.exe 1. Télécharger le MSI Podman Windows : -2. Le renommer en `podman-installer-windows-amd64.msi` et le placer dans `Resources/`. -3. Générer le package Inno Setup de l’agent (`studioE5-agent-setup.exe`) et le placer dans `Resources/`. -4. Builder et publier le wizard. + Le renommer en `podman-installer-windows-amd64.msi` et le placer dans `Resources/`. +2. Générer le package Inno Setup de l’agent (`studioE5-agent-setup.exe`) et le placer dans `Resources/`. +3. Télécharger le package WSL2 complet (offline) : + + Par exemple : `Microsoft.WSL_2.7.10.0_x64_ARM64.msixbundle`. + Le placer dans `Resources/`. +4. Télécharger l’image Podman machine pour WSL (offline) : + + Par exemple : `podman-machine.x86_64.wsl.tar.zst`. + Le placer dans `Resources/`. +5. Télécharger Docker Compose standalone (offline) : + + Par exemple : `docker-compose-windows-x86_64.exe`. + Le placer dans `Resources/`. +6. *(Optionnel, fallback)* Télécharger le noyau WSL2 : + + Par exemple : `wsl.2.7.10.0.x64.msi`, à renommer en `wsl_update_x64.msi`. + Le placer dans `Resources/`. +6. Builder et publier le wizard. ## Lancement @@ -80,4 +101,5 @@ bin\Release\net8.0-windows\win-x64\publish\StudioE5-SetupWizard.exe - Le wizard doit être exécuté **en administrateur**. - L’installation de WSL2 nécessite un **redémarrage** de l’ordinateur. Le wizard s’enregistre dans `RunOnce` pour se relancer automatiquement. +- Le wizard configure WSL2 avec **8 Go de RAM et 4 CPU** via le fichier `.wslconfig` de l’utilisateur. - Le MSI Podman doit correspondre à l’architecture `x64`. diff --git a/agent/installer/setup-wizard/SUIVI_INSTALLER.md b/agent/installer/setup-wizard/SUIVI_INSTALLER.md new file mode 100644 index 0000000..5f11dc8 --- /dev/null +++ b/agent/installer/setup-wizard/SUIVI_INSTALLER.md @@ -0,0 +1,127 @@ +# Feuille de route — Installateur studioE5 Agent + +## Objectif + +Fournir un **installateur professionnel Windows** pour studioE5 Agent, guidé pas à pas, qui gère les prérequis (WSL2 / Podman) et propose une désinstallation complète. + +## Architecture choisie + +- **Wizard C# Windows Forms (.NET 8)** : `setup-wizard/` + - Détecte les prérequis. + - Installe WSL2 si besoin (avec reprise après redémarrage via `RunOnce`). + - Installe Podman depuis le MSI officiel. + - Initialise et démarre la machine Podman. + - Lance le package Inno Setup de studioE5 Agent. + - Mode désinstallation via `/uninstall`. +- **Package agent (Inno Setup)** : `studioE5-agent.iss` + - Installe `studioE5-agent.exe` + binaires Tailscale. + - Crée les raccourcis. + - Gère la désinstallation. + +## État actuel + +### ✅ Réalisé + +- Wizard C# avec 7 étapes guidées. +- Détection des prérequis : Windows, RAM, disque, WSL2, Podman. +- Installation WSL2 en plusieurs étapes contrôlées avec suivi visuel : + 1. Activation des fonctionnalités Windows (`Microsoft-Windows-Subsystem-Linux` et `VirtualMachinePlatform`) avec gestion du code 3010 (redémarrage nécessaire). + 2. Installation du package WSL2 complet depuis le bundle Microsoft Store offline (`Microsoft.WSL_*.msixbundle`) via `Add-AppxPackage`. + 3. Mise à jour du noyau WSL2 depuis le MSI bundlé (`wsl_update_x64.msi`) ou, en dernier recours, via `wsl --update`. + 4. Définition de WSL2 comme version par défaut (`wsl --set-default-version 2`). + 5. L’étape `wsl --install --no-distribution` n’est plus utilisée : l’installation est entièrement offline grâce au bundle. + 6. Conversion des distributions WSL1 existantes vers WSL2 (`wsl --set-version 2`) si nécessaire. +- Reprise automatique après redémarrage grâce à `RunOnce` (y compris redémarrage post-installation WSL2). +- Versioning indépendant du wizard (fichier `setup-wizard/VERSION`, affiché dans la fenêtre et la page d’accueil). +- Amélioration de l’interface : fenêtre agrandie à 800×600, `AutoScaleMode = Dpi`, meilleur rendu sur petits écrans (11 pouces). +- `RunCommand` capture désormais stdout + stderr pour des diagnostics et fallbacks plus fiables. +- Détection des prérequis vérifie que WSL2 (et non WSL1) est installé via `wsl --status`. +- Installation Podman via MSI bundlé. +- Configuration Podman (`machine init` + `machine start`), avec initialisation offline possible depuis une image locale (`podman-machine.*.wsl.tar.zst`). +- Lancement du package Inno Setup agent. +- Mode désinstallation complet. +- Script Inno Setup de base pour l’agent. + +### 🔄 En cours / À tester + +- Compilation et test du wizard sur Windows. +- Packaging final (wizard + MSI Podman + setup agent) en un seul dossier distribuable. + +### ⏳ À venir + +- Signature de l’exécutable pour éviter les alertes SmartScreen. +- Support macOS et Linux. +- Installateur silencieux possible pour déploiement GPO. + +## Build du wizard + +### Prérequis + +- Windows 10/11 +- .NET 8 SDK +- Inno Setup 6 (pour générer `studioE5-agent-setup.exe`) + +### Fichiers à placer + +Dans `setup-wizard/Resources/` : + +```text +podman-installer-windows-amd64.msi +studioE5-agent-setup.exe +Microsoft.WSL_2.7.10.0_x64_ARM64.msixbundle # package WSL2 complet (offline) +podman-machine.x86_64.wsl.tar.zst # image Podman machine pour WSL (offline) +docker-compose-windows-x86_64.exe # Docker Compose standalone (offline) +wsl_update_x64.msi # optionnel, fallback noyau WSL2 +``` + +### Commande + +```powershell +cd setup-wizard +dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true +``` + +### Sortie + +```text +setup-wizard\bin\Release\net8.0-windows\win-x64\publish\StudioE5-SetupWizard.exe +``` + +## Build du package agent (Inno Setup) + +Structure attendue : + +```text +agent/ +├── studioE5-agent.exe +├── tailscale-bin/ +│ └── windows/ +│ ├── tailscale.exe +│ ├── tailscaled.exe +│ └── wintun.dll +└── installer/ + └── studioE5-agent.iss +``` + +Ouvrir `studioE5-agent.iss` avec Inno Setup Compiler et compiler (`Ctrl+F9`). + +Le fichier généré se trouve dans `installer-output/`. + +## Notes importantes + +- Le wizard doit être exécuté **en administrateur**. +- L’installation de WSL2 nécessite un **redémarrage** de l’ordinateur après l’activation des fonctionnalités Windows. +- L’installation de WSL2 est découpée en étapes et chaque étape est enregistrée pour permettre la reprise après redémarrage. +- Podman nécessite WSL2 : le wizard vérifie donc que la version par défaut de WSL est 2. +- Le MSI Podman officiel pour Windows est `podman-installer-windows-amd64.msi`. +- Le bundle WSL2 offline est disponible sur : `Microsoft.WSL_2.7.10.0_x64_ARM64.msixbundle`. +- L’image Podman machine offline est disponible sur : `podman-machine.x86_64.wsl.tar.zst`. +- Docker Compose standalone est disponible sur : `docker-compose-windows-x86_64.exe`. +- Le wizard crée automatiquement un fichier `.wslconfig` allouant **8 Go de RAM et 4 CPU** à WSL2, ce qui est nécessaire pour des applications lourdes comme PrestaShop. +- Pour la désinstallation, le MSI Podman doit être présent dans `Resources/`. + +## Liens utiles + +- Releases Podman : +- Inno Setup : +- .NET 8 SDK : diff --git a/agent/installer/setup-wizard/SetupWizard.csproj b/agent/installer/setup-wizard/SetupWizard.csproj index 43d914f..1903f54 100644 --- a/agent/installer/setup-wizard/SetupWizard.csproj +++ b/agent/installer/setup-wizard/SetupWizard.csproj @@ -6,8 +6,10 @@ enable true enable + fr StudioE5.SetupWizard StudioE5-SetupWizard + app.manifest @@ -15,10 +17,16 @@ - + + PreserveNewest - + + + + + + PreserveNewest diff --git a/agent/installer/setup-wizard/VERSION b/agent/installer/setup-wizard/VERSION new file mode 100644 index 0000000..17e51c3 --- /dev/null +++ b/agent/installer/setup-wizard/VERSION @@ -0,0 +1 @@ +0.1.1 diff --git a/agent/installer/setup-wizard/app.manifest b/agent/installer/setup-wizard/app.manifest new file mode 100644 index 0000000..df2f4db --- /dev/null +++ b/agent/installer/setup-wizard/app.manifest @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/agent/installer/setup-wizard/install-wsl2.ps1 b/agent/installer/setup-wizard/install-wsl2.ps1 new file mode 100644 index 0000000..7b27cd0 --- /dev/null +++ b/agent/installer/setup-wizard/install-wsl2.ps1 @@ -0,0 +1,120 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Installe ou répare WSL2 de manière fiable. +.DESCRIPTION + Ce script : + 1. Vérifie si WSL2 est déjà prêt. + 2. Active les fonctionnalités Windows nécessaires. + 3. Définit WSL2 comme version par défaut. + 4. Met à jour le noyau WSL2. + 5. Installe WSL sans distribution si possible. + Un redémarrage peut être nécessaire après l’activation des fonctionnalités. +#> + +$ErrorActionPreference = "Stop" + +function Test-Wsl2Ready { + try { + $output = & wsl.exe --status 2>&1 + $exitCode = $LASTEXITCODE + Write-Host "[Test] wsl --status exit code: $exitCode" -ForegroundColor Cyan + if ($output) { + Write-Host "[Test] wsl --status output:" -ForegroundColor Cyan + $output | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + } + + if ($exitCode -eq 0 -or ($output -match "Version par défaut\s*:\s*2") -or ($output -match "Default Version\s*:\s*2")) { + return $true + } + } + catch { + Write-Host "[Test] wsl --status a échoué : $_" -ForegroundColor Yellow + } + return $false +} + +function Enable-WindowsFeatureIfNeeded { + param([string]$FeatureName) + + Write-Host "[Feature] Activation de $FeatureName..." -ForegroundColor Cyan + $result = & dism.exe /online /enable-feature /featurename:$FeatureName /all /norestart 2>&1 + $exitCode = $LASTEXITCODE + $result | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + + if ($exitCode -eq 0) { + Write-Host "[Feature] $FeatureName activé (pas de redémarrage nécessaire)." -ForegroundColor Green + return $false + } + elseif ($exitCode -eq 3010) { + Write-Host "[Feature] $FeatureName activé, mais un redémarrage est nécessaire (code 3010)." -ForegroundColor Yellow + return $true + } + else { + throw "Échec de l'activation de $FeatureName (code $exitCode)." + } +} + +function Install-Wsl2 { + Write-Host "[WSL] Tentative d'installation sans distribution..." -ForegroundColor Cyan + try { + & wsl.exe --install --no-distribution 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + if ($LASTEXITCODE -ne 0) { throw "wsl --install --no-distribution a retourné le code $LASTEXITCODE" } + Write-Host "[WSL] Installation sans distribution réussie." -ForegroundColor Green + return + } + catch { + Write-Host "[WSL] Option --no-distribution non supportée ou échec : $_" -ForegroundColor Yellow + } + + Write-Host "[WSL] Fallback : installation classique de WSL..." -ForegroundColor Cyan + & wsl.exe --install 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + if ($LASTEXITCODE -ne 0) { throw "wsl --install a retourné le code $LASTEXITCODE" } + Write-Host "[WSL] Installation classique réussie." -ForegroundColor Green +} + +# === Début du script === + +Write-Host "=== Installation / réparation WSL2 ===" -ForegroundColor Green + +if (Test-Wsl2Ready) { + Write-Host "WSL2 est déjà prêt. Rien à faire." -ForegroundColor Green + exit 0 +} + +Write-Host "WSL2 n'est pas détecté. Lancement de l'installation..." -ForegroundColor Yellow + +$rebootNeeded = $false +$rebootNeeded = (Enable-WindowsFeatureIfNeeded -FeatureName "Microsoft-Windows-Subsystem-Linux") -or $rebootNeeded +$rebootNeeded = (Enable-WindowsFeatureIfNeeded -FeatureName "VirtualMachinePlatform") -or $rebootNeeded + +if ($rebootNeeded) { + Write-Host "`nUn redémarrage est nécessaire pour activer les fonctionnalités Windows." -ForegroundColor Yellow + Write-Host "Après le redémarrage, relance ce script pour terminer l'installation de WSL2." -ForegroundColor Yellow + + $response = Read-Host "Redémarrer maintenant ? (O/N)" + if ($response -eq "O" -or $response -eq "o") { + Restart-Computer -Force + } + exit 3010 +} + +Write-Host "[WSL] Définition de WSL2 comme version par défaut..." -ForegroundColor Cyan +& wsl.exe --set-default-version 2 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } +if ($LASTEXITCODE -ne 0) { throw "wsl --set-default-version 2 a échoué (code $LASTEXITCODE)." } + +Write-Host "[WSL] Mise à jour du noyau WSL2..." -ForegroundColor Cyan +& wsl.exe --update 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } +# 3010 = succès mais redémarrage possible +if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) { throw "wsl --update a échoué (code $LASTEXITCODE)." } + +Install-Wsl2 + +if (Test-Wsl2Ready) { + Write-Host "`nWSL2 est maintenant prêt." -ForegroundColor Green + exit 0 +} +else { + Write-Host "`nWSL2 ne semble toujours pas prêt. Essayez de redémarrer et de relancer le script." -ForegroundColor Red + exit 1 +} diff --git a/agent/studioE5-agent-setup-wizard.zip b/agent/studioE5-agent-setup-wizard.zip index a9bf299bc1bbec8e7c34b7961e2fbce0b49511d2..dc227cf8c730accc5747f3c523d2fbf1ce6d4d58 100644 GIT binary patch literal 19138 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 literal 12552 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