mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: implement rollback functionality with pre-update backup and recovery options | Rebase
This commit is contained in:
@@ -71,6 +71,10 @@ jobs:
|
|||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r osx-arm64 --self-contained true -o publish/server-osx-arm64
|
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r osx-arm64 --self-contained true -o publish/server-osx-arm64
|
||||||
|
|
||||||
|
- name: Publish Server linux-arm64
|
||||||
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r linux-arm64 --self-contained true -o publish/server-linux-arm64
|
||||||
|
|
||||||
- name: Publish Client win-x64
|
- name: Publish Client win-x64
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r win-x64 --self-contained true -o publish/client-win-x64
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r win-x64 --self-contained true -o publish/client-win-x64
|
||||||
@@ -87,6 +91,10 @@ jobs:
|
|||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-arm64 --self-contained true -o publish/client-osx-arm64
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-arm64 --self-contained true -o publish/client-osx-arm64
|
||||||
|
|
||||||
|
- name: Publish Client linux-arm64
|
||||||
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r linux-arm64 --self-contained true -o publish/client-linux-arm64
|
||||||
|
|
||||||
- name: Zip artifacts
|
- name: Zip artifacts
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: |
|
run: |
|
||||||
@@ -95,10 +103,12 @@ jobs:
|
|||||||
zip -r ../EchoHub-Server-linux-x64.zip server-linux-x64/
|
zip -r ../EchoHub-Server-linux-x64.zip server-linux-x64/
|
||||||
zip -r ../EchoHub-Server-osx-x64.zip server-osx-x64/
|
zip -r ../EchoHub-Server-osx-x64.zip server-osx-x64/
|
||||||
zip -r ../EchoHub-Server-osx-arm64.zip server-osx-arm64/
|
zip -r ../EchoHub-Server-osx-arm64.zip server-osx-arm64/
|
||||||
|
zip -r ../EchoHub-Server-linux-arm64.zip server-linux-arm64/
|
||||||
zip -r ../EchoHub-Client-win-x64.zip client-win-x64/
|
zip -r ../EchoHub-Client-win-x64.zip client-win-x64/
|
||||||
zip -r ../EchoHub-Client-linux-x64.zip client-linux-x64/
|
zip -r ../EchoHub-Client-linux-x64.zip client-linux-x64/
|
||||||
zip -r ../EchoHub-Client-osx-x64.zip client-osx-x64/
|
zip -r ../EchoHub-Client-osx-x64.zip client-osx-x64/
|
||||||
zip -r ../EchoHub-Client-osx-arm64.zip client-osx-arm64/
|
zip -r ../EchoHub-Client-osx-arm64.zip client-osx-arm64/
|
||||||
|
zip -r ../EchoHub-Client-linux-arm64.zip client-linux-arm64/
|
||||||
|
|
||||||
- name: Build release notes
|
- name: Build release notes
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
@@ -139,9 +149,11 @@ jobs:
|
|||||||
EchoHub-Server-linux-x64.zip \
|
EchoHub-Server-linux-x64.zip \
|
||||||
EchoHub-Server-osx-x64.zip \
|
EchoHub-Server-osx-x64.zip \
|
||||||
EchoHub-Server-osx-arm64.zip \
|
EchoHub-Server-osx-arm64.zip \
|
||||||
|
EchoHub-Server-linux-arm64.zip \
|
||||||
EchoHub-Client-win-x64.zip \
|
EchoHub-Client-win-x64.zip \
|
||||||
EchoHub-Client-linux-x64.zip \
|
EchoHub-Client-linux-x64.zip \
|
||||||
EchoHub-Client-osx-x64.zip \
|
EchoHub-Client-osx-x64.zip \
|
||||||
EchoHub-Client-osx-arm64.zip
|
EchoHub-Client-osx-arm64.zip \
|
||||||
|
EchoHub-Client-linux-arm64.zip
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
- Add Docker support for EchoHub.Server — `docker compose up -d` for easy self-hosting with persistent volume for database, uploads, and logs
|
- Add Docker support for EchoHub.Server — `docker compose up -d` for easy self-hosting with persistent volume for database, uploads, and logs
|
||||||
- IRC account creation — connecting with a new username auto-registers the account (PASS and SASL PLAIN)
|
- IRC account creation — connecting with a new username auto-registers the account (PASS and SASL PLAIN)
|
||||||
|
- Auto-updater rollback — pre-update backup created automatically before each update; restore via File > Rollback menu or `--rollback` CLI flag
|
||||||
|
- Update failure recovery — if an update fails mid-extraction, offers to restore from the backup immediately
|
||||||
|
- Defensive Unix permission check — verify execute permission on startup after auto-update (defense-in-depth)
|
||||||
|
|
||||||
## Refactoring
|
## Refactoring
|
||||||
|
|
||||||
@@ -21,3 +24,4 @@
|
|||||||
## CI
|
## CI
|
||||||
|
|
||||||
- Add Docker workflow — builds and pushes multi-arch (`amd64`/`arm64`) server image to GHCR on release
|
- Add Docker workflow — builds and pushes multi-arch (`amd64`/`arm64`) server image to GHCR on release
|
||||||
|
- Add `linux-arm64` builds to release pipeline — server and client binaries for ARM Linux (Raspberry Pi, cloud ARM instances)
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public sealed class AppOrchestrator : IDisposable
|
|||||||
_mainWindow.OnAudioPlayRequested += HandleAudioPlayRequested;
|
_mainWindow.OnAudioPlayRequested += HandleAudioPlayRequested;
|
||||||
_mainWindow.OnFileDownloadRequested += HandleFileDownloadRequested;
|
_mainWindow.OnFileDownloadRequested += HandleFileDownloadRequested;
|
||||||
_mainWindow.OnCheckForUpdatesRequested += HandleCheckForUpdatesRequested;
|
_mainWindow.OnCheckForUpdatesRequested += HandleCheckForUpdatesRequested;
|
||||||
|
_mainWindow.OnRollbackRequested += HandleRollbackRequested;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Command Handler Wiring ─────────────────────────────────────────────
|
// ── Command Handler Wiring ─────────────────────────────────────────────
|
||||||
@@ -907,6 +908,32 @@ public sealed class AppOrchestrator : IDisposable
|
|||||||
RunAsync(_updateService.CheckNowAsync, "Failed to check for updates");
|
RunAsync(_updateService.CheckNowAsync, "Failed to check for updates");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleRollbackRequested()
|
||||||
|
{
|
||||||
|
if (!UpdateBackupService.BackupExists())
|
||||||
|
{
|
||||||
|
MessageBox.ErrorQuery(_app, "No Backup", "No backup is available to restore.", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = UpdateBackupService.GetBackupInfo();
|
||||||
|
var confirm = MessageBox.Query(_app, "Rollback Update",
|
||||||
|
$"Restore to version {info?.Version ?? "unknown"}?\n\nThe app will restart.", "Restore", "Cancel");
|
||||||
|
|
||||||
|
if (confirm != 0) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateBackupService.RestoreBackup();
|
||||||
|
// RestoreBackup calls Environment.Exit(0)
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Rollback failed");
|
||||||
|
MessageBox.ErrorQuery(_app, "Rollback Failed", $"Could not restore: {ex.Message}", "OK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Private Helpers ────────────────────────────────────────────────────
|
// ── Private Helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void FetchAndUpdateOnlineUsers()
|
private void FetchAndUpdateOnlineUsers()
|
||||||
|
|||||||
@@ -1,12 +1,58 @@
|
|||||||
using EchoHub.Client;
|
using EchoHub.Client;
|
||||||
using EchoHub.Client.Config;
|
using EchoHub.Client.Config;
|
||||||
|
using EchoHub.Client.Services;
|
||||||
using EchoHub.Client.Themes;
|
using EchoHub.Client.Themes;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Terminal.Gui.App;
|
using Terminal.Gui.App;
|
||||||
using Terminal.Gui.Drawing;
|
|
||||||
|
|
||||||
|
// == CLI rollback: works without TUI, before anything else ================
|
||||||
|
if (args.Contains("--rollback"))
|
||||||
|
{
|
||||||
|
if (UpdateBackupService.BackupExists())
|
||||||
|
{
|
||||||
|
var info = UpdateBackupService.GetBackupInfo();
|
||||||
|
Console.WriteLine($"Rolling back to version {info?.Version ?? "unknown"}...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateBackupService.RestoreBackup();
|
||||||
|
// RestoreBackup calls Environment.Exit(0)
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Rollback failed: {ex.Message}");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("No backup available to restore.");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Unix permission self-check (defense-in-depth after auto-update) ==
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
var exePath = Environment.ProcessPath;
|
||||||
|
if (!string.IsNullOrEmpty(exePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mode = File.GetUnixFileMode(exePath);
|
||||||
|
if ((mode & UnixFileMode.UserExecute) == 0)
|
||||||
|
{
|
||||||
|
File.SetUnixFileMode(exePath, mode | UnixFileMode.UserExecute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best-effort; if we're running, we already have execute permission
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Normal startup ==
|
||||||
var appSettingsPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
var appSettingsPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
||||||
if (!File.Exists(appSettingsPath))
|
if (!File.Exists(appSettingsPath))
|
||||||
{
|
{
|
||||||
@@ -31,6 +77,38 @@ Log.Logger = new LoggerConfiguration()
|
|||||||
|
|
||||||
Log.Information("EchoHub client starting");
|
Log.Information("EchoHub client starting");
|
||||||
|
|
||||||
|
// == Post-update detection: stale backup cleanup or flag for rollback menu ================
|
||||||
|
if (UpdateBackupService.BackupExists())
|
||||||
|
{
|
||||||
|
var backupInfo = UpdateBackupService.GetBackupInfo();
|
||||||
|
if (backupInfo is not null && DateTimeOffset.UtcNow - backupInfo.CreatedAt > TimeSpan.FromDays(7))
|
||||||
|
{
|
||||||
|
Log.Information("Deleting stale update backup from {Date}", backupInfo.CreatedAt);
|
||||||
|
UpdateBackupService.DeleteBackup();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Information("Post-update: backup of v{OldVersion} available for rollback",
|
||||||
|
backupInfo?.Version ?? "unknown");
|
||||||
|
UpdateBackupService.IsPostUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Windows: clean up .old executable left by rollback restore ===========
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
var currentExe = Environment.ProcessPath;
|
||||||
|
if (!string.IsNullOrEmpty(currentExe))
|
||||||
|
{
|
||||||
|
var oldExe = currentExe + ".old";
|
||||||
|
if (File.Exists(oldExe))
|
||||||
|
{
|
||||||
|
try { File.Delete(oldExe); }
|
||||||
|
catch { /* locked or permission issue — will be cleaned next launch */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var config = ConfigManager.Load();
|
var config = ConfigManager.Load();
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace EchoHub.Client.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages pre-update backups and rollback restoration for the auto-updater.
|
||||||
|
/// Backup location: ~/.echohub/update-backup/
|
||||||
|
/// </summary>
|
||||||
|
public static class UpdateBackupService
|
||||||
|
{
|
||||||
|
private static readonly string BackupDir = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
".echohub", "update-backup");
|
||||||
|
|
||||||
|
private static readonly string BackupZipPath = Path.Combine(BackupDir, "backup.zip");
|
||||||
|
private static readonly string BackupInfoPath = Path.Combine(BackupDir, "backup-info.json");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if a backup exists from a recent update (set at startup).
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsPostUpdate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a ZIP backup of the current app directory before an update.
|
||||||
|
/// Deletes any previous backup first. Uses fastest compression for speed.
|
||||||
|
/// </summary>
|
||||||
|
public static void CreateBackup()
|
||||||
|
{
|
||||||
|
var appDir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
var version = UpdateChecker.CurrentVersion;
|
||||||
|
|
||||||
|
if (Directory.Exists(BackupDir))
|
||||||
|
Directory.Delete(BackupDir, true);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(BackupDir);
|
||||||
|
|
||||||
|
Log.Information("Creating pre-update backup of {AppDir} (v{Version})", appDir, version);
|
||||||
|
|
||||||
|
ZipFile.CreateFromDirectory(appDir, BackupZipPath, CompressionLevel.Fastest, includeBaseDirectory: false);
|
||||||
|
|
||||||
|
var info = new BackupInfo(version, appDir, DateTimeOffset.UtcNow);
|
||||||
|
var json = JsonSerializer.Serialize(info, BackupJsonContext.Default.BackupInfo);
|
||||||
|
File.WriteAllText(BackupInfoPath, json);
|
||||||
|
|
||||||
|
Log.Information("Backup created at {BackupPath}", BackupZipPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if a valid backup exists (both ZIP and metadata file present).
|
||||||
|
/// </summary>
|
||||||
|
public static bool BackupExists()
|
||||||
|
=> File.Exists(BackupZipPath) && File.Exists(BackupInfoPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads backup metadata. Returns null if no backup exists or metadata is unreadable.
|
||||||
|
/// </summary>
|
||||||
|
public static BackupInfo? GetBackupInfo()
|
||||||
|
{
|
||||||
|
if (!File.Exists(BackupInfoPath))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(BackupInfoPath);
|
||||||
|
return JsonSerializer.Deserialize(json, BackupJsonContext.Default.BackupInfo);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Failed to read backup metadata");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the backup ZIP to the app directory, then restarts the process.
|
||||||
|
/// This method does not return — it calls Environment.Exit(0).
|
||||||
|
/// </summary>
|
||||||
|
public static void RestoreBackup()
|
||||||
|
{
|
||||||
|
var info = GetBackupInfo()
|
||||||
|
?? throw new InvalidOperationException("No backup metadata found");
|
||||||
|
|
||||||
|
var appDir = info.AppDirectory;
|
||||||
|
Log.Information("Restoring backup v{Version} to {AppDir}", info.Version, appDir);
|
||||||
|
|
||||||
|
// On Windows, rename the running executable so extraction can overwrite it
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
var currentExe = Environment.ProcessPath;
|
||||||
|
if (!string.IsNullOrEmpty(currentExe) && File.Exists(currentExe))
|
||||||
|
{
|
||||||
|
var oldExe = currentExe + ".old";
|
||||||
|
if (File.Exists(oldExe))
|
||||||
|
File.Delete(oldExe);
|
||||||
|
File.Move(currentExe, oldExe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ZipFile.ExtractToDirectory(BackupZipPath, appDir, overwriteFiles: true);
|
||||||
|
|
||||||
|
// Restore execute permission on Unix
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
var exePath = Environment.ProcessPath
|
||||||
|
?? Path.Combine(appDir, "EchoHub.Client");
|
||||||
|
|
||||||
|
if (File.Exists(exePath))
|
||||||
|
{
|
||||||
|
var mode = File.GetUnixFileMode(exePath);
|
||||||
|
File.SetUnixFileMode(exePath, mode | UnixFileMode.UserExecute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the restored version and exit
|
||||||
|
var processPath = Environment.ProcessPath
|
||||||
|
?? Path.Combine(appDir, "EchoHub.Client");
|
||||||
|
|
||||||
|
Log.Information("Launching restored version v{Version}: {Path}", info.Version, processPath);
|
||||||
|
Process.Start(new ProcessStartInfo(processPath) { UseShellExecute = false });
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the backup directory and all contents.
|
||||||
|
/// </summary>
|
||||||
|
public static void DeleteBackup()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(BackupDir))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(BackupDir, true);
|
||||||
|
Log.Information("Update backup deleted");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Failed to delete update backup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BackupInfo(
|
||||||
|
string Version,
|
||||||
|
string AppDirectory,
|
||||||
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
|
[System.Text.Json.Serialization.JsonSerializable(typeof(BackupInfo))]
|
||||||
|
internal partial class BackupJsonContext : System.Text.Json.Serialization.JsonSerializerContext;
|
||||||
@@ -58,23 +58,52 @@ public sealed class UpdateChecker : IDisposable
|
|||||||
{
|
{
|
||||||
Log.Information("Update available: v{Version}", version);
|
Log.Information("Update available: v{Version}", version);
|
||||||
|
|
||||||
var confirmed = false;
|
|
||||||
_app.Invoke(() =>
|
_app.Invoke(() =>
|
||||||
{
|
{
|
||||||
confirmed = UpdateConfirmDialog.Show(_app, CurrentVersion, version);
|
var confirmed = UpdateConfirmDialog.Show(_app, CurrentVersion, version);
|
||||||
|
|
||||||
|
|
||||||
if (confirmed)
|
if (confirmed)
|
||||||
{
|
{
|
||||||
_progressDialog = new UpdateProgressDialog(_app, version);
|
_progressDialog = new UpdateProgressDialog(_app, version);
|
||||||
|
|
||||||
// Start the update; progress is reported via OnProgressChanged
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
// Create backup before the update starts
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_app.Invoke(() => _progressDialog?.UpdateProgress(0f, "Creating backup..."));
|
||||||
|
UpdateBackupService.CreateBackup();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Failed to create pre-update backup");
|
||||||
|
|
||||||
|
var proceed = false;
|
||||||
|
_app.Invoke(() =>
|
||||||
|
{
|
||||||
|
proceed = MessageBox.Query(
|
||||||
|
_app,
|
||||||
|
"Backup Warning",
|
||||||
|
$"Could not create backup: {ex.Message}\n\nContinue update without backup?",
|
||||||
|
"Continue", "Cancel") == 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!proceed)
|
||||||
|
{
|
||||||
|
_app.Invoke(() =>
|
||||||
|
{
|
||||||
|
_progressDialog?.Close();
|
||||||
|
_progressDialog = null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_app.Invoke(() => _progressDialog?.UpdateProgress(0f, "Downloading update..."));
|
||||||
await _updater.UpdateAsync();
|
await _updater.UpdateAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
_progressDialog?.Show();
|
_progressDialog.Show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -111,11 +140,41 @@ public sealed class UpdateChecker : IDisposable
|
|||||||
|
|
||||||
private void OnException(Exception exception)
|
private void OnException(Exception exception)
|
||||||
{
|
{
|
||||||
Log.Error(exception, "Update check failed");
|
Log.Error(exception, "Update failed");
|
||||||
_app.Invoke(() =>
|
_app.Invoke(() =>
|
||||||
{
|
{
|
||||||
_progressDialog?.Close();
|
_progressDialog?.Close();
|
||||||
_progressDialog = null;
|
_progressDialog = null;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateBackupService.RestoreBackup();
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MessageBox.ErrorQuery(_app, "Update Failed",
|
||||||
|
$"The update failed: {exception.Message}\n\nYou may need to re-download the application.", "OK");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using EchoHub.Client.Services;
|
||||||
using EchoHub.Client.Themes;
|
using EchoHub.Client.Themes;
|
||||||
using EchoHub.Client.UI.Chat;
|
using EchoHub.Client.UI.Chat;
|
||||||
using EchoHub.Client.UI.Helpers;
|
using EchoHub.Client.UI.Helpers;
|
||||||
@@ -124,6 +125,11 @@ public sealed class MainWindow : Runnable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action? OnDeleteChannelRequested;
|
public event Action? OnDeleteChannelRequested;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the user requests to rollback to the previous version.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? OnRollbackRequested;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fired when the user activates (Enter/click) an audio message. Parameters: attachmentUrl, fileName.
|
/// Fired when the user activates (Enter/click) an audio message. Parameters: attachmentUrl, fileName.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -323,13 +329,20 @@ public sealed class MainWindow : Runnable
|
|||||||
};
|
};
|
||||||
allUserItems.AddRange(themeItems);
|
allUserItems.AddRange(themeItems);
|
||||||
|
|
||||||
|
var fileItems = new List<View>();
|
||||||
|
if (UpdateBackupService.BackupExists())
|
||||||
|
{
|
||||||
|
var info = UpdateBackupService.GetBackupInfo();
|
||||||
|
var label = info is not null ? $"_Rollback to v{info.Version}..." : "_Rollback Update...";
|
||||||
|
fileItems.Add(new MenuItem(label, "Restore previous version", () => OnRollbackRequested?.Invoke(), Key.Empty));
|
||||||
|
fileItems.Add(new Line());
|
||||||
|
}
|
||||||
|
fileItems.Add(new MenuItem($"_Check for Updates", "Check for new version", () => OnCheckForUpdatesRequested?.Invoke(), Key.Empty));
|
||||||
|
fileItems.Add(new MenuItem("_Quit", "Quit EchoHub", () => _app.RequestStop(), Key.Empty));
|
||||||
|
|
||||||
var menuBar = new MenuBar(
|
var menuBar = new MenuBar(
|
||||||
[
|
[
|
||||||
new MenuBarItem("_File",
|
new MenuBarItem("_File", fileItems),
|
||||||
[
|
|
||||||
new MenuItem("_Quit", "Quit EchoHub", () => _app.RequestStop(), Key.Empty),
|
|
||||||
new MenuItem($"_Check for Updates", "Check for new version", () => OnCheckForUpdatesRequested?.Invoke(), Key.Empty)
|
|
||||||
]),
|
|
||||||
new MenuBarItem("_Server", new View[]
|
new MenuBarItem("_Server", new View[]
|
||||||
{
|
{
|
||||||
new MenuItem("_Connect...", "Connect to a server", () => OnConnectRequested?.Invoke(), Key.Empty),
|
new MenuItem("_Connect...", "Connect to a server", () => OnConnectRequested?.Invoke(), Key.Empty),
|
||||||
|
|||||||
Reference in New Issue
Block a user