diff --git a/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs b/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs index f9a2951f..45f93c22 100644 --- a/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs +++ b/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs @@ -11,7 +11,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl /// /// Send UDP data to a remote endpoint (IP address) /// - public async Task SendAsync(IPEndPoint remote, byte[] data) + public async Task SendAsync(IPEndPoint remote, byte[] data, CancellationToken ct = default) { var addrData = new Socks5AddressData { @@ -22,7 +22,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl Port = (ushort)remote.Port }; var packet = BuildSocks5UdpPacket(addrData, data); - await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint); + await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct); } /// @@ -31,7 +31,8 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl /// Domain name or IP address /// Port number /// Data to send - public async Task SendAsync(string host, ushort port, byte[] data) + /// Cancellation token + public async Task SendAsync(string host, ushort port, byte[] data, CancellationToken ct = default) { var addrData = new Socks5AddressData(); @@ -53,7 +54,8 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl addrData.Port = port; var packet = BuildSocks5UdpPacket(addrData, data); - await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint); + //await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint); + await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct); } /// diff --git a/v2rayN/ServiceLib.UdpTest/UdpTestService.cs b/v2rayN/ServiceLib.UdpTest/UdpTestService.cs index be4b943d..27daeca1 100644 --- a/v2rayN/ServiceLib.UdpTest/UdpTestService.cs +++ b/v2rayN/ServiceLib.UdpTest/UdpTestService.cs @@ -88,17 +88,18 @@ public class UdpTestService return (targetServerHost, _udpTest.GetDefaultTargetPort()); } - public async Task SendUdpRequestAsync(string targetServerHost, int socks5Port, TimeSpan operationTimeout) + public async Task SendUdpRequestAsync(string targetServerHost, int socks5Port, CancellationToken ct = default) { - using var cts = new CancellationTokenSource(operationTimeout); - var cancellationToken = cts.Token; + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token); + var linkedCt = linkedCts.Token; var udpRequestPacket = _udpTest.BuildUdpRequestPacket(); if (udpRequestPacket == null || udpRequestPacket.Length == 0) { throw new InvalidOperationException("Failed to build UDP request packet."); } using var channel = new Socks5UdpChannel("127.0.0.1", socks5Port); - if (!await channel.EstablishUdpAssociationAsync(cancellationToken).ConfigureAwait(false)) + if (!await channel.EstablishUdpAssociationAsync(linkedCt).ConfigureAwait(false)) { throw new Exception("Failed to establish UDP association with SOCKS5 proxy."); } @@ -116,8 +117,8 @@ public class UdpTestService { var stopwatch = new Stopwatch(); stopwatch.Start(); - await channel.SendAsync(targetHost, targetPort, udpRequestPacket).ConfigureAwait(false); - var (_, receiveResult) = await channel.ReceiveAsync(cancellationToken).ConfigureAwait(false); + await channel.SendAsync(targetHost, targetPort, udpRequestPacket, linkedCt).ConfigureAwait(false); + var (_, receiveResult) = await channel.ReceiveAsync(linkedCt).ConfigureAwait(false); stopwatch.Stop(); udpReceiveResult = receiveResult; diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 2a59f380..daecda93 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -1018,12 +1018,12 @@ public class Utils return new Dictionary(); } - public static async Task GetCliWrapOutput(string filePath, string? arg) + public static async Task GetCliWrapOutput(string filePath, string? arg, CancellationToken cancellationToken = default) { - return await GetCliWrapOutput(filePath, arg != null ? new List() { arg } : null); + return await GetCliWrapOutput(filePath, arg != null ? new List() { arg } : null, cancellationToken); } - public static async Task GetCliWrapOutput(string filePath, IEnumerable? args) + public static async Task GetCliWrapOutput(string filePath, IEnumerable? args, CancellationToken cancellationToken = default) { try { @@ -1040,7 +1040,7 @@ public class Utils } } - var result = await cmd.ExecuteBufferedAsync(); + var result = await cmd.ExecuteBufferedAsync(cancellationToken); if (result.IsSuccess) { return result.StandardOutput ?? ""; diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index dc125cc8..009985d7 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -96,6 +96,11 @@ public class Global public const string StringTrue = "true"; public const string StringFalse = "false"; public const int SqliteMaxBatchSize = 10000; + public static readonly TimeSpan LocalFetch = TimeSpan.FromSeconds(5); + public static readonly TimeSpan DirectFetch = TimeSpan.FromSeconds(10); + public static readonly TimeSpan ProxyFetch = TimeSpan.FromSeconds(30); + public static readonly TimeSpan DirectDownloadConnect = TimeSpan.FromSeconds(5); + public static readonly TimeSpan ProxyDownloadConnect = TimeSpan.FromSeconds(10); public const string SingboxDirectDNSTagPrefix = "direct-dns-"; public const string SingboxRemoteDNSTagPrefix = "remote-dns-"; diff --git a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs index 2cea1e0f..5bd808b5 100644 --- a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs @@ -66,34 +66,41 @@ public static class ConnectionHandler /// /// Measures response time by sending HTTP requests through proxy. /// - public static async Task GetRealPingTime(IWebProxy? webProxy, int downloadTimeout = 9) + public static async Task GetRealPingTime(IWebProxy? webProxy, CancellationToken cancellationToken = default) { var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl; var responseTime = -1; try { - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout)); + using var timeoutCts = new CancellationTokenSource(); + timeoutCts.CancelAfter(Global.LocalFetch); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + var linkedToken = linkedCts.Token; using var client = new HttpClient(new SocketsHttpHandler() { Proxy = webProxy, UseProxy = webProxy != null, - ConnectTimeout = TimeSpan.FromSeconds(3) + ConnectTimeout = Global.LocalFetch, }); List oneTime = []; for (var i = 0; i < 2; i++) { var timer = Stopwatch.StartNew(); - await client.GetAsync(url, cts.Token).ConfigureAwait(false); + await client.GetAsync(url, linkedToken).ConfigureAwait(false); timer.Stop(); oneTime.Add((int)timer.Elapsed.TotalMilliseconds); - await Task.Delay(100, cts.Token); + await Task.Delay(100, linkedToken); } responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault(); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch { + // Ignore } return responseTime; } @@ -101,7 +108,7 @@ public static class ConnectionHandler /// /// Gets IP and country information through specified proxy. /// - public static async Task GetIPInfo(IWebProxy? webProxy) + public static async Task GetIPInfo(IWebProxy? webProxy, CancellationToken cancellationToken = default) { try { @@ -112,7 +119,7 @@ public static class ConnectionHandler } var downloadHandle = new DownloadService(); - var result = await downloadHandle.TryDownloadString(url, webProxy, ""); + var result = await downloadHandle.TryDownloadString(url, webProxy, "", cancellationToken); if (result == null) { return null; @@ -129,6 +136,10 @@ public static class ConnectionHandler return new IpInfoResult(country, ip); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch { return null; diff --git a/v2rayN/ServiceLib/Helper/DownloaderHelper.cs b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs index e7d553ef..bbae3db5 100644 --- a/v2rayN/ServiceLib/Helper/DownloaderHelper.cs +++ b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs @@ -8,16 +8,14 @@ public class DownloaderHelper private static readonly Lazy _instance = new(() => new()); public static DownloaderHelper Instance => _instance.Value; - public async Task DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout, - IReadOnlyDictionary? requestHeaders = null, string? acceptHeader = null) + public async Task DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, + IReadOnlyDictionary? requestHeaders = null, string? acceptHeader = null, CancellationToken cancellationToken = default) { if (url.IsNullOrEmpty()) { return null; } - var connectTimeout = Math.Clamp(timeout / 5, 2, 5); - Uri uri = new(url); //Authorization Header var headers = new WebHeaderCollection(); @@ -31,12 +29,11 @@ public class DownloaderHelper Headers = headers, Accept = acceptHeader, UserAgent = userAgent, - ConnectTimeout = connectTimeout * 1000, + ConnectTimeout = GetConnectTimeoutMs(webProxy != null), Proxy = webProxy }; var downloadOpt = new DownloadConfiguration() { - BlockTimeout = timeout * 1000, MaxTryAgainOnFailure = 2, RequestConfiguration = requestConfiguration, CustomHttpMessageHandlerFactory = () => HttpRequestHeadersHelper.CreateHandler(GetSocketsHttpHandler(requestConfiguration), requestHeaders), @@ -51,31 +48,26 @@ public class DownloaderHelper } }; - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(timeout)); - - await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token); + await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken); using StreamReader reader = new(stream); - return await reader.ReadToEndAsync(cts.Token); + return await reader.ReadToEndAsync(cancellationToken); } - public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, IProgress progress, int timeout) + public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, Action onProgress, CancellationToken cancellationToken = default) { if (url.IsNullOrEmpty()) { throw new ArgumentNullException(nameof(url)); } - var connectTimeout = Math.Clamp(timeout / 5, 2, 5); var requestConfiguration = new RequestConfiguration() { - ConnectTimeout = connectTimeout * 1000, + ConnectTimeout = GetConnectTimeoutMs(true), Proxy = webProxy }; var downloadOpt = new DownloadConfiguration() { - BlockTimeout = timeout * 1000, MaxTryAgainOnFailure = 2, RequestConfiguration = requestConfiguration, CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration), @@ -88,49 +80,45 @@ public class DownloaderHelper downloader.DownloadProgressChanged += (sender, value) => { - if (progress != null && value.BytesPerSecondSpeed > 0) + if (!(value.BytesPerSecondSpeed > 0)) { - hasValue = true; - if (value.BytesPerSecondSpeed > maxSpeed) - { - maxSpeed = value.BytesPerSecondSpeed; - } + return; + } + hasValue = true; + if (value.BytesPerSecondSpeed > maxSpeed) + { + maxSpeed = value.BytesPerSecondSpeed; + } - var ts = DateTime.Now - lastUpdateTime; - if (ts.TotalMilliseconds >= 1000) - { - lastUpdateTime = DateTime.Now; - var speed = (maxSpeed / 1000 / 1000).ToString("#0.0"); - progress.Report(speed); - } + var ts = DateTime.Now - lastUpdateTime; + if (ts.TotalMilliseconds >= 1000) + { + lastUpdateTime = DateTime.Now; + var speed = (maxSpeed / 1000 / 1000).ToString("#0.0"); + onProgress.Invoke(speed); } }; downloader.DownloadFileCompleted += (sender, value) => { - if (progress != null) + if (hasValue && maxSpeed > 0) { - if (hasValue && maxSpeed > 0) - { - var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0"); - progress.Report(finalSpeed); - } - else if (value.Error != null) - { - progress.Report(value.Error?.Message); - } - else - { - progress.Report("0"); - } + var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0"); + onProgress.Invoke(finalSpeed); + } + else if (value.Error != null) + { + onProgress.Invoke(value.Error?.Message); + } + else + { + onProgress.Invoke("0"); } }; - //progress.Report("......"); - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(timeout)); - await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token); + //progress.Invoke("......"); + await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken); } - public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default) + public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action onProgress, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); if (request.FilePath.IsNullOrEmpty()) @@ -147,9 +135,9 @@ public class DownloaderHelper Request = request, }; - var requestConfiguration = new RequestConfiguration() + var requestConfiguration = new RequestConfiguration { - ConnectTimeout = (int)connectTimeout.TotalMilliseconds, + ConnectTimeout = GetConnectTimeoutMs(webProxy != null), Proxy = webProxy, }; var downloadOpt = new DownloadConfiguration() @@ -196,7 +184,7 @@ public class DownloaderHelper await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken); } - public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List requests, Action> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default) + public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List requests, Action> onProgress, CancellationToken cancellationToken = default) { if (requests is not { Count: > 0 }) { @@ -215,7 +203,7 @@ public class DownloaderHelper var requestConfiguration = new RequestConfiguration() { - ConnectTimeout = (int)connectTimeout.TotalMilliseconds, + ConnectTimeout = GetConnectTimeoutMs(webProxy != null), Proxy = webProxy, KeepAlive = true, @@ -225,7 +213,7 @@ public class DownloaderHelper var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = 4, - //CancellationToken = cancellationToken, + CancellationToken = cancellationToken, }; await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) => @@ -344,4 +332,9 @@ public class DownloaderHelper return handler; } + + private int GetConnectTimeoutMs(bool isProxy) + { + return (int)(isProxy ? Global.ProxyDownloadConnect : Global.DirectDownloadConnect).TotalMilliseconds; + } } diff --git a/v2rayN/ServiceLib/Manager/CertPemManager.cs b/v2rayN/ServiceLib/Manager/CertPemManager.cs index 0df8301d..9b86bdf3 100644 --- a/v2rayN/ServiceLib/Manager/CertPemManager.cs +++ b/v2rayN/ServiceLib/Manager/CertPemManager.cs @@ -28,14 +28,13 @@ public class CertPemManager /// Get certificate in PEM format from a server with CA pinning validation /// public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, - List? verifyPeerCertByName = null, int timeout = 4) + List? verifyPeerCertByName = null) { try { var (domain, _, port, _) = Utils.ParseUrl(target); - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(timeout)); + using var cts = new CancellationTokenSource(Global.LocalFetch); using var client = new TcpClient(); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); @@ -63,8 +62,8 @@ public class CertPemManager } catch (OperationCanceledException) { - Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds")); - return (null, $"Connection timeout after {timeout} seconds"); + Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds")); + return (null, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"); } catch (Exception ex) { @@ -77,15 +76,14 @@ public class CertPemManager /// Get certificate chain in PEM format from a server with CA pinning validation /// public async Task<(List, string?)> GetCertChainPemAsync(string target, string serverName, - List? verifyPeerCertByName = null, int timeout = 4) + List? verifyPeerCertByName = null) { var pemList = new List(); try { var (domain, _, port, _) = Utils.ParseUrl(target); - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(timeout)); + using var cts = new CancellationTokenSource(Global.LocalFetch); using var client = new TcpClient(); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); @@ -116,8 +114,8 @@ public class CertPemManager } catch (OperationCanceledException) { - Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds")); - return (pemList, $"Connection timeout after {timeout} seconds"); + Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds")); + return (pemList, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"); } catch (Exception ex) { diff --git a/v2rayN/ServiceLib/Manager/CoreManager.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs index 5e1527df..b2da9c77 100644 --- a/v2rayN/ServiceLib/Manager/CoreManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreManager.cs @@ -216,7 +216,7 @@ public class CoreManager await _updateFunc?.Invoke(notify, msg); } - private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000) + private static async Task WaitForProxyPort(CoreConfigContext? preContext) { if (preContext is null) { @@ -227,7 +227,7 @@ public class CoreManager return; } - using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs)); + using var rootCts = new CancellationTokenSource(Global.LocalFetch); var rootToken = rootCts.Token; var port = preContext.Node.Port; diff --git a/v2rayN/ServiceLib/Services/DownloadService.cs b/v2rayN/ServiceLib/Services/DownloadService.cs index 8ec66f38..58a83ab2 100644 --- a/v2rayN/ServiceLib/Services/DownloadService.cs +++ b/v2rayN/ServiceLib/Services/DownloadService.cs @@ -20,24 +20,31 @@ public class DownloadService /// /// Downloads data with the specified proxy and reports progress messages. /// - public async Task DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func updateFunc) + public async Task DownloadDataAsync(string url, IWebProxy webProxy, Func updateFunc, CancellationToken cancellationToken = default) { try { - var progress = new Progress(); - progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}"); - await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy, url, - progress, - downloadTimeout); + OnProgress, + cancellationToken); + + void OnProgress(string message) + { + cancellationToken.ThrowIfCancellationRequested(); + updateFunc.Invoke(false, $"{message}"); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception ex) { - await updateFunc?.Invoke(false, ex.Message); + await updateFunc.Invoke(false, ex.Message); if (ex.InnerException != null) { - await updateFunc?.Invoke(false, ex.InnerException.Message); + await updateFunc.Invoke(false, ex.InnerException.Message); } } return 0; @@ -46,23 +53,28 @@ public class DownloadService /// /// Downloads a file and reports progress through events. /// - public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, TimeSpan connectTimeout) + public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, CancellationToken cancellationToken = default) { try { UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}")); - var webProxy = await GetWebProxy(blProxy); + var webProxy = await GetWebProxy(blProxy, cancellationToken); await DownloaderHelper.Instance.DownloadFileAsync(webProxy, request, OnProgress, - connectTimeout); + cancellationToken); void OnProgress(FileDownloadState state) { + cancellationToken.ThrowIfCancellationRequested(); UpdateCompleted?.Invoke(this, new UpdateResult(state.Completed, $"{Utils.HumanFy((long)state.SpeedBytesPerSecond / 1024)}/s | {Utils.HumanFy(state.DownloadedBytes / 1024)}/{Utils.HumanFy(state.TotalBytes / 1024)}")); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { Logging.SaveLog(_tag, ex); @@ -75,20 +87,21 @@ public class DownloadService } } - public async Task DownloadSmallFilesAsync(List requests, bool blProxy, TimeSpan connectTimeout) + public async Task DownloadSmallFilesAsync(List requests, bool blProxy, CancellationToken cancellationToken = default) { try { UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}")); - var webProxy = await GetWebProxy(blProxy); + var webProxy = await GetWebProxy(blProxy, cancellationToken); await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy, requests, OnProgress, - connectTimeout); + cancellationToken); void OnProgress(ReadOnlyMemory states) { + cancellationToken.ThrowIfCancellationRequested(); var span = states.Span; var completedCount = 0; var downloadingStates = new List(); @@ -129,6 +142,10 @@ public class DownloadService UpdateCompleted?.Invoke(this, new UpdateResult(allCompleted, $"{completedCount}/{span.Length} | {Utils.HumanFy((long)totalSpeed / 1024)}/s {Utils.HumanFy(totalDownloadedBytes / 1024)}/{Utils.HumanFy(totalTotalBytes / 1024)} {downloadingFileName}")); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { Logging.SaveLog(_tag, ex); @@ -144,12 +161,12 @@ public class DownloadService /// /// Gets redirect target URL without following redirects automatically. /// - public async Task UrlRedirectAsync(string url, bool blProxy) + public async Task UrlRedirectAsync(string url, bool blProxy, CancellationToken cancellationToken = default) { var webRequestHandler = new SocketsHttpHandler { AllowAutoRedirect = false, - Proxy = await GetWebProxy(blProxy) + Proxy = await GetWebProxy(blProxy, cancellationToken) }; var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy(); if (certificateChainPolicy != null) @@ -159,7 +176,7 @@ public class DownloadService } using var client = new HttpClient(webRequestHandler); - var response = await client.GetAsync(url); + var response = await client.GetAsync(url, cancellationToken); if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null) { return response.Headers.Location.ToString(); @@ -175,26 +192,29 @@ public class DownloadService /// /// Tries to download string content using proxy switch setting. /// - public async Task TryDownloadString(string url, bool blProxy, string userAgent) + public async Task TryDownloadString(string url, bool blProxy, string userAgent, CancellationToken cancellationToken = default) { - var webProxy = await GetWebProxy(blProxy); - return await TryDownloadString(url, webProxy, userAgent); + var webProxy = await GetWebProxy(blProxy, cancellationToken); + return await TryDownloadString(url, webProxy, userAgent, cancellationToken); } /// /// Tries to download string content with a specified proxy. /// - public async Task TryDownloadString(string url, IWebProxy? webProxy, string userAgent) + public async Task TryDownloadString(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default) { - var timeout = 15; try { - var result1 = await DownloadStringAsync(url, webProxy, userAgent, timeout); + var result1 = await DownloadStringAsync(url, webProxy, userAgent, cancellationToken); if (result1.IsNotEmpty()) { return result1; } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { Logging.SaveLog(_tag, ex); @@ -207,12 +227,16 @@ public class DownloadService try { - var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, timeout); + var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, cancellationToken); if (result2.IsNotEmpty()) { return result2; } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { Logging.SaveLog(_tag, ex); @@ -229,17 +253,16 @@ public class DownloadService /// /// Downloads string content via HttpClient. /// - private async Task DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout) + private async Task DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default) { try { - var connectTimeout = Math.Clamp(timeout / 5, 2, 5); var handler = new SocketsHttpHandler { Proxy = webProxy, UseProxy = webProxy != null, AutomaticDecompression = DecompressionMethods.All, - ConnectTimeout = TimeSpan.FromSeconds(connectTimeout) + ConnectTimeout = webProxy is null ? Global.DirectDownloadConnect : Global.ProxyDownloadConnect, }; var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy(); if (certificateChainPolicy != null) @@ -248,10 +271,8 @@ public class DownloadService handler.SslOptions.RemoteCertificateValidationCallback = null; } - using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders)) - { - Timeout = Timeout.InfiniteTimeSpan - }; + using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders)); + client.Timeout = Timeout.InfiniteTimeSpan; if (userAgent.IsNullOrEmpty()) { @@ -270,10 +291,15 @@ public class DownloadService client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo)); } - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(timeout)); + using var timeoutCts = new CancellationTokenSource(); + timeoutCts.CancelAfter(webProxy is null ? Global.DirectFetch : Global.ProxyFetch); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); - return await client.GetStringAsync(url, cts.Token); + return await client.GetStringAsync(url, linkedCts.Token); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception ex) { @@ -291,7 +317,7 @@ public class DownloadService /// /// Downloads string content via DownloaderHelper. /// - private async Task DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout) + private async Task DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default) { try { @@ -299,9 +325,13 @@ public class DownloadService { userAgent = Utils.GetVersion(false); } - var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout, RequestHeaders, AcceptHeader); + var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, RequestHeaders, AcceptHeader, cancellationToken); return result; } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { Logging.SaveLog(_tag, ex); @@ -317,14 +347,14 @@ public class DownloadService /// /// Creates local SOCKS proxy when proxy switch is enabled. /// - private async Task GetWebProxy(bool blProxy) + private async Task GetWebProxy(bool blProxy, CancellationToken cancellationToken = default) { if (!blProxy) { return null; } var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); - if (await SocketCheck(Global.Loopback, port) == false) + if (await SocksPortCheck(Global.Loopback, port, cancellationToken) == false) { return null; } @@ -335,18 +365,64 @@ public class DownloadService /// /// Checks whether the specified TCP endpoint is reachable. /// - private async Task SocketCheck(string ip, int port) + private async Task SocksPortCheck(string ip, int port, CancellationToken cancellationToken = default) { - try + using var rootTimeOutCts = new CancellationTokenSource(Global.LocalFetch); + using var rootCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, rootTimeOutCts.Token); + var rootToken = rootCts.Token; + + // SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth) + ReadOnlyMemory greeting = new byte[] { 0x05, 0x01, 0x00 }; + var buf = new byte[2]; + + while (!rootToken.IsCancellationRequested) { - IPEndPoint point = new(IPAddress.Parse(ip), port); - using Socket? sock = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - await sock.ConnectAsync(point); - return true; - } - catch (Exception) - { - return false; + using var tcp = new TcpClient(); + using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token); + var linkedToken = linkedCts.Token; + try + { + await tcp.ConnectAsync(ip, port, linkedToken); + var stream = tcp.GetStream(); + + await stream.WriteAsync(greeting, linkedToken); + + var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken); + + // Server selection: VER=5, METHOD=0x00 — proxy is fully ready + if (read == 2 && buf[0] == 0x05) + { + return true; + } + } + catch (OperationCanceledException) + { + if (!rootToken.IsCancellationRequested) + { + continue; + } + Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready."); + return false; + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused) + { + // Connection refused, proxy not ready yet, wait 50ms before retrying + try + { + await Task.Delay(50, rootToken); + } + catch (OperationCanceledException) + { + Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready."); + return false; + } + } + catch + { + // Ignore other exceptions and continue + } } + return false; } } diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs index af0a5d04..84d2ecf8 100644 --- a/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -7,63 +7,117 @@ public class SpeedtestService(Config config, Func updateF private static readonly string _tag = "SpeedtestService"; private readonly Config? _config = config; private readonly Func? _updateFunc = updateFunc; - private static readonly ConcurrentBag _lstExitLoop = []; + private readonly Lock _runLock = new(); + private CancellationTokenSource? _runCts; private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize; private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1); - public void RunLoop(ESpeedActionType actionType, List selecteds) + public Task RunLoop(ESpeedActionType actionType, List selecteds, CancellationToken ct = default) { - Task.Run(async () => + CancellationTokenSource runCts; + + lock (_runLock) { - await RunAsync(actionType, selecteds); - await ProfileExManager.Instance.SaveTo(); - await UpdateFunc("", ResUI.SpeedtestingCompleted); - }); + _runCts?.Cancel(); + + runCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + + _runCts = runCts; + } + + return RunLoopAsync(actionType, selecteds, runCts); } public void ExitLoop() { - if (!_lstExitLoop.IsEmpty) + CancellationTokenSource? runCts; + + lock (_runLock) + { + runCts = _runCts; + } + + if (runCts is not null) { _ = UpdateFunc("", ResUI.SpeedtestingStop); - - _lstExitLoop.Clear(); + runCts.Cancel(); } } - private static bool ShouldStopTest(string exitLoopKey) + private async Task RunLoopAsync(ESpeedActionType actionType, List selecteds, CancellationTokenSource runCts) { - return _lstExitLoop.All(p => p != exitLoopKey); - } - - private async Task RunAsync(ESpeedActionType actionType, List selecteds) - { - var exitLoopKey = Utils.GetGuid(false); - _lstExitLoop.Add(exitLoopKey); - - var lstSelected = await GetClearItem(actionType, selecteds); - - switch (actionType) + try { - case ESpeedActionType.Tcping: - await RunTcpingAsync(lstSelected, exitLoopKey); - break; + await RunAsync(actionType, selecteds, runCts.Token); + } + catch (OperationCanceledException) when (runCts.IsCancellationRequested) + { + // Ignored + } + finally + { + try + { + await ProfileExManager.Instance.SaveTo(); + } + finally + { + await UpdateFunc("", ResUI.SpeedtestingCompleted); + } - case ESpeedActionType.Realping: - await RunRealPingBatchAsync(lstSelected, exitLoopKey); - break; + lock (_runLock) + { + if (ReferenceEquals(_runCts, runCts)) + { + _runCts = null; + } + } - case ESpeedActionType.UdpTest: - await RunUdpTestBatchAsync(lstSelected, exitLoopKey); - break; + runCts.Dispose(); + } + } + + private async Task RunAsync(ESpeedActionType actionType, List selecteds, CancellationToken ct = default) + { + var lstSelected = await GetClearItem(actionType, selecteds); + var completedIds = new ConcurrentDictionary(); - case ESpeedActionType.Speedtest: - await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey); - break; + try + { + switch (actionType) + { + case ESpeedActionType.Tcping: + await RunTcpingAsync(lstSelected, completedIds, ct); + break; - case ESpeedActionType.Mixedtest: - await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, true, exitLoopKey); - break; + case ESpeedActionType.Realping: + await RunRealPingBatchAsync(lstSelected, completedIds, 0, ct); + break; + + case ESpeedActionType.UdpTest: + await RunUdpTestBatchAsync(lstSelected, completedIds, 0, ct); + break; + + case ESpeedActionType.Speedtest: + await RunMixedTestAsync(lstSelected, completedIds, 1, true, ct); + break; + + case ESpeedActionType.Mixedtest: + await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, true, + ct); + break; + } + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + _ = UpdateFunc("", ResUI.SpeedtestingStop); + await SetTestResultAsync(lstSelected.Where(it => !completedIds.ContainsKey(it.IndexId)).ToList(), + actionType, ResUI.SpeedtestingSkip).ConfigureAwait(false); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + _ = UpdateFunc("", ex.Message); } } @@ -103,29 +157,7 @@ public class SpeedtestService(Config config, Func updateF } //clear test result - foreach (var it in lstSelected) - { - switch (actionType) - { - case ESpeedActionType.Tcping: - case ESpeedActionType.Realping: - case ESpeedActionType.UdpTest: - await UpdateFunc(it.IndexId, ResUI.Speedtesting, ""); - ProfileExManager.Instance.SetTestDelay(it.IndexId, 0); - break; - - case ESpeedActionType.Speedtest: - await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait); - ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0); - break; - - case ESpeedActionType.Mixedtest: - await UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait); - ProfileExManager.Instance.SetTestDelay(it.IndexId, 0); - ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0); - break; - } - } + await SetTestResultAsync(lstSelected, actionType, ResUI.Speedtesting).ConfigureAwait(false); if (lstSelected.Count > 1 && (actionType == ESpeedActionType.Speedtest || actionType == ESpeedActionType.Mixedtest)) { @@ -135,56 +167,68 @@ public class SpeedtestService(Config config, Func updateF return lstSelected; } - private async Task RunTcpingAsync(List selecteds, string exitLoopKey) + private async Task SetTestResultAsync(List lstSelected, ESpeedActionType actionType, string message) + { + foreach (var it in lstSelected) + { + switch (actionType) + { + case ESpeedActionType.Tcping: + case ESpeedActionType.Realping: + case ESpeedActionType.UdpTest: + await UpdateFunc(it.IndexId, message, ""); + break; + case ESpeedActionType.Speedtest: + await UpdateFunc(it.IndexId, "", message); + break; + case ESpeedActionType.Mixedtest: + await UpdateFunc(it.IndexId, message, message); + break; + } + } + } + + private async Task RunTcpingAsync(List selecteds, + ConcurrentDictionary completedIds, CancellationToken ct = default) { var pageSize = Math.Min(selecteds.Count, _speedTestPageSize); var lstBatch = GetTestBatchItem(selecteds, pageSize); foreach (var lst in lstBatch) { - if (ShouldStopTest(exitLoopKey)) - { - await UpdateFunc("", ResUI.SpeedtestingSkip); - return; - } + ct.ThrowIfCancellationRequested(); - List tasks = []; - - foreach (var it in lst) + var parallelOptions = new ParallelOptions { - if (ShouldStopTest(exitLoopKey)) + CancellationToken = ct, + }; + + await Parallel.ForEachAsync(lst, parallelOptions, async (item, innerCt) => + { + try { - return; + var responseTime = await GetTcpingTime(item.Address, item.Port, innerCt); + + ProfileExManager.Instance.SetTestDelay(item.IndexId, responseTime); + await UpdateFunc(item.IndexId, responseTime.ToString()); + completedIds.TryAdd(item.IndexId, 0); } - - tasks.Add(Task.Run(async () => + catch (OperationCanceledException) when (ct.IsCancellationRequested) { - try - { - var responseTime = await GetTcpingTime(it.Address, it.Port); + throw; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + }); - ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); - await UpdateFunc(it.IndexId, responseTime.ToString()); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - })); - } - - await Task.WhenAll(tasks); - - if (ShouldStopTest(exitLoopKey)) - { - return; - } - - await Task.Delay(_delayInterval); + await Task.Delay(_delayInterval, ct); } } - private async Task RunRealPingBatchAsync(List lstSelected, string exitLoopKey, int pageSize = 0) + private async Task RunRealPingBatchAsync(List lstSelected, + ConcurrentDictionary completedIds, int pageSize = 0, CancellationToken ct = default) { if (pageSize <= 0) { @@ -195,38 +239,35 @@ public class SpeedtestService(Config config, Func updateF List lstFailed = []; foreach (var lst in lstTest) { - var ret = await RunRealPingAsync(lst, exitLoopKey); + var ret = await RunRealPingAsync(lst, completedIds, ct); if (ret == false) { lstFailed.AddRange(lst); } - await Task.Delay(_delayInterval); + await Task.Delay(_delayInterval, ct); } //Retest the failed part var pageSizeNext = pageSize / 2; if (lstFailed.Count > 0 && pageSizeNext > 0) { - if (ShouldStopTest(exitLoopKey)) - { - await UpdateFunc("", ResUI.SpeedtestingSkip); - return; - } + ct.ThrowIfCancellationRequested(); await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount) { - await RunRealPingBatchAsync(lstFailed, exitLoopKey, pageSizeNext); + await RunRealPingBatchAsync(lstFailed, completedIds, pageSizeNext, ct); } else { - await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, false, exitLoopKey); + await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, false, ct); } } } - private async Task RunRealPingAsync(List selecteds, string exitLoopKey) + private async Task RunRealPingAsync(List selecteds, + ConcurrentDictionary completedIds, CancellationToken ct = default) { ProcessService processService = null; try @@ -236,28 +277,39 @@ public class SpeedtestService(Config config, Func updateF { return false; } - await Task.Delay(1000); + await Task.Delay(1000, ct); - List tasks = []; - foreach (var it in selecteds) + var parallelOptions = new ParallelOptions + { + CancellationToken = ct, + }; + + await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) => { if (!it.AllowTest) { await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip); - continue; + completedIds.TryAdd(it.IndexId, 0); + return; } - if (ShouldStopTest(exitLoopKey)) + try { - return false; + await DoRealPing(it, completedIds, innerCt); } - - tasks.Add(Task.Run(async () => + catch (OperationCanceledException) when (ct.IsCancellationRequested) { - await DoRealPing(it); - })); - } - await Task.WhenAll(tasks); + throw; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + }); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; } catch (Exception ex) { @@ -273,7 +325,8 @@ public class SpeedtestService(Config config, Func updateF return true; } - private async Task RunUdpTestBatchAsync(List lstSelected, string exitLoopKey, int pageSize = 0) + private async Task RunUdpTestBatchAsync(List lstSelected, + ConcurrentDictionary completedIds, int pageSize = 0, CancellationToken ct = default) { if (pageSize <= 0) { @@ -284,30 +337,27 @@ public class SpeedtestService(Config config, Func updateF List lstFailed = []; foreach (var lst in lstTest) { - var ret = await RunUdpTestAsync(lst, exitLoopKey); + var ret = await RunUdpTestAsync(lst, completedIds, ct); if (ret == false) { lstFailed.AddRange(lst); } - await Task.Delay(_delayInterval); + await Task.Delay(_delayInterval, ct); } //Retest the failed part if (lstFailed.Count > 0) { - if (ShouldStopTest(exitLoopKey)) - { - await UpdateFunc("", ResUI.SpeedtestingSkip); - return; - } + ct.ThrowIfCancellationRequested(); await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); - await RunUdpTestAsync(lstFailed, exitLoopKey); + await RunUdpTestAsync(lstFailed, completedIds, ct); } } - private async Task RunUdpTestAsync(List selecteds, string exitLoopKey) + private async Task RunUdpTestAsync(List selecteds, + ConcurrentDictionary completedIds, CancellationToken ct = default) { ProcessService processService = null; try @@ -317,27 +367,39 @@ public class SpeedtestService(Config config, Func updateF { return false; } - await Task.Delay(1000); + await Task.Delay(1000, ct); - List tasks = []; - foreach (var it in selecteds) + var parallelOptions = new ParallelOptions + { + CancellationToken = ct, + }; + + await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) => { if (!it.AllowTest) { - continue; + await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip); + completedIds.TryAdd(it.IndexId, 0); + return; } - if (ShouldStopTest(exitLoopKey)) + try { - return false; + await DoUdpTest(it, completedIds, innerCt); } - - tasks.Add(Task.Run(async () => + catch (OperationCanceledException) when (ct.IsCancellationRequested) { - await DoUdpTest(it); - })); - } - await Task.WhenAll(tasks); + throw; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + }); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; } catch (Exception ex) { @@ -353,81 +415,77 @@ public class SpeedtestService(Config config, Func updateF return true; } - private async Task RunMixedTestAsync(List selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey) + private async Task RunMixedTestAsync(List selecteds, + ConcurrentDictionary completedIds, int concurrencyCount, bool blSpeedTest, + CancellationToken ct = default) { - using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount); var downloadHandle = new DownloadService(); - List tasks = []; - foreach (var it in selecteds) + + var parallelOptions = new ParallelOptions { - if (ShouldStopTest(exitLoopKey)) + MaxDegreeOfParallelism = concurrencyCount, + CancellationToken = ct, + }; + + await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) => + { + innerCt.ThrowIfCancellationRequested(); + + ProcessService processService = null; + try { - await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); - continue; + processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it); + if (processService is null) + { + await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); + return; + } + + await Task.Delay(1000, innerCt); + + var delay = await DoRealPing(it, completedIds, innerCt); + if (blSpeedTest) + { + if (delay > 0) + { + await DoSpeedTest(downloadHandle, it, completedIds, innerCt); + } + else + { + await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); + } + } } - await concurrencySemaphore.WaitAsync(); - - tasks.Add(Task.Run(async () => + catch (OperationCanceledException) when (ct.IsCancellationRequested) { - ProcessService processService = null; - try + throw; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + finally + { + if (processService != null) { - processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it); - if (processService is null) - { - await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); - return; - } - - await Task.Delay(1000); - - var delay = await DoRealPing(it); - if (blSpeedTest) - { - if (ShouldStopTest(exitLoopKey)) - { - await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); - return; - } - - if (delay > 0) - { - await DoSpeedTest(downloadHandle, it); - } - else - { - await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); - } - } + await processService.StopAsync(); } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - finally - { - if (processService != null) - { - await processService?.StopAsync(); - } - concurrencySemaphore.Release(); - } - })); - } - await Task.WhenAll(tasks); + } + }); } - private async Task DoRealPing(ServerTestItem it) + private async Task DoRealPing(ServerTestItem it, + ConcurrentDictionary completedIds, CancellationToken ct = default) { var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); - var responseTime = await ConnectionHandler.GetRealPingTime(webProxy); + var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, ct); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); await UpdateFunc(it.IndexId, responseTime.ToString()); if (!_config.UiItem.HideColumnIpInfo && responseTime > 0) { - var ipInfo = await ConnectionHandler.GetIPInfo(webProxy); + var ipInfo = await ConnectionHandler.GetIPInfo(webProxy, ct); var ipStr = ipInfo?.ToString() ?? Global.None; ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr); await UpdateIpInfoFunc(it.IndexId, ipStr); @@ -437,17 +495,22 @@ public class SpeedtestService(Config config, Func updateF await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip); } + completedIds.TryAdd(it.IndexId, 0); return responseTime; } - private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it) + private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it, + ConcurrentDictionary completedIds, CancellationToken ct = default) { await UpdateFunc(it.IndexId, "", ResUI.Speedtesting); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var url = _config.SpeedTestItem.SpeedTestUrl; var timeout = _config.SpeedTestItem.SpeedTestTimeout; - await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, msg) => + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token); + var linkedCt = linkedCts.Token; + await downloadHandle.DownloadDataAsync(url, webProxy, async (success, msg) => { decimal.TryParse(msg, out var dec); if (dec > 0) @@ -455,34 +518,34 @@ public class SpeedtestService(Config config, Func updateF ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec); } await UpdateFunc(it.IndexId, "", msg); - }); + }, linkedCt); + completedIds.TryAdd(it.IndexId, 0); } - private async Task DoUdpTest(ServerTestItem it) + private async Task DoUdpTest(ServerTestItem it, + ConcurrentDictionary completedIds, CancellationToken ct = default) { var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl); - var responseTime = -1; - try - { - responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, TimeSpan.FromSeconds(5))).TotalMilliseconds; - } - catch - { - // ignored - } + var responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, ct)).TotalMilliseconds; ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); await UpdateFunc(it.IndexId, responseTime.ToString()); + completedIds.TryAdd(it.IndexId, 0); return responseTime; } - private async Task GetTcpingTime(string url, int port) + private async Task GetTcpingTime(string? url, int port, CancellationToken ct = default) { var responseTime = -1; + if (url.IsNullOrEmpty() || port <= 0) + { + return responseTime; + } + if (!IPAddress.TryParse(url, out var ipAddress)) { - var ipHostInfo = await Dns.GetHostEntryAsync(url); + var ipHostInfo = await Dns.GetHostEntryAsync(url, ct); ipAddress = ipHostInfo.AddressList.First(); } @@ -492,13 +555,11 @@ public class SpeedtestService(Config config, Func updateF var timer = Stopwatch.StartNew(); try { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - await clientSocket.ConnectAsync(endPoint, cts.Token).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token); + await clientSocket.ConnectAsync(endPoint, linkedCts.Token).ConfigureAwait(false); responseTime = (int)timer.ElapsedMilliseconds; } - catch (OperationCanceledException) - { - } finally { timer.Stop(); diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs index 42eb37ac..a1180f4f 100644 --- a/v2rayN/ServiceLib/Services/UpdateService.cs +++ b/v2rayN/ServiceLib/Services/UpdateService.cs @@ -4,10 +4,9 @@ public partial class UpdateService(Config config, Func updat { 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, bool blProxy = true) + public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default) { var url = string.Empty; var fileName = string.Empty; @@ -39,7 +38,7 @@ public partial class UpdateService(Config config, Func updat url = result.Url!; fileName = Utils.GetTempPath(Utils.GetGuid()); - await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, TimeSpan.FromSeconds(_timeout)); + await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken); } else { @@ -47,7 +46,7 @@ public partial class UpdateService(Config config, Func updat } } - public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true) + public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default) { var url = string.Empty; var fileName = string.Empty; @@ -89,7 +88,7 @@ public partial class UpdateService(Config config, Func updat 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, TimeSpan.FromSeconds(_timeout)); + await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken); } else { @@ -100,7 +99,7 @@ public partial class UpdateService(Config config, Func updat } } - public async Task CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true) + public async Task CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default) { if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type)) { @@ -109,10 +108,10 @@ public partial class UpdateService(Config config, Func updat var downloadHandle = new DownloadService(); var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease); - return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy); + return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy, cancellationToken); } - public async Task> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true) + public async Task> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default) { var msgs = new List(); foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes()) @@ -122,7 +121,7 @@ public partial class UpdateService(Config config, Func updat continue; } - var result = await CheckHasUpdateOnly(type, preRelease, blProxy); + var result = await CheckHasUpdateOnly(type, preRelease, blProxy, cancellationToken); if (result.Success && result.Version != null) { var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version); @@ -137,7 +136,7 @@ public partial class UpdateService(Config config, Func updat return msgs; } - public async Task UpdateGeoFileAll(bool blProxy = true) + public async Task UpdateGeoFileAll(bool blProxy = true, CancellationToken cancellationToken = default) { var requests = new List(); requests.AddRange(GetGeoFilesRequest()); @@ -145,22 +144,22 @@ public partial class UpdateService(Config config, Func updat 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); + 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) + private async Task CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default) { try { - var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy); + var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy, cancellationToken); if (!result.Success || result.Version is null) { return result; } - return await ParseDownloadUrl(type, result); + return await ParseDownloadUrl(type, result, cancellationToken); } catch (Exception ex) { @@ -170,14 +169,14 @@ public partial class UpdateService(Config config, Func updat } } - private async Task GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy) + 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); + var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName, cancellationToken); if (result.IsNullOrEmpty()) { return new UpdateResult(false, ""); @@ -209,7 +208,7 @@ public partial class UpdateService(Config config, Func updat else { var url = Path.Combine(coreInfo.Url, "latest"); - var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy); + var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy, cancellationToken); if (lastUrl == null) { return new UpdateResult(false, ""); @@ -223,7 +222,7 @@ public partial class UpdateService(Config config, Func updat [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) + private async Task GetCoreVersion(ECoreType type, CancellationToken cancellationToken = default) { try { @@ -246,7 +245,7 @@ public partial class UpdateService(Config config, Func updat return new SemanticVersion(""); } - var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg); + var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg, cancellationToken); var echo = result ?? ""; var version = SemVerRegex().Match(echo).Groups["version"].Value; return new SemanticVersion(version); @@ -259,7 +258,7 @@ public partial class UpdateService(Config config, Func updat } } - private async Task ParseDownloadUrl(ECoreType type, UpdateResult result) + private async Task ParseDownloadUrl(ECoreType type, UpdateResult result, CancellationToken cancellationToken = default) { try { @@ -276,7 +275,7 @@ public partial class UpdateService(Config config, Func updat case ECoreType.v2fly_v5: case ECoreType.mihomo: { - curVersion = await GetCoreVersion(type); + curVersion = await GetCoreVersion(type, cancellationToken); message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v")); url = string.Format(coreUrl, version); break; @@ -284,7 +283,7 @@ public partial class UpdateService(Config config, Func updat case ECoreType.sing_box: { - curVersion = await GetCoreVersion(type); + curVersion = await GetCoreVersion(type, cancellationToken); message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v")); url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v")); break; @@ -531,7 +530,7 @@ public partial class UpdateService(Config config, Func updat }; } - private async Task DownloadGeoFiles(List requests, bool blProxy) + private async Task DownloadGeoFiles(List requests, bool blProxy, CancellationToken cancellationToken = default) { var tmpFilePathDict = new Dictionary(); var tmpFileRequests = new List(); @@ -586,7 +585,7 @@ public partial class UpdateService(Config config, Func updat _ = UpdateFunc(false, args.GetException().Message); }; - await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, TimeSpan.FromSeconds(_timeout)); + await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, cancellationToken); } #endregion Geo private