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

- Wizard: installation 100% offline (WSL bundle, Podman MSI, machine image, docker-compose)
- Wizard: suppression de wsl --install --no-distribution
- Wizard: .wslconfig avec 8Go RAM / 4 CPU
- Wizard: operations asynchrones pour eviter le freeze UI
- Wizard: detection automatique de podman.exe
- Wizard: version 0.1.1
- Agent: passage en v0.3.18
- Serveur: registry PrestaShop en HTTPS via gitea.alfrednobel.edudeploy.com
- Caddy: config gitea.alfrednobel.edudeploy.com
- Docs: mise a jour SUIVI_INSTALLER.md, README.md, seed.ts
This commit is contained in:
EduBox Dev
2026-07-02 22:52:28 +00:00
parent 3c519629d2
commit fc61404271
15 changed files with 1094 additions and 62 deletions
+5
View File
@@ -26,6 +26,11 @@ headscale.studioe5.edudeploy.com:443 {
reverse_proxy headscale:8080 reverse_proxy headscale:8080
} }
gitea.alfrednobel.edudeploy.com {
tls admin@edudeploy.com
reverse_proxy 151.80.60.98:3001
}
studioe5.edudeploy.com:443 { studioe5.edudeploy.com:443 {
route /studioE5-agent* { route /studioE5-agent* {
file_server { file_server {
+1 -1
View File
@@ -1 +1 @@
0.3.17 0.3.18
+24 -3
View File
@@ -24,9 +24,20 @@ Fournir un **installateur professionnel Windows** pour studioE5 Agent, guidé pa
- Wizard C# avec 7 étapes guidées. - Wizard C# avec 7 étapes guidées.
- Détection des prérequis : Windows, RAM, disque, WSL2, Podman. - 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` nest plus utilisée : linstallation est entièrement offline grâce au bundle.
6. Conversion des distributions WSL1 existantes vers WSL2 (`wsl --set-version <nom> 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 daccueil).
- Amélioration de linterface : 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é. - 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. - Lancement du package Inno Setup agent.
- Mode désinstallation complet. - Mode désinstallation complet.
- Script Inno Setup de base pour lagent. - Script Inno Setup de base pour lagent.
@@ -57,6 +68,10 @@ Dans `setup-wizard/Resources/` :
```text ```text
podman-installer-windows-amd64.msi podman-installer-windows-amd64.msi
studioE5-agent-setup.exe 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 ### Commande
@@ -95,8 +110,14 @@ Le fichier généré se trouve dans `installer-output/`.
## Notes importantes ## Notes importantes
- Le wizard doit être exécuté **en administrateur**. - Le wizard doit être exécuté **en administrateur**.
- Linstallation de WSL2 nécessite un **redémarrage** de lordinateur. - Linstallation de WSL2 nécessite un **redémarrage** de lordinateur après lactivation des fonctionnalités Windows.
- Linstallation 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 MSI Podman officiel pour Windows est `podman-installer-windows-amd64.msi`.
- Le bundle WSL2 offline est disponible sur <https://github.com/microsoft/WSL/releases> : `Microsoft.WSL_2.7.10.0_x64_ARM64.msixbundle`.
- Limage Podman machine offline est disponible sur <https://github.com/containers/podman-machine-os/releases> : `podman-machine.x86_64.wsl.tar.zst`.
- Docker Compose standalone est disponible sur <https://github.com/docker/compose/releases> : `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/`. - Pour la désinstallation, le MSI Podman doit être présent dans `Resources/`.
## Liens utiles ## Liens utiles
@@ -24,6 +24,18 @@ public class InstallerState
[JsonPropertyName("virtualEnvironmentInstalled")] [JsonPropertyName("virtualEnvironmentInstalled")]
public bool VirtualEnvironmentInstalled { get; set; } 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")] [JsonPropertyName("podmanInstalled")]
public bool PodmanInstalled { get; set; } public bool PodmanInstalled { get; set; }
+482 -35
View File
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using System.Text;
using Microsoft.Win32; using Microsoft.Win32;
namespace StudioE5.SetupWizard; namespace StudioE5.SetupWizard;
@@ -13,6 +15,7 @@ public partial class MainForm : Form
private readonly Button _btnCancel; private readonly Button _btnCancel;
private readonly Label _titleLabel; private readonly Label _titleLabel;
private readonly Label _subtitleLabel; private readonly Label _subtitleLabel;
private readonly Label _statusLabel;
private WizardStep _currentStep = WizardStep.Welcome; private WizardStep _currentStep = WizardStep.Welcome;
private PrerequisiteResult? _lastCheck; private PrerequisiteResult? _lastCheck;
@@ -20,7 +23,8 @@ public partial class MainForm : Form
public MainForm(bool startInUninstallMode = false) public MainForm(bool startInUninstallMode = false)
{ {
_state = InstallerState.Load(); _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); Size = new Size(700, 520);
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
FormBorderStyle = FormBorderStyle.FixedDialog; FormBorderStyle = FormBorderStyle.FixedDialog;
@@ -34,7 +38,7 @@ public partial class MainForm : Form
Width = 640, Width = 640,
Height = 32, Height = 32,
Font = new Font("Segoe UI", 14, FontStyle.Bold), Font = new Font("Segoe UI", 14, FontStyle.Bold),
Text = "Installateur studioE5 Agent" Text = $"Installateur studioE5 Agent v{version}"
}; };
_subtitleLabel = new Label _subtitleLabel = new Label
@@ -47,12 +51,23 @@ public partial class MainForm : Form
ForeColor = Color.Gray 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 _contentPanel = new Panel
{ {
Left = 24, Left = 24,
Top = 84, Top = 100,
Width = 640, Width = 640,
Height = 320, Height = 304,
BorderStyle = BorderStyle.None BorderStyle = BorderStyle.None
}; };
@@ -99,6 +114,7 @@ public partial class MainForm : Form
Controls.Add(_titleLabel); Controls.Add(_titleLabel);
Controls.Add(_subtitleLabel); Controls.Add(_subtitleLabel);
Controls.Add(_statusLabel);
Controls.Add(_contentPanel); Controls.Add(_contentPanel);
Controls.Add(separator); Controls.Add(separator);
Controls.Add(_btnBack); Controls.Add(_btnBack);
@@ -116,18 +132,30 @@ public partial class MainForm : Form
private void ResumeAfterReboot() private void ResumeAfterReboot()
{ {
// If we are resuming after a reboot, skip directly to the relevant step. // Si aucune étape n'a été faite, c'est une nouvelle installation (pas une reprise).
if (_state.VirtualEnvironmentInstalled && !_state.PodmanInstalled) 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); GoToStep(WizardStep.InstallPodman);
return; return;
} }
if (_state.PodmanInstalled && !_state.PodmanConfigured) if (!_state.PodmanConfigured)
{ {
GoToStep(WizardStep.ConfigurePodman); GoToStep(WizardStep.ConfigurePodman);
return; return;
} }
if (_state.PodmanConfigured && !_state.AgentInstalled) if (!_state.AgentInstalled)
{ {
GoToStep(WizardStep.InstallAgent); GoToStep(WizardStep.InstallAgent);
return; return;
@@ -136,6 +164,19 @@ public partial class MainForm : Form
GoToStep(WizardStep.Welcome); 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() private void GoBack()
{ {
switch (_currentStep) switch (_currentStep)
@@ -167,10 +208,14 @@ public partial class MainForm : Form
GoToStep(WizardStep.Prerequisites); GoToStep(WizardStep.Prerequisites);
break; break;
case WizardStep.Prerequisites: case WizardStep.Prerequisites:
if (_lastCheck?.VirtualEnvironmentInstalled == true) if (_lastCheck?.VirtualEnvironmentInstalled != true)
GoToStep(WizardStep.InstallPodman);
else
GoToStep(WizardStep.InstallVirtualEnvironment); GoToStep(WizardStep.InstallVirtualEnvironment);
else if (_lastCheck?.PodmanInstalled != true)
GoToStep(WizardStep.InstallPodman);
else if (_lastCheck?.PodmanMachineReady != true)
GoToStep(WizardStep.ConfigurePodman);
else
GoToStep(WizardStep.InstallAgent);
break; break;
case WizardStep.InstallVirtualEnvironment: case WizardStep.InstallVirtualEnvironment:
InstallVirtualEnvironment(); InstallVirtualEnvironment();
@@ -237,8 +282,9 @@ public partial class MainForm : Form
private void ShowWelcome() private void ShowWelcome()
{ {
var version = GetVersion();
_titleLabel.Text = "Bienvenue dans l'installateur studioE5"; _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 var label = new Label
{ {
@@ -246,7 +292,8 @@ public partial class MainForm : Form
Top = 20, Top = 20,
Width = _contentPanel.Width, Width = _contentPanel.Width,
Height = 200, 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" + "1. Vérification des prérequis\n" +
"2. Installation de l'environnement virtuel (si nécessaire)\n" + "2. Installation de l'environnement virtuel (si nécessaire)\n" +
"3. Installation de Podman\n" + "3. Installation de Podman\n" +
@@ -306,46 +353,174 @@ public partial class MainForm : Form
_contentPanel.Controls.Add(label); _contentPanel.Controls.Add(label);
_btnBack.Visible = true; _btnBack.Visible = true;
_btnNext.Text = result.AllReady ? "Suivant >" : (result.VirtualEnvironmentInstalled ? "Installer Podman" : "Installer l'environnement virtuel"); _btnNext.Text = GetPrerequisitesNextButtonText(result);
_btnNext.Enabled = true; _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() private void ShowInstallVirtualEnvironment()
{ {
_titleLabel.Text = "Installation de l'environnement virtuel"; _titleLabel.Text = "Installation de l'environnement virtuel";
_subtitleLabel.Text = "L'environnement virtuel permet de faire tourner Podman sur Windows."; _subtitleLabel.Text = "L'environnement virtuel permet de faire tourner Podman sur Windows.";
var progress = GetWslInstallationProgress();
var label = new Label var label = new Label
{ {
Left = 0, Left = 0,
Top = 20, Top = 20,
Width = _contentPanel.Width, Width = _contentPanel.Width,
Height = 120, Height = 160,
Text = "L'environnement virtuel (WSL2) n'est pas installé.\n\n" + Text = "L'environnement virtuel (WSL2) n'est pas installé.\n\n" +
"Cliquez sur 'Installer' pour lancer l'installation.\n" + "L'assistant va effectuer les opérations suivantes :\n" +
"Un redémarrage de l'ordinateur sera nécessaire. L'installateur se relancera automatiquement.", "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) 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(label);
_contentPanel.Controls.Add(progressLabel);
_btnBack.Visible = true; _btnBack.Visible = true;
_btnNext.Text = "Installer"; _btnNext.Text = progress.Contains("Terminé") ? "Continuer" : "Installer";
_btnNext.Enabled = true; _btnNext.Enabled = true;
} }
private void InstallVirtualEnvironment() 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; _btnNext.Enabled = false;
_btnBack.Enabled = false; _btnBack.Enabled = false;
UseWaitCursor = true;
try try
{ {
RunCommand("wsl.exe", "--install --no-distribution", "Installation de l'environnement virtuel en cours..."); // Si WSL2 est déjà prêt, on marque toutes les étapes comme faites et on continue.
_state.VirtualEnvironmentInstalled = true; 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(); _state.Save();
if (rebootRequired)
{
// Au moins une fonctionnalité nécessite un redémarrage.
RegisterRunOnce(); RegisterRunOnce();
GoToStep(WizardStep.RestartRequired); 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) catch (Exception ex)
{ {
@@ -355,13 +530,122 @@ public partial class MainForm : Form
{ {
_btnNext.Enabled = true; _btnNext.Enabled = true;
_btnBack.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() private void ShowRestartRequired()
{ {
_titleLabel.Text = "Redémarrage nécessaire"; _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 var label = new Label
{ {
@@ -369,8 +653,8 @@ public partial class MainForm : Form
Top = 20, Top = 20,
Width = _contentPanel.Width, Width = _contentPanel.Width,
Height = 120, Height = 120,
Text = "Veuillez redémarrer votre ordinateur pour finaliser l'installation de l'environnement virtuel.\n\n" + 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.", "L'installateur se relancera automatiquement après le redémarrage pour continuer l'installation de WSL2.",
Font = new Font("Segoe UI", 10) Font = new Font("Segoe UI", 10)
}; };
@@ -405,10 +689,11 @@ public partial class MainForm : Form
_btnNext.Enabled = true; _btnNext.Enabled = true;
} }
private void InstallPodman() private async void InstallPodman()
{ {
_btnNext.Enabled = false; _btnNext.Enabled = false;
_btnBack.Enabled = false; _btnBack.Enabled = false;
UseWaitCursor = true;
try try
{ {
@@ -416,9 +701,17 @@ public partial class MainForm : Form
if (!File.Exists(msiPath)) 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."); 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.PodmanInstalled = true;
_state.Save(); _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); GoToStep(WizardStep.ConfigurePodman);
} }
catch (Exception ex) catch (Exception ex)
@@ -429,6 +722,91 @@ public partial class MainForm : Form
{ {
_btnNext.Enabled = true; _btnNext.Enabled = true;
_btnBack.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; _btnNext.Enabled = true;
} }
private void ConfigurePodman() private async void ConfigurePodman()
{ {
_btnNext.Enabled = false; _btnNext.Enabled = false;
_btnBack.Enabled = false; _btnBack.Enabled = false;
UseWaitCursor = true;
try try
{ {
RunCommand("podman.exe", "machine init", "Initialisation de la machine Podman..."); var podmanPath = PrerequisiteChecker.GetPodmanExePath();
RunCommand("podman.exe", "machine start", "Démarrage de la machine Podman..."); 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.PodmanConfigured = true;
_state.Save(); _state.Save();
SetStatus(null);
GoToStep(WizardStep.InstallAgent); GoToStep(WizardStep.InstallAgent);
} }
catch (Exception ex) catch (Exception ex)
@@ -476,9 +877,19 @@ public partial class MainForm : Form
{ {
_btnNext.Enabled = true; _btnNext.Enabled = true;
_btnBack.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() private void ShowInstallAgent()
{ {
_titleLabel.Text = "Installation de studioE5 Agent"; _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) var psi = new ProcessStartInfo(fileName, arguments)
{ {
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
}; };
using var process = Process.Start(psi); using var process = Process.Start(psi);
if (process == null) if (process == null)
throw new InvalidOperationException($"Impossible de démarrer {fileName}"); throw new InvalidOperationException($"Impossible de démarrer {fileName}");
process.WaitForExit(); // Lire stdout/stderr en parallèle pour éviter le blocage du processus
if (process.ExitCode != 0) // 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(); try { process.Kill(); } catch { /* ignored */ }
throw new InvalidOperationException($"{description} a échoué (code {process.ExitCode}) : {error}"); 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() private void RegisterRunOnce()
@@ -1,6 +1,9 @@
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Management; using System.Management;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace StudioE5.SetupWizard; namespace StudioE5.SetupWizard;
@@ -19,13 +22,20 @@ public static class PrerequisiteChecker
{ {
public static PrerequisiteResult Check() 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( return new PrerequisiteResult(
WindowsCompatible: IsWindowsCompatible(), WindowsCompatible: IsWindowsCompatible(),
RamMB: GetTotalPhysicalMemoryMB(), RamMB: GetTotalPhysicalMemoryMB(),
FreeDiskMB: GetFreeDiskSpaceMB("C:\\"), FreeDiskMB: GetFreeDiskSpaceMB("C:\\"),
VirtualEnvironmentInstalled: IsWSLInstalled(), VirtualEnvironmentInstalled: virtualEnvironmentInstalled,
PodmanInstalled: IsPodmanInstalled(), PodmanInstalled: IsPodmanInstalled(),
PodmanMachineReady: IsPodmanMachineReady() PodmanMachineReady: podmanMachineReady
); );
} }
@@ -70,7 +80,143 @@ public static class PrerequisiteChecker
} }
} }
public static bool IsWSL2Ready()
{
// PowerShell gère mieux l'encodage de la sortie WSL que Process.Start en C#.
if (IsWSL2ReadyViaPowerShell())
return true;
// Fallback natif si PowerShell n'est pas disponible.
return IsWSL2ReadyNative();
}
private static bool IsWSL2ReadyViaPowerShell()
{
try
{
var tempFile = Path.GetTempFileName();
var script =
"$status = & wsl.exe --status 2>&1; " +
"$ready = ($status -match 'Version par d\\u00E9faut\\s*:\\s*2') -or " +
"($status -match 'Default Version\\s*:\\s*2'); " +
"$ready | Out-File -FilePath '" + tempFile + "' -Encoding utf8 -NoNewline";
var psi = new ProcessStartInfo("powershell.exe", $"-ExecutionPolicy Bypass -Command \"{script}\"")
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using var process = Process.Start(psi);
if (process == null) return false;
process.WaitForExit();
if (!File.Exists(tempFile))
return false;
var result = File.ReadAllText(tempFile).Trim();
File.Delete(tempFile);
return result.Equals("True", StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
private static bool IsWSL2ReadyNative()
{
try
{
// wsl --status est plus fiable que --version pour savoir si WSL2 est prêt.
var psi = new ProcessStartInfo("wsl.exe", "--status")
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using var process = Process.Start(psi);
if (process == null) return false;
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit();
// wsl --status peut retourner un code non nul même quand linfo utile est affichée
// (par exemple si aucune distribution nest installée). On parse quand même.
var combined = output + "\n" + error;
var normalized = combined
.Replace('\u00A0', ' ')
.Replace('\u202F', ' ');
if (normalized.Contains("Version par défaut : 2", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("Default Version: 2", StringComparison.OrdinalIgnoreCase) ||
normalized.Contains("Version défaut : 2", StringComparison.OrdinalIgnoreCase))
{
return true;
}
var defaultVersion = ParseWslDefaultVersion(combined);
if (defaultVersion == 2)
return true;
// Si aucune version par défaut n'est trouvée, on tente les autres méthodes.
return (defaultVersion == 0 && WslVersionIndicatesWsl2()) ||
WslListIndicatesWsl2();
}
catch
{
return false;
}
}
private static bool IsWSLInstalled() 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 try
{ {
@@ -79,12 +225,23 @@ public static class PrerequisiteChecker
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
}; };
using var process = Process.Start(psi); using var process = Process.Start(psi);
if (process == null) return false; if (process == null) return false;
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit(); 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 catch
{ {
@@ -92,16 +249,110 @@ public static class PrerequisiteChecker
} }
} }
private static bool IsPodmanInstalled() private static bool WslListIndicatesWsl2()
{ {
try try
{ {
var psi = new ProcessStartInfo("podman.exe", "--version") var psi = new ProcessStartInfo("wsl.exe", "--list --verbose")
{ {
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
RedirectStandardOutput = 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); using var process = Process.Start(psi);
if (process == null) return false; if (process == null) return false;
@@ -116,14 +367,20 @@ public static class PrerequisiteChecker
private static bool IsPodmanMachineReady() private static bool IsPodmanMachineReady()
{ {
var podmanPath = GetPodmanExePath();
if (string.IsNullOrEmpty(podmanPath))
return false;
try try
{ {
var psi = new ProcessStartInfo("podman.exe", "machine list --format json") var psi = new ProcessStartInfo(podmanPath, "machine list --format json")
{ {
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
}; };
using var process = Process.Start(psi); using var process = Process.Start(psi);
if (process == null) return false; if (process == null) return false;
+26 -4
View File
@@ -29,9 +29,14 @@ setup-wizard/
├── MainForm.cs ├── MainForm.cs
├── InstallerState.cs ├── InstallerState.cs
├── PrerequisiteChecker.cs ├── PrerequisiteChecker.cs
├── app.manifest
└── Resources/ └── Resources/
├── podman-installer-windows-amd64.msi # MSI officiel Podman pour Windows ├── podman-installer-windows-amd64.msi # MSI officiel Podman pour Windows
── studioE5-agent-setup.exe # Package Inno Setup de l'agent ── 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 ## Build
@@ -58,9 +63,25 @@ bin\Release\net8.0-windows\win-x64\publish\StudioE5-SetupWizard.exe
1. Télécharger le MSI Podman Windows : 1. Télécharger le MSI Podman Windows :
<https://github.com/containers/podman/releases> <https://github.com/containers/podman/releases>
2. Le renommer en `podman-installer-windows-amd64.msi` et le placer dans `Resources/`. Le renommer en `podman-installer-windows-amd64.msi` et le placer dans `Resources/`.
3. Générer le package Inno Setup de lagent (`studioE5-agent-setup.exe`) et le placer dans `Resources/`. 2. Générer le package Inno Setup de lagent (`studioE5-agent-setup.exe`) et le placer dans `Resources/`.
4. Builder et publier le wizard. 3. Télécharger le package WSL2 complet (offline) :
<https://github.com/microsoft/WSL/releases>
Par exemple : `Microsoft.WSL_2.7.10.0_x64_ARM64.msixbundle`.
Le placer dans `Resources/`.
4. Télécharger limage Podman machine pour WSL (offline) :
<https://github.com/containers/podman-machine-os/releases>
Par exemple : `podman-machine.x86_64.wsl.tar.zst`.
Le placer dans `Resources/`.
5. Télécharger Docker Compose standalone (offline) :
<https://github.com/docker/compose/releases>
Par exemple : `docker-compose-windows-x86_64.exe`.
Le placer dans `Resources/`.
6. *(Optionnel, fallback)* Télécharger le noyau WSL2 :
<https://github.com/microsoft/WSL/releases>
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 ## Lancement
@@ -80,4 +101,5 @@ bin\Release\net8.0-windows\win-x64\publish\StudioE5-SetupWizard.exe
- Le wizard doit être exécuté **en administrateur**. - Le wizard doit être exécuté **en administrateur**.
- Linstallation de WSL2 nécessite un **redémarrage** de lordinateur. Le wizard senregistre dans `RunOnce` pour se relancer automatiquement. - Linstallation de WSL2 nécessite un **redémarrage** de lordinateur. Le wizard senregistre dans `RunOnce` pour se relancer automatiquement.
- Le wizard configure WSL2 avec **8 Go de RAM et 4 CPU** via le fichier `.wslconfig` de lutilisateur.
- Le MSI Podman doit correspondre à larchitecture `x64`. - Le MSI Podman doit correspondre à larchitecture `x64`.
@@ -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` nest plus utilisée : linstallation est entièrement offline grâce au bundle.
6. Conversion des distributions WSL1 existantes vers WSL2 (`wsl --set-version <nom> 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 daccueil).
- Amélioration de linterface : 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 lagent.
### 🔄 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 lexé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**.
- Linstallation de WSL2 nécessite un **redémarrage** de lordinateur après lactivation des fonctionnalités Windows.
- Linstallation 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 <https://github.com/microsoft/WSL/releases> : `Microsoft.WSL_2.7.10.0_x64_ARM64.msixbundle`.
- Limage Podman machine offline est disponible sur <https://github.com/containers/podman-machine-os/releases> : `podman-machine.x86_64.wsl.tar.zst`.
- Docker Compose standalone est disponible sur <https://github.com/docker/compose/releases> : `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 : <https://github.com/containers/podman/releases>
- Inno Setup : <https://jrsoftware.org/isdl.php>
- .NET 8 SDK : <https://dotnet.microsoft.com/download/dotnet/8.0>
@@ -6,8 +6,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<SatelliteResourceLanguages>fr</SatelliteResourceLanguages>
<RootNamespace>StudioE5.SetupWizard</RootNamespace> <RootNamespace>StudioE5.SetupWizard</RootNamespace>
<AssemblyName>StudioE5-SetupWizard</AssemblyName> <AssemblyName>StudioE5-SetupWizard</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -15,10 +17,16 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Resources\podman-installer-windows-amd64.msi"> <!-- Fichier de version affiché dans le wizard. -->
<Content Include="VERSION">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Resources\studioE5-agent-setup.exe"> </ItemGroup>
<ItemGroup>
<!-- Tous les fichiers placés dans Resources/ sont copiés dans le répertoire de sortie. -->
<!-- Attendus : MSI Podman, setup agent, bundle WSL, image Podman machine, MSI noyau WSL (optionnel). -->
<Content Include="Resources\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
+1
View File
@@ -0,0 +1 @@
0.1.1
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="StudioE5.SetupWizard.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- Force l'exécution en tant qu'administrateur -->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
@@ -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 lactivation 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
}
Binary file not shown.
+3 -3
View File
@@ -30,9 +30,9 @@ docker build -t edubox-prestashop:9 .
```bash ```bash
docker tag edubox-prestashop:9 \ 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 \ 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 ## Patches appliqués
@@ -75,7 +75,7 @@ Le template PrestaShop 9 dans `server/prisma/seed.ts` utilise cette image :
```yaml ```yaml
app: 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 ## Mise à jour vers une nouvelle version de PrestaShop
+1 -1
View File
@@ -84,7 +84,7 @@ async function main() {
{ {
name: "PrestaShop 9 vierge (edubox)", name: "PrestaShop 9 vierge (edubox)",
type: "prestashop", 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", dbImage: "mariadb:10.11",
dbName: "prestashop", dbName: "prestashop",
dbUser: "prestashop", dbUser: "prestashop",