namespace ServiceLib.Services; public class UpdateService(Config config, Func updateFunc) { private readonly Config? _config = config; private readonly Func? _updateFunc = updateFunc; private readonly int _timeout = 30; private static readonly string _tag = "UpdateService"; public async Task CheckUpdateGuiN(bool preRelease) { 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); if (result.Success) { await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN)); await UpdateFunc(false, result.Msg); url = result.Url.ToString(); fileName = Utils.GetTempPath(Utils.GetGuid()); await downloadHandle.DownloadFileAsync(url, fileName, true, _timeout); } else { await UpdateFunc(false, result.Msg); } } public async Task CheckUpdateCore(ECoreType type, bool preRelease) { 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); if (result.Success) { await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, type)); await UpdateFunc(false, result.Msg); url = result.Url.ToString(); var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url); fileName = Utils.GetTempPath(Utils.GetGuid() + ext); await downloadHandle.DownloadFileAsync(url, fileName, true, _timeout); } else { if (!result.Msg.IsNullOrEmpty()) { await UpdateFunc(false, result.Msg); } } } public async Task UpdateGeoFileAll() { await UpdateGeoFiles(); await UpdateOtherFiles(); await UpdateSrsFileAll(); await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); } #region CheckUpdate private private async Task CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease) { try { var result = await GetRemoteVersion(downloadHandle, type, preRelease); if (!result.Success || result.Version is null) { return result; } return await ParseDownloadUrl(type, result); } 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) { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); var tagName = string.Empty; if (preRelease) { var url = coreInfo?.ReleaseApiUrl; var result = await downloadHandle.TryDownloadString(url, true, Global.AppName); 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; } else { var url = Path.Combine(coreInfo.Url, "latest"); var lastUrl = await downloadHandle.UrlRedirectAsync(url, true); if (lastUrl == null) { return new UpdateResult(false, ""); } tagName = lastUrl?.Split("/tag/").LastOrDefault(); } return new UpdateResult(true, new SemanticVersion(tagName)); } private async Task GetCoreVersion(ECoreType type) { 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); 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; } 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) { 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: { 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")); 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); break; } case ECoreType.v2rayN: { curVersion = new SemanticVersion(Utils.GetVersionInfo()); message = string.Format(ResUI.IsLatestN, type, curVersion); url = string.Format(coreUrl, version); break; } default: throw new ArgumentException("Type"); } if (curVersion >= version && version != 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.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 async Task UpdateGeoFiles() { var geoUrl = string.IsNullOrEmpty(_config?.ConstItem.GeoSourceUrl) ? Global.GeoUrl : _config.ConstItem.GeoSourceUrl; List files = ["geosite", "geoip"]; foreach (var geoName in files) { var fileName = $"{geoName}.dat"; var targetPath = Utils.GetBinPath($"{fileName}"); var url = string.Format(geoUrl, geoName); await DownloadGeoFile(url, fileName, targetPath); } } private async Task UpdateOtherFiles() { //If it is not in China area, no update is required if (_config.ConstItem.GeoSourceUrl.IsNotEmpty()) { return; } foreach (var url in Global.OtherGeoUrls) { var fileName = Path.GetFileName(url); var targetPath = Utils.GetBinPath($"{fileName}"); await DownloadGeoFile(url, fileName, targetPath); } } private async Task UpdateSrsFileAll() { var geoipFiles = new List(); var geoSiteFiles = new List(); //Collect used files list var routingItems = await AppManager.Instance.RoutingItems(); foreach (var routing in routingItems) { var rules = JsonUtils.Deserialize>(routing.RuleSet); foreach (var item in rules ?? []) { foreach (var ip in item.Ip ?? []) { var prefix = "geoip:"; if (ip.StartsWith(prefix)) { geoipFiles.Add(ip.Substring(prefix.Length)); } } foreach (var domain in item.Domain ?? []) { var prefix = "geosite:"; if (domain.StartsWith(prefix)) { geoSiteFiles.Add(domain.Substring(prefix.Length)); } } } } //append dns items TODO geoSiteFiles.Add("google"); geoSiteFiles.Add("cn"); geoSiteFiles.Add("geolocation-cn"); geoSiteFiles.Add("category-ads-all"); var path = Utils.GetBinPath("srss"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } foreach (var item in geoipFiles.Distinct()) { await UpdateSrsFile("geoip", item); } foreach (var item in geoSiteFiles.Distinct()) { await UpdateSrsFile("geosite", item); } } private async Task UpdateSrsFile(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); await DownloadGeoFile(url, fileName, targetPath); } private async Task DownloadGeoFile(string url, string fileName, string targetPath) { var tmpFileName = Utils.GetTempPath(Utils.GetGuid()); DownloadService downloadHandle = new(); downloadHandle.UpdateCompleted += (sender2, args) => { if (args.Success) { _ = UpdateFunc(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName)); try { if (File.Exists(tmpFileName)) { File.Copy(tmpFileName, targetPath, true); File.Delete(tmpFileName); //await UpdateFunc(true, ""); } } catch (Exception ex) { _ = UpdateFunc(false, ex.Message); } } else { _ = UpdateFunc(false, args.Msg); } }; downloadHandle.Error += (sender2, args) => { _ = UpdateFunc(false, args.GetException().Message); }; await downloadHandle.DownloadFileAsync(url, tmpFileName, true, _timeout); } #endregion Geo private private async Task UpdateFunc(bool notify, string msg) { await _updateFunc?.Invoke(notify, msg); } }