diff --git a/AlwaysUpToDate.Test/Program.cs b/AlwaysUpToDate.Test/Program.cs
index daab61d..5a3017e 100644
--- a/AlwaysUpToDate.Test/Program.cs
+++ b/AlwaysUpToDate.Test/Program.cs
@@ -5,7 +5,7 @@ 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 readonly Updater updater = new Updater(new TimeSpan(0, 0, 1), "https://echohub.voidcube.cloud/api/app/version", true);
private static async Task Main(string[] args)
{
diff --git a/AlwaysUpToDate/DownloadManager.cs b/AlwaysUpToDate/DownloadManager.cs
index 839fa6d..851cb1b 100644
--- a/AlwaysUpToDate/DownloadManager.cs
+++ b/AlwaysUpToDate/DownloadManager.cs
@@ -178,11 +178,13 @@ namespace AlwaysUpToDate
using HttpResponseMessage response = await httpClient.GetAsync(updateInfoUrl);
_ = response.EnsureSuccessStatusCode();
- using Stream stream = await response.Content.ReadAsStreamAsync();
- UpdateManifest manifest = (UpdateManifest)manifestSerializer.Deserialize(stream);
+ string xmlManifest = await response.Content.ReadAsStringAsync();
+ UpdateManifest manifest = (UpdateManifest)manifestSerializer.Deserialize(new StringReader(xmlManifest));
TargetOS currentOS = GetCurrentOS();
- UpdateItem updateItem = manifest.Items?.FirstOrDefault(i => i.OS == currentOS);
+ TargetArchitecture currentArch = GetCurrentArchitecture();
+ UpdateItem updateItem = manifest.Items?.FirstOrDefault(i => i.OS == currentOS && i.Architecture == currentArch)
+ ?? manifest.Items?.FirstOrDefault(i => i.OS == currentOS && i.Architecture == TargetArchitecture.Any);
if (updateItem == null)
{
@@ -246,6 +248,18 @@ namespace AlwaysUpToDate
throw new PlatformNotSupportedException();
}
+ private static TargetArchitecture GetCurrentArchitecture()
+ {
+ return RuntimeInformation.ProcessArchitecture switch
+ {
+ Architecture.X86 => TargetArchitecture.X86,
+ Architecture.X64 => TargetArchitecture.X64,
+ Architecture.Arm => TargetArchitecture.Arm,
+ Architecture.Arm64 => TargetArchitecture.Arm64,
+ _ => TargetArchitecture.Any,
+ };
+ }
+
private async Task DownloadFile()
{
try
@@ -255,6 +269,8 @@ namespace AlwaysUpToDate
UpdateStarted?.Invoke(pendingUpdateItem?.Version);
+ TriggerProgressChanged(UpdateStep.Downloading, null, 0);
+
using HttpResponseMessage response = await httpClient.GetAsync(updateUrl);
_ = response.EnsureSuccessStatusCode();
@@ -366,6 +382,7 @@ namespace AlwaysUpToDate
}
_ = Process.Start(new ProcessStartInfo(executablePath) { UseShellExecute = false });
+ Environment.Exit(0);
}
catch (Exception ex)
{
@@ -383,29 +400,31 @@ namespace AlwaysUpToDate
byte[] buffer = new byte[8192];
bool isMoreToRead = true;
- using FileStream fileStream = new FileStream(Path.Join(installPath, "Update.zip"), FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
- do
+ using (FileStream fileStream = new FileStream(Path.Join(installPath, "Update.zip"), FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
- int bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
- if (bytesRead == 0)
+ do
{
- isMoreToRead = false;
- TriggerProgressChanged(UpdateStep.Downloading, totalDownloadSize, totalBytesRead);
- continue;
- }
+ int bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
+ if (bytesRead == 0)
+ {
+ isMoreToRead = false;
+ TriggerProgressChanged(UpdateStep.Downloading, totalDownloadSize, totalBytesRead);
+ continue;
+ }
- await fileStream.WriteAsync(buffer, 0, bytesRead);
+ await fileStream.WriteAsync(buffer, 0, bytesRead);
- totalBytesRead += bytesRead;
- readCount += 1;
+ totalBytesRead += bytesRead;
+ readCount += 1;
- if (readCount >= 10)
- {
- readCount = 0;
- TriggerProgressChanged(UpdateStep.Downloading, totalDownloadSize, totalBytesRead);
+ if (readCount >= 10)
+ {
+ readCount = 0;
+ TriggerProgressChanged(UpdateStep.Downloading, totalDownloadSize, totalBytesRead);
+ }
}
+ while (isMoreToRead);
}
- while (isMoreToRead);
ExtractZipFile();
}
diff --git a/AlwaysUpToDate/UpdateInfo.cs b/AlwaysUpToDate/UpdateInfo.cs
index eb6147c..7a59b1f 100644
--- a/AlwaysUpToDate/UpdateInfo.cs
+++ b/AlwaysUpToDate/UpdateInfo.cs
@@ -18,14 +18,42 @@ namespace AlwaysUpToDate
///
/// Represents a single update entry in the manifest, targeting a specific OS.
+ /// Supports both combined OS-architecture values (e.g., osx-arm64) and
+ /// separate <os> / <arch> elements.
///
public class UpdateItem
{
///
- /// Gets or sets the target operating system for this update.
+ /// Gets or sets the raw OS string from the manifest (e.g., "windows", "osx-arm64", "linux").
+ /// Supports combined OS-architecture values for backward compatibility.
///
[XmlElement("os")]
- public TargetOS OS { get; set; }
+ public string RawOS { get; set; }
+
+ ///
+ /// Gets or sets the raw architecture string from the manifest.
+ /// When present, overrides any architecture embedded in .
+ ///
+ [XmlElement("arch")]
+ public string RawArchitecture { get; set; }
+
+ ///
+ /// Gets the parsed target operating system, extracted from .
+ /// Recognizes windows/win, macos/osx, and linux prefixes.
+ ///
+ [XmlIgnore]
+ public TargetOS OS => ParseTargetOS(RawOS);
+
+ ///
+ /// Gets the parsed target architecture. Uses if present;
+ /// otherwise extracts the architecture suffix from (e.g., osx-arm64 → ).
+ /// Defaults to when no architecture is specified.
+ ///
+ [XmlIgnore]
+ public TargetArchitecture Architecture =>
+ !string.IsNullOrEmpty(RawArchitecture)
+ ? ParseArchitectureString(RawArchitecture)
+ : ParseEmbeddedArchitecture(RawOS);
///
/// Gets or sets the version string of the update in X.X.X.X format.
@@ -57,6 +85,60 @@ namespace AlwaysUpToDate
///
[XmlElement("checksum")]
public Checksum Checksum { get; set; }
+
+ private static TargetOS ParseTargetOS(string raw)
+ {
+ if (string.IsNullOrEmpty(raw))
+ {
+ return default;
+ }
+
+ int dashIndex = raw.IndexOf('-');
+ string osPart = dashIndex >= 0 ? raw[..dashIndex] : raw;
+
+ switch (osPart.ToLowerInvariant())
+ {
+ case "windows":
+ case "win":
+ return TargetOS.Windows;
+ case "macos":
+ case "osx":
+ return TargetOS.MacOS;
+ case "linux":
+ return TargetOS.Linux;
+ default:
+ throw new System.NotSupportedException($"Unknown operating system: '{osPart}'");
+ }
+ }
+
+ private static TargetArchitecture ParseEmbeddedArchitecture(string raw)
+ {
+ if (string.IsNullOrEmpty(raw))
+ {
+ return TargetArchitecture.Any;
+ }
+
+ int dashIndex = raw.IndexOf('-');
+ if (dashIndex < 0)
+ {
+ return TargetArchitecture.Any;
+ }
+
+ return ParseArchitectureString(raw[(dashIndex + 1)..]);
+ }
+
+ private static TargetArchitecture ParseArchitectureString(string raw)
+ {
+ return (raw?.ToLowerInvariant()) switch
+ {
+ "x86" => TargetArchitecture.X86,
+ "x64" => TargetArchitecture.X64,
+ "arm" => TargetArchitecture.Arm,
+ "arm64" => TargetArchitecture.Arm64,
+ "any" => TargetArchitecture.Any,
+ _ => throw new System.NotSupportedException($"Unknown architecture: '{raw}'"),
+ };
+ }
}
///
@@ -117,6 +199,33 @@ namespace AlwaysUpToDate
Linux,
}
+ ///
+ /// Specifies the target processor architecture for an update item.
+ /// means the update applies to all architectures.
+ ///
+ public enum TargetArchitecture
+ {
+ /// Any architecture (default). Used when the update is architecture-independent.
+ [XmlEnum("any")]
+ Any,
+
+ /// Intel/AMD 32-bit (x86).
+ [XmlEnum("x86")]
+ X86,
+
+ /// Intel/AMD 64-bit (x64).
+ [XmlEnum("x64")]
+ X64,
+
+ /// ARM 32-bit.
+ [XmlEnum("arm")]
+ Arm,
+
+ /// ARM 64-bit.
+ [XmlEnum("arm64")]
+ Arm64,
+ }
+
///
/// Describes the current phase of the update process, reported via .
///