namespace ServiceLib.Services; public partial class UpdateService(Config config, Func updateFunc) { private readonly Config? _config = config; private readonly Func? _updateFunc = updateFunc; private static readonly string _tag = "UpdateService"; public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default) { var url = string.Empty; var fileName = string.Empty; DownloadService downloadHandle = new(); downloadHandle.UpdateCompleted += (sender2, args) => { if (args.Success) { _ = UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully); _ = UpdateFunc(true, Utils.UrlEncode(fileName)); } else { _ = UpdateFunc(false, args.Msg); } }; downloadHandle.Error += (sender2, args) => { _ = UpdateFunc(false, args.GetException().Message); }; await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, ECoreType.v2rayN)); var result = await CheckUpdateAsync(downloadHandle, ECoreType.v2rayN, preRelease, blProxy); if (result.Success) { await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN)); await UpdateFunc(false, result.Msg); url = result.Url!; fileName = Utils.GetTempPath(Utils.GetGuid()); await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken); } else { await UpdateFunc(false, result.Msg); } } public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default) { var url = string.Empty; var fileName = string.Empty; DownloadService downloadHandle = new(); downloadHandle.UpdateCompleted += (sender2, args) => { if (args.Success) { _ = UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully); _ = UpdateFunc(false, ResUI.MsgUnpacking); try { _ = UpdateFunc(true, fileName); } catch (Exception ex) { _ = UpdateFunc(false, ex.Message); } } else { _ = UpdateFunc(false, args.Msg); } }; downloadHandle.Error += (sender2, args) => { _ = UpdateFunc(false, args.GetException().Message); }; await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, type)); var result = await CheckUpdateAsync(downloadHandle, type, preRelease, blProxy); if (result.Success) { await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, type)); await UpdateFunc(false, result.Msg); url = result.Url!; var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url); fileName = Utils.GetTempPath(Utils.GetGuid() + ext); await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken); } else { if (!result.Msg.IsNullOrEmpty()) { await UpdateFunc(false, result.Msg); } } } public async Task CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default) { if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type)) { return new UpdateResult(false, ResUI.MsgNotSupport); } var downloadHandle = new DownloadService(); var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease); return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy, cancellationToken); } public async Task> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default) { var msgs = new List(); foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes()) { if (!(_config.CheckUpdateItem.SelectedCoreTypes?.Contains(type.ToString()) ?? true)) { continue; } var result = await CheckHasUpdateOnly(type, preRelease, blProxy, cancellationToken); if (result.Success && result.Version != null) { var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version); msgs.Add(msg); AppManager.Instance.SetLastCheckUpdateResult(type, msg); } else { AppManager.Instance.SetLastCheckUpdateResult(type, result.Msg); } } return msgs; } public async Task UpdateGeoFileAll(bool blProxy = true, CancellationToken cancellationToken = default) { var requests = new List(); requests.AddRange(GetGeoFilesRequest()); requests.AddRange(GetOtherFilesRequest()); requests.AddRange(await GetSrsFileAllRequest()); // NOTE: srs files are more small, so we reverse the order to ensure a good download experience for the user. requests.Reverse(); await DownloadGeoFiles(requests, blProxy, cancellationToken); await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); } #region CheckUpdate private private async Task CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default) { try { var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy, cancellationToken); if (!result.Success || result.Version is null) { return result; } return await ParseDownloadUrl(type, result, cancellationToken); } catch (Exception ex) { Logging.SaveLog(_tag, ex); await UpdateFunc(false, ex.Message); return new UpdateResult(false, ex.Message); } } private async Task GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default) { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); var tagName = string.Empty; if (preRelease || coreInfo?.LockedMaxVersion != null) { var url = coreInfo?.ReleaseApiUrl; var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName, cancellationToken); if (result.IsNullOrEmpty()) { return new UpdateResult(false, ""); } var gitHubReleases = JsonUtils.Deserialize>(result); 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 { var url = Path.Combine(coreInfo.Url, "latest"); var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy, cancellationToken); if (lastUrl == null) { return new UpdateResult(false, ""); } tagName = lastUrl?.Split("/tag/").LastOrDefault(); } return new UpdateResult(true, new SemanticVersion(tagName)); } [GeneratedRegex(@"v?(?\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?)", RegexOptions.IgnoreCase)] private static partial Regex SemVerRegex(); private async Task GetCoreVersion(ECoreType type, CancellationToken cancellationToken = default) { try { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); var filePath = string.Empty; foreach (var name in coreInfo.CoreExes) { var vName = Utils.GetBinPath(Utils.GetExeName(name), coreInfo.CoreType.ToString()); if (File.Exists(vName)) { filePath = vName; break; } } if (!File.Exists(filePath)) { var msg = string.Format(ResUI.NotFoundCore, @"", "", ""); //ShowMsg(true, msg); return new SemanticVersion(""); } var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg, cancellationToken); var echo = result ?? ""; var version = SemVerRegex().Match(echo).Groups["version"].Value; return new SemanticVersion(version); } catch (Exception ex) { Logging.SaveLog(_tag, ex); await UpdateFunc(false, ex.Message); return new SemanticVersion(""); } } private async Task ParseDownloadUrl(ECoreType type, UpdateResult result, CancellationToken cancellationToken = default) { try { var version = result.Version ?? new SemanticVersion(0, 0, 0); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); var coreUrl = await GetUrlFromCore(coreInfo) ?? string.Empty; SemanticVersion curVersion; string message; string? url; switch (type) { case ECoreType.v2fly: case ECoreType.Xray: case ECoreType.v2fly_v5: case ECoreType.mihomo: { curVersion = await GetCoreVersion(type, cancellationToken); message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v")); url = string.Format(coreUrl, version); break; } case ECoreType.sing_box: { curVersion = await GetCoreVersion(type, cancellationToken); 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.ToStandardVersionString("v")); url = string.Format(coreUrl, version); break; } default: throw new ArgumentException("Type"); } if (curVersion >= version && !version.Equals(new SemanticVersion(0, 0, 0))) { return new UpdateResult(false, message); } result.Url = url; return result; } catch (Exception ex) { Logging.SaveLog(_tag, ex); await UpdateFunc(false, ex.Message); return new UpdateResult(false, ex.Message); } } private async Task GetUrlFromCore(CoreInfo? coreInfo) { if (Utils.IsWindows()) { var url = RuntimeInformation.ProcessArchitecture switch { Architecture.Arm64 => coreInfo?.DownloadUrlWinArm64, Architecture.X64 => coreInfo?.DownloadUrlWin64, _ => null, }; if (coreInfo?.CoreType != ECoreType.v2rayN) { return url; } //Check for avalonia desktop windows version if (File.Exists(Path.Combine(Utils.GetBaseDirectory(), "libHarfBuzzSharp.dll"))) { return url?.Replace(".zip", "-desktop.zip"); } return url; } else if (Utils.IsLinux()) { return RuntimeInformation.ProcessArchitecture switch { Architecture.Arm64 => coreInfo?.DownloadUrlLinuxArm64, Architecture.RiscV64 => coreInfo?.DownloadUrlLinuxRiscV64, Architecture.LoongArch64 => coreInfo?.DownloadUrlLinuxLoong64, Architecture.X64 => coreInfo?.DownloadUrlLinux64, _ => null, }; } else if (Utils.IsMacOS()) { return RuntimeInformation.ProcessArchitecture switch { Architecture.Arm64 => coreInfo?.DownloadUrlOSXArm64, Architecture.X64 => coreInfo?.DownloadUrlOSX64, _ => null, }; } return await Task.FromResult(""); } #endregion CheckUpdate private #region Geo private private List GetGeoFilesRequest() { var geoUrl = string.IsNullOrEmpty(_config?.ConstItem.GeoSourceUrl) ? Global.GeoUrl : _config.ConstItem.GeoSourceUrl; List files = ["geosite", "geoip"]; return [ .. from geoName in files let fileName = $"{geoName}.dat" let targetPath = Utils.GetBinPath($"{fileName}") let url = string.Format(geoUrl, geoName) select new FileDownloadRequest() { FileUrl = url, FilePath = targetPath, DisplayFileName = fileName, }, ]; } private List GetOtherFilesRequest() { //If it is not in China area, no update is required if (_config.ConstItem.GeoSourceUrl.IsNotEmpty()) { return []; } return [ .. Global.OtherGeoUrls.Select(url => { var fileName = Path.GetFileName(url); var targetPath = Utils.GetBinPath($"{fileName}"); return new FileDownloadRequest() { FileUrl = url, FilePath = targetPath, DisplayFileName = fileName, }; }), ]; } private async Task> GetSrsFileAllRequest() { var geoipFiles = new List(); var geoSiteFiles = new List(); // Collect from routing rules var routingItems = await AppManager.Instance.RoutingItems(); foreach (var routing in routingItems) { var rules = JsonUtils.Deserialize>(routing.RuleSet); foreach (var item in rules ?? []) { AddPrefixedItems(item.Ip, Global.GeoIPPrefix, geoipFiles); AddPrefixedItems(item.Domain, Global.GeoSitePrefix, geoSiteFiles); } } // Collect from DNS configuration var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); if (dnsItem != null) { ExtractDnsRuleSets(dnsItem.NormalDNS, geoipFiles, geoSiteFiles); ExtractDnsRuleSets(dnsItem.TunDNS, geoipFiles, geoSiteFiles); } // Append default items geoSiteFiles.AddRange(["google", "cn", "geolocation-cn", "category-ads-all"]); // Download files var path = Utils.GetBinPath("srss"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } return [ .. geoipFiles.Distinct().Select(f => (type: "geoip", file: f)) .Concat(geoSiteFiles.Distinct().Select(f => (type: "geosite", file: f))) .Select(item => GetSrsFileRequest(item.type, item.file)), ]; } private void AddPrefixedItems(List? items, string prefix, List output) { if (items == null) { return; } foreach (var item in items) { if (item.StartsWith(prefix)) { output.Add(item.Substring(prefix.Length)); } } } private void ExtractDnsRuleSets(string? dnsJson, List geoipFiles, List geoSiteFiles) { if (string.IsNullOrEmpty(dnsJson)) { return; } try { var dns = JsonUtils.Deserialize(dnsJson); if (dns?.rules != null) { foreach (var rule in dns.rules) { ExtractSrsRuleSets(rule, geoipFiles, geoSiteFiles); } } } catch { } } private void ExtractSrsRuleSets(Rule4Sbox? rule, List geoipFiles, List geoSiteFiles) { if (rule == null) { return; } AddPrefixedItems(rule.rule_set, "geosite-", geoSiteFiles); AddPrefixedItems(rule.rule_set, "geoip-", geoipFiles); // Handle nested rules recursively if (rule.rules != null) { foreach (var nestedRule in rule.rules) { ExtractSrsRuleSets(nestedRule, geoipFiles, geoSiteFiles); } } } private FileDownloadRequest GetSrsFileRequest(string type, string srsName) { var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl) ? Global.SingboxRulesetUrl : _config.ConstItem.SrsSourceUrl; var fileName = $"{type}-{srsName}.srs"; var targetPath = Path.Combine(Utils.GetBinPath("srss"), fileName); var url = string.Format(srsUrl, type, $"{type}-{srsName}", srsName); return new FileDownloadRequest() { FileUrl = url, FilePath = targetPath, DisplayFileName = fileName, }; } private async Task DownloadGeoFiles(List requests, bool blProxy, CancellationToken cancellationToken = default) { var tmpFilePathDict = new Dictionary(); var tmpFileRequests = new List(); foreach (var request in requests) { var tmpFilePath = Utils.GetTempPath(Utils.GetGuid()); tmpFilePathDict[request.FilePath] = tmpFilePath; tmpFileRequests.Add(request with { FilePath = tmpFilePath, }); } DownloadService downloadHandle = new(); downloadHandle.UpdateCompleted += (sender2, args) => { if (args.Success) { //_ = UpdateFunc(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName)); foreach (var request in requests) { try { //if (File.Exists(tmpFileName)) //{ // File.Copy(tmpFileName, targetPath, true); // File.Delete(tmpFileName); // //await UpdateFunc(true, ""); //} var tmpFileName = tmpFilePathDict[request.FilePath]; if (File.Exists(tmpFileName)) { File.Copy(tmpFileName, request.FilePath, true); File.Delete(tmpFileName); } } catch (Exception ex) { _ = UpdateFunc(false, ex.Message); } } } else { _ = UpdateFunc(false, args.Msg); } }; downloadHandle.Error += (sender2, args) => { _ = UpdateFunc(false, args.GetException().Message); }; await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, cancellationToken); } #endregion Geo private private async Task UpdateFunc(bool notify, string msg) { await _updateFunc?.Invoke(notify, msg); } }