mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: Implement update confirmation handling and streamline update process
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user