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;
/// <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)
{
_app = app;
+15 -1
View File
@@ -126,10 +126,24 @@ try
var theme = ThemeManager.GetTheme(config.ActiveTheme);
ThemeManager.ApplyTheme(theme);
using var orchestrator = new AppOrchestrator(app, config);
var orchestrator = new AppOrchestrator(app, config);
app.Run(orchestrator.MainWindow);
// Capture any confirmed update before tearing anything down, then restore the console.
var pendingUpdate = orchestrator.PendingUpdate;
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)
{
+63 -54
View File
@@ -15,9 +15,17 @@ public sealed class UpdateChecker : IDisposable
private readonly Updater _updater;
private readonly IApplication _app;
private UpdateProgressDialog? _progressDialog;
private string? _pendingVersion;
private bool _applying;
private UpdateStep _lastStep = (UpdateStep)(-1);
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";
@@ -40,7 +48,6 @@ public sealed class UpdateChecker : IDisposable
#endif
}
public async Task CheckNowAsync()
{
_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);
_app.Invoke(() =>
{
var confirmed = UpdateConfirmDialog.Show(_app, CurrentVersion, version);
if (!confirmed)
return;
if (confirmed)
{
_progressDialog = new UpdateProgressDialog(_app, version);
// 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
// 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
{
_progressDialog?.UpdateProgress(0f, "Creating backup...");
Console.WriteLine("Creating backup...");
UpdateBackupService.CreateBackup();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to create pre-update backup");
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;
}
Console.WriteLine($"Warning: could not create a backup ({ex.Message}). Continuing without one.");
}
_progressDialog?.UpdateProgress(0f, "Downloading update...");
await _updater.UpdateAsync();
});
_progressDialog.Show();
}
});
Console.WriteLine("Downloading update...");
await _updater.UpdateAsync(); // download → extract → restart → Environment.Exit(0)
}
private void OnProgressChanged(UpdateStep step, long itemsProcessed, long? totalItems, double? progressPercentage)
{
var fraction = progressPercentage.HasValue ? (float)(progressPercentage.Value / 100.0) : 0f;
var statusText = $"{step}: {itemsProcessed}/{totalItems ?? 0} ({progressPercentage ?? 0:F0}%)";
// Before the TUI is torn down (i.e. during a check) there is no progress surface; the
// 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)
@@ -135,40 +143,40 @@ public sealed class UpdateChecker : IDisposable
private void OnException(Exception exception)
{
Log.Error(exception, "Update failed");
_app.Invoke(() =>
// Headless failure (post-shutdown): report and offer rollback on the console.
if (_applying)
{
_progressDialog?.Close();
_progressDialog = null;
Console.Error.WriteLine();
Console.Error.WriteLine($"Update failed: {exception.Message}");
if (UpdateBackupService.BackupExists())
{
var restore = MessageBox.Query(
_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)
{
Console.WriteLine("Restoring the previous version...");
try
{
UpdateBackupService.RestoreBackup();
// RestoreBackup calls Environment.Exit(0)
UpdateBackupService.RestoreBackup(); // calls Environment.Exit(0)
}
catch (Exception restoreEx)
{
Log.Error(restoreEx, "Backup restoration failed");
MessageBox.ErrorQuery(_app, "Restore Failed",
$"Could not restore backup: {restoreEx.Message}\n\nYou may need to re-download the application.", "OK");
}
Console.Error.WriteLine($"Restore failed: {restoreEx.Message}. Re-download EchoHub to recover.");
Environment.Exit(1);
}
}
else
{
MessageBox.ErrorQuery(_app, "Update Failed",
$"The update failed: {exception.Message}\n\nYou may need to re-download the application.", "OK");
Console.Error.WriteLine("No backup available. Re-download EchoHub if it no longer starts.");
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.NoUpdateAvailable -= OnNoUpdateAvailable;
_updater.OnException -= OnException;
_updater.Dispose();
}
}