Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static void Discover(UpdateContext context)
// otherwise the default blocks the manifest value and causes issues:
// • MainAppName "Client" → can't find the real executable
// • UpdateAppName "Update.exe" → can't launch the upgrade process
// • ClientVersion "1.0.0.0" → endless update loop (version never updates)
// • ClientVersion "1.0.0" → endless update loop (version never updates)
if (!string.IsNullOrWhiteSpace(manifest.MainAppName))
context.MainAppName = manifest.MainAppName;
if (!string.IsNullOrWhiteSpace(manifest.UpdateAppName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public abstract class UpdateConfiguration
/// Comparing <c>ClientVersion</c> against the latest version from the server determines whether the
/// main application needs updating (<see cref="UpdateContext.IsMainUpdate" />).
/// </remarks>
public string ClientVersion { get; set; } = "1.0.0.0";
public string ClientVersion { get; set; } = "1.0.0";

/// <summary>
/// A list of specific files to exclude from the update process.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public class UpdateRequestBuilder
/// "Token": "mytoken",
/// "Scheme": "https",
/// "MainAppName": "MyApp",
/// "ClientVersion": "1.0.0.0"
/// "ClientVersion": "1.0.0"
/// }
/// </code>
/// </para>
Expand Down
2 changes: 1 addition & 1 deletion src/c#/GeneralUpdate.Core/Configuration/VersionEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public class VersionEntry : VersionIdentity
public override string? Url { get; set; }

/// <summary>
/// The version number string of this version information (e.g., "1.0.0.1").
/// The version number string of this version information (e.g., "1.0.0").
/// </summary>
[JsonPropertyName("version")]
public override string? Version { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/c#/GeneralUpdate.Core/Configuration/VersionIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public abstract class VersionIdentity
/// <summary>Download URL of the update package.</summary>
public virtual string? Url { get; set; }

/// <summary>Version number string (e.g., "1.0.0.1").</summary>
/// <summary>Version number string (e.g., "1.0.0").</summary>
public virtual string? Version { get; set; }

/// <summary>Application type identifier.</summary>
Expand Down
40 changes: 21 additions & 19 deletions src/c#/GeneralUpdate.Core/Download/DownloadPlanBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.Download.Models;
using GeneralUpdate.Core.Utilities;

namespace GeneralUpdate.Core.Download;

Expand Down Expand Up @@ -66,6 +67,7 @@ public static bool HasUpdate(
.Where(a => (a.AppType ?? (int)AppType.Client) == (int)appType)
.Select(a => ParseVersion(a.Version))
.Where(v => v != null)
.Select(v => v!.Value)
.ToList();

if (serverVersions.Count == 0)
Expand All @@ -74,11 +76,11 @@ public static bool HasUpdate(
// If the local version cannot be read or parsed, we can't prove we're
// up to date — err on the side of updating rather than silently skipping.
if (string.IsNullOrWhiteSpace(localVersion)
|| !Version.TryParse(localVersion, out var local))
|| !Semver.TryParse(localVersion, out var local))
return true;

// Compare: max server version > local version?
return serverVersions.Max()! > local;
return serverVersions.Max() > local;
}

/// <summary>
Expand All @@ -100,8 +102,10 @@ public static DownloadPlan Build(
if (assets == null) return DownloadPlan.Empty;
var parsedClient = ParseVersion(clientVersion);
if (parsedClient == null) return DownloadPlan.Empty;
var cv = parsedClient.Value;

var parsedUpgrade = ParseVersion(upgradeClientVersion) ?? parsedClient;
var uv = parsedUpgrade.Value;

// 1. Filter out frozen packages
var active = assets
Expand All @@ -118,17 +122,16 @@ public static DownloadPlan Build(
var candidates = active
.Where(a =>
{
var pv = ParseVersion(a.Version);
if (pv == null) return false;
if (!Semver.TryParse(a.Version, out var pv)) return false;

var localVersion = (a.AppType == (int)AppType.Upgrade)
? parsedUpgrade
: parsedClient;
? uv
: cv;

return pv > localVersion;
Comment thread
JusterZhu marked this conversation as resolved.
})
.Where(a => IsCompatible(a.MinClientVersion, clientVersion))
.OrderBy(a => ParseVersion(a.Version))
.OrderBy(a => { Semver.TryParse(a.Version, out var sv); return sv; })
.ToList();

if (candidates.Count == 0) return DownloadPlan.Empty;
Expand All @@ -142,28 +145,27 @@ public static DownloadPlan Build(
.Where(a => a.IsCrossVersion)
.Where(a =>
{
var fromVer = ParseVersion(a.FromVersion);
if (fromVer == null) return false;
if (!Semver.TryParse(a.FromVersion, out var fromVer)) return false;
var localVersion = (a.AppType == (int)AppType.Upgrade)
? parsedUpgrade
: parsedClient;
? uv
: cv;
return fromVer == localVersion;
})
.OrderByDescending(a => ParseVersion(a.Version))
.OrderByDescending(a => { Semver.TryParse(a.Version, out var sv); return sv; })
.FirstOrDefault();

if (matchingCvp != null)
{
// CVP covers one AppType in a single hop. Still need chain packages
// for other AppTypes, and for the same AppType beyond the CVP's target.
var cvpAppType = matchingCvp.AppType;
var cvpVersion = ParseVersion(matchingCvp.Version);
Semver.TryParse(matchingCvp.Version, out var cvpVersion);
var planAssets = new List<DownloadAsset> { matchingCvp };
planAssets.AddRange(candidates
.Where(a => !a.IsCrossVersion)
.Where(a => a.AppType != cvpAppType
|| (cvpVersion != null && ParseVersion(a.Version) > cvpVersion))
.OrderBy(a => ParseVersion(a.Version)));
|| (Semver.TryParse(a.Version, out var av) && av > cvpVersion))
.OrderBy(a => { Semver.TryParse(a.Version, out var sv); return sv; }));
return new DownloadPlan(planAssets, isForcibly);
}

Expand Down Expand Up @@ -197,15 +199,15 @@ internal static bool IsCompatible(string? minClientVersion, string currentVersio
var min = ParseVersion(minClientVersion);
var cur = ParseVersion(currentVersion);
if (min == null || cur == null) return true;
return cur >= min;
return cur.Value >= min.Value;
}

/// <summary>Parses a version string and returns null if the string cannot be parsed.</summary>
/// <param name="version">The version string to parse.</param>
/// <returns>A parsed <see cref="Version"/> object, or null if parsing fails.</returns>
internal static Version? ParseVersion(string? version)
/// <returns>A parsed <see cref="SemVersion"/> value, or null if parsing fails.</returns>
internal static SemVersion? ParseVersion(string? version)
Comment thread
JusterZhu marked this conversation as resolved.
{
if (string.IsNullOrWhiteSpace(version)) return null;
return Version.TryParse(version, out var v) ? v : null;
return Semver.TryParse(version, out var v) ? v : null;
}
}
7 changes: 4 additions & 3 deletions src/c#/GeneralUpdate.Core/Strategy/ClientStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using GeneralUpdate.Core.JsonContext;
using GeneralUpdate.Core.Ipc;
using GeneralUpdate.Core.Pipeline;
using GeneralUpdate.Core.Utilities;

namespace GeneralUpdate.Core.Strategy;

Expand Down Expand Up @@ -943,16 +944,16 @@ private bool CanSkip(bool isForcibly, UpdateInfoEventArgs updateInfo)
/// <remarks>
/// Reads the known failed version number from the <c>UpgradeFail</c> environment variable.
/// If the <c>UpgradeFail</c> environment variable is empty or <paramref name="version"/> is empty, returns <c>false</c>.
/// Version comparison uses the semantic version comparison of the <see cref="Version"/> class.
/// Version comparison uses the semantic version comparison of the <see cref="Semver"/> class.
/// This mechanism avoids repeatedly attempting known failed upgrades.
/// </remarks>
private bool CheckFail(string version)
{
var fail = Environments.GetEnvironmentVariable("UpgradeFail");
if (string.IsNullOrEmpty(fail) || string.IsNullOrEmpty(version))
return false;
if (!Version.TryParse(fail, out var failVersion) ||
!Version.TryParse(version, out var versionParsed))
if (!Semver.TryParse(fail, out var failVersion) ||
!Semver.TryParse(version, out var versionParsed))
return false;
return failVersion >= versionParsed;
}
Expand Down
37 changes: 28 additions & 9 deletions src/c#/GeneralUpdate.Core/Strategy/OssStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using GeneralUpdate.Core.Download.Abstractions;
using GeneralUpdate.Core.Download.Models;
using GeneralUpdate.Core.Download.Orchestrators;
using GeneralUpdate.Core.Utilities;

namespace GeneralUpdate.Core.Strategy;

Expand Down Expand Up @@ -350,7 +351,12 @@ private async Task ExecuteUpgradeAsync()
throw new InvalidOperationException("No versions found in Oss configuration.");

assets = versions.OrderBy(v => v.PubTime)
.Where(v => new Version(v.Version ?? "0.0.0") > new Version(_configInfo.ClientVersion))
.Where(v =>
{
var sv = ParseSemVer(v.Version);
var cv = ParseSemVer(_configInfo.ClientVersion);
return sv != null && cv != null && sv.Value > cv.Value;
})
.Select(v =>
{
if (string.IsNullOrWhiteSpace(v.Url))
Expand All @@ -366,12 +372,17 @@ private async Task ExecuteUpgradeAsync()
if (assets.Count == 0)
throw new InvalidOperationException("No assets to download.");

// Compute LastVersion deterministically via Version comparison
// Compute LastVersion deterministically via SemVer comparison
// so hooks see the correct TargetVersion regardless of source ordering.
_configInfo.LastVersion = assets
.Select(a => new Version(a.Version))
.Max()!
.ToString();
// Guard: if ALL versions are unparseable, fall back to "0.0.0".
var parsedVersions = assets
.Select(a => Semver.TryParse(a.Version, out var sv) ? (SemVersion?)sv : null)
.Where(sv => sv != null)
.Select(sv => sv!.Value)
.ToList();
_configInfo.LastVersion = parsedVersions.Count > 0
? parsedVersions.Max().ToString()
: "0.0.0";
ctx = BuildUpdateContext();

// Hooks: allow cancellation before download. Called after assets are
Expand Down Expand Up @@ -483,15 +494,15 @@ private static async Task DownloadVersionConfig(string url, string path)
/// <param name="serverVersion">The latest server version string.</param>
/// <returns>Returns true if the server version is higher than the client version; otherwise false.</returns>
/// <remarks>
/// This method attempts to parse both version strings as <c>Version</c> types for comparison.
/// This method attempts to parse both version strings as SemVer 2.0 for comparison.
/// If either version string is null, empty, or cannot be parsed, returns false indicating no upgrade is needed.
/// </remarks>
private static bool IsOssUpgrade(string clientVersion, string serverVersion)
{
if (string.IsNullOrWhiteSpace(clientVersion) || string.IsNullOrWhiteSpace(serverVersion))
return false;
return Version.TryParse(clientVersion, out var cv)
&& Version.TryParse(serverVersion, out var sv)
return Semver.TryParse(clientVersion, out var cv)
&& Semver.TryParse(serverVersion, out var sv)
&& cv < sv;
}

Expand Down Expand Up @@ -655,5 +666,13 @@ private async Task SafeReportUpdateFailedAsync(Hooks.HookContext ctx, Exception
catch (Exception ex) { GeneralTracer.Warn($"Report UpdateFailed failed: {ex.Message}"); }
}

/// <summary>
/// Lightweight SemVer parse guard that returns null (not an exception) for unparseable strings.
/// </summary>
private static SemVersion? ParseSemVer(string? version)
{
return Semver.TryParse(version, out var sv) ? sv : null;
}

#endregion
}
Loading
Loading