Lock sing-box max version (#10091)

* Refactor Semantic Version

* Lock max version

* Allow sing-box update to pre-release
This commit is contained in:
DHR60
2026-09-04 02:17:31 +00:00
committed by GitHub
parent fed59a7313
commit 4a59c7f4d6
5 changed files with 205 additions and 135 deletions
@@ -229,6 +229,7 @@ public sealed class CoreInfoManager
DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz", DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz",
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz", DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz", DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
LockedMaxVersion = new SemanticVersion(1, 14, int.MaxValue),
Match = "sing-box", Match = "sing-box",
VersionArg = "version", VersionArg = "version",
}, },
@@ -16,6 +16,7 @@ public class CoreInfo
public string? DownloadUrlLinuxLoong64 { get; set; } public string? DownloadUrlLinuxLoong64 { get; set; }
public string? DownloadUrlOSX64 { get; set; } public string? DownloadUrlOSX64 { get; set; }
public string? DownloadUrlOSXArm64 { get; set; } public string? DownloadUrlOSXArm64 { get; set; }
public SemanticVersion? LockedMaxVersion { get; set; }
public string? Match { get; set; } public string? Match { get; set; }
public string? VersionArg { get; set; } public string? VersionArg { get; set; }
public bool AbsolutePath { get; set; } public bool AbsolutePath { get; set; }
+171 -103
View File
@@ -1,18 +1,20 @@
namespace ServiceLib.Models.Dto; namespace ServiceLib.Models.Dto;
public class SemanticVersion public class SemanticVersion : IEquatable<SemanticVersion>, IComparable
{ {
private readonly string? build;
private readonly int major; private readonly int major;
private readonly int minor; private readonly int minor;
private readonly int patch; private readonly int patch;
private readonly string version; private readonly string? prerelease;
private readonly string raw;
public SemanticVersion(int major, int minor, int patch) public SemanticVersion(int major, int minor, int patch)
{ {
this.major = major; this.major = major;
this.minor = minor; this.minor = minor;
this.patch = patch; this.patch = patch;
version = $"{major}.{minor}.{patch}"; raw = $"{major}.{minor}.{patch}";
} }
public SemanticVersion(string? version) public SemanticVersion(string? version)
@@ -24,26 +26,41 @@ public class SemanticVersion
major = 0; major = 0;
minor = 0; minor = 0;
patch = 0; patch = 0;
raw = $"{major}.{minor}.{patch}";
return; return;
} }
this.version = version.RemovePrefix('v'); raw = version;
var parts = this.version.Split('.'); var span = version.StartsWith("v", StringComparison.OrdinalIgnoreCase)
if (parts.Length == 2) ? version.AsSpan(1)
: version.AsSpan();
var plusIdx = span.IndexOf('+');
var metadataSpan = plusIdx >= 0 ? span[(plusIdx + 1)..] : [];
var leftOfPlus = plusIdx >= 0 ? span[..plusIdx] : span;
var dashIdx = leftOfPlus.IndexOf('-');
var versionSpan = dashIdx >= 0 ? leftOfPlus[..dashIdx] : leftOfPlus;
var preReleaseSpan = dashIdx >= 0 ? leftOfPlus[(dashIdx + 1)..] : [];
build = metadataSpan.Length > 0 ? metadataSpan.ToString() : null;
prerelease = preReleaseSpan.Length > 0 ? preReleaseSpan.ToString() : null;
var parts = versionSpan.ToString().Split('.');
switch (parts.Length)
{ {
major = int.Parse(parts.First()); case 2:
minor = int.Parse(parts.Last()); major = int.Parse(parts.First());
patch = 0; minor = int.Parse(parts.Last());
} patch = 0;
else if (parts.Length is 3 or 4) break;
{
major = int.Parse(parts[0]); case 3 or 4:
minor = int.Parse(parts[1]); major = int.Parse(parts[0]);
patch = int.Parse(parts[2]); minor = int.Parse(parts[1]);
} patch = int.Parse(parts[2]);
else break;
{
throw new ArgumentException("Invalid version string"); default:
throw new ArgumentException("Invalid version string");
} }
} }
catch catch
@@ -51,135 +68,186 @@ public class SemanticVersion
major = 0; major = 0;
minor = 0; minor = 0;
patch = 0; patch = 0;
raw = $"{major}.{minor}.{patch}";
}
}
public bool Equals(SemanticVersion? other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return major == other.major && minor == other.minor && patch == other.patch && prerelease == other.prerelease;
}
public static bool TryParse(string? version, out SemanticVersion? result)
{
try
{
result = new SemanticVersion(version);
return true;
}
catch
{
result = null;
return false;
} }
} }
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
if (obj is SemanticVersion other) return obj is SemanticVersion other && Equals(other);
{
return major == other.major && minor == other.minor && patch == other.patch;
}
else
{
return false;
}
} }
public override int GetHashCode() public override int GetHashCode()
{ {
return major.GetHashCode() ^ minor.GetHashCode() ^ patch.GetHashCode(); return HashCode.Combine(major, minor, patch, prerelease);
} }
/// <summary>
/// Use ToVersionString(string? prefix) instead if possible.
/// </summary>
/// <returns>major.minor.patch</returns>
public override string ToString() public override string ToString()
{ {
return version; return raw;
} }
public string ToVersionString(string? prefix = null) public string ToStandardVersionString(string? prefix = null)
{ {
if (prefix == null) var sb = new StringBuilder();
if (!string.IsNullOrEmpty(prefix))
{ {
return version; sb.Append(prefix);
} }
else sb.Append($"{major}.{minor}.{patch}");
if (!string.IsNullOrEmpty(prerelease))
{ {
return $"{prefix}{version}"; sb.Append($"-{prerelease}");
} }
if (!string.IsNullOrEmpty(build))
{
sb.Append($"+{build}");
}
return sb.ToString();
} }
public static bool operator ==(SemanticVersion v1, SemanticVersion v2) public static bool operator <(SemanticVersion left, SemanticVersion right)
{ return v1.Equals(v2); } {
return left.CompareTo(right) < 0;
}
public static bool operator !=(SemanticVersion v1, SemanticVersion v2) public static bool operator >(SemanticVersion left, SemanticVersion right)
{ return !v1.Equals(v2); } {
return left.CompareTo(right) > 0;
}
public static bool operator >=(SemanticVersion v1, SemanticVersion v2) public static bool operator <=(SemanticVersion left, SemanticVersion right)
{ return v1.GreaterEquals(v2); } {
return left.CompareTo(right) <= 0;
}
public static bool operator <=(SemanticVersion v1, SemanticVersion v2) public static bool operator >=(SemanticVersion left, SemanticVersion right)
{ return v1.LessEquals(v2); } {
return left.CompareTo(right) >= 0;
}
#region Private #region Private
private bool GreaterEquals(SemanticVersion other) public int CompareTo(SemanticVersion other)
{ {
if (major < other.major) if (major != other.major)
{ {
return false; return major.CompareTo(other.major);
} }
else if (major > other.major) if (minor != other.minor)
{ {
return true; return minor.CompareTo(other.minor);
} }
else if (patch != other.patch)
{ {
if (minor < other.minor) return patch.CompareTo(other.patch);
}
return ComparePreRelease(prerelease, other.prerelease);
}
public int CompareTo(object? obj)
{
if (obj is null)
{
return 1;
}
if (obj is SemanticVersion other)
{
return CompareTo(other);
}
throw new ArgumentException("Object is not a SemanticVersion");
}
private static int ComparePreRelease(string? left, string? right)
{
if (string.IsNullOrEmpty(left) && string.IsNullOrEmpty(right))
{
return 0;
}
if (string.IsNullOrEmpty(left))
{
return 1;
}
if (string.IsNullOrEmpty(right))
{
return -1;
}
var leftSpan = left.AsSpan();
var rightSpan = right.AsSpan();
using var leftEnum = leftSpan.Split('.').GetEnumerator();
using var rightEnum = rightSpan.Split('.').GetEnumerator();
while (true)
{
var hasLeft = leftEnum.MoveNext();
var hasRight = rightEnum.MoveNext();
if (!hasLeft && !hasRight)
{ {
return false; return 0;
} }
else if (minor > other.minor) if (!hasLeft)
{ {
return true; return -1;
} }
else if (!hasRight)
{ {
if (patch < other.patch) return 1;
{ }
return false;
} var leftSegment = leftSpan[leftEnum.Current];
else if (patch > other.patch) var rightSegment = rightSpan[rightEnum.Current];
{
return true; var segCmp = CompareSegment(leftSegment, rightSegment);
} if (segCmp != 0)
else {
{ return segCmp;
return true;
}
} }
} }
} }
private bool LessEquals(SemanticVersion other) private static int CompareSegment(ReadOnlySpan<char> left, ReadOnlySpan<char> right)
{ {
if (major < other.major) var leftIsNum = ulong.TryParse(left, out var leftNum);
var rightIsNum = ulong.TryParse(right, out var rightNum);
return (leftIsNum, rightIsNum) switch
{ {
return true; (true, true) => leftNum.CompareTo(rightNum),
}
else if (major > other.major) (false, true) => 1,
{ (true, false) => -1,
return false;
} (false, false) => left.SequenceCompareTo(right),
else };
{
if (minor < other.minor)
{
return true;
}
else if (minor > other.minor)
{
return false;
}
else
{
if (patch < other.patch)
{
return true;
}
else if (patch > other.patch)
{
return false;
}
else
{
return true;
}
}
}
} }
#endregion Private #endregion Private
+31 -31
View File
@@ -1,6 +1,6 @@
namespace ServiceLib.Services; namespace ServiceLib.Services;
public class UpdateService(Config config, Func<bool, string, Task> updateFunc) public partial class UpdateService(Config config, Func<bool, string, Task> updateFunc)
{ {
private readonly Config? _config = config; private readonly Config? _config = config;
private readonly Func<bool, string, Task>? _updateFunc = updateFunc; private readonly Func<bool, string, Task>? _updateFunc = updateFunc;
@@ -174,7 +174,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
{ {
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type);
var tagName = string.Empty; var tagName = string.Empty;
if (preRelease) if (preRelease || coreInfo?.LockedMaxVersion != null)
{ {
var url = coreInfo?.ReleaseApiUrl; var url = coreInfo?.ReleaseApiUrl;
var result = await downloadHandle.TryDownloadString(url, true, Global.AppName); var result = await downloadHandle.TryDownloadString(url, true, Global.AppName);
@@ -187,6 +187,24 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
var gitHubRelease = preRelease ? gitHubReleases?.First() : gitHubReleases?.First(r => r.Prerelease == false); var gitHubRelease = preRelease ? gitHubReleases?.First() : gitHubReleases?.First(r => r.Prerelease == false);
tagName = gitHubRelease?.TagName; tagName = gitHubRelease?.TagName;
//var body = gitHubRelease?.Body; //var body = gitHubRelease?.Body;
if (coreInfo?.LockedMaxVersion != null)
{
var lockedMaxVersion = coreInfo.LockedMaxVersion;
var remoteVersion = new SemanticVersion(tagName);
if (remoteVersion > lockedMaxVersion)
{
var fallbackRelease = gitHubReleases?
.Where(r => preRelease || !r.Prerelease)
.Select(r => new { Release = r, IsValid = SemanticVersion.TryParse(r.TagName, out var v), Version = v })
.Where(x => x.IsValid && x.Version <= coreInfo.LockedMaxVersion)
.MaxBy(x => x.Version)?
.Release;
gitHubRelease = fallbackRelease;
tagName = gitHubRelease?.TagName;
}
}
} }
else else
{ {
@@ -202,6 +220,9 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
return new UpdateResult(true, new SemanticVersion(tagName)); return new UpdateResult(true, new SemanticVersion(tagName));
} }
[GeneratedRegex(@"v?(?<version>\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?)", RegexOptions.IgnoreCase)]
private static partial Regex SemVerRegex();
private async Task<SemanticVersion> GetCoreVersion(ECoreType type) private async Task<SemanticVersion> GetCoreVersion(ECoreType type)
{ {
try try
@@ -227,23 +248,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg); var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg);
var echo = result ?? ""; var echo = result ?? "";
var version = string.Empty; var version = SemVerRegex().Match(echo).Groups["version"].Value;
switch (type)
{
case ECoreType.v2fly:
case ECoreType.Xray:
case ECoreType.v2fly_v5:
version = Regex.Match(echo, $"{coreInfo.Match} ([0-9.]+) \\(").Groups[1].Value;
break;
case ECoreType.mihomo:
version = Regex.Match(echo, $"v[0-9.]+").Groups[0].Value;
break;
case ECoreType.sing_box:
version = Regex.Match(echo, $"([0-9.]+)").Groups[1].Value;
break;
}
return new SemanticVersion(version); return new SemanticVersion(version);
} }
catch (Exception ex) catch (Exception ex)
@@ -269,30 +274,25 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
case ECoreType.v2fly: case ECoreType.v2fly:
case ECoreType.Xray: case ECoreType.Xray:
case ECoreType.v2fly_v5: case ECoreType.v2fly_v5:
{
curVersion = await GetCoreVersion(type);
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToVersionString("v"));
url = string.Format(coreUrl, version.ToVersionString("v"));
break;
}
case ECoreType.mihomo: case ECoreType.mihomo:
{ {
curVersion = await GetCoreVersion(type); curVersion = await GetCoreVersion(type);
message = string.Format(ResUI.IsLatestCore, type, curVersion); message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
url = string.Format(coreUrl, version.ToVersionString("v")); url = string.Format(coreUrl, version);
break; break;
} }
case ECoreType.sing_box: case ECoreType.sing_box:
{ {
curVersion = await GetCoreVersion(type); curVersion = await GetCoreVersion(type);
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToVersionString("v")); message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
url = string.Format(coreUrl, version.ToVersionString("v"), version); url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v"));
break; break;
} }
case ECoreType.v2rayN: case ECoreType.v2rayN:
{ {
curVersion = new SemanticVersion(Utils.GetVersionInfo()); curVersion = new SemanticVersion(Utils.GetVersionInfo());
message = string.Format(ResUI.IsLatestN, type, curVersion); message = string.Format(ResUI.IsLatestN, type, curVersion.ToStandardVersionString("v"));
url = string.Format(coreUrl, version); url = string.Format(coreUrl, version);
break; break;
} }
@@ -300,7 +300,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
throw new ArgumentException("Type"); throw new ArgumentException("Type");
} }
if (curVersion >= version && version != new SemanticVersion(0, 0, 0)) if (curVersion >= version && !version.Equals(new SemanticVersion(0, 0, 0)))
{ {
return new UpdateResult(false, message); return new UpdateResult(false, message);
} }
@@ -193,7 +193,7 @@ public partial class CheckUpdateViewModel : MyReactiveObject
} }
await CheckUpdateN(EnableCheckPreReleaseUpdate); await CheckUpdateN(EnableCheckPreReleaseUpdate);
} }
else if (item.CoreType == ECoreType.Xray) else if (item.CoreType is ECoreType.Xray or ECoreType.sing_box)
{ {
await CheckUpdateCore(item, EnableCheckPreReleaseUpdate); await CheckUpdateCore(item, EnableCheckPreReleaseUpdate);
} }