From 21bafe52e0b8e08b53cd74cf3a3e04dc159ec33e Mon Sep 17 00:00:00 2001
From: DHR60 <192860629+DHR60@users.noreply.github.com>
Date: Sat, 19 Sep 2026 02:41:40 +0000
Subject: [PATCH] Optimize download and speedtest (#10147)
* Optimize RunMixedTestAsync
* Use stopCts instead of exitLoopKey
* Refactor
---
v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs | 10 +-
v2rayN/ServiceLib.UdpTest/UdpTestService.cs | 13 +-
v2rayN/ServiceLib/Common/Utils.cs | 8 +-
v2rayN/ServiceLib/Global.cs | 5 +
.../ServiceLib/Handler/ConnectionHandler.cs | 27 +-
v2rayN/ServiceLib/Helper/DownloaderHelper.cs | 97 ++--
v2rayN/ServiceLib/Manager/CertPemManager.cs | 18 +-
v2rayN/ServiceLib/Manager/CoreManager.cs | 4 +-
v2rayN/ServiceLib/Services/DownloadService.cs | 172 ++++--
.../ServiceLib/Services/SpeedtestService.cs | 493 ++++++++++--------
v2rayN/ServiceLib/Services/UpdateService.cs | 47 +-
11 files changed, 520 insertions(+), 374 deletions(-)
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