diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 54aeff3..c1ae1a8 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PS_Launcher" -#define MyAppVersion "0.19.0" +#define MyAppVersion "0.20.0" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index 054698d..8cb1f06 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -15,9 +15,9 @@ PROSERVE Launcher © 2026 ASTERION VR — All rights reserved PSLauncher.App - 0.19.0 - 0.19.0.0 - 0.19.0.0 + 0.20.0 + 0.20.0.0 + 0.20.0.0 true diff --git a/src/PSLauncher.App/ViewModels/MainViewModel.cs b/src/PSLauncher.App/ViewModels/MainViewModel.cs index 2ccad32..7168365 100644 --- a/src/PSLauncher.App/ViewModels/MainViewModel.cs +++ b/src/PSLauncher.App/ViewModels/MainViewModel.cs @@ -53,6 +53,13 @@ public sealed partial class MainViewModel : ObservableObject private RemoteManifest? _lastManifest; private CancellationTokenSource? _activeDownloadCts; private VersionRowViewModel? _activeRow; + /// + /// Référence au Task de l'install/DL en cours, exposée aux commandes externes + /// (ex. RestartFromZero) qui ont besoin d'attendre la fin réelle des écritures + /// disque avant de supprimer le partial. Sinon les segments continuent à écrire + /// pendant 1-2 sec après le Cancel() et recréent le fichier qu'on vient de delete. + /// + private Task? _activeInstallTask; public ObservableCollection OtherVersions { get; } = new(); @@ -333,7 +340,7 @@ public sealed partial class MainViewModel : ObservableObject private void WireRowHandlers(VersionRowViewModel row) { row.LaunchHandler = LaunchVersion; - row.InstallHandler = r => _ = InstallVersionAsync(r); + row.InstallHandler = r => _activeInstallTask = InstallVersionAsync(r); row.UninstallHandler = r => _ = UninstallVersionAsync(r); row.ShowReleaseNotesHandler = r => _ = ShowReleaseNotesAsync(r); row.OpenFolderHandler = OpenVersionFolder; @@ -545,12 +552,26 @@ public sealed partial class MainViewModel : ObservableObject MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No); if (confirm != MessageBoxResult.Yes) return; - // Cancel le DL actif si c'est cette row + // Cancel le DL actif si c'est cette row, puis attend la propagation complète + // (les 16 segments doivent chacun stopper leurs écritures et fermer leur + // FileStream sinon DiscardResumableState échouera/sera réécrasé). if (_activeRow is not null && _activeRow == row && _activeDownloadCts is not null) { _activeDownloadCts.Cancel(); - // Attend la fin de la tâche en cours pour libérer les handles fichier - try { await Task.Delay(200); } catch { } + // Attend que InstallVersionAsync termine son catch OperationCanceledException + // — y compris le flush + state save + libération des FileStream segment. + // Timeout de sécurité 10s au cas où une connexion HTTPS bloque sur close. + if (_activeInstallTask is { } running && !running.IsCompleted) + { + StatusMessage = "Annulation en cours…"; + try + { + var awaitTask = await Task.WhenAny(running, Task.Delay(TimeSpan.FromSeconds(10))); + if (!ReferenceEquals(awaitTask, running)) + _logger.LogWarning("Install task did not finish cancelling within 10s, proceeding anyway"); + } + catch (Exception ex) { _logger.LogDebug(ex, "Awaiting install task during restart"); } + } } _downloadManager.DiscardResumableState(row.Version); @@ -682,9 +703,13 @@ public sealed partial class MainViewModel : ObservableObject row.ProgressPercent = 0; var dlProgress = new Progress(p => UpdateDlProgress(row, p)); // Reporter dédié pour la phase verify SHA-256 (peut prendre 30s-3min sur 14 Go). - // On bascule le StatusMessage à ce moment-là pour que l'utilisateur sache ce qui se passe. + // On bascule row.State sur Verifying à la première remontée, ce qui change + // le badge de la row de "⬇ Téléchargement…" vers "🔍 Vérification…" — les + // deux phases sont distinctes côté UX bien que `DownloadAsync` les enchaîne. var hashProgress = new Progress(pct => { + if (row.State == VersionRowState.Downloading) + row.State = VersionRowState.Verifying; var percent = (int)Math.Round(pct * 100.0); ProgressPercent = percent; row.ProgressPercent = percent; @@ -879,6 +904,7 @@ public sealed partial class MainViewModel : ObservableObject _activeDownloadCts?.Dispose(); _activeDownloadCts = null; _activeRow = null; + _activeInstallTask = null; IsBusy = false; } } diff --git a/src/PSLauncher.App/ViewModels/VersionRowViewModel.cs b/src/PSLauncher.App/ViewModels/VersionRowViewModel.cs index ec756ef..2fbe606 100644 --- a/src/PSLauncher.App/ViewModels/VersionRowViewModel.cs +++ b/src/PSLauncher.App/ViewModels/VersionRowViewModel.cs @@ -10,6 +10,7 @@ public enum VersionRowState InstalledIdle, // Installée localement, pas d'action en cours AvailableIdle, // Disponible côté serveur, pas installée Downloading, + Verifying, // SHA-256 du ZIP téléchargé en cours Installing, Uninstalling, } @@ -77,13 +78,17 @@ public sealed partial class VersionRowViewModel : ObservableObject public bool IsInstalled => State is VersionRowState.InstalledIdle or VersionRowState.Uninstalling; public bool IsRemoteOnly => State is VersionRowState.AvailableIdle; - public bool IsBusy => State is VersionRowState.Downloading or VersionRowState.Installing or VersionRowState.Uninstalling; + public bool IsBusy => State is VersionRowState.Downloading + or VersionRowState.Verifying + or VersionRowState.Installing + or VersionRowState.Uninstalling; public string StateLabel => State switch { VersionRowState.InstalledIdle => Strings.StatusInstalled, VersionRowState.AvailableIdle => Strings.StatusAvailable, VersionRowState.Downloading => Strings.StatusDownloading, + VersionRowState.Verifying => Strings.StatusVerifying, VersionRowState.Installing => Strings.StatusInstalling, VersionRowState.Uninstalling => Strings.StatusUninstalling, _ => string.Empty diff --git a/src/PSLauncher.Core/Localization/Strings.cs b/src/PSLauncher.Core/Localization/Strings.cs index 913181e..a9f653f 100644 --- a/src/PSLauncher.Core/Localization/Strings.cs +++ b/src/PSLauncher.Core/Localization/Strings.cs @@ -95,6 +95,7 @@ public static class Strings public static string StatusDownloading => T("⬇ Téléchargement…", "⬇ Downloading…", "⬇ 下载中…", "⬇ กำลังดาวน์โหลด…", "⬇ جارٍ التنزيل…"); public static string StatusInstalling => T("📦 Installation…", "📦 Installing…", "📦 安装中…", "📦 กำลังติดตั้ง…", "📦 جارٍ التثبيت…"); public static string StatusUninstalling => T("🗑 Suppression…", "🗑 Uninstalling…", "🗑 卸载中…", "🗑 กำลังถอนการติดตั้ง…", "🗑 جارٍ الإزالة…"); + public static string StatusVerifying => T("🔍 Vérification…", "🔍 Verifying…", "🔍 校验中…", "🔍 กำลังตรวจสอบ…", "🔍 جارٍ التحقق…"); // ==================== ACTIONS ==================== public static string ActionLaunch => T("▶ Lancer", "▶ Launch", "▶ 启动", "▶ เปิด", "▶ تشغيل"); diff --git a/src/PSLauncher.Updater/PSLauncher.Updater.csproj b/src/PSLauncher.Updater/PSLauncher.Updater.csproj index f9f6501..4adacda 100644 --- a/src/PSLauncher.Updater/PSLauncher.Updater.csproj +++ b/src/PSLauncher.Updater/PSLauncher.Updater.csproj @@ -8,7 +8,7 @@ latest PS_Launcher.Updater PSLauncher.Updater - 0.19.0 + 0.20.0 ..\PSLauncher.App\Resources\favicon.ico ASTERION VR © 2026 ASTERION VR — All rights reserved