v0.27.4 — Exe per-version configurable + auto-install redists Unreal

== Exe par-version, configurable via le backoffice ==

Pour éviter de devoir bumper le launcher à chaque changement d'Unreal Engine
(PROSERVE_UE_5_5.exe → PROSERVE_UE_5_7.exe), l'exe à lancer est maintenant
déclaré par version dans le manifest serveur, configurable depuis le form
admin.

- server/admin/versions.php : nouveau champ "Nom de l'exécutable" dans le
  form d'ajout de version (regex strict anti-path-traversal). Par défaut
  PROSERVE_UE_5_7.exe.
- InstallationRegistry : nouveau .proserve-meta.json écrit dans chaque dossier
  d'install fraîchement extrait (contient l'exe declaré par le manifest).
  Scan() lit cette meta pour résoudre l'exe sans avoir besoin du manifest
  en mémoire (offline / pré-check).
- Fallback glob PROSERVE_UE_*.exe pour les vieux installs sans metadata —
  garantit la backward compat de l'UE 5.5 actuel sans intervention.

== Auto-install des redists Unreal depuis _redist/ ==

Quand une version change d'Unreal (typique : UE 5.5 → 5.7), les redists
Microsoft VC + UEPrereqSetup doivent être installés sur le PC. Au lieu de
demander à l'opérateur de les installer à la main, le launcher détecte
maintenant un dossier _redist/ dans la racine du ZIP de release et lance
automatiquement tous les .exe dedans.

- MainViewModel.InstallRedistsAsync : après le ZipInstaller, scan _redist/
  pour .exe, lance chacun avec /install /quiet /norestart + Verb=runas
  (UAC popup par installer, inévitable car les redists écrivent dans
  Program Files). Tri alphabétique (préfixe 01_, 02_, … pour forcer un
  ordre si besoin).
- InstallationRegistry.MarkRedistInstalledAsync : trace redistInstalledAt
  dans .proserve-meta.json après succès. Reinstall de la même version :
  skip silencieux, pas de UAC popup chain.
- Best-effort sur les exit codes : les "already installed" retournent
  souvent un code non-zero, on log mais on continue (l'install PROSERVE
  ne doit pas être bloquée par un redist mineur).

Côté ZIP de release : crée _redist/ à la racine avec les .exe à installer
(typiquement VC_redist.x64.exe + UEPrereqSetup_x64.exe).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 15:11:57 +02:00
parent aab2e41152
commit 9b17dfa84c
7 changed files with 263 additions and 9 deletions

View File

@@ -1081,6 +1081,29 @@ public sealed partial class MainViewModel : ObservableObject
});
await _zipInstaller.InstallAsync(zipPath, target, installProgress, ct);
// Persiste le nom du .exe à lancer dans le dossier d'install fraîchement
// extrait. InstallationRegistry.Scan() le lira pour résoudre l'exe sans
// avoir besoin du manifest en mémoire (cas démarrage offline / pré-check).
// Le nom vient du manifest serveur (champ "executable" par version),
// configurable dans le backoffice.
try
{
if (!string.IsNullOrWhiteSpace(row.Remote.Executable))
await _registry.WriteInstallMetadataAsync(target, row.Remote.Executable, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to write install metadata (will fall back to glob detection)");
}
// Installation des redists Unreal (VC_redist, UEPrereqSetup, etc.) si la
// version contient un dossier `_redist/`. Run silent + UAC élévation par
// installer. Track via metadata pour ne pas re-runner à chaque réinstall.
// Si un installer fail (already-installed retournent souvent un exit code
// non-zero), on log mais on continue — l'install PROSERVE ne doit pas être
// bloquée par un redist mineur.
await InstallRedistsAsync(target, row.Version, ct);
// Promote le ZIP au cache LAN (sidecar SHA écrit) puis prune les vieilles
// versions selon MaxCachedVersions. Le ZIP est déjà au bon endroit
// (DownloadManager.VerifyAndFinalizeAsync l'a Move'd vers `proserve-X.zip`),
@@ -1311,6 +1334,83 @@ public sealed partial class MainViewModel : ObservableObject
}
}
/// <summary>
/// Si <c>_redist/</c> existe dans le dossier d'install, lance tous les .exe
/// dedans en silent + UAC élevé pour installer les redists Unreal (VC_redist,
/// UEPrereqSetup, etc.). Skip si déjà fait (tracé dans .proserve-meta.json).
/// Best-effort : on log mais on ne fail pas l'install si un redist plante
/// (typiquement les "already installed" retournent un exit code non-zero).
/// </summary>
private async Task InstallRedistsAsync(string installDir, string version, CancellationToken ct)
{
var redistDir = Path.Combine(installDir, "_redist");
if (!Directory.Exists(redistDir))
{
_logger.LogDebug("No _redist/ in v{Version}, skipping redists install", version);
return;
}
if (_registry.IsRedistInstalled(installDir))
{
_logger.LogInformation("Redists already installed for v{Version} (per metadata), skipping", version);
return;
}
var exes = Directory.GetFiles(redistDir, "*.exe", SearchOption.TopDirectoryOnly);
if (exes.Length == 0)
{
_logger.LogDebug("_redist/ exists but contains no .exe, skipping");
return;
}
Array.Sort(exes, StringComparer.OrdinalIgnoreCase);
StatusMessage = Strings.StatusInstallingRedists(version);
ProgressDetail = null;
for (int i = 0; i < exes.Length; i++)
{
ct.ThrowIfCancellationRequested();
var exe = exes[i];
var name = Path.GetFileName(exe);
ProgressDetail = Strings.ProgressRedistInstalling(i + 1, exes.Length, name);
_logger.LogInformation("Running redist {Idx}/{Total} : {Exe}", i + 1, exes.Length, name);
try
{
// Silent flags qui couvrent VC_redist (/install /quiet /norestart) et
// UEPrereqSetup (/install /quiet /norestart). MSI std accepte aussi.
// UseShellExecute=true + Verb=runas → UAC popup pour élévation admin.
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = exe,
Arguments = "/install /quiet /norestart",
UseShellExecute = true,
Verb = "runas",
CreateNoWindow = true,
};
using var proc = System.Diagnostics.Process.Start(psi);
if (proc is null) continue;
await proc.WaitForExitAsync(ct).ConfigureAwait(false);
_logger.LogInformation("Redist {Name} exit code {Code}", name, proc.ExitCode);
// Codes typiques :
// 0 = succès
// 3010 = succès mais reboot requis (on ne fait pas, le user gérera)
// 1638 = "already installed, newer version" — OK, on continue
// 1602/1603 = user cancel UAC ou install error — log mais continue
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Redist {Name} install failed (continuing)", name);
}
}
try
{
await _registry.MarkRedistInstalledAsync(installDir, ct).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to mark redist installed in metadata");
}
}
private async Task UninstallVersionAsync(VersionRowViewModel row)
{
if (row.Installed is null) return;