v0.20.0 — Cancel-from-red-button waits for segments + new Verifying row state
Two related UX bugs fixed. 1) Red "Annuler" button on a row : DL appeared to keep going The button calls RestartFromZeroAsync which used to fire _activeDownloadCts.Cancel() then sleep 200 ms then delete the .partial. With 16 parallel segments mid-write, 200 ms is way too short — the segments were still flushing buffers when the file got deleted, then recreated it from their own write streams, making it look like the download "continued". Now we keep a reference to the in-flight install Task (_activeInstallTask) and properly await it (with a 10 s safety timeout) before discarding state. This guarantees all segment FileStreams are closed and the .partial deletion is the final word. 2) Progress bar said "Downloading…" during SHA-256 verification The footer message switched to "🔍 Vérification SHA-256 v…" but the row's badge stayed on "⬇ Downloading…" because row.State stayed at Downloading throughout DownloadAsync (which internally chains DL + verify). New VersionRowState.Verifying inserted between Downloading and Installing, applied on the first hashProgress callback. Badge now reads "🔍 Vérification…" / "🔍 Verifying…" during the hash phase. VersionRowViewModel.IsBusy now also includes Verifying so commands that gate on busy stay disabled during verification. Versions bumped to 0.20.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<Product>PROSERVE Launcher</Product>
|
||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||
<Version>0.19.0</Version>
|
||||
<AssemblyVersion>0.19.0.0</AssemblyVersion>
|
||||
<FileVersion>0.19.0.0</FileVersion>
|
||||
<Version>0.20.0</Version>
|
||||
<AssemblyVersion>0.20.0.0</AssemblyVersion>
|
||||
<FileVersion>0.20.0.0</FileVersion>
|
||||
|
||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
|
||||
@@ -53,6 +53,13 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
private RemoteManifest? _lastManifest;
|
||||
private CancellationTokenSource? _activeDownloadCts;
|
||||
private VersionRowViewModel? _activeRow;
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private Task? _activeInstallTask;
|
||||
|
||||
public ObservableCollection<VersionRowViewModel> 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<DownloadProgress>(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<double>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", "▶ 启动", "▶ เปิด", "▶ تشغيل");
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>PS_Launcher.Updater</AssemblyName>
|
||||
<RootNamespace>PSLauncher.Updater</RootNamespace>
|
||||
<Version>0.19.0</Version>
|
||||
<Version>0.20.0</Version>
|
||||
<ApplicationIcon>..\PSLauncher.App\Resources\favicon.ico</ApplicationIcon>
|
||||
<Company>ASTERION VR</Company>
|
||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||
|
||||
Reference in New Issue
Block a user