mirror of
https://github.com/2dust/v2rayN.git
synced 2026-09-14 02:12:06 +03:00
Lock sing-box max version (#10091)
* Refactor Semantic Version * Lock max version * Allow sing-box update to pre-release
This commit is contained in:
@@ -229,6 +229,7 @@ public sealed class CoreInfoManager
|
||||
DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz",
|
||||
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
|
||||
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
|
||||
LockedMaxVersion = new SemanticVersion(1, 14, int.MaxValue),
|
||||
Match = "sing-box",
|
||||
VersionArg = "version",
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ public class CoreInfo
|
||||
public string? DownloadUrlLinuxLoong64 { get; set; }
|
||||
public string? DownloadUrlOSX64 { get; set; }
|
||||
public string? DownloadUrlOSXArm64 { get; set; }
|
||||
public SemanticVersion? LockedMaxVersion { get; set; }
|
||||
public string? Match { get; set; }
|
||||
public string? VersionArg { get; set; }
|
||||
public bool AbsolutePath { get; set; }
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
public class SemanticVersion
|
||||
public class SemanticVersion : IEquatable<SemanticVersion>, IComparable
|
||||
{
|
||||
private readonly string? build;
|
||||
private readonly int major;
|
||||
private readonly int minor;
|
||||
private readonly int patch;
|
||||
private readonly string version;
|
||||
private readonly string? prerelease;
|
||||
private readonly string raw;
|
||||
|
||||
public SemanticVersion(int major, int minor, int patch)
|
||||
{
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.patch = patch;
|
||||
version = $"{major}.{minor}.{patch}";
|
||||
raw = $"{major}.{minor}.{patch}";
|
||||
}
|
||||
|
||||
public SemanticVersion(string? version)
|
||||
@@ -24,26 +26,41 @@ public class SemanticVersion
|
||||
major = 0;
|
||||
minor = 0;
|
||||
patch = 0;
|
||||
raw = $"{major}.{minor}.{patch}";
|
||||
return;
|
||||
}
|
||||
this.version = version.RemovePrefix('v');
|
||||
raw = version;
|
||||
|
||||
var parts = this.version.Split('.');
|
||||
if (parts.Length == 2)
|
||||
var span = version.StartsWith("v", StringComparison.OrdinalIgnoreCase)
|
||||
? 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());
|
||||
minor = int.Parse(parts.Last());
|
||||
patch = 0;
|
||||
}
|
||||
else if (parts.Length is 3 or 4)
|
||||
{
|
||||
major = int.Parse(parts[0]);
|
||||
minor = int.Parse(parts[1]);
|
||||
patch = int.Parse(parts[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Invalid version string");
|
||||
case 2:
|
||||
major = int.Parse(parts.First());
|
||||
minor = int.Parse(parts.Last());
|
||||
patch = 0;
|
||||
break;
|
||||
|
||||
case 3 or 4:
|
||||
major = int.Parse(parts[0]);
|
||||
minor = int.Parse(parts[1]);
|
||||
patch = int.Parse(parts[2]);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentException("Invalid version string");
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -51,135 +68,186 @@ public class SemanticVersion
|
||||
major = 0;
|
||||
minor = 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)
|
||||
{
|
||||
if (obj is SemanticVersion other)
|
||||
{
|
||||
return major == other.major && minor == other.minor && patch == other.patch;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return obj is SemanticVersion other && Equals(other);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{ return v1.Equals(v2); }
|
||||
public static bool operator <(SemanticVersion left, SemanticVersion right)
|
||||
{
|
||||
return left.CompareTo(right) < 0;
|
||||
}
|
||||
|
||||
public static bool operator !=(SemanticVersion v1, SemanticVersion v2)
|
||||
{ return !v1.Equals(v2); }
|
||||
public static bool operator >(SemanticVersion left, SemanticVersion right)
|
||||
{
|
||||
return left.CompareTo(right) > 0;
|
||||
}
|
||||
|
||||
public static bool operator >=(SemanticVersion v1, SemanticVersion v2)
|
||||
{ return v1.GreaterEquals(v2); }
|
||||
public static bool operator <=(SemanticVersion left, SemanticVersion right)
|
||||
{
|
||||
return left.CompareTo(right) <= 0;
|
||||
}
|
||||
|
||||
public static bool operator <=(SemanticVersion v1, SemanticVersion v2)
|
||||
{ return v1.LessEquals(v2); }
|
||||
public static bool operator >=(SemanticVersion left, SemanticVersion right)
|
||||
{
|
||||
return left.CompareTo(right) >= 0;
|
||||
}
|
||||
|
||||
#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 false;
|
||||
}
|
||||
else if (patch > other.patch)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
var leftSegment = leftSpan[leftEnum.Current];
|
||||
var rightSegment = rightSpan[rightEnum.Current];
|
||||
|
||||
var segCmp = CompareSegment(leftSegment, rightSegment);
|
||||
if (segCmp != 0)
|
||||
{
|
||||
return segCmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
else if (major > other.major)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
(true, true) => leftNum.CompareTo(rightNum),
|
||||
|
||||
(false, true) => 1,
|
||||
(true, false) => -1,
|
||||
|
||||
(false, false) => left.SequenceCompareTo(right),
|
||||
};
|
||||
}
|
||||
|
||||
#endregion Private
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 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 tagName = string.Empty;
|
||||
if (preRelease)
|
||||
if (preRelease || coreInfo?.LockedMaxVersion != null)
|
||||
{
|
||||
var url = coreInfo?.ReleaseApiUrl;
|
||||
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);
|
||||
tagName = gitHubRelease?.TagName;
|
||||
//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
|
||||
{
|
||||
@@ -202,6 +220,9 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
||||
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)
|
||||
{
|
||||
try
|
||||
@@ -227,23 +248,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
||||
|
||||
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg);
|
||||
var echo = result ?? "";
|
||||
var version = string.Empty;
|
||||
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;
|
||||
}
|
||||
var version = SemVerRegex().Match(echo).Groups["version"].Value;
|
||||
return new SemanticVersion(version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -269,30 +274,25 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
||||
case ECoreType.v2fly:
|
||||
case ECoreType.Xray:
|
||||
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:
|
||||
{
|
||||
curVersion = await GetCoreVersion(type);
|
||||
message = string.Format(ResUI.IsLatestCore, type, curVersion);
|
||||
url = string.Format(coreUrl, version.ToVersionString("v"));
|
||||
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
|
||||
url = string.Format(coreUrl, version);
|
||||
break;
|
||||
}
|
||||
|
||||
case ECoreType.sing_box:
|
||||
{
|
||||
curVersion = await GetCoreVersion(type);
|
||||
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToVersionString("v"));
|
||||
url = string.Format(coreUrl, version.ToVersionString("v"), version);
|
||||
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
|
||||
url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v"));
|
||||
break;
|
||||
}
|
||||
case ECoreType.v2rayN:
|
||||
{
|
||||
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);
|
||||
break;
|
||||
}
|
||||
@@ -300,7 +300,7 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
||||
}
|
||||
await CheckUpdateN(EnableCheckPreReleaseUpdate);
|
||||
}
|
||||
else if (item.CoreType == ECoreType.Xray)
|
||||
else if (item.CoreType is ECoreType.Xray or ECoreType.sing_box)
|
||||
{
|
||||
await CheckUpdateCore(item, EnableCheckPreReleaseUpdate);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user