launcher: in-place self-update via download + swap + restart

Adds Services/SelfUpdateService.cs implementing the Chrome-style pattern:
  1. Download new exe to "<current>.new"
  2. Rename running exe to "<current>.old" (NTFS allows MoveFile on a
     running exe -- it resolves processes by handle, not by path)
  3. Rename ".new" to canonical path
  4. Spawn the new exe with our argv
  5. Environment.Exit(0)
  6. Next start of the new exe runs CleanupAfterUpdate() in App.OFIC()
     which deletes the leftover ".old"

UI: the existing "Download" banner button is now "Install update". Click
runs the self-update flow with a percent/MB progress label; any failure
(network, AV write-block, dir not writable) falls back to opening the
URL in the browser so users always have a path forward.

Bumps to 0.4.7. Also fixes a stale fallback URL that pointed at
sijbers.uk/pack/... -- now correctly points at bns.sijbers.uk/launcher/.
This commit is contained in:
Matt Sijbers
2026-05-20 22:07:03 +01:00
parent ed70ff52e9
commit f311429bc2
5 changed files with 187 additions and 8 deletions
+41 -6
View File
@@ -128,22 +128,57 @@ public partial class MainWindow : Window
if (current >= advertised) return;
UpdateBannerText.Text = $"A newer launcher (v{advertised}) is available -- you're on v{current.Major}.{current.Minor}.{current.Build}.";
UpdateBannerDownloadButton.Tag = manifest.LauncherUrl ?? "https://sijbers.uk/pack/BrassAndSigil-Launcher.exe";
UpdateBannerDownloadButton.Tag = manifest.LauncherUrl ?? "https://bns.sijbers.uk/launcher/BrassAndSigil-Launcher.exe";
UpdateBanner.IsVisible = true;
AppendLog($"[update] Newer launcher available: v{advertised} (running v{current})");
}
private void OnUpdateBannerDownloadClick(object? sender, RoutedEventArgs e)
private async void OnUpdateBannerDownloadClick(object? sender, RoutedEventArgs e)
{
var url = UpdateBannerDownloadButton.Tag as string
?? "https://sijbers.uk/pack/BrassAndSigil-Launcher.exe";
var url = UpdateBannerDownloadButton.Tag as string;
if (string.IsNullOrEmpty(url))
{
AppendLog("[update] No download URL configured in manifest.");
return;
}
// First click: try in-place self-update. On failure (network blip, write-protected
// dir, antivirus blocking), fall back to opening the browser so the user can still
// grab the new exe manually.
UpdateBannerDownloadButton.IsEnabled = false;
var originalLabel = UpdateBannerDownloadButton.Content as string ?? "Install update";
UpdateBannerDownloadButton.Content = "Updating...";
try
{
Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
var svc = new ModpackLauncher.Services.SelfUpdateService();
var progress = new Progress<(long bytes, long? total)>(p =>
{
var mb = p.bytes / 1024.0 / 1024.0;
UpdateBannerDownloadButton.Content = p.total.HasValue
? $"{(double)p.bytes * 100 / p.total.Value:F0}%"
: $"{mb:F1} MB";
});
await svc.DownloadAndInstallAsync(url, progress, AppendLog);
// If the method returns, it means it didn't exit -- that shouldn't normally
// happen, but reset the UI just in case.
UpdateBannerDownloadButton.Content = originalLabel;
UpdateBannerDownloadButton.IsEnabled = true;
}
catch (Exception ex)
{
AppendLog($"[update] Couldn't open browser: {ex.Message}");
AppendLog($"[update] Auto-update failed: {ex.Message}");
AppendLog("[update] Opening browser as fallback...");
try
{
Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
}
catch (Exception ex2)
{
AppendLog($"[update] Browser fallback also failed: {ex2.Message}");
}
UpdateBannerDownloadButton.Content = originalLabel;
UpdateBannerDownloadButton.IsEnabled = true;
}
}