From e9b3fd63cc34a6e8ea3d0e925b478264934ee19a Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:30:13 +0100 Subject: [PATCH] Add update progress steps, checksum verification, and improved cleanup --- AlwaysUpToDate.Test/Program.cs | 4 +- AlwaysUpToDate/DownloadManager.cs | 140 ++++++++++++++++++++++-------- AlwaysUpToDate/UpdateInfo.cs | 26 +++++- README.md | 10 +-- 4 files changed, 137 insertions(+), 43 deletions(-) diff --git a/AlwaysUpToDate.Test/Program.cs b/AlwaysUpToDate.Test/Program.cs index 7b7c4b8..d72c1a3 100644 --- a/AlwaysUpToDate.Test/Program.cs +++ b/AlwaysUpToDate.Test/Program.cs @@ -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() diff --git a/AlwaysUpToDate/DownloadManager.cs b/AlwaysUpToDate/DownloadManager.cs index ce86b8c..3c14030 100644 --- a/AlwaysUpToDate/DownloadManager.cs +++ b/AlwaysUpToDate/DownloadManager.cs @@ -6,15 +6,16 @@ using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Threading; using System.Threading.Tasks; -using System.Timers; using System.Xml.Serialization; namespace AlwaysUpToDate { 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; @@ -32,11 +33,12 @@ namespace AlwaysUpToDate private static readonly XmlSerializer manifestSerializer = new XmlSerializer(typeof(UpdateManifest)); 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 installPath; private string updateUrl; - private bool updating; + private UpdateItem pendingUpdateItem; + private int updating; private bool disposed; public Updater(TimeSpan interval, Uri updateInfoUri, bool onlyUpdateOnce = false) : this(interval, updateInfoUri?.ToString(), "./", onlyUpdateOnce) @@ -86,14 +88,19 @@ namespace AlwaysUpToDate public async Task Update() { ThrowIfDisposed(); - if (!string.IsNullOrWhiteSpace(updateUrl) && !updating) + if (!string.IsNullOrWhiteSpace(updateUrl) && Interlocked.CompareExchange(ref updating, 1, 0) == 0) { 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 { using HttpResponseMessage response = await httpClient.GetAsync(updateInfoUrl); @@ -110,16 +117,23 @@ namespace AlwaysUpToDate 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)) { return; } - if (version > assemblyVersion && !updating) + if (version > assemblyVersion && Interlocked.CompareExchange(ref updating, 0, 0) == 0) { updateUrl = updateItem.DownloadUrl; + pendingUpdateItem = updateItem; if (!updateItem.IsMandatory) { UpdateAvailable?.Invoke(updateItem.Version, updateItem.ChangelogUrl); @@ -165,7 +179,7 @@ namespace AlwaysUpToDate try { updateTimer.Stop(); - updating = true; + _ = Interlocked.Exchange(ref updating, 1); using HttpResponseMessage response = await httpClient.GetAsync(updateUrl); _ = response.EnsureSuccessStatusCode(); @@ -175,6 +189,7 @@ namespace AlwaysUpToDate } catch (Exception ex) { + _ = Interlocked.Exchange(ref updating, 0); OnException?.Invoke(ex); } } @@ -183,6 +198,17 @@ namespace AlwaysUpToDate { 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; if (string.IsNullOrEmpty(executablePath)) @@ -190,25 +216,32 @@ namespace AlwaysUpToDate 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); + long totalEntries = zipArchive.Entries.Count; + long processedEntries = 0; foreach (ZipArchiveEntry entry in zipArchive.Entries) { string destinationPath = Path.GetFullPath(Path.Join(installPath, entry.FullName)); if (!destinationPath.StartsWith(fullInstallPath, StringComparison.OrdinalIgnoreCase)) { + processedEntries++; continue; } 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))) { - 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)); } @@ -223,36 +256,46 @@ namespace AlwaysUpToDate { 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); - } - catch (Exception ex) - { - Debug.WriteLine(ex); - } + File.Delete(filePath); } + 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)) - //{ - // Process.Start("chmod", $"+x \"{executablePath}\"")?.WaitForExit(); - //} + TriggerProgressChanged(UpdateStep.Restarting, null, 0); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("chmod", $"+x \"{executablePath}\"")?.WaitForExit(); + } _ = Process.Start(new ProcessStartInfo(executablePath) { UseShellExecute = false }); - Environment.Exit(0); } catch (Exception ex) { + _ = Interlocked.Exchange(ref updating, 0); OnException?.Invoke(ex); } } @@ -273,7 +316,7 @@ namespace AlwaysUpToDate if (bytesRead == 0) { isMoreToRead = false; - TriggerProgressChanged(totalDownloadSize, totalBytesRead); + TriggerProgressChanged(UpdateStep.Downloading, totalDownloadSize, totalBytesRead); continue; } @@ -285,7 +328,7 @@ namespace AlwaysUpToDate if (readCount >= 10) { readCount = 0; - TriggerProgressChanged(totalDownloadSize, totalBytesRead); + TriggerProgressChanged(UpdateStep.Downloading, totalDownloadSize, totalBytesRead); } } while (isMoreToRead); @@ -298,15 +341,42 @@ namespace AlwaysUpToDate } } - private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead) + private bool VerifyChecksum(string filePath) { - double? progressPercentage = null; - if (totalDownloadSize.HasValue) + UpdateItem item = pendingUpdateItem; + 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() diff --git a/AlwaysUpToDate/UpdateInfo.cs b/AlwaysUpToDate/UpdateInfo.cs index 6559ff9..ea789a3 100644 --- a/AlwaysUpToDate/UpdateInfo.cs +++ b/AlwaysUpToDate/UpdateInfo.cs @@ -31,10 +31,25 @@ namespace AlwaysUpToDate public Checksum Checksum { get; set; } } + public enum HashAlgorithmType + { + [XmlEnum("sha1")] + SHA1, + + [XmlEnum("md5")] + MD5, + + [XmlEnum("sha256")] + SHA256, + + [XmlEnum("sha512")] + SHA512, + } + public class Checksum { [XmlAttribute("algorithm")] - public string Algorithm { get; set; } + public HashAlgorithmType Algorithm { get; set; } [XmlText] public string Value { get; set; } @@ -51,4 +66,13 @@ namespace AlwaysUpToDate [XmlEnum("linux")] Linux, } + + public enum UpdateStep + { + Downloading, + VerifyingChecksum, + Extracting, + CleaningUp, + Restarting, + } } \ No newline at end of file diff --git a/README.md b/README.md index a6f5792..66f0aa5 100644 --- a/README.md +++ b/README.md @@ -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 @@ -74,10 +74,10 @@ private static void Updater_UpdateAvailible(string version, string additionalInf updater.Update(); } ``` -### Download process reporting +### Update process reporting ```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}%"); } ```