Michael Bucari-Tovo 9280b29512 Fix upgrade bug when Libation's working dir isn't program files dir
Add MockUpgrader for testing the Upgrade process.
Fixes issue #1302
2025-07-17 13:10:42 -06:00

231 lines
6.4 KiB
C#

using AppScaffolding;
using Dinah.Core.Net.Http;
using LibationFileManager;
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
#nullable enable
namespace LibationUiBase
{
public class UpgradeEventArgs
{
public required UpgradeProperties UpgradeProperties { get; init; }
public bool CapUpgrade { get; internal init; }
private bool _ignore = false;
private bool _installUpgrade = true;
public bool Ignore
{
get => _ignore;
set
{
_ignore = value;
_installUpgrade &= !Ignore;
}
}
public bool InstallUpgrade
{
get => _installUpgrade;
set
{
_installUpgrade = value;
_ignore &= !InstallUpgrade;
}
}
}
public class Upgrader : UpgraderBase
{
protected override async Task<UpgradeProperties?> 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<string?> 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<byte> 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<UpgradeProperties?> CheckForUpgradeAsync()
{
if (!CheckForUpgradeSucceeds)
{
OnUpgradeFailed("Mock Check For Upgrade Failed", null);
return Task.FromResult<UpgradeProperties?>(null);
}
return Task.FromResult<UpgradeProperties?>(new UpgradeProperties(
"http://fake.url/to/bundle.zip",
"",
Path.GetFileName(MockUpgradeBundle),
LibationScaffolding.BuildVersion,
"<RELEASE NOTES>"));
}
protected override async Task<string?> 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>? DownloadProgress;
public event EventHandler<bool>? DownloadCompleted;
public event EventHandler<string>? 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<UpgradeProperties?> CheckForUpgradeAsync();
protected abstract Task<string?> DownloadUpgradeAsync(UpgradeProperties upgradeProperties);
public async Task CheckForUpgradeAsync(Func<UpgradeEventArgs, Task> upgradeAvailableHandler)
{
try
{
if (await CheckForUpgradeAsync() is not UpgradeProperties upgradeProperties)
return;
const string ignoreUpgrade = "IgnoreUpgrade";
var config = Configuration.Instance;
if (config.GetString(propertyName: ignoreUpgrade) == upgradeProperties.LatestRelease.ToString())
return;
var interop = InteropFactory.Create();
if (!interop.CanUpgrade)
Serilog.Log.Logger.Information("Can't perform upgrade automatically");
var upgradeEventArgs = new UpgradeEventArgs
{
UpgradeProperties = upgradeProperties,
CapUpgrade = interop.CanUpgrade
};
await upgradeAvailableHandler(upgradeEventArgs);
if (upgradeEventArgs.Ignore)
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpgrade);
if (!upgradeEventArgs.InstallUpgrade) return;
//Download the upgrade file in the background,
DownloadBegin?.Invoke(this, EventArgs.Empty);
string? upgradeBundle = await DownloadUpgradeAsync(upgradeProperties);
if (string.IsNullOrEmpty(upgradeBundle) || !File.Exists(upgradeBundle))
{
DownloadCompleted?.Invoke(this, false);
}
else
{
DownloadCompleted?.Invoke(this, true);
//Install the upgrade
Serilog.Log.Logger.Information($"Begin running auto-upgrader");
interop.InstallUpgrade(upgradeBundle);
Serilog.Log.Logger.Information($"Completed running auto-upgrader");
}
}
catch (Exception ex)
{
var message = "An error occurred while checking for app upgrades.";
Serilog.Log.Logger.Error(ex, message);
OnUpgradeFailed(message, ex);
}
}
}
}