feat: Implement update confirmation handling and streamline update process

This commit is contained in:
HueByte
2026-07-16 07:04:26 +02:00
parent 494dcb46cf
commit dbf6565d18
3 changed files with 99 additions and 70 deletions
+6
View File
@@ -40,6 +40,12 @@ public sealed class AppOrchestrator : IDisposable
public MainWindow MainWindow => _mainWindow; public MainWindow MainWindow => _mainWindow;
/// <summary>
/// Set when the user confirms an update. The host must run this after the Terminal.Gui main
/// loop exits (console restored), so the updater's in-place restart doesn't fight the TUI.
/// </summary>
public Func<Task>? PendingUpdate => _updateService.PendingUpdate;
public AppOrchestrator(IApplication app, ClientConfig config) public AppOrchestrator(IApplication app, ClientConfig config)
{ {
_app = app; _app = app;
+15 -1
View File
@@ -126,10 +126,24 @@ try
var theme = ThemeManager.GetTheme(config.ActiveTheme); var theme = ThemeManager.GetTheme(config.ActiveTheme);
ThemeManager.ApplyTheme(theme); ThemeManager.ApplyTheme(theme);
using var orchestrator = new AppOrchestrator(app, config); var orchestrator = new AppOrchestrator(app, config);
app.Run(orchestrator.MainWindow); app.Run(orchestrator.MainWindow);
// Capture any confirmed update before tearing anything down, then restore the console.
var pendingUpdate = orchestrator.PendingUpdate;
app.Dispose(); app.Dispose();
// Apply the update on a clean console: the TUI has released it, so the updater can extract
// and restart the process without deadlocking against the alternate-screen buffer. This call
// ends by starting the new version and calling Environment.Exit, so it does not return.
if (pendingUpdate is not null)
{
Log.Information("Applying confirmed update after shutdown");
await pendingUpdate();
}
orchestrator.Dispose();
} }
catch (Exception ex) catch (Exception ex)
{ {
+63 -54
View File
@@ -15,9 +15,17 @@ public sealed class UpdateChecker : IDisposable
private readonly Updater _updater; private readonly Updater _updater;
private readonly IApplication _app; private readonly IApplication _app;
private UpdateProgressDialog? _progressDialog; private string? _pendingVersion;
private bool _applying;
private UpdateStep _lastStep = (UpdateStep)(-1);
private bool _manualCheck; private bool _manualCheck;
/// <summary>
/// Set when the user confirms an update. The host runs this <b>after</b> the Terminal.Gui
/// main loop has exited and the console is restored, so the library's in-place restart doesn't
/// deadlock against a TUI that still owns the console.
/// </summary>
public Func<Task>? PendingUpdate { get; private set; }
public static string CurrentVersion => typeof(UpdateChecker).Assembly.GetName().Version?.ToString(3) ?? "0.0.0"; public static string CurrentVersion => typeof(UpdateChecker).Assembly.GetName().Version?.ToString(3) ?? "0.0.0";
@@ -40,7 +48,6 @@ public sealed class UpdateChecker : IDisposable
#endif #endif
} }
public async Task CheckNowAsync() public async Task CheckNowAsync()
{ {
_manualCheck = true; _manualCheck = true;
@@ -54,65 +61,66 @@ public sealed class UpdateChecker : IDisposable
} }
} }
private async void OnUpdateAvailable(string version, string changelogUrl) private void OnUpdateAvailable(string version, string changelogUrl)
{ {
Log.Information("Update available: v{Version}", version); Log.Information("Update available: v{Version}", version);
_app.Invoke(() => _app.Invoke(() =>
{ {
var confirmed = UpdateConfirmDialog.Show(_app, CurrentVersion, version); var confirmed = UpdateConfirmDialog.Show(_app, CurrentVersion, version);
if (!confirmed)
return;
if (confirmed) // Defer the actual download/extract/restart to after the TUI is torn down.
{ // Running it under the live main loop lets the library restart the process while
_progressDialog = new UpdateProgressDialog(_app, version); // this one still holds the console in raw/alternate-screen mode — the two processes
// then deadlock over the console (the "stuck at N/N extracting" hang).
_pendingVersion = version;
PendingUpdate = ApplyUpdateAsync;
_app.RequestStop();
});
}
_ = Task.Run(async () => /// <summary>
/// Runs the update on a plain console (invoked by the host after the main loop exits).
/// Ends by restarting the app and exiting the process, or restoring the backup on failure.
/// </summary>
private async Task ApplyUpdateAsync()
{ {
// Create backup before the update starts _applying = true;
Console.WriteLine();
Console.WriteLine($"Updating EchoHub to v{_pendingVersion}...");
try try
{ {
_progressDialog?.UpdateProgress(0f, "Creating backup..."); Console.WriteLine("Creating backup...");
UpdateBackupService.CreateBackup(); UpdateBackupService.CreateBackup();
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Failed to create pre-update backup"); Log.Error(ex, "Failed to create pre-update backup");
Console.WriteLine($"Warning: could not create a backup ({ex.Message}). Continuing without one.");
var proceed = false;
proceed = MessageBox.Query(
_app,
"Backup Warning",
$"Could not create backup: {ex.Message}\n\nContinue update without backup?",
"Continue", "Cancel") == 0;
if (!proceed)
{
_progressDialog?.Close();
_progressDialog = null;
return;
}
} }
_progressDialog?.UpdateProgress(0f, "Downloading update..."); Console.WriteLine("Downloading update...");
await _updater.UpdateAsync(); await _updater.UpdateAsync(); // download → extract → restart → Environment.Exit(0)
});
_progressDialog.Show();
}
});
} }
private void OnProgressChanged(UpdateStep step, long itemsProcessed, long? totalItems, double? progressPercentage) private void OnProgressChanged(UpdateStep step, long itemsProcessed, long? totalItems, double? progressPercentage)
{ {
var fraction = progressPercentage.HasValue ? (float)(progressPercentage.Value / 100.0) : 0f; // Before the TUI is torn down (i.e. during a check) there is no progress surface; the
var statusText = $"{step}: {itemsProcessed}/{totalItems ?? 0} ({progressPercentage ?? 0:F0}%)"; // real work happens headless after shutdown, so report it on the console.
if (!_applying)
return;
if (!progressPercentage.HasValue) if (step != _lastStep)
{ {
statusText = $"{step}..."; Console.WriteLine();
_lastStep = step;
} }
_progressDialog?.UpdateProgress(fraction, statusText); var pct = progressPercentage ?? 0;
Console.Write($"\r {step}: {itemsProcessed}/{totalItems ?? 0} ({pct:F0}%) ");
} }
private void OnUpdateStarted(string version) private void OnUpdateStarted(string version)
@@ -135,40 +143,40 @@ public sealed class UpdateChecker : IDisposable
private void OnException(Exception exception) private void OnException(Exception exception)
{ {
Log.Error(exception, "Update failed"); Log.Error(exception, "Update failed");
_app.Invoke(() =>
// Headless failure (post-shutdown): report and offer rollback on the console.
if (_applying)
{ {
_progressDialog?.Close(); Console.Error.WriteLine();
_progressDialog = null; Console.Error.WriteLine($"Update failed: {exception.Message}");
if (UpdateBackupService.BackupExists()) if (UpdateBackupService.BackupExists())
{ {
var restore = MessageBox.Query( Console.WriteLine("Restoring the previous version...");
_app,
"Update Failed",
$"The update failed: {exception.Message}\n\n"
+ "A backup of the previous version is available.\nRestore now? (The app will restart.)",
"Restore", "Cancel");
if (restore == 0)
{
try try
{ {
UpdateBackupService.RestoreBackup(); UpdateBackupService.RestoreBackup(); // calls Environment.Exit(0)
// RestoreBackup calls Environment.Exit(0)
} }
catch (Exception restoreEx) catch (Exception restoreEx)
{ {
Log.Error(restoreEx, "Backup restoration failed"); Log.Error(restoreEx, "Backup restoration failed");
MessageBox.ErrorQuery(_app, "Restore Failed", Console.Error.WriteLine($"Restore failed: {restoreEx.Message}. Re-download EchoHub to recover.");
$"Could not restore backup: {restoreEx.Message}\n\nYou may need to re-download the application.", "OK"); Environment.Exit(1);
}
} }
} }
else else
{ {
MessageBox.ErrorQuery(_app, "Update Failed", Console.Error.WriteLine("No backup available. Re-download EchoHub if it no longer starts.");
$"The update failed: {exception.Message}\n\nYou may need to re-download the application.", "OK"); Environment.Exit(1);
} }
return;
}
// Failure during a check while the TUI is still running.
_app.Invoke(() =>
{
MessageBox.ErrorQuery(_app, "Update Check Failed",
$"Could not check for updates: {exception.Message}", "OK");
}); });
} }
@@ -179,5 +187,6 @@ public sealed class UpdateChecker : IDisposable
_updater.UpdateStarted -= OnUpdateStarted; _updater.UpdateStarted -= OnUpdateStarted;
_updater.NoUpdateAvailable -= OnNoUpdateAvailable; _updater.NoUpdateAvailable -= OnNoUpdateAvailable;
_updater.OnException -= OnException; _updater.OnException -= OnException;
_updater.Dispose();
} }
} }