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 a9bf299..dc227cf 100644 Binary files a/agent/studioE5-agent-setup-wizard.zip and b/agent/studioE5-agent-setup-wizard.zip differ diff --git a/prestashop-image/README.md b/prestashop-image/README.md index 1d06304..dd2fc1a 100644 --- a/prestashop-image/README.md +++ b/prestashop-image/README.md @@ -30,9 +30,9 @@ docker build -t edubox-prestashop:9 . ```bash docker tag edubox-prestashop:9 \ - 151.80.60.98:3001/yacine/edubox/edubox-prestashop:9-edubox-9 + gitea.alfrednobel.edudeploy.com/yacine/edubox/edubox-prestashop:9-edubox-9 docker push \ - 151.80.60.98:3001/yacine/edubox/edubox-prestashop:9-edubox-9 + gitea.alfrednobel.edudeploy.com/yacine/edubox/edubox-prestashop:9-edubox-9 ``` ## Patches appliqués @@ -75,7 +75,7 @@ Le template PrestaShop 9 dans `server/prisma/seed.ts` utilise cette image : ```yaml app: - image: 151.80.60.98:3001/yacine/edubox/edubox-prestashop:9-edubox-8 + image: gitea.alfrednobel.edudeploy.com/yacine/edubox/edubox-prestashop:9-edubox-8 ``` ## Mise à jour vers une nouvelle version de PrestaShop diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts index eb3b523..1e725d2 100644 --- a/server/prisma/seed.ts +++ b/server/prisma/seed.ts @@ -84,7 +84,7 @@ async function main() { { name: "PrestaShop 9 vierge (edubox)", type: "prestashop", - dockerImage: "151.80.60.98:3001/yacine/edubox/edubox-prestashop:9-edubox-9", + dockerImage: "gitea.alfrednobel.edudeploy.com/yacine/edubox/edubox-prestashop:9-edubox-9", dbImage: "mariadb:10.11", dbName: "prestashop", dbUser: "prestashop",