Add update progress steps, checksum verification, and improved cleanup

This commit is contained in:
Stone_Red
2026-02-20 20:30:13 +01:00
parent dd0d24cf47
commit e9b3fd63cc
4 changed files with 137 additions and 43 deletions
+2 -2
View File
@@ -37,9 +37,9 @@ internal class Program
} }
} }
private static void Updater_ProgressChanged(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage) private static void Updater_ProgressChanged(UpdateStep step, long? totalItems, long itemsProcessed, double? progressPercentage)
{ {
Console.WriteLine($"{totalBytesDownloaded}/{totalFileSize} {progressPercentage}%"); Console.WriteLine($"[{step}] {itemsProcessed}/{totalItems} {progressPercentage}%");
} }
private static void Updater_NoUpdateAvailable() private static void Updater_NoUpdateAvailable()
+105 -35
View File
@@ -6,15 +6,16 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers;
using System.Xml.Serialization; using System.Xml.Serialization;
namespace AlwaysUpToDate namespace AlwaysUpToDate
{ {
public class Updater : IDisposable public class Updater : IDisposable
{ {
public delegate void UpdaterChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage); public delegate void UpdaterChangedHandler(UpdateStep step, long? totalItems, long itemsProcessed, double? progressPercentage);
public event UpdaterChangedHandler ProgressChanged; public event UpdaterChangedHandler ProgressChanged;
@@ -32,11 +33,12 @@ namespace AlwaysUpToDate
private static readonly XmlSerializer manifestSerializer = new XmlSerializer(typeof(UpdateManifest)); private static readonly XmlSerializer manifestSerializer = new XmlSerializer(typeof(UpdateManifest));
private readonly HttpClient httpClient = new HttpClient(); private readonly HttpClient httpClient = new HttpClient();
private readonly Timer updateTimer = new Timer(); private readonly System.Timers.Timer updateTimer = new System.Timers.Timer();
private readonly string updateInfoUrl; private readonly string updateInfoUrl;
private readonly string installPath; private readonly string installPath;
private string updateUrl; private string updateUrl;
private bool updating; private UpdateItem pendingUpdateItem;
private int updating;
private bool disposed; private bool disposed;
public Updater(TimeSpan interval, Uri updateInfoUri, bool onlyUpdateOnce = false) : this(interval, updateInfoUri?.ToString(), "./", onlyUpdateOnce) public Updater(TimeSpan interval, Uri updateInfoUri, bool onlyUpdateOnce = false) : this(interval, updateInfoUri?.ToString(), "./", onlyUpdateOnce)
@@ -86,14 +88,19 @@ namespace AlwaysUpToDate
public async Task Update() public async Task Update()
{ {
ThrowIfDisposed(); ThrowIfDisposed();
if (!string.IsNullOrWhiteSpace(updateUrl) && !updating) if (!string.IsNullOrWhiteSpace(updateUrl) && Interlocked.CompareExchange(ref updating, 1, 0) == 0)
{ {
await DownloadFile(); await DownloadFile();
} }
} }
private async void UpdateTimer_Elapsed(object sender, ElapsedEventArgs e) private async void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{ {
if (Interlocked.CompareExchange(ref updating, 0, 0) != 0)
{
return;
}
try try
{ {
using HttpResponseMessage response = await httpClient.GetAsync(updateInfoUrl); using HttpResponseMessage response = await httpClient.GetAsync(updateInfoUrl);
@@ -110,16 +117,23 @@ namespace AlwaysUpToDate
return; return;
} }
Version assemblyVersion = Assembly.GetEntryAssembly().GetName().Version; Assembly entryAssembly = Assembly.GetEntryAssembly();
if (entryAssembly == null)
{
return;
}
Version assemblyVersion = entryAssembly.GetName().Version;
if (!Version.TryParse(updateItem.Version, out Version version)) if (!Version.TryParse(updateItem.Version, out Version version))
{ {
return; return;
} }
if (version > assemblyVersion && !updating) if (version > assemblyVersion && Interlocked.CompareExchange(ref updating, 0, 0) == 0)
{ {
updateUrl = updateItem.DownloadUrl; updateUrl = updateItem.DownloadUrl;
pendingUpdateItem = updateItem;
if (!updateItem.IsMandatory) if (!updateItem.IsMandatory)
{ {
UpdateAvailable?.Invoke(updateItem.Version, updateItem.ChangelogUrl); UpdateAvailable?.Invoke(updateItem.Version, updateItem.ChangelogUrl);
@@ -165,7 +179,7 @@ namespace AlwaysUpToDate
try try
{ {
updateTimer.Stop(); updateTimer.Stop();
updating = true; _ = Interlocked.Exchange(ref updating, 1);
using HttpResponseMessage response = await httpClient.GetAsync(updateUrl); using HttpResponseMessage response = await httpClient.GetAsync(updateUrl);
_ = response.EnsureSuccessStatusCode(); _ = response.EnsureSuccessStatusCode();
@@ -175,6 +189,7 @@ namespace AlwaysUpToDate
} }
catch (Exception ex) catch (Exception ex)
{ {
_ = Interlocked.Exchange(ref updating, 0);
OnException?.Invoke(ex); OnException?.Invoke(ex);
} }
} }
@@ -183,6 +198,17 @@ namespace AlwaysUpToDate
{ {
try try
{ {
string zipPath = Path.Join(installPath, "Update.zip");
TriggerProgressChanged(UpdateStep.VerifyingChecksum, null, 0);
if (!VerifyChecksum(zipPath))
{
File.Delete(zipPath);
_ = Interlocked.Exchange(ref updating, 0);
throw new InvalidOperationException("Checksum verification failed for the downloaded update.");
}
TriggerProgressChanged(UpdateStep.VerifyingChecksum, 1, 1);
string executablePath = Process.GetCurrentProcess().MainModule?.FileName ?? Assembly.GetEntryAssembly()?.Location; string executablePath = Process.GetCurrentProcess().MainModule?.FileName ?? Assembly.GetEntryAssembly()?.Location;
if (string.IsNullOrEmpty(executablePath)) if (string.IsNullOrEmpty(executablePath))
@@ -190,25 +216,32 @@ namespace AlwaysUpToDate
throw new InvalidOperationException("Unable to determine the executable path."); throw new InvalidOperationException("Unable to determine the executable path.");
} }
using (ZipArchive zipArchive = ZipFile.OpenRead(Path.Join(installPath, "Update.zip"))) using (ZipArchive zipArchive = ZipFile.OpenRead(zipPath))
{ {
string fullInstallPath = Path.GetFullPath(installPath + Path.DirectorySeparatorChar); string fullInstallPath = Path.GetFullPath(installPath + Path.DirectorySeparatorChar);
long totalEntries = zipArchive.Entries.Count;
long processedEntries = 0;
foreach (ZipArchiveEntry entry in zipArchive.Entries) foreach (ZipArchiveEntry entry in zipArchive.Entries)
{ {
string destinationPath = Path.GetFullPath(Path.Join(installPath, entry.FullName)); string destinationPath = Path.GetFullPath(Path.Join(installPath, entry.FullName));
if (!destinationPath.StartsWith(fullInstallPath, StringComparison.OrdinalIgnoreCase)) if (!destinationPath.StartsWith(fullInstallPath, StringComparison.OrdinalIgnoreCase))
{ {
processedEntries++;
continue; continue;
} }
if (File.Exists(Path.Join(installPath, entry.FullName))) if (File.Exists(Path.Join(installPath, entry.FullName)))
{ {
string moveName = entry.FullName; string moveName = Path.GetFileNameWithoutExtension(entry.FullName) + "_OLD_" + Path.GetExtension(entry.FullName);
int counter = 1;
while (File.Exists(Path.Join(installPath, moveName))) while (File.Exists(Path.Join(installPath, moveName)))
{ {
moveName += "_OLD_"; moveName = Path.GetFileNameWithoutExtension(entry.FullName) + "_OLD_" + counter + Path.GetExtension(entry.FullName);
counter++;
} }
File.Move(Path.Join(installPath, entry.FullName), Path.Join(installPath, moveName)); File.Move(Path.Join(installPath, entry.FullName), Path.Join(installPath, moveName));
} }
@@ -223,36 +256,46 @@ namespace AlwaysUpToDate
{ {
entry.ExtractToFile(Path.Join(installPath, entry.FullName), true); entry.ExtractToFile(Path.Join(installPath, entry.FullName), true);
} }
processedEntries++;
TriggerProgressChanged(UpdateStep.Extracting, totalEntries, processedEntries);
} }
} }
foreach (string filePath in Directory.GetFiles(installPath, "*.*", SearchOption.AllDirectories)) string[] allFiles = Directory.GetFiles(installPath, "*.*", SearchOption.AllDirectories);
string[] oldFiles = Array.FindAll(allFiles, f => f.Contains("_OLD_"));
long totalOldFiles = oldFiles.Length;
long deletedFiles = 0;
foreach (string filePath in oldFiles)
{ {
if (filePath.Contains("_OLD_")) try
{ {
try File.Delete(filePath);
{
File.Delete(filePath);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
} }
catch (Exception ex)
{
Debug.WriteLine(ex);
}
deletedFiles++;
TriggerProgressChanged(UpdateStep.CleaningUp, totalOldFiles, deletedFiles);
} }
File.Delete(Path.Join(installPath, "Update.zip")); File.Delete(zipPath);
//if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) TriggerProgressChanged(UpdateStep.Restarting, null, 0);
//{
// Process.Start("chmod", $"+x \"{executablePath}\"")?.WaitForExit(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
//} {
Process.Start("chmod", $"+x \"{executablePath}\"")?.WaitForExit();
}
_ = Process.Start(new ProcessStartInfo(executablePath) { UseShellExecute = false }); _ = Process.Start(new ProcessStartInfo(executablePath) { UseShellExecute = false });
Environment.Exit(0);
} }
catch (Exception ex) catch (Exception ex)
{ {
_ = Interlocked.Exchange(ref updating, 0);
OnException?.Invoke(ex); OnException?.Invoke(ex);
} }
} }
@@ -273,7 +316,7 @@ namespace AlwaysUpToDate
if (bytesRead == 0) if (bytesRead == 0)
{ {
isMoreToRead = false; isMoreToRead = false;
TriggerProgressChanged(totalDownloadSize, totalBytesRead); TriggerProgressChanged(UpdateStep.Downloading, totalDownloadSize, totalBytesRead);
continue; continue;
} }
@@ -285,7 +328,7 @@ namespace AlwaysUpToDate
if (readCount >= 10) if (readCount >= 10)
{ {
readCount = 0; readCount = 0;
TriggerProgressChanged(totalDownloadSize, totalBytesRead); TriggerProgressChanged(UpdateStep.Downloading, totalDownloadSize, totalBytesRead);
} }
} }
while (isMoreToRead); while (isMoreToRead);
@@ -298,15 +341,42 @@ namespace AlwaysUpToDate
} }
} }
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead) private bool VerifyChecksum(string filePath)
{ {
double? progressPercentage = null; UpdateItem item = pendingUpdateItem;
if (totalDownloadSize.HasValue) if (item?.Checksum == null || string.IsNullOrWhiteSpace(item.Checksum.Value))
{ {
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2); return true;
} }
ProgressChanged?.Invoke(totalDownloadSize, totalBytesRead, progressPercentage); using HashAlgorithm algorithm = CreateHashAlgorithm(item.Checksum.Algorithm);
using FileStream stream = File.OpenRead(filePath);
byte[] hash = algorithm.ComputeHash(stream);
string hashString = BitConverter.ToString(hash).Replace("-", "");
return string.Equals(hashString, item.Checksum.Value, StringComparison.OrdinalIgnoreCase);
}
private static HashAlgorithm CreateHashAlgorithm(HashAlgorithmType algorithm)
{
return algorithm switch
{
HashAlgorithmType.MD5 => MD5.Create(),
HashAlgorithmType.SHA256 => SHA256.Create(),
HashAlgorithmType.SHA512 => SHA512.Create(),
HashAlgorithmType.SHA1 => SHA1.Create(),
_ => throw new NotSupportedException($"Hash algorithm '{algorithm}' is not supported."),
};
}
private void TriggerProgressChanged(UpdateStep step, long? totalSize, long totalProcessed)
{
double? progressPercentage = null;
if (totalSize.HasValue && totalSize.Value > 0)
{
progressPercentage = Math.Round((double)totalProcessed / totalSize.Value * 100, 2);
}
ProgressChanged?.Invoke(step, totalSize, totalProcessed, progressPercentage);
} }
private void ThrowIfDisposed() private void ThrowIfDisposed()
+25 -1
View File
@@ -31,10 +31,25 @@ namespace AlwaysUpToDate
public Checksum Checksum { get; set; } public Checksum Checksum { get; set; }
} }
public enum HashAlgorithmType
{
[XmlEnum("sha1")]
SHA1,
[XmlEnum("md5")]
MD5,
[XmlEnum("sha256")]
SHA256,
[XmlEnum("sha512")]
SHA512,
}
public class Checksum public class Checksum
{ {
[XmlAttribute("algorithm")] [XmlAttribute("algorithm")]
public string Algorithm { get; set; } public HashAlgorithmType Algorithm { get; set; }
[XmlText] [XmlText]
public string Value { get; set; } public string Value { get; set; }
@@ -51,4 +66,13 @@ namespace AlwaysUpToDate
[XmlEnum("linux")] [XmlEnum("linux")]
Linux, Linux,
} }
public enum UpdateStep
{
Downloading,
VerifyingChecksum,
Extracting,
CleaningUp,
Restarting,
}
} }
+5 -5
View File
@@ -57,8 +57,8 @@ private static void Updater_UpdateAvailible(string version, string additionalInf
{ {
} }
private static void Updater_ProgressChanged(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage) private static void Updater_ProgressChanged(UpdateStep step, long? totalItems, long itemsProcessed, double? progressPercentage)
{ {
} }
``` ```
### Handeling the update ### Handeling the update
@@ -74,10 +74,10 @@ private static void Updater_UpdateAvailible(string version, string additionalInf
updater.Update(); updater.Update();
} }
``` ```
### Download process reporting ### Update process reporting
```cs ```cs
private static void Updater_ProgressChanged(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage) private static void Updater_ProgressChanged(UpdateStep step, long? totalItems, long itemsProcessed, double? progressPercentage)
{ {
Console.WriteLine($"{totalBytesDownloaded}/{totalFileSize} {progressPercentage}%"); Console.WriteLine($"[{step}] {itemsProcessed}/{totalItems} {progressPercentage}%");
} }
``` ```