Optimize download and speedtest (#10147)

* Optimize RunMixedTestAsync

* Use stopCts instead of exitLoopKey

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