Files
EchoHub/src/EchoHub.Client/Services/UpdateChecker.cs
T
HueByte e797ec2542 feat: add /meta command for channel metadata retrieval
- Implemented the `/meta` command to fetch and display channel metadata including room ID, topic, message count, unique user count, estimated size, and protection level.
- Added `ChannelMetaDto` to encapsulate channel metadata.
- Updated `ChannelsController` to handle the new `/meta` endpoint.
- Introduced `UploadLimits` configuration for admin-defined upload size limits for files, images, audio, and avatars.
- Enhanced error handling and user feedback for metadata retrieval.
- Updated documentation to reflect changes in encryption and room metadata.
- Added tests for the new functionality and upload limits.
2026-07-16 07:04:58 +02:00

225 lines
7.3 KiB
C#

using System.Text;
using AlwaysUpToDate;
using EchoHub.Client.UI.Dialogs;
using Serilog;
using Terminal.Gui.App;
using Terminal.Gui.Views;
namespace EchoHub.Client.Services;
public sealed class UpdateChecker : IDisposable
{
private const string ManifestUrl = "https://echohub.voidcube.cloud/api/app/version";
private readonly Updater _updater;
private readonly IApplication _app;
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";
public UpdateChecker(IApplication app)
{
_app = app;
_updater = new Updater(TimeSpan.FromHours(1), ManifestUrl, false);
_updater.UpdateAvailable += OnUpdateAvailable;
_updater.ProgressChanged += OnProgressChanged;
_updater.UpdateStarted += OnUpdateStarted;
_updater.NoUpdateAvailable += OnNoUpdateAvailable;
_updater.OnException += OnException;
}
public void Start()
{
#if RELEASE
_updater.Start();
#endif
}
public async Task CheckNowAsync()
{
_manualCheck = true;
try
{
await _updater.CheckForUpdateAsync();
}
finally
{
_manualCheck = false;
}
}
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;
// 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();
});
}
/// <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()
{
_applying = true;
// The TUI restored the console on shutdown; make sure the block-glyph bar renders.
try { Console.OutputEncoding = Encoding.UTF8; } catch { /* redirected/non-interactive */ }
Console.WriteLine();
Console.WriteLine($"Updating EchoHub to v{_pendingVersion}...");
try
{
Console.WriteLine("Creating backup...");
UpdateBackupService.CreateBackup();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to create pre-update backup");
Console.WriteLine($"Warning: could not create a backup ({ex.Message}). Continuing without one.");
}
Console.WriteLine("Downloading update...");
await _updater.UpdateAsync(); // download → extract → restart → Environment.Exit(0)
}
private const int BarWidth = 28;
private void OnProgressChanged(UpdateStep step, long itemsProcessed, long? totalItems, double? progressPercentage)
{
// Before the TUI is torn down (i.e. during a check) there is no progress surface; the
// real work happens headless after shutdown, so draw a progress bar on the console.
if (!_applying)
return;
// Finish the previous step's line so each step keeps its completed bar.
if (step != _lastStep)
{
if (_lastStep != (UpdateStep)(-1))
Console.WriteLine();
_lastStep = step;
}
var label = Humanize(step);
if (progressPercentage is { } percent)
{
var pct = (int)Math.Clamp(Math.Round(percent), 0, 100);
var filled = pct * BarWidth / 100;
var bar = new string('█', filled) + new string('░', BarWidth - filled); // █ / ░
Console.Write($"\r {label,-13} [{bar}] {pct,3}% ");
}
else
{
// Steps with no measurable total (verifying, restarting): show an indeterminate marker.
Console.Write($"\r {label,-13} working... ");
}
}
private static string Humanize(UpdateStep step) => step switch
{
UpdateStep.Downloading => "Downloading",
UpdateStep.VerifyingChecksum => "Verifying",
UpdateStep.Extracting => "Extracting",
UpdateStep.CleaningUp => "Cleaning up",
UpdateStep.Restarting => "Restarting",
_ => step.ToString(),
};
private void OnUpdateStarted(string version)
{
Log.Information("Update started: v{Version}", version);
}
private void OnNoUpdateAvailable()
{
Log.Debug("No update available");
if (_manualCheck)
{
_app.Invoke(() =>
{
MessageBox.Query(_app, "Check for Updates", $"You are already on the latest version (v{CurrentVersion}).", "OK");
});
}
}
private void OnException(Exception exception)
{
Log.Error(exception, "Update failed");
// Headless failure (post-shutdown): report and offer rollback on the console.
if (_applying)
{
Console.Error.WriteLine();
Console.Error.WriteLine($"Update failed: {exception.Message}");
if (UpdateBackupService.BackupExists())
{
Console.WriteLine("Restoring the previous version...");
try
{
UpdateBackupService.RestoreBackup(); // calls Environment.Exit(0)
}
catch (Exception restoreEx)
{
Log.Error(restoreEx, "Backup restoration failed");
Console.Error.WriteLine($"Restore failed: {restoreEx.Message}. Re-download EchoHub to recover.");
Environment.Exit(1);
}
}
else
{
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");
});
}
public void Dispose()
{
_updater.UpdateAvailable -= OnUpdateAvailable;
_updater.ProgressChanged -= OnProgressChanged;
_updater.UpdateStarted -= OnUpdateStarted;
_updater.NoUpdateAvailable -= OnNoUpdateAvailable;
_updater.OnException -= OnException;
_updater.Dispose();
}
}