diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs index 7132e6f5..783e69e1 100644 --- a/Source/LibationAvalonia/Views/MainWindow.axaml.cs +++ b/Source/LibationAvalonia/Views/MainWindow.axaml.cs @@ -166,20 +166,23 @@ namespace LibationAvalonia.Views private void Configure_Upgrade() { setProgressVisible(false); -#if !DEBUG - async System.Threading.Tasks.Task upgradeAvailable(LibationUiBase.UpgradeEventArgs e) +#pragma warning disable CS8321 // Local function is declared but never used + async Task upgradeAvailable(LibationUiBase.UpgradeEventArgs e) { - var notificationResult = await new Dialogs.UpgradeNotificationDialog(e.UpgradeProperties, e.CapUpgrade).ShowDialogAsync(this); + var notificationResult = await new UpgradeNotificationDialog(e.UpgradeProperties, e.CapUpgrade).ShowDialogAsync(this); e.Ignore = notificationResult == DialogResult.Ignore; e.InstallUpgrade = notificationResult == DialogResult.OK; } +#pragma warning restore CS8321 // Local function is declared but never used var upgrader = new LibationUiBase.Upgrader(); - upgrader.DownloadProgress += async (_, e) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => ViewModel.DownloadProgress = e.ProgressPercentage); - upgrader.DownloadBegin += async (_, _) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(true)); - upgrader.DownloadCompleted += async (_, _) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(false)); + upgrader.DownloadProgress += async (_, e) => await Dispatcher.UIThread.InvokeAsync(() => ViewModel.DownloadProgress = e.ProgressPercentage); + upgrader.DownloadBegin += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(true)); + upgrader.DownloadCompleted += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(false)); + upgrader.UpgradeFailed += async (_, message) => await Dispatcher.UIThread.InvokeAsync(() => { setProgressVisible(false); MessageBox.Show(this, message, "Upgrade Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); }); +#if !DEBUG Opened += async (_, _) => await upgrader.CheckForUpgradeAsync(upgradeAvailable); #endif } diff --git a/Source/LibationUiBase/Upgrader.cs b/Source/LibationUiBase/Upgrader.cs index 1513966a..1a552fe4 100644 --- a/Source/LibationUiBase/Upgrader.cs +++ b/Source/LibationUiBase/Upgrader.cs @@ -6,11 +6,12 @@ using System.IO; using System.Net.Http; using System.Threading.Tasks; +#nullable enable namespace LibationUiBase { public class UpgradeEventArgs { - public UpgradeProperties UpgradeProperties { get; internal init; } + public required UpgradeProperties UpgradeProperties { get; init; } public bool CapUpgrade { get; internal init; } private bool _ignore = false; private bool _installUpgrade = true; @@ -34,18 +35,147 @@ namespace LibationUiBase } } - public class Upgrader + public class Upgrader : UpgraderBase { - public event EventHandler DownloadBegin; - public event EventHandler DownloadProgress; - public event EventHandler DownloadCompleted; + protected override async Task CheckForUpgradeAsync() + { + try + { + return await Task.Run(LibationScaffolding.GetLatestRelease); + } + catch (Exception ex) + { + string message = "An error occurred while checking for app upgrades."; + Serilog.Log.Logger.Error(ex, message); + OnUpgradeFailed(message, ex); + return null; + } + } + + protected override async Task DownloadUpgradeAsync(UpgradeProperties upgradeProperties) + { + if (upgradeProperties.ZipUrl is null) + { + string message = "Download link for new version not found."; + Serilog.Log.Logger.Warning(message); + OnUpgradeFailed(message, null); + return null; + } + + //Silently download the upgrade in the background, save it to a temp file. + + var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl)); + + Serilog.Log.Logger.Information($"Downloading {zipFile}"); + + try + { + using var dlClient = new HttpClient(); + using var response = await dlClient.GetAsync(upgradeProperties.ZipUrl, HttpCompletionOption.ResponseHeadersRead); + using var dlStream = await response.Content.ReadAsStreamAsync(); + using var tempFile = File.OpenWrite(zipFile); + + int read; + long totalRead = 0; + Memory buffer = new byte[128 * 1024]; + long contentLength = response.Content.Headers.ContentLength ?? 0; + + while ((read = await dlStream.ReadAsync(buffer)) > 0) + { + await tempFile.WriteAsync(buffer[..read]); + totalRead += read; + + OnDownloadProgress( + new DownloadProgress + { + BytesReceived = totalRead, + TotalBytesToReceive = contentLength, + ProgressPercentage = contentLength > 0 ? 100d * totalRead / contentLength : 0 + }); + } + + return zipFile; + } + catch (Exception ex) + { + var message = $"Failed to download the upgrade: {upgradeProperties.ZipUrl}"; + Serilog.Log.Logger.Error(ex, message); + OnUpgradeFailed(message, ex); + return null; + } + } + } + + public class MockUpgrader : UpgraderBase + { + public int DownloadTimeMs { get; set; } = 3000; + public int DownloadSizeInBytes { get; set; } = 150 * 1024 * 1024; + public bool CheckForUpgradeSucceeds { get; set; } = true; + public bool DownloadUpgradeSucceeds { get; set; } = true; + public string? MockUpgradeBundle { get; set; } + + protected override Task CheckForUpgradeAsync() + { + if (!CheckForUpgradeSucceeds) + { + OnUpgradeFailed("Mock Check For Upgrade Failed", null); + return Task.FromResult(null); + } + return Task.FromResult(new UpgradeProperties( + "http://fake.url/to/bundle.zip", + "", + Path.GetFileName(MockUpgradeBundle), + LibationScaffolding.BuildVersion, + "")); + } + + protected override async Task DownloadUpgradeAsync(UpgradeProperties upgradeProperties) + { + if (!File.Exists(MockUpgradeBundle)) + { + OnUpgradeFailed("Mock Download bundle file not found", null); + return null; + } + + for (int i = 1; i <= 100; i++) + { + await Task.Delay(DownloadTimeMs / 100); + OnDownloadProgress(new() + { + BytesReceived = DownloadSizeInBytes / 100, + ProgressPercentage = i, + TotalBytesToReceive = DownloadSizeInBytes * i / 100 + }); + } + if (!DownloadUpgradeSucceeds) + { + OnUpgradeFailed("Mock Download Upgrade Failed", null); + return null; + } + + return MockUpgradeBundle; + } + } + + public abstract class UpgraderBase + { + public event EventHandler? DownloadBegin; + public event EventHandler? DownloadProgress; + public event EventHandler? DownloadCompleted; + public event EventHandler? UpgradeFailed; + + protected void OnDownloadProgress(DownloadProgress args) => DownloadProgress?.Invoke(this, args); + protected void OnUpgradeFailed(string message, Exception? ex) + => UpgradeFailed?.Invoke(this, (message + Environment.NewLine + Environment.NewLine + ex?.Message).Trim()); + protected abstract Task CheckForUpgradeAsync(); + protected abstract Task DownloadUpgradeAsync(UpgradeProperties upgradeProperties); public async Task CheckForUpgradeAsync(Func upgradeAvailableHandler) { try { - var upgradeProperties = await Task.Run(LibationScaffolding.GetLatestRelease); - if (upgradeProperties is null) return; + if (await CheckForUpgradeAsync() is not UpgradeProperties upgradeProperties) + return; const string ignoreUpgrade = "IgnoreUpgrade"; var config = Configuration.Instance; @@ -73,7 +203,7 @@ namespace LibationUiBase //Download the upgrade file in the background, DownloadBegin?.Invoke(this, EventArgs.Empty); - string upgradeBundle = await DownloadUpgradeAsync(upgradeProperties); + string? upgradeBundle = await DownloadUpgradeAsync(upgradeProperties); if (string.IsNullOrEmpty(upgradeBundle) || !File.Exists(upgradeBundle)) { @@ -91,57 +221,9 @@ namespace LibationUiBase } catch (Exception ex) { - Serilog.Log.Logger.Error(ex, "An error occured while checking for app upgrades."); - } - } - - private async Task DownloadUpgradeAsync(UpgradeProperties upgradeProperties) - { - if (upgradeProperties.ZipUrl is null) - { - Serilog.Log.Logger.Warning("Download link for new version not found"); - return null; - } - - //Silently download the upgrade in the background, save it to a temp file. - - var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl)); - - Serilog.Log.Logger.Information($"Downloading {zipFile}"); - - try - { - using var dlClient = new HttpClient(); - using var response = await dlClient.GetAsync(upgradeProperties.ZipUrl, HttpCompletionOption.ResponseHeadersRead); - using var dlStream = await response.Content.ReadAsStreamAsync(); - using var tempFile = File.OpenWrite(zipFile); - - int read; - long totalRead = 0; - Memory buffer = new byte[128 * 1024]; - long contentLength = response.Content.Headers.ContentLength ?? 0; - - while ((read = await dlStream.ReadAsync(buffer)) > 0) - { - await tempFile.WriteAsync(buffer[..read]); - totalRead += read; - - DownloadProgress?.Invoke( - this, - new DownloadProgress - { - BytesReceived = totalRead, - TotalBytesToReceive = contentLength, - ProgressPercentage = contentLength > 0 ? 100d * totalRead / contentLength : 0 - }); - } - - return zipFile; - } - catch (Exception ex) - { - Serilog.Log.Logger.Error(ex, "Failed to download the upgrade: {bundle}", upgradeProperties.ZipUrl); - return null; + var message = "An error occurred while checking for app upgrades."; + Serilog.Log.Logger.Error(ex, message); + OnUpgradeFailed(message, ex); } } } diff --git a/Source/LibationWinForms/Form1.Upgrade.cs b/Source/LibationWinForms/Form1.Upgrade.cs index 2d2ceb04..23b5ba6f 100644 --- a/Source/LibationWinForms/Form1.Upgrade.cs +++ b/Source/LibationWinForms/Form1.Upgrade.cs @@ -1,6 +1,7 @@ using LibationUiBase; using LibationWinForms.Dialogs; using System.Threading.Tasks; +using System.Windows.Forms; namespace LibationWinForms { @@ -9,22 +10,25 @@ namespace LibationWinForms private void Configure_Upgrade() { setProgressVisible(false); -#if !DEBUG +#pragma warning disable CS8321 // Local function is declared but never used Task upgradeAvailable(UpgradeEventArgs e) { var notificationResult = new UpgradeNotificationDialog(e.UpgradeProperties).ShowDialog(this); - e.Ignore = notificationResult == System.Windows.Forms.DialogResult.Ignore; - e.InstallUpgrade = notificationResult == System.Windows.Forms.DialogResult.Yes; + e.Ignore = notificationResult == DialogResult.Ignore; + e.InstallUpgrade = notificationResult == DialogResult.Yes; return Task.CompletedTask; } +#pragma warning restore CS8321 // Local function is declared but never used var upgrader = new Upgrader(); upgrader.DownloadProgress += (_, e) => Invoke(() => upgradePb.Value = int.Max(0, int.Min(100, (int)(e.ProgressPercentage ?? 0)))); upgrader.DownloadBegin += (_, _) => Invoke(() => setProgressVisible(true)); upgrader.DownloadCompleted += (_, _) => Invoke(() => setProgressVisible(false)); + upgrader.UpgradeFailed += (_, message) => Invoke(() => { setProgressVisible(false); MessageBox.Show(this, message, "Upgrade Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); }); +#if !DEBUG Shown += async (_, _) => await upgrader.CheckForUpgradeAsync(upgradeAvailable); #endif } diff --git a/Source/LoadByOS/WindowsConfigApp/WinInterop.cs b/Source/LoadByOS/WindowsConfigApp/WinInterop.cs index 32fce3da..34be4466 100644 --- a/Source/LoadByOS/WindowsConfigApp/WinInterop.cs +++ b/Source/LoadByOS/WindowsConfigApp/WinInterop.cs @@ -30,11 +30,12 @@ namespace WindowsConfigApp public void InstallUpgrade(string upgradeBundle) { + const string ExtractorExeName = "ZipExtractor.exe"; var thisExe = Environment.ProcessPath; var thisDir = Path.GetDirectoryName(thisExe); - var zipExtractor = Path.Combine(Path.GetTempPath(), "ZipExtractor.exe"); + var zipExtractor = Path.Combine(Path.GetTempPath(), ExtractorExeName); - File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true); + File.Copy(Path.Combine(thisDir, ExtractorExeName), zipExtractor, overwrite: true); RunAsRoot(zipExtractor, $"--input {upgradeBundle.SurroundWithQuotes()} " +