From 0f07a2d2a38ec677cb6da7602cf52b659b600ebb Mon Sep 17 00:00:00 2001 From: EduBox Dev Date: Sun, 28 Jun 2026 20:49:57 +0000 Subject: [PATCH] installer: wizard C# Windows d'installation guidee (WSL2, Podman, agent, desinstallation) --- .../installer/setup-wizard/InstallerState.cs | 78 +++ agent/installer/setup-wizard/MainForm.cs | 662 ++++++++++++++++++ .../setup-wizard/PrerequisiteChecker.cs | 142 ++++ agent/installer/setup-wizard/Program.cs | 19 + agent/installer/setup-wizard/README.md | 83 +++ .../installer/setup-wizard/SetupWizard.csproj | 22 + agent/installer/studioE5-agent.iss | 190 +++++ 7 files changed, 1196 insertions(+) create mode 100644 agent/installer/setup-wizard/InstallerState.cs create mode 100644 agent/installer/setup-wizard/MainForm.cs create mode 100644 agent/installer/setup-wizard/PrerequisiteChecker.cs create mode 100644 agent/installer/setup-wizard/Program.cs create mode 100644 agent/installer/setup-wizard/README.md create mode 100644 agent/installer/setup-wizard/SetupWizard.csproj create mode 100644 agent/installer/studioE5-agent.iss diff --git a/agent/installer/setup-wizard/InstallerState.cs b/agent/installer/setup-wizard/InstallerState.cs new file mode 100644 index 0000000..ada7459 --- /dev/null +++ b/agent/installer/setup-wizard/InstallerState.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace StudioE5.SetupWizard; + +public enum WizardStep +{ + Welcome, + Prerequisites, + InstallVirtualEnvironment, + RestartRequired, + InstallPodman, + ConfigurePodman, + InstallAgent, + Finished, + Uninstall +} + +public class InstallerState +{ + [JsonPropertyName("step")] + public WizardStep Step { get; set; } = WizardStep.Welcome; + + [JsonPropertyName("virtualEnvironmentInstalled")] + public bool VirtualEnvironmentInstalled { get; set; } + + [JsonPropertyName("podmanInstalled")] + public bool PodmanInstalled { get; set; } + + [JsonPropertyName("podmanConfigured")] + public bool PodmanConfigured { get; set; } + + [JsonPropertyName("agentInstalled")] + public bool AgentInstalled { get; set; } + + private static string StateFilePath + { + get + { + var dir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "studioE5", + "installer"); + Directory.CreateDirectory(dir); + return Path.Combine(dir, "installer-state.json"); + } + } + + public static InstallerState Load() + { + var path = StateFilePath; + if (!File.Exists(path)) + return new InstallerState(); + + try + { + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json) ?? new InstallerState(); + } + catch + { + return new InstallerState(); + } + } + + public void Save() + { + var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(StateFilePath, json); + } + + public static void Delete() + { + var path = StateFilePath; + if (File.Exists(path)) + File.Delete(path); + } +} diff --git a/agent/installer/setup-wizard/MainForm.cs b/agent/installer/setup-wizard/MainForm.cs new file mode 100644 index 0000000..5e9a5c4 --- /dev/null +++ b/agent/installer/setup-wizard/MainForm.cs @@ -0,0 +1,662 @@ +using System.Diagnostics; +using System.Reflection; +using Microsoft.Win32; + +namespace StudioE5.SetupWizard; + +public partial class MainForm : Form +{ + private readonly InstallerState _state; + private readonly Panel _contentPanel; + private readonly Button _btnBack; + private readonly Button _btnNext; + private readonly Button _btnCancel; + private readonly Label _titleLabel; + private readonly Label _subtitleLabel; + + private WizardStep _currentStep = WizardStep.Welcome; + private PrerequisiteResult? _lastCheck; + + public MainForm(bool startInUninstallMode = false) + { + _state = InstallerState.Load(); + Text = startInUninstallMode ? "Désinstallation studioE5" : "Installateur studioE5 Agent"; + Size = new Size(700, 520); + StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + + _titleLabel = new Label + { + Left = 24, + Top = 18, + Width = 640, + Height = 32, + Font = new Font("Segoe UI", 14, FontStyle.Bold), + Text = "Installateur studioE5 Agent" + }; + + _subtitleLabel = new Label + { + Left = 24, + Top = 50, + Width = 640, + Height = 24, + Font = new Font("Segoe UI", 9), + ForeColor = Color.Gray + }; + + _contentPanel = new Panel + { + Left = 24, + Top = 84, + Width = 640, + Height = 320, + BorderStyle = BorderStyle.None + }; + + var separator = new Panel + { + Left = 0, + Top = 416, + Width = 700, + Height = 1, + BackColor = Color.LightGray, + Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom + }; + + _btnBack = new Button + { + Left = 360, + Top = 440, + Width = 100, + Height = 30, + Text = "< Précédent", + Visible = false + }; + _btnBack.Click += (s, e) => GoBack(); + + _btnNext = new Button + { + Left = 470, + Top = 440, + Width = 100, + Height = 30, + Text = "Suivant >" + }; + _btnNext.Click += (s, e) => GoNext(); + + _btnCancel = new Button + { + Left = 584, + Top = 440, + Width = 80, + Height = 30, + Text = "Annuler" + }; + _btnCancel.Click += (s, e) => Application.Exit(); + + Controls.Add(_titleLabel); + Controls.Add(_subtitleLabel); + Controls.Add(_contentPanel); + Controls.Add(separator); + Controls.Add(_btnBack); + Controls.Add(_btnNext); + Controls.Add(_btnCancel); + + if (startInUninstallMode) + { + GoToStep(WizardStep.Uninstall); + return; + } + + ResumeAfterReboot(); + } + + private void ResumeAfterReboot() + { + // If we are resuming after a reboot, skip directly to the relevant step. + if (_state.VirtualEnvironmentInstalled && !_state.PodmanInstalled) + { + GoToStep(WizardStep.InstallPodman); + return; + } + if (_state.PodmanInstalled && !_state.PodmanConfigured) + { + GoToStep(WizardStep.ConfigurePodman); + return; + } + if (_state.PodmanConfigured && !_state.AgentInstalled) + { + GoToStep(WizardStep.InstallAgent); + return; + } + + GoToStep(WizardStep.Welcome); + } + + private void GoBack() + { + switch (_currentStep) + { + case WizardStep.Prerequisites: + GoToStep(WizardStep.Welcome); + break; + case WizardStep.InstallVirtualEnvironment: + case WizardStep.RestartRequired: + GoToStep(WizardStep.Prerequisites); + break; + case WizardStep.InstallPodman: + GoToStep(WizardStep.Prerequisites); + break; + case WizardStep.ConfigurePodman: + GoToStep(WizardStep.InstallPodman); + break; + case WizardStep.InstallAgent: + GoToStep(WizardStep.ConfigurePodman); + break; + } + } + + private void GoNext() + { + switch (_currentStep) + { + case WizardStep.Welcome: + GoToStep(WizardStep.Prerequisites); + break; + case WizardStep.Prerequisites: + if (_lastCheck?.VirtualEnvironmentInstalled == true) + GoToStep(WizardStep.InstallPodman); + else + GoToStep(WizardStep.InstallVirtualEnvironment); + break; + case WizardStep.InstallVirtualEnvironment: + InstallVirtualEnvironment(); + break; + case WizardStep.RestartRequired: + Application.Exit(); + break; + case WizardStep.InstallPodman: + InstallPodman(); + break; + case WizardStep.ConfigurePodman: + ConfigurePodman(); + break; + case WizardStep.InstallAgent: + InstallAgent(); + break; + case WizardStep.Finished: + Application.Exit(); + break; + case WizardStep.Uninstall: + RunUninstall(); + break; + } + } + + private void GoToStep(WizardStep step) + { + _currentStep = step; + _state.Step = step; + _state.Save(); + _contentPanel.Controls.Clear(); + + switch (step) + { + case WizardStep.Welcome: + ShowWelcome(); + break; + case WizardStep.Prerequisites: + ShowPrerequisites(); + break; + case WizardStep.InstallVirtualEnvironment: + ShowInstallVirtualEnvironment(); + break; + case WizardStep.RestartRequired: + ShowRestartRequired(); + break; + case WizardStep.InstallPodman: + ShowInstallPodman(); + break; + case WizardStep.ConfigurePodman: + ShowConfigurePodman(); + break; + case WizardStep.InstallAgent: + ShowInstallAgent(); + break; + case WizardStep.Finished: + ShowFinished(); + break; + case WizardStep.Uninstall: + ShowUninstall(); + break; + } + } + + private void ShowWelcome() + { + _titleLabel.Text = "Bienvenue dans l'installateur studioE5"; + _subtitleLabel.Text = "Cet assistant va vous guider pour installer studioE5 Agent sur votre poste."; + + var label = new Label + { + Left = 0, + Top = 20, + Width = _contentPanel.Width, + Height = 200, + Text = "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" + + "4. Configuration de Podman\n" + + "5. Installation de studioE5 Agent\n\n" + + "Cliquez sur 'Suivant' pour commencer.", + Font = new Font("Segoe UI", 10) + }; + + _contentPanel.Controls.Add(label); + + _btnBack.Visible = false; + _btnNext.Text = "Suivant >"; + _btnNext.Enabled = true; + _btnCancel.Text = "Annuler"; + } + + private void ShowPrerequisites() + { + _titleLabel.Text = "Vérification des prérequis"; + _subtitleLabel.Text = "Assurez-vous que votre poste est prêt avant de continuer."; + + _lastCheck = PrerequisiteChecker.Check(); + var result = _lastCheck; + + var text = $"Système d'exploitation : {(result.WindowsCompatible ? "✅ Compatible" : "❌ Non compatible (Windows 10 2004+ requis)")}\n" + + $"Mémoire vive (RAM) : {(result.RamMB >= 8192 ? "✅" : result.RamMB >= 4096 ? "⚠️" : "❌")} {result.RamMB} Mo (8 Go recommandés)\n" + + $"Espace disque disponible : {(result.FreeDiskMB >= 10240 ? "✅" : result.FreeDiskMB >= 5120 ? "⚠️" : "❌")} {result.FreeDiskMB} Mo (10 Go recommandés)\n" + + $"Environnement virtuel : {(result.VirtualEnvironmentInstalled ? "✅ Installé" : "❌ Non installé")}\n" + + $"Podman : {(result.PodmanInstalled ? "✅ Installé" : "❌ Non installé")}\n" + + $"Machine Podman : {(result.PodmanMachineReady ? "✅ Prête" : "❌ Non prête")}\n\n"; + + if (result.AllReady) + { + text += "Tous les prérequis sont satisfaits. Vous pouvez continuer."; + } + else + { + text += "Ordre d'installation recommandé :\n" + + "1. Installer l'environnement virtuel\n" + + "2. Installer Podman\n" + + "3. Configurer Podman\n" + + "4. Installer studioE5 Agent"; + } + + var label = new Label + { + Left = 0, + Top = 20, + Width = _contentPanel.Width, + Height = 240, + Text = text, + Font = new Font("Consolas", 10), + ForeColor = Color.Black + }; + + _contentPanel.Controls.Add(label); + + _btnBack.Visible = true; + _btnNext.Text = result.AllReady ? "Suivant >" : (result.VirtualEnvironmentInstalled ? "Installer Podman" : "Installer l'environnement virtuel"); + _btnNext.Enabled = true; + } + + private void ShowInstallVirtualEnvironment() + { + _titleLabel.Text = "Installation de l'environnement virtuel"; + _subtitleLabel.Text = "L'environnement virtuel permet de faire tourner Podman sur Windows."; + + var label = new Label + { + Left = 0, + Top = 20, + Width = _contentPanel.Width, + Height = 120, + 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.", + Font = new Font("Segoe UI", 10) + }; + + _contentPanel.Controls.Add(label); + + _btnBack.Visible = true; + _btnNext.Text = "Installer"; + _btnNext.Enabled = true; + } + + private void InstallVirtualEnvironment() + { + _btnNext.Enabled = false; + _btnBack.Enabled = false; + + try + { + RunCommand("wsl.exe", "--install --no-distribution", "Installation de l'environnement virtuel en cours..."); + _state.VirtualEnvironmentInstalled = true; + _state.Save(); + RegisterRunOnce(); + GoToStep(WizardStep.RestartRequired); + } + catch (Exception ex) + { + MessageBox.Show($"Erreur lors de l'installation de l'environnement virtuel : {ex.Message}", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + _btnNext.Enabled = true; + _btnBack.Enabled = true; + } + } + + private void ShowRestartRequired() + { + _titleLabel.Text = "Redémarrage nécessaire"; + _subtitleLabel.Text = "L'environnement virtuel a été installé."; + + var label = new Label + { + Left = 0, + 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.", + Font = new Font("Segoe UI", 10) + }; + + _contentPanel.Controls.Add(label); + + _btnBack.Visible = false; + _btnNext.Text = "Redémarrer maintenant"; + _btnNext.Enabled = true; + _btnCancel.Text = "Redémarrer plus tard"; + } + + private void ShowInstallPodman() + { + _titleLabel.Text = "Installation de Podman"; + _subtitleLabel.Text = "Podman est le moteur de conteneurs utilisé par studioE5."; + + var label = new Label + { + Left = 0, + Top = 20, + Width = _contentPanel.Width, + Height = 120, + Text = "Podman va être installé sur votre poste.\n\n" + + "Cliquez sur 'Installer' pour lancer l'installation silencieuse.", + Font = new Font("Segoe UI", 10) + }; + + _contentPanel.Controls.Add(label); + + _btnBack.Visible = true; + _btnNext.Text = "Installer Podman"; + _btnNext.Enabled = true; + } + + private void InstallPodman() + { + _btnNext.Enabled = false; + _btnBack.Enabled = false; + + try + { + var msiPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "Resources", "podman-setup.msi"); + if (!File.Exists(msiPath)) + throw new FileNotFoundException("Le fichier podman-setup.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..."); + _state.PodmanInstalled = true; + _state.Save(); + GoToStep(WizardStep.ConfigurePodman); + } + catch (Exception ex) + { + MessageBox.Show($"Erreur lors de l'installation de Podman : {ex.Message}", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + _btnNext.Enabled = true; + _btnBack.Enabled = true; + } + } + + private void ShowConfigurePodman() + { + _titleLabel.Text = "Configuration de Podman"; + _subtitleLabel.Text = "Initialisation de la machine Podman."; + + var label = new Label + { + Left = 0, + Top = 20, + Width = _contentPanel.Width, + Height = 120, + Text = "La machine virtuelle Podman va être créée et démarrée.\n\n" + + "Cette opération peut prendre plusieurs minutes la première fois.", + Font = new Font("Segoe UI", 10) + }; + + _contentPanel.Controls.Add(label); + + _btnBack.Visible = true; + _btnNext.Text = "Configurer Podman"; + _btnNext.Enabled = true; + } + + private void ConfigurePodman() + { + _btnNext.Enabled = false; + _btnBack.Enabled = false; + + try + { + RunCommand("podman.exe", "machine init", "Initialisation de la machine Podman..."); + RunCommand("podman.exe", "machine start", "Démarrage de la machine Podman..."); + _state.PodmanConfigured = true; + _state.Save(); + GoToStep(WizardStep.InstallAgent); + } + catch (Exception ex) + { + MessageBox.Show($"Erreur lors de la configuration de Podman : {ex.Message}", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + _btnNext.Enabled = true; + _btnBack.Enabled = true; + } + } + + private void ShowInstallAgent() + { + _titleLabel.Text = "Installation de studioE5 Agent"; + _subtitleLabel.Text = "L'environnement est prêt. Il ne reste plus qu'à installer l'agent."; + + var label = new Label + { + Left = 0, + Top = 20, + Width = _contentPanel.Width, + Height = 120, + Text = "Cliquez sur 'Installer studioE5 Agent' pour lancer le package d'installation.\n\n" + + "Une fois l'installation terminée, fermez cet assistant.", + Font = new Font("Segoe UI", 10) + }; + + _contentPanel.Controls.Add(label); + + _btnBack.Visible = true; + _btnNext.Text = "Installer studioE5 Agent"; + _btnNext.Enabled = true; + } + + private void InstallAgent() + { + try + { + var setupPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "Resources", "studioE5-agent-setup.exe"); + if (!File.Exists(setupPath)) + throw new FileNotFoundException("Le fichier studioE5-agent-setup.exe est introuvable. Vérifiez qu'il est bien inclus dans le package."); + + var psi = new ProcessStartInfo(setupPath) + { + UseShellExecute = true, + Verb = "runas" + }; + Process.Start(psi); + + _state.AgentInstalled = true; + _state.Save(); + GoToStep(WizardStep.Finished); + } + catch (Exception ex) + { + MessageBox.Show($"Erreur lors du lancement de l'installation de l'agent : {ex.Message}", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void ShowFinished() + { + _titleLabel.Text = "Installation terminée"; + _subtitleLabel.Text = "studioE5 Agent est prêt à être utilisé."; + + var label = new Label + { + Left = 0, + Top = 20, + Width = _contentPanel.Width, + Height = 120, + Text = "L'installation est terminée.\n\n" + + "Vous pouvez lancer studioE5 Agent depuis le menu Démarrer ou le bureau.", + Font = new Font("Segoe UI", 10) + }; + + _contentPanel.Controls.Add(label); + + _btnBack.Visible = false; + _btnNext.Text = "Terminer"; + _btnCancel.Text = "Fermer"; + } + + private void ShowUninstall() + { + _titleLabel.Text = "Désinstallation de studioE5"; + _subtitleLabel.Text = "Suppression complète de studioE5 Agent et des composants associés."; + + var label = new Label + { + Left = 0, + Top = 20, + Width = _contentPanel.Width, + Height = 200, + Text = "Cette opération va :\n\n" + + "- Arrêter studioE5 Agent\n" + + "- Désinstaller studioE5 Agent\n" + + "- Supprimer les données élèves\n" + + "- Désinstaller Podman\n" + + "- Proposer de désactiver l'environnement virtuel\n\n" + + "Cliquez sur 'Désinstaller' pour commencer.", + Font = new Font("Segoe UI", 10) + }; + + _contentPanel.Controls.Add(label); + + _btnBack.Visible = false; + _btnNext.Text = "Désinstaller"; + _btnCancel.Text = "Annuler"; + } + + private void RunUninstall() + { + _btnNext.Enabled = false; + + try + { + // 1. Kill agent + RunCommand("taskkill.exe", "/f /im studioE5-agent.exe", "Arrêt de studioE5 Agent..."); + + // 2. Uninstall agent via Inno Setup uninstaller + var agentUninstaller = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "studioE5-agent", "unins000.exe"); + if (File.Exists(agentUninstaller)) + RunCommand(agentUninstaller, "/SILENT", "Désinstallation de studioE5 Agent..."); + + // 3. Delete student data + var dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "studioE5-agent"); + if (Directory.Exists(dataDir)) + { + Directory.Delete(dataDir, true); + } + + // 4. Uninstall Podman + var podmanMsiPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "Resources", "podman-setup.msi"); + if (File.Exists(podmanMsiPath)) + { + RunCommand("msiexec.exe", $"/x \"{podmanMsiPath}\" /qn /norestart", "Désinstallation de Podman..."); + } + else + { + MessageBox.Show("Le MSI de Podman n'a pas été trouvé dans le package. Veuillez désinstaller Podman manuellement via Ajouter/Supprimer des programmes.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + // 5. Optionally disable WSL2 + var result = MessageBox.Show("Voulez-vous désactiver l'environnement virtuel (WSL2) ?", "Environnement virtuel", MessageBoxButtons.YesNo, MessageBoxIcon.Question); + if (result == DialogResult.Yes) + { + RunCommand("wsl.exe", "--uninstall", "Désactivation de l'environnement virtuel..."); + } + + InstallerState.Delete(); + MessageBox.Show("Désinstallation terminée.", "Terminé", MessageBoxButtons.OK, MessageBoxIcon.Information); + Application.Exit(); + } + catch (Exception ex) + { + MessageBox.Show($"Erreur lors de la désinstallation : {ex.Message}", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + _btnNext.Enabled = true; + } + } + + private void RunCommand(string fileName, string arguments, string description) + { + var psi = new ProcessStartInfo(fileName, arguments) + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + using var process = Process.Start(psi); + if (process == null) + throw new InvalidOperationException($"Impossible de démarrer {fileName}"); + + process.WaitForExit(); + if (process.ExitCode != 0) + { + var error = process.StandardError.ReadToEnd(); + throw new InvalidOperationException($"{description} a échoué (code {process.ExitCode}) : {error}"); + } + } + + private void RegisterRunOnce() + { + var executablePath = Application.ExecutablePath; + var key = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce", true); + key?.SetValue("StudioE5SetupWizard", $"\"{executablePath}\""); + } +} diff --git a/agent/installer/setup-wizard/PrerequisiteChecker.cs b/agent/installer/setup-wizard/PrerequisiteChecker.cs new file mode 100644 index 0000000..54f45c2 --- /dev/null +++ b/agent/installer/setup-wizard/PrerequisiteChecker.cs @@ -0,0 +1,142 @@ +using System.Diagnostics; +using System.Management; +using System.Runtime.InteropServices; + +namespace StudioE5.SetupWizard; + +public record PrerequisiteResult( + bool WindowsCompatible, + ulong RamMB, + ulong FreeDiskMB, + bool VirtualEnvironmentInstalled, + bool PodmanInstalled, + bool PodmanMachineReady) +{ + public bool AllReady => WindowsCompatible && RamMB >= 4096 && FreeDiskMB >= 5120 && VirtualEnvironmentInstalled && PodmanInstalled && PodmanMachineReady; +} + +public static class PrerequisiteChecker +{ + public static PrerequisiteResult Check() + { + return new PrerequisiteResult( + WindowsCompatible: IsWindowsCompatible(), + RamMB: GetTotalPhysicalMemoryMB(), + FreeDiskMB: GetFreeDiskSpaceMB("C:\\"), + VirtualEnvironmentInstalled: IsWSLInstalled(), + PodmanInstalled: IsPodmanInstalled(), + PodmanMachineReady: IsPodmanMachineReady() + ); + } + + private static bool IsWindowsCompatible() + { + var os = Environment.OSVersion; + if (os.Platform != PlatformID.Win32NT) + return false; + + // Windows 10 version 2004 (build 19041) or Windows 11. + return Environment.OSVersion.Version.Build >= 19041; + } + + private static ulong GetTotalPhysicalMemoryMB() + { + try + { + using var searcher = new ManagementObjectSearcher("SELECT TotalVisibleMemorySize FROM Win32_OperatingSystem"); + foreach (ManagementObject obj in searcher.Get()) + { + var kb = Convert.ToUInt64(obj["TotalVisibleMemorySize"]); + return kb / 1024; + } + } + catch + { + // ignored + } + return 0; + } + + private static ulong GetFreeDiskSpaceMB(string path) + { + try + { + var drive = new DriveInfo(Path.GetPathRoot(path) ?? path); + return (ulong)(drive.AvailableFreeSpace / (1024 * 1024)); + } + catch + { + return 0; + } + } + + private static bool IsWSLInstalled() + { + try + { + var psi = new ProcessStartInfo("wsl.exe", "--version") + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + using var process = Process.Start(psi); + if (process == null) return false; + process.WaitForExit(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static bool IsPodmanInstalled() + { + try + { + var psi = new ProcessStartInfo("podman.exe", "--version") + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + using var process = Process.Start(psi); + if (process == null) return false; + process.WaitForExit(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static bool IsPodmanMachineReady() + { + try + { + var psi = new ProcessStartInfo("podman.exe", "machine list --format json") + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + using var process = Process.Start(psi); + if (process == null) return false; + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode != 0) return false; + + // Very permissive check: if podman machine list returns any JSON, we consider it ready. + return output.TrimStart().StartsWith("[") || output.TrimStart().StartsWith("{"); + } + catch + { + return false; + } + } +} diff --git a/agent/installer/setup-wizard/Program.cs b/agent/installer/setup-wizard/Program.cs new file mode 100644 index 0000000..7780a1f --- /dev/null +++ b/agent/installer/setup-wizard/Program.cs @@ -0,0 +1,19 @@ +using StudioE5.SetupWizard; + +static class Program +{ + [STAThread] + static void Main(string[] args) + { + ApplicationConfiguration.Initialize(); + + if (args.Contains("/uninstall", StringComparer.OrdinalIgnoreCase)) + { + Application.Run(new MainForm(startInUninstallMode: true)); + } + else + { + Application.Run(new MainForm(startInUninstallMode: false)); + } + } +} diff --git a/agent/installer/setup-wizard/README.md b/agent/installer/setup-wizard/README.md new file mode 100644 index 0000000..0d19ecd --- /dev/null +++ b/agent/installer/setup-wizard/README.md @@ -0,0 +1,83 @@ +# StudioE5 Setup Wizard + +Assistant d’installation graphique Windows pour studioE5 Agent. + +## Rôle + +Ce wizard guide l’utilisateur pas à pas pour : + +1. Vérifier les prérequis (RAM, disque, Windows, environnement virtuel, Podman). +2. Installer l’**environnement virtuel** (WSL2) si nécessaire, avec reprise après redémarrage. +3. Installer **Podman** depuis le MSI bundlé. +4. Initialiser et démarrer la **machine Podman**. +5. Lancer le package **Inno Setup** de studioE5 Agent. + +Il propose aussi un mode **désinstallation** complet (`/uninstall`). + +## Prérequis de build + +- Windows 10/11 +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- Visual Studio 2022 ou Visual Studio Code (optionnel) + +## Structure + +```text +setup-wizard/ +├── SetupWizard.csproj +├── Program.cs +├── MainForm.cs +├── InstallerState.cs +├── PrerequisiteChecker.cs +└── Resources/ + ├── podman-setup.msi # MSI officiel Podman pour Windows + └── studioE5-agent-setup.exe # Package Inno Setup de l'agent +``` + +## Build + +Ouvrir un terminal PowerShell dans ce dossier et exécuter : + +```powershell +dotnet build -c Release +``` + +Pour publier un exécutable autonome (pas besoin du runtime .NET sur le poste cible) : + +```powershell +dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true +``` + +L’exécutable se trouve dans : + +```text +bin\Release\net8.0-windows\win-x64\publish\StudioE5-SetupWizard.exe +``` + +## Préparation du package + +1. Télécharger le MSI Podman Windows : + +2. Le renommer en `podman-setup.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. + +## Lancement + +### Mode installation + +```powershell +.\StudioE5-SetupWizard.exe +``` + +### Mode désinstallation + +```powershell +.\StudioE5-SetupWizard.exe /uninstall +``` + +## Notes + +- 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 MSI Podman doit correspondre à l’architecture `x64`. diff --git a/agent/installer/setup-wizard/SetupWizard.csproj b/agent/installer/setup-wizard/SetupWizard.csproj new file mode 100644 index 0000000..c21bb9c --- /dev/null +++ b/agent/installer/setup-wizard/SetupWizard.csproj @@ -0,0 +1,22 @@ + + + + WinExe + net8.0-windows + enable + true + enable + StudioE5.SetupWizard + StudioE5-SetupWizard + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/agent/installer/studioE5-agent.iss b/agent/installer/studioE5-agent.iss new file mode 100644 index 0000000..73e9f9c --- /dev/null +++ b/agent/installer/studioE5-agent.iss @@ -0,0 +1,190 @@ +; studioE5 Agent Installer (Inno Setup) +; Build with Inno Setup Compiler (ISCC) on Windows. +; This installer bundles the agent and Tailscale binaries. It checks +; prerequisites and guides the user through installing missing system +; components (WSL2 + Podman) before installing studioE5. + +#define MyAppName "studioE5 Agent" +#define MyAppVersion "0.3.17" +#define MyAppPublisher "studioE5" +#define MyAppURL "https://studioe5.edudeploy.com" +#define MyAppExeName "studioE5-agent.exe" + +[Setup] +AppId={{studioE5-agent-ondemand} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\studioE5-agent +DisableProgramGroupPage=yes +OutputDir=..\..\installer-output +OutputBaseFilename=studioE5-agent-{#MyAppVersion}-setup +Compression=lzma +SolidCompression=yes +WizardStyle=modern +PrivilegesRequired=admin +ArchitecturesAllowed=x64 +ArchitecturesInstallIn64BitMode=x64 +[Languages] +Name: "french"; MessagesFile: "compiler:Languages\French.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "..\..\agent\studioE5-agent.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\..\agent\tailscale-bin\windows\tailscale.exe"; DestDir: "{app}\tailscale-bin\windows"; Flags: ignoreversion +Source: "..\..\agent\tailscale-bin\windows\tailscaled.exe"; DestDir: "{app}\tailscale-bin\windows"; Flags: ignoreversion +Source: "..\..\agent\tailscale-bin\windows\wintun.dll"; DestDir: "{app}\tailscale-bin\windows"; Flags: ignoreversion +Source: "..\..\README.md"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{autoprograms}\studioE5 Agent"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\studioE5 Agent"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "Lancer studioE5 Agent"; Flags: nowait postinstall skipifsilent + +[UninstallRun] +Filename: "{cmd}"; Parameters: "/c taskkill /f /im studioE5-agent.exe"; Flags: runhidden waituntilterminated + +[Code] +var + PrereqPage: TWizardPage; + lblStatus: TLabel; + btnCheck: TButton; + +function GetPhysicallyInstalledSystemMemoryKB(var TotalMemoryInKilobytes: Int64): Boolean; +external 'GetPhysicallyInstalledSystemMemory@kernel32.dll stdcall'; + +function GetTotalPhysicalMemoryMB(): Cardinal; +var + MemKB: Int64; +begin + if GetPhysicallyInstalledSystemMemoryKB(MemKB) then + Result := Cardinal(MemKB div 1024) + else + Result := 0; +end; + +function IsWSL2Installed(): Boolean; +var + ResultCode: Integer; +begin + Result := Exec('wsl.exe', '--version', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0); +end; + +function IsPodmanReady(): Boolean; +var + ResultCode: Integer; +begin + Result := Exec('podman.exe', 'machine list', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0); +end; + +function GetFreeDiskSpaceMB(const Path: string): Cardinal; +var + FreeBytes, TotalBytes: Int64; +begin + if GetDiskFreeSpaceEx(Path, FreeBytes, TotalBytes, nil) then + Result := Cardinal(FreeBytes div (1024 * 1024)) + else + Result := 0; +end; + +procedure UpdatePrereqStatus(); +var + Msg: string; + RamMB, FreeMB: Cardinal; + WSLReady, PodmanReady: Boolean; +begin + RamMB := GetTotalPhysicalMemoryMB(); + FreeMB := GetFreeDiskSpaceMB('C:\'); + WSLReady := IsWSL2Installed(); + PodmanReady := IsPodmanReady(); + + Msg := 'Vérification des prérequis :' + #13#10#13#10; + + if RamMB >= 8192 then + Msg := Msg + '✅ Mémoire vive (RAM) : ' + IntToStr(RamMB div 1024) + ' Go' + #13#10 + else + Msg := Msg + '⚠️ Mémoire vive (RAM) : ' + IntToStr(RamMB div 1024) + ' Go (8 Go recommandés)' + #13#10; + + if FreeMB >= 10240 then + Msg := Msg + '✅ Espace disque disponible : ' + IntToStr(FreeMB div 1024) + ' Go' + #13#10 + else + Msg := Msg + '⚠️ Espace disque disponible : ' + IntToStr(FreeMB div 1024) + ' Go (10 Go recommandés)' + #13#10; + + if WSLReady then + Msg := Msg + '✅ Environnement de virtualisation (WSL2) installé' + #13#10 + else + Msg := Msg + '❌ Environnement de virtualisation (WSL2) non installé' + #13#10; + + if PodmanReady then + Msg := Msg + '✅ Service de conteneurs (Podman) prêt' + #13#10 + else + Msg := Msg + '❌ Service de conteneurs (Podman) non prêt' + #13#10; + + Msg := Msg + #13#10; + + if WSLReady and PodmanReady and (RamMB >= 4096) and (FreeMB >= 5120) then + Msg := Msg + 'Tous les prérequis sont satisfaits. Vous pouvez installer studioE5 Agent.' + else + begin + Msg := Msg + 'Ordre d''installation recommandé :' + #13#10; + if not WSLReady then + Msg := Msg + '1. Installer WSL2 : ouvrir PowerShell en administrateur et exécuter : wsl --install --no-distribution' + #13#10; + if not PodmanReady then + Msg := Msg + '2. Installer Podman : télécharger et exécuter le MSI depuis https://github.com/containers/podman/releases' + #13#10; + if not PodmanReady then + Msg := Msg + '3. Initialiser Podman : podman machine init && podman machine start' + #13#10; + Msg := Msg + #13#10 + 'Après avoir installé les éléments manquants, relancez cet installateur.'; + end; + + lblStatus.Caption := Msg; +end; + +procedure btnCheckClick(Sender: TObject); +begin + UpdatePrereqStatus(); +end; + +procedure InitializeWizard(); +begin + PrereqPage := CreateCustomPage(wpWelcome, 'Vérification des prérequis', 'Assurez-vous que votre poste est prêt avant d''installer studioE5 Agent.'); + + lblStatus := TLabel.Create(WizardForm); + lblStatus.Parent := PrereqPage.Surface; + lblStatus.Left := 0; + lblStatus.Top := 0; + lblStatus.Width := PrereqPage.SurfaceWidth; + lblStatus.Height := 220; + lblStatus.AutoSize := False; + lblStatus.WordWrap := True; + + btnCheck := TButton.Create(WizardForm); + btnCheck.Parent := PrereqPage.Surface; + btnCheck.Left := 0; + btnCheck.Top := lblStatus.Top + lblStatus.Height + 12; + btnCheck.Width := 160; + btnCheck.Height := 25; + btnCheck.Caption := 'Vérifier les prérequis'; + btnCheck.OnClick := @btnCheckClick; + + UpdatePrereqStatus(); +end; + +function NextButtonClick(CurPageID: Integer): Boolean; +begin + Result := True; + if CurPageID = PrereqPage.ID then + begin + if not (IsWSL2Installed() and IsPodmanReady()) then + begin + MsgBox('Certains prérequis sont manquants. Veuillez les installer avant de continuer.', mbError, MB_OK); + Result := False; + end; + end; +end;