Migrate update system to XML manifest and improve multi-OS support

This commit is contained in:
Stone_Red
2026-02-20 20:05:33 +01:00
parent e7102ab0b3
commit dd0d24cf47
5 changed files with 278 additions and 129 deletions
-44
View File
@@ -1,44 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using AlwaysUpToDate;
namespace AlwaysUpToDate_Test
{
internal class Program
{
static private Updater updater = new Updater(new TimeSpan(0, 0, 1), "https://raw.githubusercontent.com/Stone-Red-Code/Test/main/test.json", "./", true);
private async static Task Main(string[] args)
{
updater.Start();
updater.ProgressChanged += Updater_ProgressChanged;
updater.UpdateAvailible += Updater_UpdateAvailible;
updater.NoUpdateAvailible += Updater_NoUpdateAvailible;
await Task.Delay(-1);
}
private static void Updater_UpdateAvailible(string version, string additionalInformation)
{
updater.Stop();
Console.WriteLine("New Update avalible: " + version);
Console.WriteLine("Do you want to install the new update? (y/n)");
char input = Console.ReadKey().KeyChar;
if (char.ToLower(input) == 'y')
updater.Update();
}
private static void Updater_ProgressChanged(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage)
{
Console.WriteLine($"{totalBytesDownloaded}/{totalFileSize} {progressPercentage}%");
}
private static void Updater_NoUpdateAvailible()
{
Console.WriteLine("You are up to date");
}
}
}
@@ -3,8 +3,8 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<RootNamespace>AlwaysUpToDate_Test</RootNamespace> <RootNamespace>AlwaysUpToDate.Test</RootNamespace>
<StartupObject>AlwaysUpToDate_Test.Program</StartupObject> <StartupObject>AlwaysUpToDate.Test.Program</StartupObject>
<AssemblyVersion>2.0.0.0</AssemblyVersion> <AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion> <FileVersion>2.0.0.0</FileVersion>
<PackageTags></PackageTags> <PackageTags></PackageTags>
+54
View File
@@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
namespace AlwaysUpToDate.Test;
internal class Program
{
private static readonly Updater updater = new Updater(new TimeSpan(0, 0, 1), "https://raw.githubusercontent.com/Stone-Red-Code/Test/main/test.xml", true);
private static async Task Main(string[] args)
{
updater.ProgressChanged += Updater_ProgressChanged;
updater.UpdateAvailable += Updater_UpdateAvailable;
updater.NoUpdateAvailable += Updater_NoUpdateAvailable;
updater.OnException += Updater_OnException;
updater.Start();
await Task.Delay(-1);
}
private static async void Updater_UpdateAvailable(string version, string changelogUrl)
{
updater.Stop();
Console.WriteLine("New Update available: " + version);
if (!string.IsNullOrEmpty(changelogUrl))
{
Console.WriteLine("Changelog: " + changelogUrl);
}
Console.WriteLine("Do you want to install the new update? (y/n)");
char input = Console.ReadKey().KeyChar;
if (char.ToLower(input) == 'y')
{
await updater.Update();
}
}
private static void Updater_ProgressChanged(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage)
{
Console.WriteLine($"{totalBytesDownloaded}/{totalFileSize} {progressPercentage}%");
}
private static void Updater_NoUpdateAvailable()
{
Console.WriteLine("You are up to date!");
}
private static void Updater_OnException(Exception exception)
{
Console.WriteLine(exception.ToString());
}
}
+175 -76
View File
@@ -1,127 +1,181 @@
using System; using System;
using System.IO;
using System.Net.Http;
using System.Timers;
using System.Text.Json;
using System.Reflection;
using System.IO.Compression;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers;
using System.Xml.Serialization;
namespace AlwaysUpToDate namespace AlwaysUpToDate
{ {
public class Updater public class Updater : IDisposable
{ {
public delegate void UpdaterChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage); public delegate void UpdaterChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public event UpdaterChangedHandler ProgressChanged; public event UpdaterChangedHandler ProgressChanged;
public delegate void UpdateAvalibleHandler(string version, string additionalInformation); public delegate void UpdateAvailableHandler(string version, string changelogUrl);
public event UpdateAvalibleHandler UpdateAvailible; public event UpdateAvailableHandler UpdateAvailable;
public delegate void NoUpdateAvalibleHandler(); public delegate void NoUpdateAvailableHandler();
public event NoUpdateAvalibleHandler NoUpdateAvailible; public event NoUpdateAvailableHandler NoUpdateAvailable;
public delegate void ExceptionHandler(Exception exception); public delegate void ExceptionHandler(Exception exception);
public event ExceptionHandler OnException; public event ExceptionHandler OnException;
private static readonly XmlSerializer manifestSerializer = new XmlSerializer(typeof(UpdateManifest));
private readonly HttpClient httpClient = new HttpClient();
private readonly Timer updateTimer = new Timer(); private readonly Timer updateTimer = new 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 updaing = false; private bool updating;
private bool disposed;
public Updater(TimeSpan interval, Uri updateInfoUri, bool onlyUpdateOnce = false) : this(interval, updateInfoUri?.ToString(), "./", onlyUpdateOnce)
{
}
public Updater(TimeSpan interval, Uri updateInfoUri, string installPath = "./", bool onlyUpdateOnce = false) : this(interval, updateInfoUri?.ToString(), installPath, onlyUpdateOnce)
{
}
public Updater(TimeSpan interval, string updateInfoUrl, bool onlyUpdateOnce = false) : this(interval, updateInfoUrl, "./", onlyUpdateOnce)
{
}
public Updater(TimeSpan interval, string updateInfoUrl, string installPath = "./", bool onlyUpdateOnce = false) public Updater(TimeSpan interval, string updateInfoUrl, string installPath = "./", bool onlyUpdateOnce = false)
{ {
this.updateInfoUrl = updateInfoUrl; this.updateInfoUrl = updateInfoUrl ?? throw new ArgumentNullException(nameof(updateInfoUrl));
this.installPath = installPath; this.installPath = installPath ?? throw new ArgumentNullException(nameof(installPath));
if (interval.TotalMilliseconds > 0) if (interval.TotalMilliseconds > 0)
{
updateTimer.Interval = interval.TotalMilliseconds; updateTimer.Interval = interval.TotalMilliseconds;
}
if (!onlyUpdateOnce) if (!onlyUpdateOnce)
{
updateTimer.Elapsed += UpdateTimer_Elapsed; updateTimer.Elapsed += UpdateTimer_Elapsed;
}
} }
public void Start() public void Start()
{ {
ThrowIfDisposed();
if (updateTimer.Interval > 0) if (updateTimer.Interval > 0)
{
updateTimer.Start(); updateTimer.Start();
}
UpdateTimer_Elapsed(null, null); UpdateTimer_Elapsed(null, null);
} }
public void Stop() public void Stop()
{ {
ThrowIfDisposed();
updateTimer.Stop(); updateTimer.Stop();
} }
public async void Update() public async Task Update()
{ {
if (!string.IsNullOrWhiteSpace(updateUrl) && !updaing) ThrowIfDisposed();
if (!string.IsNullOrWhiteSpace(updateUrl) && !updating)
{
await DownloadFile(); await DownloadFile();
}
} }
public async void UpdateTimer_Elapsed(object sender, ElapsedEventArgs e) private async void UpdateTimer_Elapsed(object sender, ElapsedEventArgs e)
{ {
try try
{ {
using HttpClient client = new HttpClient(); using HttpResponseMessage response = await httpClient.GetAsync(updateInfoUrl);
HttpResponseMessage response = await client.GetAsync(updateInfoUrl); _ = response.EnsureSuccessStatusCode();
Stream stream = await response.Content.ReadAsStreamAsync(); using Stream stream = await response.Content.ReadAsStreamAsync();
UpdateInfo updateInfo = await JsonSerializer.DeserializeAsync<UpdateInfo>(stream); UpdateManifest manifest = (UpdateManifest)manifestSerializer.Deserialize(stream);
TargetOS currentOS = GetCurrentOS();
UpdateItem updateItem = manifest.Items?.FirstOrDefault(i => i.OS == currentOS);
if (updateItem == null)
{
return;
}
Version assemblyVersion = Assembly.GetEntryAssembly().GetName().Version; Version assemblyVersion = Assembly.GetEntryAssembly().GetName().Version;
bool succes = Version.TryParse(updateInfo.Version, out Version version);
if (!succes) if (!Version.TryParse(updateItem.Version, out Version version))
return;
if (version > assemblyVersion && !updaing)
{ {
updateUrl = updateInfo.FileUrl; return;
if (!updateInfo.Mandatory) }
if (version > assemblyVersion && !updating)
{
updateUrl = updateItem.DownloadUrl;
if (!updateItem.IsMandatory)
{ {
if (UpdateAvailible != null) UpdateAvailable?.Invoke(updateItem.Version, updateItem.ChangelogUrl);
UpdateAvailible(updateInfo.Version, updateInfo.AdditionalInformation);
} }
else else
{ {
Update(); await Update();
} }
} }
else else
{ {
if (NoUpdateAvailible != null) NoUpdateAvailable?.Invoke();
NoUpdateAvailible();
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
if (OnException != null) OnException?.Invoke(ex);
OnException(ex);
} }
} }
private static TargetOS GetCurrentOS()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return TargetOS.Windows;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return TargetOS.MacOS;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return TargetOS.Linux;
}
throw new PlatformNotSupportedException();
}
private async Task DownloadFile() private async Task DownloadFile()
{ {
try try
{ {
updateTimer.Stop(); updateTimer.Stop();
updaing = true; updating = true;
using HttpClient client = new HttpClient();
HttpResponseMessage response = client.GetAsync(updateUrl).Result; using HttpResponseMessage response = await httpClient.GetAsync(updateUrl);
using Stream contentStream = response.Content.ReadAsStreamAsync().Result; _ = response.EnsureSuccessStatusCode();
using Stream contentStream = await response.Content.ReadAsStreamAsync();
await ProcessContentStream(response.Content.Headers.ContentLength, contentStream); await ProcessContentStream(response.Content.Headers.ContentLength, contentStream);
} }
catch (Exception ex) catch (Exception ex)
{ {
if (OnException != null) OnException?.Invoke(ex);
OnException(ex);
} }
} }
@@ -129,32 +183,48 @@ namespace AlwaysUpToDate
{ {
try try
{ {
ZipArchive zipArchive = ZipFile.OpenRead(Path.Join(installPath, "Update.zip")); string executablePath = Process.GetCurrentProcess().MainModule?.FileName ?? Assembly.GetEntryAssembly()?.Location;
string executablePath = Process.GetCurrentProcess().MainModule.FileName;
Console.WriteLine(executablePath);
foreach (ZipArchiveEntry entry in zipArchive.Entries) if (string.IsNullOrEmpty(executablePath))
{ {
if (File.Exists(Path.Join(installPath, entry.FullName))) throw new InvalidOperationException("Unable to determine the executable path.");
}
using (ZipArchive zipArchive = ZipFile.OpenRead(Path.Join(installPath, "Update.zip")))
{
string fullInstallPath = Path.GetFullPath(installPath + Path.DirectorySeparatorChar);
foreach (ZipArchiveEntry entry in zipArchive.Entries)
{ {
string moveName = entry.FullName; string destinationPath = Path.GetFullPath(Path.Join(installPath, entry.FullName));
while (File.Exists(Path.Join(installPath, moveName))) if (!destinationPath.StartsWith(fullInstallPath, StringComparison.OrdinalIgnoreCase))
{ {
moveName += "_OLD_"; continue;
}
if (File.Exists(Path.Join(installPath, entry.FullName)))
{
string moveName = entry.FullName;
while (File.Exists(Path.Join(installPath, moveName)))
{
moveName += "_OLD_";
}
File.Move(Path.Join(installPath, entry.FullName), Path.Join(installPath, moveName));
}
if (entry.FullName.EndsWith('/'))
{
if (!Directory.Exists(Path.Join(installPath, entry.FullName)))
{
_ = Directory.CreateDirectory(Path.Join(installPath, entry.FullName));
}
}
else
{
entry.ExtractToFile(Path.Join(installPath, entry.FullName), true);
} }
File.Move(Path.Join(installPath, entry.FullName), Path.Join(installPath, moveName));
}
if (entry.FullName.EndsWith("/"))
{
if (!Directory.Exists(Path.Join(installPath, entry.FullName)))
Directory.CreateDirectory(Path.Join(installPath, entry.FullName));
}
else
{
entry.ExtractToFile(Path.Join(installPath, entry.FullName), true);
} }
} }
zipArchive.Dispose();
foreach (string filePath in Directory.GetFiles(installPath, "*.*", SearchOption.AllDirectories)) foreach (string filePath in Directory.GetFiles(installPath, "*.*", SearchOption.AllDirectories))
{ {
@@ -172,14 +242,19 @@ namespace AlwaysUpToDate
} }
File.Delete(Path.Join(installPath, "Update.zip")); File.Delete(Path.Join(installPath, "Update.zip"));
Process.Start(executablePath);
//if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
//{
// Process.Start("chmod", $"+x \"{executablePath}\"")?.WaitForExit();
//}
_ = Process.Start(new ProcessStartInfo(executablePath) { UseShellExecute = false });
Environment.Exit(0);
} }
catch (Exception ex) catch (Exception ex)
{ {
if (OnException != null) OnException?.Invoke(ex);
OnException(ex);
} }
Environment.Exit(0);
} }
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream) private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
@@ -191,10 +266,10 @@ namespace AlwaysUpToDate
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
bool isMoreToRead = true; bool isMoreToRead = true;
using var fileStream = new FileStream($"{installPath}Update.zip", FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); using FileStream fileStream = new FileStream(Path.Join(installPath, "Update.zip"), FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
do do
{ {
int bytesRead = contentStream.ReadAsync(buffer, 0, buffer.Length).Result; int bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) if (bytesRead == 0)
{ {
isMoreToRead = false; isMoreToRead = false;
@@ -214,27 +289,51 @@ namespace AlwaysUpToDate
} }
} }
while (isMoreToRead); while (isMoreToRead);
fileStream.Close();
ExtractZipFile(); ExtractZipFile();
} }
catch (Exception ex) catch (Exception ex)
{ {
if (OnException != null) OnException?.Invoke(ex);
OnException(ex);
} }
} }
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead) private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
{ {
if (ProgressChanged == null)
return;
double? progressPercentage = null; double? progressPercentage = null;
if (totalDownloadSize.HasValue) if (totalDownloadSize.HasValue)
{
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2); progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
}
if (ProgressChanged != null) ProgressChanged?.Invoke(totalDownloadSize, totalBytesRead, progressPercentage);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage); }
private void ThrowIfDisposed()
{
if (disposed)
{
throw new ObjectDisposedException(nameof(Updater));
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
updateTimer.Dispose();
httpClient.Dispose();
}
disposed = true;
}
} }
} }
} }
+47 -7
View File
@@ -1,14 +1,54 @@
using System; using System.Collections.Generic;
using System.Collections.Generic; using System.Xml.Serialization;
using System.Text;
namespace AlwaysUpToDate namespace AlwaysUpToDate
{ {
internal class UpdateInfo [XmlRoot("updates")]
public class UpdateManifest
{ {
[XmlElement("item")]
public List<UpdateItem> Items { get; set; } = new List<UpdateItem>();
}
public class UpdateItem
{
[XmlElement("os")]
public TargetOS OS { get; set; }
[XmlElement("version")]
public string Version { get; set; } public string Version { get; set; }
public string FileUrl { get; set; }
public string AdditionalInformation { get; set; } [XmlElement("url")]
public bool Mandatory { get; set; } public string DownloadUrl { get; set; }
[XmlElement("changelog")]
public string ChangelogUrl { get; set; }
[XmlElement("mandatory")]
public bool IsMandatory { get; set; }
[XmlElement("checksum")]
public Checksum Checksum { get; set; }
}
public class Checksum
{
[XmlAttribute("algorithm")]
public string Algorithm { get; set; }
[XmlText]
public string Value { get; set; }
}
public enum TargetOS
{
[XmlEnum("windows")]
Windows,
[XmlEnum("macos")]
MacOS,
[XmlEnum("linux")]
Linux,
} }
} }