mirror of
https://github.com/2dust/v2rayN.git
synced 2026-09-26 00:22:07 +03:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1cb99cd6e | |||
| 28139853a8 | |||
| 5d9718a0bb | |||
| 740729c847 | |||
| 13cd2ef80d | |||
| be3566df2f | |||
| 6e9ec6cbee | |||
| e6e95e00d7 | |||
| 2fbe81041b | |||
| 05901b5673 | |||
| bdd0ee1620 | |||
| 00d1001def | |||
| 21bafe52e0 | |||
| cef40d38ee | |||
| 4f5a6be12d | |||
| 7674d7e472 |
@@ -146,7 +146,7 @@ jobs:
|
||||
$xrayTag = "v$xrayVer"
|
||||
$singTag = "v$singVer"
|
||||
|
||||
$xrayUrl = "https://github.com/autorepobot/Xray/releases/download/$xrayTag/Xray-windows-32.zip"
|
||||
$xrayUrl = "https://github.com/XTLS/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
|
||||
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
|
||||
|
||||
Write-Host "Bundled Xray version: $xrayVer"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>7.25.1</Version>
|
||||
<Version>7.25.2</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.1.2" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.1.3" />
|
||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
||||
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
|
||||
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
||||
@@ -23,15 +23,15 @@
|
||||
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
|
||||
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
|
||||
<PackageVersion Include="NLog" Version="6.2.0" />
|
||||
<PackageVersion Include="NLog" Version="6.2.1" />
|
||||
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||
<PackageVersion Include="TUnit" Version="1.66.27" />
|
||||
<PackageVersion Include="TUnit" Version="1.68.17" />
|
||||
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
|
||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
||||
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace ServiceLib.Tests.Helper;
|
||||
|
||||
public class RegexGuardTests
|
||||
{
|
||||
[Test]
|
||||
public async Task IsRegexMatch_NormalPattern_ShouldMatch()
|
||||
{
|
||||
await Utils.IsRegexMatch("HK-node-01", "HK|香港").Should().BeTrue();
|
||||
await Utils.IsRegexMatch("JP-node-01", "HK|香港").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsRegexMatch_EmptyPattern_ShouldPassThrough()
|
||||
{
|
||||
await Utils.IsRegexMatch("anything", "").Should().BeTrue();
|
||||
await Utils.IsRegexMatch("anything", null).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsRegexMatch_InvalidPattern_ShouldFailOpen()
|
||||
{
|
||||
await Utils.IsRegexMatch("node-01", "([unclosed").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsRegexMatch_EvilPattern_ShouldTimeoutAndFailOpen()
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var result = Utils.IsRegexMatch(new string('a', 30) + "!", "(a+)+$");
|
||||
sw.Stop();
|
||||
|
||||
await result.Should().BeTrue();
|
||||
await (sw.Elapsed < TimeSpan.FromSeconds(30)).Should().BeTrue().Because(
|
||||
$"evil pattern must be cut off by timeout, took {sw.Elapsed}");
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,21 @@ namespace ServiceLib.UdpTest;
|
||||
|
||||
public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable
|
||||
{
|
||||
private bool _initialized;
|
||||
private IPEndPoint _relayEndPoint;
|
||||
private TcpClient _tcpClient;
|
||||
private UdpClient _udpClient;
|
||||
private IPEndPoint _relayEndPoint;
|
||||
|
||||
private bool _initialized = false;
|
||||
public void Dispose()
|
||||
{
|
||||
_tcpClient?.Dispose();
|
||||
_udpClient?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send UDP data to a remote endpoint (IP address)
|
||||
/// Send UDP data to a remote endpoint (IP address)
|
||||
/// </summary>
|
||||
public async Task SendAsync(IPEndPoint remote, byte[] data)
|
||||
public async Task SendAsync(IPEndPoint remote, byte[] data, CancellationToken ct = default)
|
||||
{
|
||||
var addrData = new Socks5AddressData
|
||||
{
|
||||
@@ -19,19 +24,20 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
? Socks5AddressData.AddrTypeIPv4
|
||||
: Socks5AddressData.AddrTypeIPv6,
|
||||
Host = remote.Address.ToString(),
|
||||
Port = (ushort)remote.Port
|
||||
Port = (ushort)remote.Port,
|
||||
};
|
||||
var packet = BuildSocks5UdpPacket(addrData, data);
|
||||
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
|
||||
await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send UDP data to a remote endpoint (domain name or IP address)
|
||||
/// Send UDP data to a remote endpoint (domain name or IP address)
|
||||
/// </summary>
|
||||
/// <param name="host">Domain name or IP address</param>
|
||||
/// <param name="port">Port number</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();
|
||||
|
||||
@@ -53,11 +59,12 @@ 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receive UDP data from remote endpoint
|
||||
/// Receive UDP data from remote endpoint
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the receive operation</param>
|
||||
/// <returns>Remote endpoint information and received data</returns>
|
||||
@@ -69,16 +76,6 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
return (remote, payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a remote endpoint that can be either an IP address or a domain name
|
||||
/// </summary>
|
||||
public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
|
||||
{
|
||||
public string Host { get; set; } = host;
|
||||
public ushort Port { get; set; } = port;
|
||||
public bool IsDomain { get; set; } = isDomain;
|
||||
}
|
||||
|
||||
private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
@@ -104,6 +101,11 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
throw new ArgumentException("Invalid SOCKS5 UDP packet: too short");
|
||||
}
|
||||
|
||||
if (packet[0] != 0x00 || packet[1] != 0x00)
|
||||
{
|
||||
throw new ArgumentException("Invalid SOCKS5 UDP packet: RSV field must be 0");
|
||||
}
|
||||
|
||||
var offset = 0;
|
||||
|
||||
// RSV (2 bytes) - Reserved field, skip
|
||||
@@ -196,83 +198,34 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
return (remote, data);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
/// <summary>
|
||||
/// Represents a remote endpoint that can be either an IP address or a domain name
|
||||
/// </summary>
|
||||
public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
|
||||
{
|
||||
_tcpClient.Dispose();
|
||||
_udpClient.Dispose();
|
||||
public string Host { get; set; } = host;
|
||||
public ushort Port { get; set; } = port;
|
||||
public bool IsDomain { get; set; } = isDomain;
|
||||
}
|
||||
|
||||
#region SOCKS5 Connection Handling
|
||||
|
||||
private const byte Socks5Version = 0x05;
|
||||
private const byte SocksCmdUdpAssociate = 0x03;
|
||||
|
||||
public async Task<bool> EstablishUdpAssociationAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
Dispose();
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
_udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
|
||||
_tcpClient = new TcpClient();
|
||||
try
|
||||
{
|
||||
await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var tcpControlStream = _tcpClient.GetStream();
|
||||
|
||||
byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
|
||||
await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
|
||||
var handshakeResponse = new byte[2];
|
||||
if (await tcpControlStream.ReadAsync(handshakeResponse, cancellationToken).ConfigureAwait(false) < 2 ||
|
||||
handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var clientAddrForSocks = new Socks5AddressData
|
||||
{
|
||||
AddressType = Socks5AddressData.AddrTypeIPv4,
|
||||
Host = "0.0.0.0",
|
||||
Port = 0
|
||||
};
|
||||
using var udpAssociateReqMs = new MemoryStream();
|
||||
udpAssociateReqMs.WriteByte(Socks5Version);
|
||||
udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
|
||||
udpAssociateReqMs.WriteByte(0x00);
|
||||
udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
|
||||
await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var verRepRsv = new byte[3];
|
||||
if (await tcpControlStream.ReadAsync(verRepRsv, cancellationToken).ConfigureAwait(false) < 3 ||
|
||||
verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var proxyRelaySocksAddr =
|
||||
await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
|
||||
if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
|
||||
_initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion SOCKS5 Connection Handling
|
||||
|
||||
#region SOCKS5 Address Handling
|
||||
|
||||
private static async Task<bool> TryReadExactlyAsync(
|
||||
Stream stream,
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await stream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class Socks5AddressData
|
||||
{
|
||||
public const byte AddrTypeIPv4 = 0x01;
|
||||
@@ -343,7 +296,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
var typeByte = new byte[1];
|
||||
try
|
||||
{
|
||||
if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
|
||||
if (!await TryReadExactlyAsync(stream, typeByte, ct).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -353,7 +306,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
{
|
||||
case AddrTypeIPv4:
|
||||
var ipv4Bytes = new byte[4];
|
||||
if (await stream.ReadAsync(ipv4Bytes.AsMemory(0, 4), ct).ConfigureAwait(false) < 4)
|
||||
if (!await TryReadExactlyAsync(stream, ipv4Bytes, ct).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -363,7 +316,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
|
||||
case AddrTypeDomain:
|
||||
var lenByte = new byte[1];
|
||||
if (await stream.ReadAsync(lenByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
|
||||
if (!await TryReadExactlyAsync(stream, lenByte, ct).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -375,8 +328,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
else
|
||||
{
|
||||
var domainBytes = new byte[lenByte[0]];
|
||||
if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct)
|
||||
.ConfigureAwait(false) < domainBytes.Length)
|
||||
if (!await TryReadExactlyAsync(stream, domainBytes, ct).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -388,7 +340,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
|
||||
case AddrTypeIPv6:
|
||||
var ipv6Bytes = new byte[16];
|
||||
if (await stream.ReadAsync(ipv6Bytes.AsMemory(0, 16), ct).ConfigureAwait(false) < 16)
|
||||
if (!await TryReadExactlyAsync(stream, ipv6Bytes, ct).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -401,7 +353,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
}
|
||||
|
||||
var portBytes = new byte[2];
|
||||
if (await stream.ReadAsync(portBytes.AsMemory(0, 2), ct).ConfigureAwait(false) < 2)
|
||||
if (!await TryReadExactlyAsync(stream, portBytes, ct).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -417,4 +369,73 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
||||
}
|
||||
|
||||
#endregion SOCKS5 Address Handling
|
||||
|
||||
#region SOCKS5 Connection Handling
|
||||
|
||||
private const byte Socks5Version = 0x05;
|
||||
private const byte SocksCmdUdpAssociate = 0x03;
|
||||
|
||||
public async Task<bool> EstablishUdpAssociationAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
Dispose();
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
_udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
|
||||
_tcpClient = new TcpClient();
|
||||
try
|
||||
{
|
||||
await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var tcpControlStream = _tcpClient.GetStream();
|
||||
|
||||
byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
|
||||
await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
|
||||
var handshakeResponse = new byte[2];
|
||||
if (!await TryReadExactlyAsync(tcpControlStream, handshakeResponse, cancellationToken).ConfigureAwait(false) ||
|
||||
handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var clientAddrForSocks = new Socks5AddressData
|
||||
{
|
||||
AddressType = Socks5AddressData.AddrTypeIPv4,
|
||||
Host = "0.0.0.0",
|
||||
Port = 0,
|
||||
};
|
||||
using var udpAssociateReqMs = new MemoryStream();
|
||||
udpAssociateReqMs.WriteByte(Socks5Version);
|
||||
udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
|
||||
udpAssociateReqMs.WriteByte(0x00);
|
||||
udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
|
||||
await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var verRepRsv = new byte[3];
|
||||
if (!await TryReadExactlyAsync(tcpControlStream, verRepRsv, cancellationToken).ConfigureAwait(false) ||
|
||||
verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var proxyRelaySocksAddr =
|
||||
await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
|
||||
if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
|
||||
_initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion SOCKS5 Connection Handling
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ public class DnsService : IUdpTest
|
||||
// Question: www.google.com, Type A, Class IN
|
||||
0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F,
|
||||
0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00,
|
||||
0x00, 0x01, 0x00, 0x01
|
||||
0x00, 0x01, 0x00, 0x01,
|
||||
];
|
||||
|
||||
public byte[] BuildUdpRequestPacket()
|
||||
|
||||
@@ -17,13 +17,13 @@ public class McBeService : IUdpTest
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
|
||||
// Client GUID (random 16 bytes)
|
||||
0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E,
|
||||
0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B
|
||||
0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B,
|
||||
];
|
||||
|
||||
private static readonly byte[] McBeMagicBytes =
|
||||
[
|
||||
0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
|
||||
];
|
||||
|
||||
private static readonly List<string> ValidGameModes =
|
||||
@@ -31,7 +31,7 @@ public class McBeService : IUdpTest
|
||||
"Survival",
|
||||
"Creative",
|
||||
"Adventure",
|
||||
"Spectator"
|
||||
"Spectator",
|
||||
];
|
||||
|
||||
public byte[] BuildUdpRequestPacket()
|
||||
@@ -43,9 +43,9 @@ public class McBeService : IUdpTest
|
||||
{
|
||||
// 0x1c | client alive time in ms (recorded from previous ping) |
|
||||
// server GUID | Magic | string length | Edition
|
||||
//
|
||||
//
|
||||
// Edition Example:
|
||||
//
|
||||
//
|
||||
// MCPE;Dedicated Server;527;1.19.1;0;10;13253860892328930865;Bedrock level;Survival;1;19132;19133;
|
||||
if (mcbeResponseBytes.Length < 48)
|
||||
{
|
||||
@@ -61,6 +61,10 @@ public class McBeService : IUdpTest
|
||||
return false; // Magic bytes do not match
|
||||
}
|
||||
var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]);
|
||||
if (mcbeResponseBytes.Length < 35 + stringLength)
|
||||
{
|
||||
return false; // Not enough data for the string
|
||||
}
|
||||
var stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray());
|
||||
var stringParts = stringData.Split(';');
|
||||
// check Game Mode str
|
||||
|
||||
@@ -31,10 +31,7 @@ public class StunService : IUdpTest
|
||||
if (stunResponseBytes.Length >= 2)
|
||||
{
|
||||
var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1];
|
||||
if (messageType is 0x0101 or 0x0111)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return messageType is 0x0101 or 0x0111;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace ServiceLib.UdpTest;
|
||||
public class UdpTestService
|
||||
{
|
||||
private const string DefaultUdpTestType = "ntp";
|
||||
private readonly IUdpTest _udpTest;
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, Func<IUdpTest>> UdpTestFactories =
|
||||
new Dictionary<string, Func<IUdpTest>>(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -16,6 +15,8 @@ public class UdpTestService
|
||||
["mcbe"] = () => new McBeService(),
|
||||
};
|
||||
|
||||
private readonly IUdpTest _udpTest;
|
||||
|
||||
private UdpTestService(IUdpTest udpTest)
|
||||
{
|
||||
_udpTest = udpTest;
|
||||
@@ -88,24 +89,26 @@ public class UdpTestService
|
||||
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);
|
||||
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.");
|
||||
}
|
||||
|
||||
var (targetHost, targetPort) = ParseHostAndPort(targetServerHost);
|
||||
|
||||
byte[] udpReceiveResult = null;
|
||||
byte[]? validUdpReceiveResult = null;
|
||||
|
||||
// Get minimum round trip time from two attempts
|
||||
var roundTripTime = TimeSpan.MaxValue;
|
||||
@@ -116,11 +119,15 @@ 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;
|
||||
if (!_udpTest.VerifyAndExtractUdpResponse(receiveResult))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
validUdpReceiveResult = receiveResult;
|
||||
|
||||
var currentRoundTripTime = stopwatch.Elapsed;
|
||||
if (currentRoundTripTime < roundTripTime)
|
||||
@@ -128,6 +135,10 @@ public class UdpTestService
|
||||
roundTripTime = currentRoundTripTime;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
|
||||
@@ -137,18 +148,10 @@ public class UdpTestService
|
||||
}
|
||||
}
|
||||
|
||||
if ((udpReceiveResult?.Length ?? 0) < 4 + 1 + 4 + 2)
|
||||
{
|
||||
throw new Exception("Received NTP response is too short.");
|
||||
}
|
||||
|
||||
if (udpReceiveResult != null && _udpTest.VerifyAndExtractUdpResponse(udpReceiveResult))
|
||||
if (validUdpReceiveResult != null)
|
||||
{
|
||||
return roundTripTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Failed to verify and extract UDP response.");
|
||||
}
|
||||
throw new Exception("Failed to verify and extract UDP response.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,13 @@ public class BulkObservableCollection<T> : ObservableCollection<T>
|
||||
{
|
||||
}
|
||||
|
||||
public BulkObservableCollection(IEnumerable<T> collection) : base(collection) { }
|
||||
public BulkObservableCollection(List<T> list) : base(list) { }
|
||||
public BulkObservableCollection(IEnumerable<T> collection) : base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
public BulkObservableCollection(List<T> list) : base(list)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
namespace ServiceLib.Base;
|
||||
|
||||
public class MyReactiveObject : ReactiveObject
|
||||
public class MyReactiveObject : ReactiveObject, IActivatableViewModel
|
||||
{
|
||||
protected static Config? _config;
|
||||
|
||||
public ViewModelActivator Activator { get; } = new();
|
||||
}
|
||||
|
||||
@@ -136,29 +136,23 @@ public static class Extension
|
||||
.Replace("\n", replacement);
|
||||
}
|
||||
|
||||
public static async Task<TOutput> HandleSafe<TInput, TOutput>(
|
||||
this Interaction<TInput, TOutput> interaction,
|
||||
TInput input,
|
||||
public static IObservable<TOutput> HandleSafe<TInput, TOutput>(
|
||||
this Interaction<TInput, TOutput> interaction, TInput input,
|
||||
TOutput defaultValue = default!,
|
||||
[CallerMemberName] string memberName = "",
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerLineNumber] int lineNumber = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await interaction.Handle(input);
|
||||
}
|
||||
catch (UnhandledInteractionException<TInput, TOutput> ex)
|
||||
{
|
||||
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}";
|
||||
Logging.SaveLog(title, ex);
|
||||
return defaultValue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var title = $"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}";
|
||||
Logging.SaveLog(title, ex);
|
||||
return defaultValue;
|
||||
}
|
||||
return Signal.Defer(() => interaction.Handle(input))
|
||||
.Catch<TOutput, UnhandledInteractionException<TInput, TOutput>>(ex =>
|
||||
{
|
||||
Logging.SaveLog($"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}", ex);
|
||||
return Signal.Return(defaultValue);
|
||||
})
|
||||
.Catch<TOutput, Exception>(ex =>
|
||||
{
|
||||
Logging.SaveLog($"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}", ex);
|
||||
return Signal.Return(defaultValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,6 +726,40 @@ public class Utils
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regex match with a timeout guard. Filter patterns can come from user
|
||||
/// input or subscription content while the tested text (remarks, log
|
||||
/// messages) is attacker-influenced, so an evil pattern like (a+)+$
|
||||
/// would otherwise hang the caller (ReDoS). On timeout or invalid
|
||||
/// pattern, fail open (return true) so no node/message is silently
|
||||
/// dropped; the incident is logged.
|
||||
/// </summary>
|
||||
public static bool IsRegexMatch(string? input, string? pattern, int timeoutSeconds = 2)
|
||||
{
|
||||
if (pattern.IsNullOrEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (input.IsNullOrEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
return Regex.IsMatch(input, pattern, RegexOptions.None, TimeSpan.FromSeconds(timeoutSeconds));
|
||||
}
|
||||
catch (RegexMatchTimeoutException ex)
|
||||
{
|
||||
Logging.SaveLog("IsRegexMatch timeout", ex);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Logging.SaveLog("IsRegexMatch invalid pattern", ex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Data Checks
|
||||
|
||||
#region Speed Test
|
||||
@@ -1018,12 +1052,12 @@ public class Utils
|
||||
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
|
||||
{
|
||||
@@ -1040,7 +1074,7 @@ public class Utils
|
||||
}
|
||||
}
|
||||
|
||||
var result = await cmd.ExecuteBufferedAsync();
|
||||
var result = await cmd.ExecuteBufferedAsync(cancellationToken);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result.StandardOutput ?? "";
|
||||
|
||||
@@ -92,10 +92,16 @@ public class Global
|
||||
public const string XrayLocalAsset = "XRAY_LOCATION_ASSET";
|
||||
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
|
||||
public const int SpeedTestPageSize = 1000;
|
||||
public const int SpeedTestConcurrencyCountMin = 10;
|
||||
public const string LinuxBash = "/bin/bash";
|
||||
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-";
|
||||
|
||||
@@ -6,11 +6,6 @@ global using System.Diagnostics;
|
||||
global using System.Net;
|
||||
global using System.Net.NetworkInformation;
|
||||
global using System.Net.Sockets;
|
||||
global using ReactiveUI.Primitives;
|
||||
global using ReactiveUI.Primitives.Concurrency;
|
||||
global using ReactiveUI.Primitives.Extensions;
|
||||
global using ReactiveUI.Primitives.Disposables;
|
||||
global using ReactiveUI.Primitives.Signals;
|
||||
global using System.Reflection;
|
||||
global using System.Runtime.InteropServices;
|
||||
global using System.Runtime.Versioning;
|
||||
@@ -22,6 +17,11 @@ global using System.Text.Json.Nodes;
|
||||
global using System.Text.Json.Serialization;
|
||||
global using System.Text.RegularExpressions;
|
||||
global using ReactiveUI;
|
||||
global using ReactiveUI.Primitives;
|
||||
global using ReactiveUI.Primitives.Concurrency;
|
||||
global using ReactiveUI.Primitives.Disposables;
|
||||
global using ReactiveUI.Primitives.Extensions;
|
||||
global using ReactiveUI.Primitives.Signals;
|
||||
global using ReactiveUI.SourceGenerators;
|
||||
global using ServiceLib.Base;
|
||||
global using ServiceLib.Common;
|
||||
|
||||
@@ -136,10 +136,7 @@ public static class ConfigHandler
|
||||
{
|
||||
config.SpeedTestItem.SpeedPingTestUrl = Global.SpeedPingTestUrls.First();
|
||||
}
|
||||
if (config.SpeedTestItem.MixedConcurrencyCount < 1)
|
||||
{
|
||||
config.SpeedTestItem.MixedConcurrencyCount = 5;
|
||||
}
|
||||
config.SpeedTestItem.MixedConcurrencyCount = Math.Max(config.SpeedTestItem.MixedConcurrencyCount, Global.SpeedTestConcurrencyCountMin);
|
||||
if (config.SpeedTestItem.UdpTestTarget.IsNullOrEmpty())
|
||||
{
|
||||
config.SpeedTestItem.UdpTestTarget = Global.UdpTestTargets.First();
|
||||
@@ -1528,7 +1525,7 @@ public static class ConfigHandler
|
||||
p != null &&
|
||||
p.IsValid() &&
|
||||
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
|
||||
Utils.IsRegexMatch(p.Remarks, extraItem.Filter)
|
||||
)
|
||||
.ToList() ?? [];
|
||||
if (matchedChildProfiles.Count == 0)
|
||||
@@ -1667,7 +1664,7 @@ public static class ConfigHandler
|
||||
//exist sub items //filter
|
||||
if (isSub && subid.IsNotEmpty() && subFilter.IsNotEmpty())
|
||||
{
|
||||
if (!Regex.IsMatch(profileItem.Remarks, subFilter))
|
||||
if (!Utils.IsRegexMatch(profileItem.Remarks, subFilter))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -66,34 +66,41 @@ public static class ConnectionHandler
|
||||
/// <summary>
|
||||
/// Measures response time by sending HTTP requests through proxy.
|
||||
/// </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 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<int> 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
|
||||
/// <summary>
|
||||
/// Gets IP and country information through specified proxy.
|
||||
/// </summary>
|
||||
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy)
|
||||
public static async Task<IpInfoResult?> 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;
|
||||
|
||||
@@ -24,6 +24,24 @@ public class WireguardFmt : BaseFmt
|
||||
|
||||
var query = Utils.ParseQueryString(url.Query);
|
||||
|
||||
var finalmaskDecoded = GetQueryDecoded(query, "fm");
|
||||
if (finalmaskDecoded.IsNotEmpty())
|
||||
{
|
||||
var node = JsonUtils.ParseJson(finalmaskDecoded);
|
||||
item.Finalmask = node != null
|
||||
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
})
|
||||
: finalmaskDecoded;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Finalmask = string.Empty;
|
||||
}
|
||||
|
||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||
{
|
||||
WgPublicKey = GetQueryDecoded(query, "publickey"),
|
||||
@@ -76,6 +94,19 @@ public class WireguardFmt : BaseFmt
|
||||
{
|
||||
dicQuery.Add("dns", Utils.UrlEncode(protoExtra.WgDns));
|
||||
}
|
||||
if (item.Finalmask.IsNotEmpty())
|
||||
{
|
||||
var node = JsonUtils.ParseJson(item.Finalmask);
|
||||
var finalmask = node != null
|
||||
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
})
|
||||
: item.Finalmask;
|
||||
dicQuery.Add("fm", Utils.UrlEncode(finalmask));
|
||||
}
|
||||
return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,14 @@ public class DownloaderHelper
|
||||
private static readonly Lazy<DownloaderHelper> _instance = new(() => new());
|
||||
public static DownloaderHelper Instance => _instance.Value;
|
||||
|
||||
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout,
|
||||
IReadOnlyDictionary<string, string>? requestHeaders = null, string? acceptHeader = null)
|
||||
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent,
|
||||
IReadOnlyDictionary<string, string>? 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<string> progress, int timeout)
|
||||
public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, Action<string> 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<FileDownloadState> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
|
||||
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> 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<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 })
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,14 +28,13 @@ public class CertPemManager
|
||||
/// Get certificate in PEM format from a server with CA pinning validation
|
||||
/// </summary>
|
||||
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
|
||||
List<string>? verifyPeerCertByName = null, int timeout = 4)
|
||||
List<string>? 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
|
||||
/// </summary>
|
||||
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>();
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -119,7 +119,7 @@ public class GroupProfileManager
|
||||
p != null &&
|
||||
p.IsValid() &&
|
||||
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
|
||||
Utils.IsRegexMatch(p.Remarks, extra.Filter)
|
||||
)
|
||||
.ToList() ?? [];
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ public class ClashUIItem
|
||||
public bool EnableMixinContent { get; set; }
|
||||
public int ProxiesSorting { get; set; }
|
||||
public bool ProxiesAutoRefresh { get; set; }
|
||||
public int ProxiesAutoDelayTestInterval { get; set; } = 10;
|
||||
public int ProxiesRefreshInterval { get; set; } = 2;
|
||||
public bool ConnectionsAutoRefresh { get; set; }
|
||||
public int ConnectionsRefreshInterval { get; set; } = 2;
|
||||
public List<ColumnItem> ConnectionsColumnItem { get; set; }
|
||||
|
||||
@@ -3,9 +3,9 @@ namespace ServiceLib.Models.Dto;
|
||||
[Serializable]
|
||||
public partial class ClashProxyModel : ReactiveObject
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public required string Type { get; set; }
|
||||
public string Type { get; set; }
|
||||
|
||||
public string? Now { get; set; }
|
||||
|
||||
|
||||
@@ -1783,7 +1783,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<value>Only Check</value>
|
||||
</data>
|
||||
<data name="MsgNotSupport" xml:space="preserve">
|
||||
<value>Not Support</value>
|
||||
<value>Not Supported</value>
|
||||
</data>
|
||||
<data name="LvTestIpInfo" xml:space="preserve">
|
||||
<value>IP Info</value>
|
||||
|
||||
@@ -693,6 +693,9 @@
|
||||
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
|
||||
<value>Проверить наличие предварительных обновлений</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableUpdateViaProxy" xml:space="preserve">
|
||||
<value>Обновлять через прокси</value>
|
||||
</data>
|
||||
<data name="TbSettingsException" xml:space="preserve">
|
||||
<value>Исключение</value>
|
||||
</data>
|
||||
@@ -1020,6 +1023,15 @@
|
||||
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
|
||||
<value>Протокол Mux для sing-box</value>
|
||||
</data>
|
||||
<data name="TbSettingsMux4RayConcurrency" xml:space="preserve">
|
||||
<value>Лимит дочерних соединений Mux для Xray</value>
|
||||
</data>
|
||||
<data name="TbSettingsMux4RayXudpConcurrency" xml:space="preserve">
|
||||
<value>Лимит дочерних XUDP-соединений Mux для Xray</value>
|
||||
</data>
|
||||
<data name="TbSettingsMux4RayXudpProxyUDP443" xml:space="preserve">
|
||||
<value>Обработка UDP443 (QUIC) в Mux для Xray</value>
|
||||
</data>
|
||||
<data name="TbRoutingRuleProcess" xml:space="preserve">
|
||||
<value>Процесс (Linux/Windows)</value>
|
||||
</data>
|
||||
@@ -1869,4 +1881,22 @@
|
||||
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
|
||||
<value>При включении блокирует DNS-запросы IPv6</value>
|
||||
</data>
|
||||
<data name="TbDNS" xml:space="preserve">
|
||||
<value>DNS</value>
|
||||
</data>
|
||||
<data name="TbSettingsMux4Ray" xml:space="preserve">
|
||||
<value>Настройки Mux для Xray</value>
|
||||
</data>
|
||||
<data name="LvRequestHeaders" xml:space="preserve">
|
||||
<value>HTTP-заголовки (JSON)</value>
|
||||
</data>
|
||||
<data name="SubRequestHeadersTips" xml:space="preserve">
|
||||
<value>Необязательно. Пример: {"X-hwid": "my_device"}. Заданные значения переопределяют заголовки по умолчанию для всех URL этой группы, включая сервис конвертации подписок</value>
|
||||
</data>
|
||||
<data name="SubRequestHeadersInvalid" xml:space="preserve">
|
||||
<value>Некорректные HTTP-заголовки. Используйте JSON-объект с уникальными именами заголовков и строковыми значениями. Имена и значения заголовков не должны содержать переносов строк</value>
|
||||
</data>
|
||||
<data name="TbSortingProcess" xml:space="preserve">
|
||||
<value>Процесс</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -3,13 +3,7 @@
|
||||
"protocol": "vmess",
|
||||
"settings": {
|
||||
"address": "v2ray.cool",
|
||||
"port": 10086,
|
||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||
"security": "auto",
|
||||
"method": "chacha20",
|
||||
"ota": false,
|
||||
"password": "123456",
|
||||
"level": 1
|
||||
"port": 10086
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp"
|
||||
|
||||
@@ -20,24 +20,31 @@ public class DownloadService
|
||||
/// <summary>
|
||||
/// Downloads data with the specified proxy and reports progress messages.
|
||||
/// </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
|
||||
{
|
||||
var progress = new Progress<string>();
|
||||
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
|
||||
/// <summary>
|
||||
/// Downloads a file and reports progress through events.
|
||||
/// </summary>
|
||||
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<FileDownloadRequest> requests, bool blProxy, TimeSpan connectTimeout)
|
||||
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> 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<FileDownloadState> states)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var span = states.Span;
|
||||
var completedCount = 0;
|
||||
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}"));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
@@ -144,12 +161,12 @@ public class DownloadService
|
||||
/// <summary>
|
||||
/// Gets redirect target URL without following redirects automatically.
|
||||
/// </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
|
||||
{
|
||||
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
|
||||
/// <summary>
|
||||
/// Tries to download string content using proxy switch setting.
|
||||
/// </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);
|
||||
return await TryDownloadString(url, webProxy, userAgent);
|
||||
var webProxy = await GetWebProxy(blProxy, cancellationToken);
|
||||
return await TryDownloadString(url, webProxy, userAgent, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to download string content with a specified proxy.
|
||||
/// </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
|
||||
{
|
||||
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
|
||||
/// <summary>
|
||||
/// Downloads string content via HttpClient.
|
||||
/// </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
|
||||
{
|
||||
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
|
||||
/// <summary>
|
||||
/// Downloads string content via DownloaderHelper.
|
||||
/// </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
|
||||
{
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Creates local SOCKS proxy when proxy switch is enabled.
|
||||
/// </summary>
|
||||
private async Task<WebProxy?> GetWebProxy(bool blProxy)
|
||||
private async Task<WebProxy?> 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
|
||||
/// <summary>
|
||||
/// Checks whether the specified TCP endpoint is reachable.
|
||||
/// </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 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,63 +7,117 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
private static readonly string _tag = "SpeedtestService";
|
||||
private readonly Config? _config = config;
|
||||
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 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);
|
||||
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<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 completedIds = new ConcurrentDictionary<string, byte>();
|
||||
|
||||
switch (actionType)
|
||||
try
|
||||
{
|
||||
case ESpeedActionType.Tcping:
|
||||
await RunTcpingAsync(lstSelected, exitLoopKey);
|
||||
break;
|
||||
switch (actionType)
|
||||
{
|
||||
case ESpeedActionType.Tcping:
|
||||
await RunTcpingAsync(lstSelected, completedIds, ct);
|
||||
break;
|
||||
|
||||
case ESpeedActionType.Realping:
|
||||
await RunRealPingBatchAsync(lstSelected, exitLoopKey);
|
||||
break;
|
||||
case ESpeedActionType.Realping:
|
||||
await RunRealPingBatchAsync(lstSelected, completedIds, 0, ct);
|
||||
break;
|
||||
|
||||
case ESpeedActionType.UdpTest:
|
||||
await RunUdpTestBatchAsync(lstSelected, exitLoopKey);
|
||||
break;
|
||||
case ESpeedActionType.UdpTest:
|
||||
await RunUdpTestBatchAsync(lstSelected, completedIds, 0, ct);
|
||||
break;
|
||||
|
||||
case ESpeedActionType.Speedtest:
|
||||
await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey);
|
||||
break;
|
||||
case ESpeedActionType.Speedtest:
|
||||
await RunMixedTestAsync(lstSelected, completedIds, 1, true, ct);
|
||||
break;
|
||||
|
||||
case ESpeedActionType.Mixedtest:
|
||||
await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, true, exitLoopKey);
|
||||
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<SpeedTestResult, Task> 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,70 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
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 lstBatch = GetTestBatchItem(selecteds, pageSize);
|
||||
|
||||
foreach (var lst in lstBatch)
|
||||
{
|
||||
if (ShouldStopTest(exitLoopKey))
|
||||
{
|
||||
await UpdateFunc("", ResUI.SpeedtestingSkip);
|
||||
return;
|
||||
}
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
List<Task> 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<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)
|
||||
{
|
||||
@@ -195,38 +241,35 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
List<ServerTestItem> 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<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;
|
||||
try
|
||||
@@ -236,28 +279,39 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
{
|
||||
return false;
|
||||
}
|
||||
await Task.Delay(1000);
|
||||
await Task.Delay(1000, ct);
|
||||
|
||||
List<Task> 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 +327,8 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
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)
|
||||
{
|
||||
@@ -284,30 +339,27 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
List<ServerTestItem> 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<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;
|
||||
try
|
||||
@@ -317,27 +369,39 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
{
|
||||
return false;
|
||||
}
|
||||
await Task.Delay(1000);
|
||||
await Task.Delay(1000, ct);
|
||||
|
||||
List<Task> 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 +417,77 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
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();
|
||||
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);
|
||||
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<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 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 +497,22 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> 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<string, byte> 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 +520,34 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec);
|
||||
}
|
||||
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 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<int> GetTcpingTime(string url, int port)
|
||||
private async Task<int> 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 +557,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> 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();
|
||||
|
||||
@@ -4,10 +4,9 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
{
|
||||
private readonly Config? _config = config;
|
||||
private readonly Func<bool, string, Task>? _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<bool, string, Task> 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<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 fileName = string.Empty;
|
||||
@@ -89,7 +88,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> 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<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))
|
||||
{
|
||||
@@ -109,10 +108,10 @@ public partial class UpdateService(Config config, Func<bool, string, Task> 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<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>();
|
||||
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
|
||||
@@ -122,7 +121,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> 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<bool, string, Task> updat
|
||||
return msgs;
|
||||
}
|
||||
|
||||
public async Task UpdateGeoFileAll(bool blProxy = true)
|
||||
public async Task UpdateGeoFileAll(bool blProxy = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var requests = new List<FileDownloadRequest>();
|
||||
requests.AddRange(GetGeoFilesRequest());
|
||||
@@ -145,22 +144,22 @@ public partial class UpdateService(Config config, Func<bool, string, Task> 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<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
|
||||
{
|
||||
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<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 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<bool, string, Task> 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<bool, string, Task> updat
|
||||
[GeneratedRegex(@"v?(?<version>\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?)", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex SemVerRegex();
|
||||
|
||||
private async Task<SemanticVersion> GetCoreVersion(ECoreType type)
|
||||
private async Task<SemanticVersion> GetCoreVersion(ECoreType type, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -246,7 +245,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> 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<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
|
||||
{
|
||||
@@ -276,7 +275,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> 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<bool, string, Task> 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<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 tmpFileRequests = new List<FileDownloadRequest>();
|
||||
@@ -586,7 +585,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
_ = UpdateFunc(false, args.GetException().Message);
|
||||
};
|
||||
|
||||
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, TimeSpan.FromSeconds(_timeout));
|
||||
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion Geo private
|
||||
|
||||
@@ -24,12 +24,19 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
|
||||
await ClashConnectionClose(true);
|
||||
});
|
||||
|
||||
_ = Task.Factory.StartNew(
|
||||
async () => await GetClashConnectionsTask(),
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default
|
||||
);
|
||||
this.WhenActivated(disposables =>
|
||||
{
|
||||
var cancelDisposable = new CancellationDisposable();
|
||||
cancelDisposable.DisposeWith(disposables);
|
||||
var token = cancelDisposable.Token;
|
||||
|
||||
Task.Factory.StartNew(
|
||||
async () => await GetClashConnectionsTask(token),
|
||||
token,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
|
||||
@@ -117,29 +124,40 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
|
||||
await GetClashConnections();
|
||||
}
|
||||
|
||||
public async Task GetClashConnectionsTask()
|
||||
public async Task GetClashConnectionsTask(CancellationToken token = default)
|
||||
{
|
||||
var numOfExecuted = 1;
|
||||
while (true)
|
||||
try
|
||||
{
|
||||
await Task.Delay(1000 * 5);
|
||||
numOfExecuted++;
|
||||
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
|
||||
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
|
||||
var numOfExecuted = 1;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
await Task.Delay(1000, token);
|
||||
numOfExecuted++;
|
||||
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
|
||||
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
|
||||
{
|
||||
continue;
|
||||
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
await GetClashConnections();
|
||||
}
|
||||
await GetClashConnections();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog("GetClashConnectionsTask", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +55,19 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
||||
|
||||
#endregion WhenAnyValue && ReactiveCommand
|
||||
|
||||
_ = Task.Factory.StartNew(
|
||||
async () => await GetClashProxiesTask(),
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default
|
||||
);
|
||||
this.WhenActivated(disposables =>
|
||||
{
|
||||
var cancelDisposable = new CancellationDisposable();
|
||||
cancelDisposable.DisposeWith(disposables);
|
||||
var token = cancelDisposable.Token;
|
||||
|
||||
Task.Factory.StartNew(
|
||||
async () => await GetClashProxiesTask(token),
|
||||
token,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
|
||||
@@ -101,27 +108,40 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
||||
|
||||
#region task
|
||||
|
||||
public async Task GetClashProxiesTask()
|
||||
public async Task GetClashProxiesTask(CancellationToken token = default)
|
||||
{
|
||||
var numOfExecuted = 1;
|
||||
while (true)
|
||||
try
|
||||
{
|
||||
await Task.Delay(1000 * 60);
|
||||
numOfExecuted++;
|
||||
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
|
||||
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
|
||||
var numOfExecuted = 1;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
continue;
|
||||
await Task.Delay(1000, token);
|
||||
numOfExecuted++;
|
||||
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
|
||||
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_config.ClashUIItem.ProxiesRefreshInterval <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (numOfExecuted % _config.ClashUIItem.ProxiesRefreshInterval != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
await GetClashProxies();
|
||||
}
|
||||
if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
await GetClashProxies();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog("GetClashProxiesTask", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -680,6 +680,13 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
{
|
||||
SetReloadEnabled(false);
|
||||
|
||||
RxSchedulers.MainThreadScheduler.Schedule(() =>
|
||||
{
|
||||
if (TabMainSelectedIndex < 0)
|
||||
{
|
||||
TabMainSelectedIndex = 0;
|
||||
}
|
||||
});
|
||||
var profileItem = await ConfigHandler.GetDefaultServer(_config);
|
||||
if (profileItem == null)
|
||||
{
|
||||
|
||||
@@ -2,12 +2,11 @@ namespace ServiceLib.ViewModels;
|
||||
|
||||
public partial class MsgViewModel : MyReactiveObject
|
||||
{
|
||||
public Interaction<string, RxVoid> DispatcherShowMsgInteraction { get; } = new();
|
||||
public Interaction<string, RxVoid> ShowMsgInteraction { get; } = new();
|
||||
|
||||
private readonly ConcurrentQueue<string> _queueMsg = new();
|
||||
private volatile bool _lastMsgFilterNotAvailable;
|
||||
private int _showLock = 0; // 0 = unlocked, 1 = locked
|
||||
public int NumMaxMsg { get; } = 500;
|
||||
public int NumMaxMsg => 500;
|
||||
|
||||
[Reactive]
|
||||
public partial string MsgFilter { get; set; }
|
||||
@@ -30,59 +29,39 @@ public partial class MsgViewModel : MyReactiveObject
|
||||
|
||||
AppEvents.SendMsgViewRequested
|
||||
.AsObservable()
|
||||
//.ObserveOn(RxSchedulers.MainThreadScheduler)
|
||||
.Subscribe(content => _ = AppendQueueMsg(content));
|
||||
.Subscribe(EnqueueQueueMsg);
|
||||
|
||||
this.WhenActivated(disposables =>
|
||||
{
|
||||
Signal.Every(TimeSpan.FromSeconds(1))
|
||||
.Where(_ => AutoRefresh && AppManager.Instance.ShowInTaskbar)
|
||||
.ObserveOn(RxSchedulers.MainThreadScheduler)
|
||||
.Subscribe(_ => FlushQueueToView())
|
||||
.DisposeWith(disposables);
|
||||
});
|
||||
}
|
||||
|
||||
public void FlushQueueMsg()
|
||||
private void FlushQueueToView()
|
||||
{
|
||||
_ = AppendQueueMsg(string.Empty);
|
||||
}
|
||||
|
||||
private async Task AppendQueueMsg(string msg)
|
||||
{
|
||||
if (AutoRefresh == false)
|
||||
if (!AutoRefresh || _queueMsg.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnqueueQueueMsg(msg);
|
||||
|
||||
if (!AppManager.Instance.ShowInTaskbar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0)
|
||||
var sb = new StringBuilder();
|
||||
while (_queueMsg.TryDequeue(out var msg))
|
||||
{
|
||||
return;
|
||||
sb.Append(msg);
|
||||
}
|
||||
|
||||
try
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
while (_queueMsg.TryDequeue(out var line))
|
||||
{
|
||||
sb.Append(line);
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DispatcherShowMsgInteraction.Handle(sb.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
_queueMsg.Enqueue(sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _showLock, 0);
|
||||
ShowMsgInteraction.HandleSafe(sb.ToString()).Subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +77,7 @@ public partial class MsgViewModel : MyReactiveObject
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Regex.IsMatch(msg, MsgFilter))
|
||||
if (!Utils.IsRegexMatch(msg, MsgFilter))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -110,11 +89,11 @@ public partial class MsgViewModel : MyReactiveObject
|
||||
}
|
||||
}
|
||||
|
||||
EnqueueWithLimit(msg);
|
||||
if (!msg.EndsWith(Environment.NewLine))
|
||||
{
|
||||
EnqueueWithLimit(Environment.NewLine);
|
||||
}
|
||||
var formattedMsg = msg.EndsWith(Environment.NewLine)
|
||||
? msg
|
||||
: msg + Environment.NewLine;
|
||||
|
||||
EnqueueWithLimit(formattedMsg);
|
||||
}
|
||||
|
||||
private void EnqueueWithLimit(string item)
|
||||
|
||||
@@ -16,15 +16,12 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
|
||||
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
|
||||
|
||||
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction =>
|
||||
ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
|
||||
{
|
||||
var msg = interaction.Input;
|
||||
Dispatcher.UIThread.Post(() => ShowMsg(msg),
|
||||
DispatcherPriority.ApplicationIdle);
|
||||
ShowMsg(msg);
|
||||
interaction.SetOutput(RxVoid.Default);
|
||||
}).DisposeWith(disposables);
|
||||
|
||||
ViewModel?.FlushQueueMsg();
|
||||
});
|
||||
|
||||
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
|
||||
|
||||
@@ -47,7 +47,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
|
||||
cmbCoreType7.ItemsSource = Global.CoreTypes;
|
||||
cmbCoreType9.ItemsSource = Global.CoreTypes;
|
||||
|
||||
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList();
|
||||
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(Global.SpeedTestConcurrencyCountMin, 20).ToList();
|
||||
cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
|
||||
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
|
||||
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
|
||||
|
||||
@@ -11,17 +11,12 @@ public partial class MsgView
|
||||
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
|
||||
|
||||
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction =>
|
||||
ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
|
||||
{
|
||||
var msg = interaction.Input;
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowMsg(msg);
|
||||
}, DispatcherPriority.ApplicationIdle);
|
||||
ShowMsg(msg);
|
||||
interaction.SetOutput(RxVoid.Default);
|
||||
}).DisposeWith(disposables);
|
||||
|
||||
ViewModel?.FlushQueueMsg();
|
||||
});
|
||||
|
||||
btnCopy.Click += menuMsgViewCopyAll_Click;
|
||||
|
||||
@@ -43,7 +43,7 @@ public partial class OptionSettingWindow
|
||||
cmbCoreType7.ItemsSource = Global.CoreTypes;
|
||||
cmbCoreType9.ItemsSource = Global.CoreTypes;
|
||||
|
||||
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList();
|
||||
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(Global.SpeedTestConcurrencyCountMin, 20).ToList();
|
||||
cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
|
||||
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
|
||||
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
|
||||
|
||||
Reference in New Issue
Block a user