fc61404271
- 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
1110 lines
39 KiB
C#
1110 lines
39 KiB
C#
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
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 readonly Label _statusLabel;
|
||
|
||
private WizardStep _currentStep = WizardStep.Welcome;
|
||
private PrerequisiteResult? _lastCheck;
|
||
|
||
public MainForm(bool startInUninstallMode = false)
|
||
{
|
||
_state = InstallerState.Load();
|
||
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;
|
||
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 v{version}"
|
||
};
|
||
|
||
_subtitleLabel = new Label
|
||
{
|
||
Left = 24,
|
||
Top = 50,
|
||
Width = 640,
|
||
Height = 24,
|
||
Font = new Font("Segoe UI", 9),
|
||
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 = 100,
|
||
Width = 640,
|
||
Height = 304,
|
||
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(_statusLabel);
|
||
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()
|
||
{
|
||
// 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.PodmanConfigured)
|
||
{
|
||
GoToStep(WizardStep.ConfigurePodman);
|
||
return;
|
||
}
|
||
if (!_state.AgentInstalled)
|
||
{
|
||
GoToStep(WizardStep.InstallAgent);
|
||
return;
|
||
}
|
||
|
||
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)
|
||
{
|
||
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.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();
|
||
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()
|
||
{
|
||
var version = GetVersion();
|
||
_titleLabel.Text = "Bienvenue dans l'installateur studioE5";
|
||
_subtitleLabel.Text = $"Version {version}";
|
||
|
||
var label = new Label
|
||
{
|
||
Left = 0,
|
||
Top = 20,
|
||
Width = _contentPanel.Width,
|
||
Height = 200,
|
||
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" +
|
||
"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 = 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 = 160,
|
||
Text = "L'environnement virtuel (WSL2) n'est pas installé.\n\n" +
|
||
"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 = progress.Contains("Terminé") ? "Continuer" : "Installer";
|
||
_btnNext.Enabled = true;
|
||
}
|
||
|
||
private string GetWslInstallationProgress()
|
||
{
|
||
var wsl2Operational = PrerequisiteChecker.IsWSL2Ready();
|
||
|
||
var steps = new List<string>
|
||
{
|
||
$"[{( _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
|
||
{
|
||
// 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)
|
||
{
|
||
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;
|
||
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 = "Les fonctionnalités Windows ont été activées.";
|
||
|
||
var label = new Label
|
||
{
|
||
Left = 0,
|
||
Top = 20,
|
||
Width = _contentPanel.Width,
|
||
Height = 120,
|
||
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)
|
||
};
|
||
|
||
_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 async void InstallPodman()
|
||
{
|
||
_btnNext.Enabled = false;
|
||
_btnBack.Enabled = false;
|
||
UseWaitCursor = true;
|
||
|
||
try
|
||
{
|
||
var msiPath = Path.Combine(AppContext.BaseDirectory, "Resources", "podman-installer-windows-amd64.msi");
|
||
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.");
|
||
|
||
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)
|
||
{
|
||
MessageBox.Show($"Erreur lors de l'installation de Podman : {ex.Message}", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
finally
|
||
{
|
||
_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.
|
||
}
|
||
}
|
||
|
||
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 async void ConfigurePodman()
|
||
{
|
||
_btnNext.Enabled = false;
|
||
_btnBack.Enabled = false;
|
||
UseWaitCursor = true;
|
||
|
||
try
|
||
{
|
||
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)
|
||
{
|
||
MessageBox.Show($"Erreur lors de la configuration de Podman : {ex.Message}", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
finally
|
||
{
|
||
_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";
|
||
_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(AppContext.BaseDirectory, "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(AppContext.BaseDirectory, "Resources", "podman-installer-windows-amd64.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 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,
|
||
StandardOutputEncoding = Encoding.UTF8,
|
||
StandardErrorEncoding = Encoding.UTF8
|
||
};
|
||
|
||
using var process = Process.Start(psi);
|
||
if (process == null)
|
||
throw new InvalidOperationException($"Impossible de démarrer {fileName}");
|
||
|
||
// 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))
|
||
{
|
||
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()
|
||
{
|
||
var executablePath = Application.ExecutablePath;
|
||
var key = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce", true);
|
||
key?.SetValue("StudioE5SetupWizard", $"\"{executablePath}\"");
|
||
}
|
||
}
|