Compare commits

...

37 Commits

Author SHA1 Message Date
2dust d433aa3b04 Fix
https://github.com/2dust/v2rayN/issues/10236
2026-09-26 11:43:22 +08:00
dependabot[bot] 91a7ed65d5 Bump ReactiveUI from 24.2.0 to 24.3.0 (#10231)
---
updated-dependencies:
- dependency-name: ReactiveUI
  dependency-version: 24.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-26 11:12:07 +08:00
Miheichev Aleksandr Sergeevich 984e685ab3 chore(deps): update Downloader to 5.9.8 (#10222)
Downloader 5.9.6 -> 5.9.8

5.9.7 adds RemoteFileInfo.ContentType. 5.9.8 fixes stop/dispose races:
a cancelled multi-chunk download could report a NullReferenceException
as its error, and a Dispose racing the completion signal could swallow
DownloadFileCompleted. Nothing DownloaderHelper calls changed its API.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 11:11:20 +08:00
DHR60 29f99ceacc Fix speed test (#10219) 2026-09-26 11:10:30 +08:00
DHR60 93d8174dbe Fix async (#10218)
* Fix async

* Fix async for statistics

* Try fix pac async

* Fix
2026-09-26 11:08:36 +08:00
DHR60 fa1e201c76 Fix sing-box dns (#10234) 2026-09-26 10:39:29 +08:00
dependabot[bot] e1cb99cd6e Bump Avalonia.Desktop from 12.1.2 to 12.1.3 (#10216)
---
updated-dependencies:
- dependency-name: Avalonia.Desktop
  dependency-version: 12.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-23 11:03:05 +08:00
2dust 28139853a8 up 7.25.2
https://github.com/2dust/v2rayN/commit/5d9718a0bb93fe4bfa6d8a9a8f985e4f16642ba1
2026-09-20 10:31:00 +08:00
2dust 5d9718a0bb Fix (#10203)
up 7.25.2
2026-09-20 10:17:59 +08:00
2dust 740729c847 Raise speed test concurrency minimum 2026-09-20 10:11:12 +08:00
2dust 13cd2ef80d Update Directory.Packages.props 2026-09-20 09:43:16 +08:00
DHR60 be3566df2f Add finalMask parsing to Wireguard configuration handling (#10201) 2026-09-19 17:38:51 +08:00
DHR60 6e9ec6cbee Fix (#10200) 2026-09-19 14:49:01 +08:00
DHR60 e6e95e00d7 Fix udptest (#10199)
* Fix

* Reformat

* Fix tcp read

* Fix verify
2026-09-19 14:42:51 +08:00
dependabot[bot] 2fbe81041b Bump TUnit from 1.66.27 to 1.68.4 (#10187)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.68.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-19 10:55:48 +08:00
sdhfsl 05901b5673 fix(security): guard subscription/message regex filters against ReDoS (#10185)
Filter patterns (subscription subFilter, policy-group Filter, message
MsgFilter) accept arbitrary user input while the tested text (remarks
from subscriptions, log lines) is attacker-influenced. Regex.IsMatch
without timeout hangs on evil patterns like (a+)+$ - a malicious
subscription can freeze the UI/log pipeline on every update.

Add Utils.IsRegexMatch with a 2s timeout; fail open (match) with a log
so no node or message is silently dropped. Apply to all four call
sites.

Co-authored-by: sdhfsl <sdhfsl@users.noreply.github.com>
2026-09-19 10:54:23 +08:00
Delyan Lev. Neimand bdd0ee1620 Resx: fix English typo (#10164)
Fix English: Not Support -> Not Supported
2026-09-19 10:42:44 +08:00
Miheichev Aleksandr Sergeevich 00d1001def i18n(ru): translate new strings for Mux, request headers and updates (#10160)
Add the 10 Russian strings missing from ResUI.ru.resx: the
update-via-proxy toggle in Check Update, the WireGuard DNS label, the
Xray Mux settings row and its concurrency, XUDP concurrency and
UDP/443 (QUIC) handling fields, the subscription HTTP headers label,
tip and validation message, and the Process column in Connections.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 10:42:05 +08:00
DHR60 21bafe52e0 Optimize download and speedtest (#10147)
* Optimize RunMixedTestAsync

* Use stopCts instead of exitLoopKey

* Refactor
2026-09-19 10:41:40 +08:00
DHR60 cef40d38ee Optimize ViewModel activation lifecycle (#10146)
* Optimize ViewModel activation lifecycle

* Fix

* Fix

* Fix

* Set period to 1s
2026-09-12 17:48:27 +08:00
2dust 4f5a6be12d Update build-windows-x86.yml 2026-09-11 20:06:06 +08:00
DHR60 7674d7e472 Delete redundant fields (#10144) 2026-09-10 13:52:28 +08:00
2dust 77c462ec7b up 7.25.1 2026-09-10 09:43:13 +08:00
2dust 526cd6834d Show language names in theme settings 2026-09-10 09:40:16 +08:00
Jamal Ali 7be23d7ebc Add Azerbaijani (az) UI localization (#10141) 2026-09-10 09:18:00 +08:00
tt2563 d952c180c9 Update ResUI.zh-Hant.resx (#10139)
Update Traditional Chinese translation
2026-09-10 09:15:00 +08:00
2dust 80fa6575a3 Bug fix 2026-09-09 14:33:31 +08:00
jryx0 f05e44d7d9 优化策略组选择 (#10136)
Co-authored-by: 张皓 <hao.zhang@gmail.com>
2026-09-09 14:30:21 +08:00
稔君 b10c16a94a Add custom HTTP headers for subscription updates (#10118)
* Add custom HTTP headers for subscription updates

* Res

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-09-09 14:24:05 +08:00
DHR60 143ada1358 Remove vnext (#10132) 2026-09-09 13:59:52 +08:00
2dust d8c7bd9d02 Bug fix
https://github.com/2dust/v2rayN/issues/10107
2026-09-09 13:55:31 +08:00
dependabot[bot] 85666af36a Bump ReactiveUI.Avalonia from 12.1.1 to 12.1.2 (#10133)
---
updated-dependencies:
- dependency-name: ReactiveUI.Avalonia
  dependency-version: 12.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 09:22:47 +08:00
DHR60 7286c67a78 Display more information for Connections (#10116)
* Display sniff host

* Display process path
2026-09-09 09:22:30 +08:00
2dust 8b8e579bc8 Bug fix
https://github.com/2dust/v2rayN/issues/10113
2026-09-08 20:32:26 +08:00
dependabot[bot] ed4dd7f6d9 Bump TUnit from 1.65.68 to 1.66.27 (#10127)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.66.27
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 19:59:09 +08:00
CrayzyCray bb62c9eda5 Enable auto decompression for string downloads (#10122) 2026-09-08 19:58:55 +08:00
2dust 3d50d9f82c Use profile IDs for server move operations
Refactor `ConfigHandler.MoveServer` to accept a `List<string>` of profile `IndexId`s instead of full `ProfileItem` objects. In `ProfilesViewModel`, remove the duplicated `_lstProfile` cache and build move lists directly from `ProfileItems`, so move and drag-reorder actions use the live UI order and avoid stale profile snapshots.
2026-09-06 10:31:45 +08:00
79 changed files with 3797 additions and 1138 deletions
+1 -1
View File
@@ -146,7 +146,7 @@ jobs:
$xrayTag = "v$xrayVer" $xrayTag = "v$xrayVer"
$singTag = "v$singVer" $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" $singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
Write-Host "Bundled Xray version: $xrayVer" Write-Host "Bundled Xray version: $xrayVer"
+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.25.0</Version> <Version>7.25.2</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
+6 -6
View File
@@ -7,27 +7,27 @@
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" /> <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="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" /> <PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.1" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.2" />
<PackageVersion Include="CliWrap" Version="3.10.5" /> <PackageVersion Include="CliWrap" Version="3.10.5" />
<PackageVersion Include="Downloader" Version="5.9.6" /> <PackageVersion Include="Downloader" Version="5.9.8" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" /> <PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="24.2.0" /> <PackageVersion Include="ReactiveUI" Version="24.3.0" />
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" /> <PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
<PackageVersion Include="ReactiveUI.WPF" Version="24.2.0" /> <PackageVersion Include="ReactiveUI.WPF" Version="24.2.0" />
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" /> <PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" /> <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="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" /> <PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="TUnit" Version="1.65.68" /> <PackageVersion Include="TUnit" Version="1.68.17" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" /> <PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" /> <PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" /> <PackageVersion Include="YamlDotNet" Version="18.1.0" />
@@ -50,8 +50,6 @@ public class CoreConfigV2rayServiceTests
await headers["Set-Cookie"]!.AsArray() await headers["Set-Cookie"]!.AsArray()
.Select(item => item!.GetValue<string>()) .Select(item => item!.GetValue<string>())
.Should().BeEquivalentTo(["a=1", "b=2"]); .Should().BeEquivalentTo(["a=1", "b=2"]);
await outbound.settings.servers.Should().BeNull();
await outbound.settings.vnext.Should().BeNull();
} }
[Test] [Test]
@@ -714,6 +712,5 @@ public class CoreConfigV2rayServiceTests
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag); var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
await proxyOutbound.Should().NotBeNull(); await proxyOutbound.Should().NotBeNull();
await proxyOutbound!.protocol.Should().BeEqualTo("shadowsocks"); await proxyOutbound!.protocol.Should().BeEqualTo("shadowsocks");
await proxyOutbound.settings.servers.Should().NotBeNull();
} }
} }
@@ -0,0 +1,77 @@
namespace ServiceLib.Tests.Helper;
public class HttpRequestHeadersHelperTests
{
[Test]
public async Task TryParse_ShouldAcceptEmptySettingsForExistingSubscriptions()
{
foreach (var json in new string?[] { null, "", " \r\n ", "{}" })
{
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
await headers.Count.Should().BeEqualTo(0);
}
}
[Test]
public async Task TryParse_ShouldPreserveValuesAndUseCaseInsensitiveNames()
{
const string json = """
{
"X-hwid": "my_test_device",
"Authorization": "Bearer test:token",
"accept": "application/json",
"Content-Type": "application/json",
"X-Empty": ""
}
""";
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
await headers["x-HWID"].Should().BeEqualTo("my_test_device");
await headers["AUTHORIZATION"].Should().BeEqualTo("Bearer test:token");
await headers["Accept"].Should().BeEqualTo("application/json");
await headers["Content-Type"].Should().BeEqualTo("application/json");
await headers["X-Empty"].Should().BeEqualTo("");
}
[Test]
[Arguments("not-json")]
[Arguments("null")]
[Arguments("[]")]
[Arguments("{\"X-Test\": 1}")]
[Arguments("{\"X-Test\": null}")]
[Arguments("{\"X-Test\": [\"one\", \"two\"]}")]
[Arguments("{\"X-Test\": \"one\", \"X-Test\": \"two\"}")]
[Arguments("{\"Accept\": \"one\", \"accept\": \"two\"}")]
[Arguments("{\"Bad Header\": \"value\"}")]
[Arguments("{\"Bad:Header\": \"value\"}")]
[Arguments("{\"\": \"value\"}")]
[Arguments("{\"X-Test\": \"one\\r\\nInjected: two\"}")]
[Arguments("{\"X-Test\": \"one\\nInjected: two\"}")]
[Arguments("{\"X-Test\": \"one\\u0000two\"}")]
public async Task TryParse_ShouldRejectInvalidHeadersWithoutReturningPartialSettings(string json)
{
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeFalse();
await headers.Count.Should().BeEqualTo(0);
}
[Test]
public async Task RequestHeaders_ShouldSurviveDatabaseMigrationAndEditing()
{
using var database = new SQLiteConnection(":memory:", false);
database.Execute("CREATE TABLE SubItem (Id TEXT PRIMARY KEY, Remarks TEXT, Url TEXT)");
database.Execute("INSERT INTO SubItem (Id, Remarks, Url) VALUES (?, ?, ?)", "existing", "Existing", "https://example.com/sub");
database.CreateTable<SubItem>();
var item = database.Find<SubItem>("existing");
await HttpRequestHeadersHelper.TryParse(item.RequestHeaders, out var oldHeaders).Should().BeTrue();
await oldHeaders.Count.Should().BeEqualTo(0);
item.RequestHeaders = "{\"X-hwid\":\"my_device\"}";
database.Update(item);
await database.Find<SubItem>(item.Id).RequestHeaders.Should().BeEqualTo(item.RequestHeaders);
item.RequestHeaders = "";
database.Update(item);
await database.Find<SubItem>(item.Id).RequestHeaders.Should().BeEqualTo("");
}
}
@@ -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}");
}
}
@@ -0,0 +1,159 @@
namespace ServiceLib.Tests.Services;
public class DownloadServiceHeadersTests
{
[Test]
[Arguments(false, false)]
[Arguments(false, true)]
[Arguments(true, false)]
[Arguments(true, true)]
public async Task TryDownloadString_ShouldSendCustomHeadersThroughBothDownloaders(bool useProxy, bool failFirstRequest)
{
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
await using var server = new SubscriptionHttpServer(failFirstRequest);
const string json = """
{
"accept": "application/json",
"user-agent": "CustomSubscriptionClient/1.0",
"authorization": "Bearer test-token",
"X-hwid": "test-device",
"Cookie": "session=test",
"Content-Type": "application/json"
}
""";
await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue();
var service = new DownloadService { AcceptHeader = "*/*", RequestHeaders = headers };
var uri = new UriBuilder(useProxy ? "http://subscription.invalid/sub" : server.Url)
{
UserName = "user",
Password = "password"
}.Uri;
IWebProxy? proxy = useProxy ? new WebProxy(server.Url) : null;
var content = await service.TryDownloadString(uri.AbsoluteUri, proxy, "OriginalClient/1.0").WaitAsync(TimeSpan.FromSeconds(20));
await content.Should().BeEqualTo(SubscriptionHttpServer.Body);
await (server.Requests.Count >= (failFirstRequest ? 2 : 1)).Should().BeTrue();
foreach (var request in server.Requests)
{
await request["Accept"].Should().BeEqualTo("application/json");
await request["User-Agent"].Should().BeEqualTo("CustomSubscriptionClient/1.0");
await request["Authorization"].Should().BeEqualTo("Bearer test-token");
await request["X-hwid"].Should().BeEqualTo("test-device");
await request["Cookie"].Should().BeEqualTo("session=test");
await request["Content-Type"].Should().BeEqualTo("application/json");
}
}
[Test]
[Arguments(false)]
[Arguments(true)]
public async Task TryDownloadString_ShouldKeepDefaultAcceptUserAgentAndBasicAuth(bool failFirstRequest)
{
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
await using var server = new SubscriptionHttpServer(failFirstRequest);
var service = new DownloadService { AcceptHeader = "*/*" };
var uri = new UriBuilder(server.Url) { UserName = "user", Password = "password" }.Uri;
var content = await service.TryDownloadString(uri.AbsoluteUri, (IWebProxy?)null, "ExistingClient/1.0").WaitAsync(TimeSpan.FromSeconds(20));
await content.Should().BeEqualTo(SubscriptionHttpServer.Body);
foreach (var request in server.Requests)
{
await request["Accept"].Should().BeEqualTo("*/*");
await request["User-Agent"].Should().BeEqualTo("ExistingClient/1.0");
await request["Authorization"].Should().BeEqualTo("Basic dXNlcjpwYXNzd29yZA==");
await request.ContainsKey("X-hwid").Should().BeFalse();
}
}
[Test]
public async Task TryDownloadString_ShouldNotShareHeadersWithOtherDownloads()
{
await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() });
await using var server = new SubscriptionHttpServer();
var subscription = new DownloadService
{
AcceptHeader = "*/*",
RequestHeaders = new Dictionary<string, string> { ["X-hwid"] = "first-device" }
};
var ordinaryDownload = new DownloadService();
await (await subscription.TryDownloadString(server.Url, (IWebProxy?)null, "TestClient/1.0")).Should().BeEqualTo(SubscriptionHttpServer.Body);
await (await ordinaryDownload.TryDownloadString(server.Url, (IWebProxy?)null, "TestClient/1.0")).Should().BeEqualTo(SubscriptionHttpServer.Body);
var requests = server.Requests.ToArray();
await requests.Length.Should().BeEqualTo(2);
await requests[0]["X-hwid"].Should().BeEqualTo("first-device");
await requests[1].ContainsKey("X-hwid").Should().BeFalse();
await requests[1].ContainsKey("Accept").Should().BeFalse();
}
private sealed class SubscriptionHttpServer : IAsyncDisposable
{
public const string Body = "subscription-test-content";
private readonly TcpListener _listener = new(IPAddress.Loopback, 0);
private readonly CancellationTokenSource _cancellation = new();
private readonly Task _serverTask;
private readonly bool _failFirstRequest;
public string Url { get; }
public ConcurrentQueue<Dictionary<string, string>> Requests { get; } = new();
public SubscriptionHttpServer(bool failFirstRequest = false)
{
_failFirstRequest = failFirstRequest;
_listener.Start();
Url = $"http://127.0.0.1:{((IPEndPoint)_listener.LocalEndpoint).Port}/subscription";
_serverTask = ServeAsync();
}
private async Task ServeAsync()
{
var cancellationToken = _cancellation.Token;
while (!cancellationToken.IsCancellationRequested)
{
using var client = await _listener.AcceptTcpClientAsync(cancellationToken);
await using var stream = client.GetStream();
using var reader = new StreamReader(stream, Encoding.ASCII, leaveOpen: true);
var requestLine = await reader.ReadLineAsync(cancellationToken);
if (requestLine == null)
{
continue;
}
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
while (await reader.ReadLineAsync(cancellationToken) is { Length: > 0 } line)
{
var separator = line.IndexOf(':');
var name = line.Substring(0, separator);
var value = line.Substring(separator + 1).Trim();
headers[name] = headers.TryGetValue(name, out var previous) ? $"{previous}, {value}" : value;
}
Requests.Enqueue(headers);
var status = _failFirstRequest && Requests.Count == 1 ? "503 Service Unavailable" : "200 OK";
var body = requestLine.StartsWith("HEAD ") ? "" : Body;
var response = $"HTTP/1.1 {status}\r\nContent-Length: {Body.Length}\r\nConnection: close\r\n\r\n{body}";
await stream.WriteAsync(Encoding.ASCII.GetBytes(response), cancellationToken);
}
}
public async ValueTask DisposeAsync()
{
await _cancellation.CancelAsync();
_listener.Stop();
try
{
await _serverTask;
}
catch (OperationCanceledException)
{
}
finally
{
_cancellation.Dispose();
}
}
}
}
+117 -96
View File
@@ -2,16 +2,21 @@ namespace ServiceLib.UdpTest;
public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable
{ {
private bool _initialized;
private IPEndPoint _relayEndPoint;
private TcpClient _tcpClient; private TcpClient _tcpClient;
private UdpClient _udpClient; private UdpClient _udpClient;
private IPEndPoint _relayEndPoint;
private bool _initialized = false; public void Dispose()
{
_tcpClient?.Dispose();
_udpClient?.Dispose();
}
/// <summary> /// <summary>
/// Send UDP data to a remote endpoint (IP address) /// Send UDP data to a remote endpoint (IP address)
/// </summary> /// </summary>
public async Task SendAsync(IPEndPoint remote, byte[] data) public async Task SendAsync(IPEndPoint remote, byte[] data, CancellationToken ct = default)
{ {
var addrData = new Socks5AddressData var addrData = new Socks5AddressData
{ {
@@ -19,10 +24,10 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
? Socks5AddressData.AddrTypeIPv4 ? Socks5AddressData.AddrTypeIPv4
: Socks5AddressData.AddrTypeIPv6, : Socks5AddressData.AddrTypeIPv6,
Host = remote.Address.ToString(), Host = remote.Address.ToString(),
Port = (ushort)remote.Port Port = (ushort)remote.Port,
}; };
var packet = BuildSocks5UdpPacket(addrData, data); var packet = BuildSocks5UdpPacket(addrData, data);
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint); await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
} }
/// <summary> /// <summary>
@@ -31,7 +36,8 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
/// <param name="host">Domain name or IP address</param> /// <param name="host">Domain name or IP address</param>
/// <param name="port">Port number</param> /// <param name="port">Port number</param>
/// <param name="data">Data to send</param> /// <param name="data">Data to send</param>
public async Task SendAsync(string host, ushort port, byte[] data) /// <param name="ct">Cancellation token</param>
public async Task SendAsync(string host, ushort port, byte[] data, CancellationToken ct = default)
{ {
var addrData = new Socks5AddressData(); var addrData = new Socks5AddressData();
@@ -53,7 +59,8 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
addrData.Port = port; addrData.Port = port;
var packet = BuildSocks5UdpPacket(addrData, data); var packet = BuildSocks5UdpPacket(addrData, data);
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint); // await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
} }
/// <summary> /// <summary>
@@ -69,16 +76,6 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
return (remote, payload); 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) private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data)
{ {
using var ms = new MemoryStream(); 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"); 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; var offset = 0;
// RSV (2 bytes) - Reserved field, skip // RSV (2 bytes) - Reserved field, skip
@@ -196,83 +198,34 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
return (remote, data); 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(); public string Host { get; set; } = host;
_udpClient.Dispose(); 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 #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 private class Socks5AddressData
{ {
public const byte AddrTypeIPv4 = 0x01; public const byte AddrTypeIPv4 = 0x01;
@@ -343,7 +296,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
var typeByte = new byte[1]; var typeByte = new byte[1];
try try
{ {
if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1) if (!await TryReadExactlyAsync(stream, typeByte, ct).ConfigureAwait(false))
{ {
return null; return null;
} }
@@ -353,7 +306,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
{ {
case AddrTypeIPv4: case AddrTypeIPv4:
var ipv4Bytes = new byte[4]; 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; return null;
} }
@@ -363,7 +316,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
case AddrTypeDomain: case AddrTypeDomain:
var lenByte = new byte[1]; 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; return null;
} }
@@ -375,8 +328,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
else else
{ {
var domainBytes = new byte[lenByte[0]]; var domainBytes = new byte[lenByte[0]];
if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct) if (!await TryReadExactlyAsync(stream, domainBytes, ct).ConfigureAwait(false))
.ConfigureAwait(false) < domainBytes.Length)
{ {
return null; return null;
} }
@@ -388,7 +340,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
case AddrTypeIPv6: case AddrTypeIPv6:
var ipv6Bytes = new byte[16]; 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; return null;
} }
@@ -401,7 +353,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
} }
var portBytes = new byte[2]; 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; return null;
} }
@@ -417,4 +369,73 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
} }
#endregion SOCKS5 Address Handling #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 // Question: www.google.com, Type A, Class IN
0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F,
0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00,
0x00, 0x01, 0x00, 0x01 0x00, 0x01, 0x00, 0x01,
]; ];
public byte[] BuildUdpRequestPacket() public byte[] BuildUdpRequestPacket()
@@ -17,13 +17,13 @@ public class McBeService : IUdpTest
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78, 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
// Client GUID (random 16 bytes) // Client GUID (random 16 bytes)
0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E, 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 = private static readonly byte[] McBeMagicBytes =
[ [
0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 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 = private static readonly List<string> ValidGameModes =
@@ -31,7 +31,7 @@ public class McBeService : IUdpTest
"Survival", "Survival",
"Creative", "Creative",
"Adventure", "Adventure",
"Spectator" "Spectator",
]; ];
public byte[] BuildUdpRequestPacket() public byte[] BuildUdpRequestPacket()
@@ -61,6 +61,10 @@ public class McBeService : IUdpTest
return false; // Magic bytes do not match return false; // Magic bytes do not match
} }
var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]); 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 stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray());
var stringParts = stringData.Split(';'); var stringParts = stringData.Split(';');
// check Game Mode str // check Game Mode str
@@ -31,10 +31,7 @@ public class StunService : IUdpTest
if (stunResponseBytes.Length >= 2) if (stunResponseBytes.Length >= 2)
{ {
var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1]; var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1];
if (messageType is 0x0101 or 0x0111) return messageType is 0x0101 or 0x0111;
{
return true;
}
} }
return true; return true;
+21 -18
View File
@@ -5,7 +5,6 @@ namespace ServiceLib.UdpTest;
public class UdpTestService public class UdpTestService
{ {
private const string DefaultUdpTestType = "ntp"; private const string DefaultUdpTestType = "ntp";
private readonly IUdpTest _udpTest;
private static readonly IReadOnlyDictionary<string, Func<IUdpTest>> UdpTestFactories = private static readonly IReadOnlyDictionary<string, Func<IUdpTest>> UdpTestFactories =
new Dictionary<string, Func<IUdpTest>>(StringComparer.OrdinalIgnoreCase) new Dictionary<string, Func<IUdpTest>>(StringComparer.OrdinalIgnoreCase)
@@ -16,6 +15,8 @@ public class UdpTestService
["mcbe"] = () => new McBeService(), ["mcbe"] = () => new McBeService(),
}; };
private readonly IUdpTest _udpTest;
private UdpTestService(IUdpTest udpTest) private UdpTestService(IUdpTest udpTest)
{ {
_udpTest = udpTest; _udpTest = udpTest;
@@ -88,24 +89,26 @@ public class UdpTestService
return (targetServerHost, _udpTest.GetDefaultTargetPort()); return (targetServerHost, _udpTest.GetDefaultTargetPort());
} }
public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port, TimeSpan operationTimeout) public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port,
CancellationToken ct = default)
{ {
using var cts = new CancellationTokenSource(operationTimeout); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var cancellationToken = cts.Token; using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
var linkedCt = linkedCts.Token;
var udpRequestPacket = _udpTest.BuildUdpRequestPacket(); var udpRequestPacket = _udpTest.BuildUdpRequestPacket();
if (udpRequestPacket == null || udpRequestPacket.Length == 0) if (udpRequestPacket == null || udpRequestPacket.Length == 0)
{ {
throw new InvalidOperationException("Failed to build UDP request packet."); throw new InvalidOperationException("Failed to build UDP request packet.");
} }
using var channel = new Socks5UdpChannel("127.0.0.1", socks5Port); using var channel = new Socks5UdpChannel("127.0.0.1", socks5Port);
if (!await channel.EstablishUdpAssociationAsync(cancellationToken).ConfigureAwait(false)) if (!await channel.EstablishUdpAssociationAsync(linkedCt).ConfigureAwait(false))
{ {
throw new Exception("Failed to establish UDP association with SOCKS5 proxy."); throw new Exception("Failed to establish UDP association with SOCKS5 proxy.");
} }
var (targetHost, targetPort) = ParseHostAndPort(targetServerHost); var (targetHost, targetPort) = ParseHostAndPort(targetServerHost);
byte[] udpReceiveResult = null; byte[]? validUdpReceiveResult = null;
// Get minimum round trip time from two attempts // Get minimum round trip time from two attempts
var roundTripTime = TimeSpan.MaxValue; var roundTripTime = TimeSpan.MaxValue;
@@ -116,11 +119,15 @@ public class UdpTestService
{ {
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
await channel.SendAsync(targetHost, targetPort, udpRequestPacket).ConfigureAwait(false); await channel.SendAsync(targetHost, targetPort, udpRequestPacket, linkedCt).ConfigureAwait(false);
var (_, receiveResult) = await channel.ReceiveAsync(cancellationToken).ConfigureAwait(false); var (_, receiveResult) = await channel.ReceiveAsync(linkedCt).ConfigureAwait(false);
stopwatch.Stop(); stopwatch.Stop();
udpReceiveResult = receiveResult; if (!_udpTest.VerifyAndExtractUdpResponse(receiveResult))
{
continue;
}
validUdpReceiveResult = receiveResult;
var currentRoundTripTime = stopwatch.Elapsed; var currentRoundTripTime = stopwatch.Elapsed;
if (currentRoundTripTime < roundTripTime) if (currentRoundTripTime < roundTripTime)
@@ -128,6 +135,10 @@ public class UdpTestService
roundTripTime = currentRoundTripTime; roundTripTime = currentRoundTripTime;
} }
} }
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
throw;
}
catch catch
{ {
if (attempt == 1 && roundTripTime == TimeSpan.MaxValue) if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
@@ -137,18 +148,10 @@ public class UdpTestService
} }
} }
if ((udpReceiveResult?.Length ?? 0) < 4 + 1 + 4 + 2) if (validUdpReceiveResult != null)
{
throw new Exception("Received NTP response is too short.");
}
if (udpReceiveResult != null && _udpTest.VerifyAndExtractUdpResponse(udpReceiveResult))
{ {
return roundTripTime; 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(IEnumerable<T> collection) : base(collection)
public BulkObservableCollection(List<T> list) : base(list) { } {
}
public BulkObservableCollection(List<T> list) : base(list)
{
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{ {
+3 -1
View File
@@ -1,6 +1,8 @@
namespace ServiceLib.Base; namespace ServiceLib.Base;
public class MyReactiveObject : ReactiveObject public class MyReactiveObject : ReactiveObject, IActivatableViewModel
{ {
protected static Config? _config; protected static Config? _config;
public ViewModelActivator Activator { get; } = new();
} }
+11 -17
View File
@@ -136,29 +136,23 @@ public static class Extension
.Replace("\n", replacement); .Replace("\n", replacement);
} }
public static async Task<TOutput> HandleSafe<TInput, TOutput>( public static IObservable<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction, this Interaction<TInput, TOutput> interaction, TInput input,
TInput input,
TOutput defaultValue = default!, TOutput defaultValue = default!,
[CallerMemberName] string memberName = "", [CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "", [CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0) [CallerLineNumber] int lineNumber = 0)
{ {
try return Signal.Defer(() => interaction.Handle(input))
.Catch<TOutput, UnhandledInteractionException<TInput, TOutput>>(ex =>
{ {
return await interaction.Handle(input); Logging.SaveLog($"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}", ex);
} return Signal.Return(defaultValue);
catch (UnhandledInteractionException<TInput, TOutput> ex) })
.Catch<TOutput, Exception>(ex =>
{ {
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}"; Logging.SaveLog($"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}", ex);
Logging.SaveLog(title, ex); return Signal.Return(defaultValue);
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;
}
} }
} }
+38 -4
View File
@@ -726,6 +726,40 @@ public class Utils
return false; 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 #endregion Data Checks
#region Speed Test #region Speed Test
@@ -1018,12 +1052,12 @@ public class Utils
return new Dictionary<string, string>(); return new Dictionary<string, string>();
} }
public static async Task<string?> GetCliWrapOutput(string filePath, string? arg) public static async Task<string?> GetCliWrapOutput(string filePath, string? arg, CancellationToken cancellationToken = default)
{ {
return await GetCliWrapOutput(filePath, arg != null ? new List<string>() { arg } : null); return await GetCliWrapOutput(filePath, arg != null ? new List<string>() { arg } : null, cancellationToken);
} }
public static async Task<string?> GetCliWrapOutput(string filePath, IEnumerable<string>? args) public static async Task<string?> GetCliWrapOutput(string filePath, IEnumerable<string>? args, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -1040,7 +1074,7 @@ public class Utils
} }
} }
var result = await cmd.ExecuteBufferedAsync(); var result = await cmd.ExecuteBufferedAsync(cancellationToken);
if (result.IsSuccess) if (result.IsSuccess)
{ {
return result.StandardOutput ?? ""; return result.StandardOutput ?? "";
+20 -8
View File
@@ -92,10 +92,16 @@ public class Global
public const string XrayLocalAsset = "XRAY_LOCATION_ASSET"; public const string XrayLocalAsset = "XRAY_LOCATION_ASSET";
public const string XrayLocalCert = "XRAY_LOCATION_CERT"; public const string XrayLocalCert = "XRAY_LOCATION_CERT";
public const int SpeedTestPageSize = 1000; public const int SpeedTestPageSize = 1000;
public const int SpeedTestConcurrencyCountMin = 10;
public const string LinuxBash = "/bin/bash"; public const string LinuxBash = "/bin/bash";
public const string StringTrue = "true"; public const string StringTrue = "true";
public const string StringFalse = "false"; public const string StringFalse = "false";
public const int SqliteMaxBatchSize = 10000; public const int SqliteMaxBatchSize = 10000;
public static readonly TimeSpan LocalFetch = TimeSpan.FromSeconds(5);
public static readonly TimeSpan DirectFetch = TimeSpan.FromSeconds(10);
public static readonly TimeSpan ProxyFetch = TimeSpan.FromSeconds(30);
public static readonly TimeSpan DirectDownloadConnect = TimeSpan.FromSeconds(5);
public static readonly TimeSpan ProxyDownloadConnect = TimeSpan.FromSeconds(10);
public const string SingboxDirectDNSTagPrefix = "direct-dns-"; public const string SingboxDirectDNSTagPrefix = "direct-dns-";
public const string SingboxRemoteDNSTagPrefix = "remote-dns-"; public const string SingboxRemoteDNSTagPrefix = "remote-dns-";
@@ -489,16 +495,22 @@ public class Global
"localhost" "localhost"
]; ];
public static readonly List<LanguageOption> LanguageOptions =
[
new("zh-Hans", "简体中文"),
new("zh-Hant", "繁體中文"),
new("en", "English"),
new("fa", "فارسی"),
new("fr", "Français"),
new("ru", "Русский"),
new("hu", "Magyar"),
new("id", "Bahasa Indonesia"),
new("az", "Azərbaycan dili")
];
public static readonly List<string> Languages = public static readonly List<string> Languages =
[ [
"zh-Hans", .. LanguageOptions.Select(t => t.Value)
"zh-Hant",
"en",
"fa",
"fr",
"ru",
"hu",
"id"
]; ];
public static readonly List<string> Alpns = public static readonly List<string> Alpns =
+5 -5
View File
@@ -6,11 +6,6 @@ global using System.Diagnostics;
global using System.Net; global using System.Net;
global using System.Net.NetworkInformation; global using System.Net.NetworkInformation;
global using System.Net.Sockets; 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.Reflection;
global using System.Runtime.InteropServices; global using System.Runtime.InteropServices;
global using System.Runtime.Versioning; 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.Json.Serialization;
global using System.Text.RegularExpressions; global using System.Text.RegularExpressions;
global using ReactiveUI; 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 ReactiveUI.SourceGenerators;
global using ServiceLib.Base; global using ServiceLib.Base;
global using ServiceLib.Common; global using ServiceLib.Common;
+12 -14
View File
@@ -136,10 +136,7 @@ public static class ConfigHandler
{ {
config.SpeedTestItem.SpeedPingTestUrl = Global.SpeedPingTestUrls.First(); config.SpeedTestItem.SpeedPingTestUrl = Global.SpeedPingTestUrls.First();
} }
if (config.SpeedTestItem.MixedConcurrencyCount < 1) config.SpeedTestItem.MixedConcurrencyCount = Math.Max(config.SpeedTestItem.MixedConcurrencyCount, Global.SpeedTestConcurrencyCountMin);
{
config.SpeedTestItem.MixedConcurrencyCount = 5;
}
if (config.SpeedTestItem.UdpTestTarget.IsNullOrEmpty()) if (config.SpeedTestItem.UdpTestTarget.IsNullOrEmpty())
{ {
config.SpeedTestItem.UdpTestTarget = Global.UdpTestTargets.First(); config.SpeedTestItem.UdpTestTarget = Global.UdpTestTargets.First();
@@ -471,12 +468,12 @@ public static class ConfigHandler
/// Supports moving to top, up, down, bottom or specific position /// Supports moving to top, up, down, bottom or specific position
/// </summary> /// </summary>
/// <param name="config">Current configuration</param> /// <param name="config">Current configuration</param>
/// <param name="lstProfile">List of server profiles</param> /// <param name="lstProfile">List of server profile index ids</param>
/// <param name="index">Index of the server to move</param> /// <param name="index">Index of the server to move</param>
/// <param name="eMove">Direction to move the server</param> /// <param name="eMove">Direction to move the server</param>
/// <param name="pos">Target position when using EMove.Position</param> /// <param name="pos">Target position when using EMove.Position</param>
/// <returns>0 if successful, -1 if failed</returns> /// <returns>0 if successful, -1 if failed</returns>
public static async Task<int> MoveServer(Config config, List<ProfileItem> lstProfile, int index, EMove eMove, int pos = -1) public static async Task<int> MoveServer(Config config, List<string> lstProfile, int index, EMove eMove, int pos = -1)
{ {
var count = lstProfile.Count; var count = lstProfile.Count;
if (index < 0 || index > lstProfile.Count - 1) if (index < 0 || index > lstProfile.Count - 1)
@@ -486,7 +483,7 @@ public static class ConfigHandler
for (var i = 0; i < lstProfile.Count; i++) for (var i = 0; i < lstProfile.Count; i++)
{ {
ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10); ProfileExManager.Instance.SetSort(lstProfile[i], (i + 1) * 10);
} }
var sort = 0; var sort = 0;
@@ -498,7 +495,7 @@ public static class ConfigHandler
{ {
return 0; return 0;
} }
sort = ProfileExManager.Instance.GetSort(lstProfile.First().IndexId) - 1; sort = ProfileExManager.Instance.GetSort(lstProfile.First()) - 1;
break; break;
} }
@@ -508,7 +505,7 @@ public static class ConfigHandler
{ {
return 0; return 0;
} }
sort = ProfileExManager.Instance.GetSort(lstProfile[index - 1].IndexId) - 1; sort = ProfileExManager.Instance.GetSort(lstProfile[index - 1]) - 1;
break; break;
} }
@@ -519,7 +516,7 @@ public static class ConfigHandler
{ {
return 0; return 0;
} }
sort = ProfileExManager.Instance.GetSort(lstProfile[index + 1].IndexId) + 1; sort = ProfileExManager.Instance.GetSort(lstProfile[index + 1]) + 1;
break; break;
} }
@@ -529,7 +526,7 @@ public static class ConfigHandler
{ {
return 0; return 0;
} }
sort = ProfileExManager.Instance.GetSort(lstProfile[^1].IndexId) + 1; sort = ProfileExManager.Instance.GetSort(lstProfile[^1]) + 1;
break; break;
} }
@@ -538,7 +535,7 @@ public static class ConfigHandler
break; break;
} }
ProfileExManager.Instance.SetSort(lstProfile[index].IndexId, sort); ProfileExManager.Instance.SetSort(lstProfile[index], sort);
return await Task.FromResult(0); return await Task.FromResult(0);
} }
@@ -1528,7 +1525,7 @@ public static class ConfigHandler
p != null && p != null &&
p.IsValid() && p.IsValid() &&
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) && (!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter)) Utils.IsRegexMatch(p.Remarks, extraItem.Filter)
) )
.ToList() ?? []; .ToList() ?? [];
if (matchedChildProfiles.Count == 0) if (matchedChildProfiles.Count == 0)
@@ -1667,7 +1664,7 @@ public static class ConfigHandler
//exist sub items //filter //exist sub items //filter
if (isSub && subid.IsNotEmpty() && subFilter.IsNotEmpty()) if (isSub && subid.IsNotEmpty() && subFilter.IsNotEmpty())
{ {
if (!Regex.IsMatch(profileItem.Remarks, subFilter)) if (!Utils.IsRegexMatch(profileItem.Remarks, subFilter))
{ {
continue; continue;
} }
@@ -2202,6 +2199,7 @@ public static class ConfigHandler
item.Enabled = subItem.Enabled; item.Enabled = subItem.Enabled;
item.AutoUpdateInterval = subItem.AutoUpdateInterval; item.AutoUpdateInterval = subItem.AutoUpdateInterval;
item.UserAgent = subItem.UserAgent; item.UserAgent = subItem.UserAgent;
item.RequestHeaders = subItem.RequestHeaders;
item.Sort = subItem.Sort; item.Sort = subItem.Sort;
item.Filter = subItem.Filter; item.Filter = subItem.Filter;
item.UpdateTime = subItem.UpdateTime; item.UpdateTime = subItem.UpdateTime;
+19 -8
View File
@@ -66,34 +66,41 @@ public static class ConnectionHandler
/// <summary> /// <summary>
/// Measures response time by sending HTTP requests through proxy. /// Measures response time by sending HTTP requests through proxy.
/// </summary> /// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout = 9) public static async Task<int> GetRealPingTime(IWebProxy? webProxy, CancellationToken cancellationToken = default)
{ {
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl; var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1; var responseTime = -1;
try try
{ {
using var cts = new CancellationTokenSource(); using var timeoutCts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout)); timeoutCts.CancelAfter(Global.LocalFetch);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
var linkedToken = linkedCts.Token;
using var client = new HttpClient(new SocketsHttpHandler() using var client = new HttpClient(new SocketsHttpHandler()
{ {
Proxy = webProxy, Proxy = webProxy,
UseProxy = webProxy != null, UseProxy = webProxy != null,
ConnectTimeout = TimeSpan.FromSeconds(3) ConnectTimeout = Global.LocalFetch,
}); });
List<int> oneTime = []; List<int> oneTime = [];
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false); await client.GetAsync(url, linkedToken).ConfigureAwait(false);
timer.Stop(); timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds); oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100, cts.Token); await Task.Delay(100, linkedToken);
} }
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault(); responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch catch
{ {
// Ignore
} }
return responseTime; return responseTime;
} }
@@ -101,7 +108,7 @@ public static class ConnectionHandler
/// <summary> /// <summary>
/// Gets IP and country information through specified proxy. /// Gets IP and country information through specified proxy.
/// </summary> /// </summary>
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy) public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -112,7 +119,7 @@ public static class ConnectionHandler
} }
var downloadHandle = new DownloadService(); var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, webProxy, ""); var result = await downloadHandle.TryDownloadString(url, webProxy, "", cancellationToken);
if (result == null) if (result == null)
{ {
return null; return null;
@@ -129,6 +136,10 @@ public static class ConnectionHandler
return new IpInfoResult(country, ip); return new IpInfoResult(country, ip);
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch catch
{ {
return null; return null;
@@ -24,6 +24,24 @@ public class WireguardFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query); 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 item.SetProtocolExtra(item.GetProtocolExtra() with
{ {
WgPublicKey = GetQueryDecoded(query, "publickey"), WgPublicKey = GetQueryDecoded(query, "publickey"),
@@ -76,6 +94,19 @@ public class WireguardFmt : BaseFmt
{ {
dicQuery.Add("dns", Utils.UrlEncode(protoExtra.WgDns)); 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); return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
} }
@@ -31,7 +31,7 @@ public static class SubscriptionHandler
} }
// Create download handler // Create download handler
var downloadHandle = CreateDownloadHandler(hashCode, updateFunc); var downloadHandle = CreateDownloadHandler(item, hashCode, updateFunc);
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}"); await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
// Get all subscription content (main subscription + additional subscriptions) // Get all subscription content (main subscription + additional subscriptions)
@@ -80,9 +80,18 @@ public static class SubscriptionHandler
return true; return true;
} }
private static DownloadService CreateDownloadHandler(string hashCode, Func<bool, string, Task> updateFunc) private static DownloadService CreateDownloadHandler(SubItem item, string hashCode, Func<bool, string, Task> updateFunc)
{ {
var downloadHandle = new DownloadService { AcceptHeader = "*/*" }; if (!HttpRequestHeadersHelper.TryParse(item.RequestHeaders, out var requestHeaders))
{
throw new FormatException(ResUI.SubRequestHeadersInvalid);
}
var downloadHandle = new DownloadService
{
AcceptHeader = "*/*",
RequestHeaders = requestHeaders
};
downloadHandle.Error += (sender2, args) => downloadHandle.Error += (sender2, args) =>
{ {
updateFunc?.Invoke(false, $"{hashCode}{args.GetException().Message}"); updateFunc?.Invoke(false, $"{hashCode}{args.GetException().Message}");
@@ -3,6 +3,7 @@ namespace ServiceLib.Handler.SysProxy;
public static class SysProxyHandler public static class SysProxyHandler
{ {
private static readonly string _tag = "SysProxyHandler"; private static readonly string _tag = "SysProxyHandler";
private static readonly Lazy<PacManager> _pacManager = new(() => new PacManager());
public static async Task<bool> UpdateSysProxy(Config config, bool forceDisable) public static async Task<bool> UpdateSysProxy(Config config, bool forceDisable)
{ {
@@ -56,7 +57,7 @@ public static class SysProxyHandler
if (type != ESysProxyType.Pac && Utils.IsWindows()) if (type != ESysProxyType.Pac && Utils.IsWindows())
{ {
PacManager.Instance.Stop(); _pacManager.Value.Stop();
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -110,7 +111,7 @@ public static class SysProxyHandler
private static async Task SetWindowsProxyPac(int port) private static async Task SetWindowsProxyPac(int port)
{ {
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac); var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);
await PacManager.Instance.StartAsync(port, portPac); await _pacManager.Value.StartAsync(port, portPac);
var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}"; var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}";
ProxySettingWindows.SetProxy(strProxy, "", 4); ProxySettingWindows.SetProxy(strProxy, "", 4);
} }
+29 -34
View File
@@ -8,15 +8,14 @@ public class DownloaderHelper
private static readonly Lazy<DownloaderHelper> _instance = new(() => new()); private static readonly Lazy<DownloaderHelper> _instance = new(() => new());
public static DownloaderHelper Instance => _instance.Value; public static DownloaderHelper Instance => _instance.Value;
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout) public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent,
IReadOnlyDictionary<string, string>? requestHeaders = null, string? acceptHeader = null, CancellationToken cancellationToken = default)
{ {
if (url.IsNullOrEmpty()) if (url.IsNullOrEmpty())
{ {
return null; return null;
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
Uri uri = new(url); Uri uri = new(url);
//Authorization Header //Authorization Header
var headers = new WebHeaderCollection(); var headers = new WebHeaderCollection();
@@ -28,16 +27,16 @@ public class DownloaderHelper
var requestConfiguration = new RequestConfiguration() var requestConfiguration = new RequestConfiguration()
{ {
Headers = headers, Headers = headers,
Accept = acceptHeader,
UserAgent = userAgent, UserAgent = userAgent,
ConnectTimeout = connectTimeout * 1000, ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
Proxy = webProxy Proxy = webProxy
}; };
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = requestConfiguration, RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration), CustomHttpMessageHandlerFactory = () => HttpRequestHeadersHelper.CreateHandler(GetSocketsHttpHandler(requestConfiguration), requestHeaders),
}; };
await using var downloader = new Downloader.DownloadService(downloadOpt); await using var downloader = new Downloader.DownloadService(downloadOpt);
@@ -49,31 +48,26 @@ public class DownloaderHelper
} }
}; };
using var cts = new CancellationTokenSource(); await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
using StreamReader reader = new(stream); using StreamReader reader = new(stream);
return await reader.ReadToEndAsync(cts.Token); return await reader.ReadToEndAsync(cancellationToken);
} }
public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, IProgress<string> progress, int timeout) public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, Action<string> onProgress, CancellationToken cancellationToken = default)
{ {
if (url.IsNullOrEmpty()) if (url.IsNullOrEmpty())
{ {
throw new ArgumentNullException(nameof(url)); throw new ArgumentNullException(nameof(url));
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var requestConfiguration = new RequestConfiguration() var requestConfiguration = new RequestConfiguration()
{ {
ConnectTimeout = connectTimeout * 1000, ConnectTimeout = GetConnectTimeoutMs(true),
Proxy = webProxy Proxy = webProxy
}; };
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = requestConfiguration, RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration), CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
@@ -86,8 +80,10 @@ public class DownloaderHelper
downloader.DownloadProgressChanged += (sender, value) => downloader.DownloadProgressChanged += (sender, value) =>
{ {
if (progress != null && value.BytesPerSecondSpeed > 0) if (!(value.BytesPerSecondSpeed > 0))
{ {
return;
}
hasValue = true; hasValue = true;
if (value.BytesPerSecondSpeed > maxSpeed) if (value.BytesPerSecondSpeed > maxSpeed)
{ {
@@ -99,36 +95,30 @@ public class DownloaderHelper
{ {
lastUpdateTime = DateTime.Now; lastUpdateTime = DateTime.Now;
var speed = (maxSpeed / 1000 / 1000).ToString("#0.0"); var speed = (maxSpeed / 1000 / 1000).ToString("#0.0");
progress.Report(speed); onProgress.Invoke(speed);
}
} }
}; };
downloader.DownloadFileCompleted += (sender, value) => downloader.DownloadFileCompleted += (sender, value) =>
{
if (progress != null)
{ {
if (hasValue && maxSpeed > 0) if (hasValue && maxSpeed > 0)
{ {
var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0"); var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0");
progress.Report(finalSpeed); onProgress.Invoke(finalSpeed);
} }
else if (value.Error != null) else if (value.Error != null)
{ {
progress.Report(value.Error?.Message); onProgress.Invoke(value.Error?.Message);
} }
else else
{ {
progress.Report("0"); onProgress.Invoke("0");
}
} }
}; };
//progress.Report("......"); //progress.Invoke("......");
using var cts = new CancellationTokenSource(); await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
} }
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default) public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, CancellationToken cancellationToken = default)
{ {
ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request);
if (request.FilePath.IsNullOrEmpty()) if (request.FilePath.IsNullOrEmpty())
@@ -145,9 +135,9 @@ public class DownloaderHelper
Request = request, Request = request,
}; };
var requestConfiguration = new RequestConfiguration() var requestConfiguration = new RequestConfiguration
{ {
ConnectTimeout = (int)connectTimeout.TotalMilliseconds, ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
Proxy = webProxy, Proxy = webProxy,
}; };
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
@@ -194,7 +184,7 @@ public class DownloaderHelper
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken); await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken);
} }
public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default) public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, CancellationToken cancellationToken = default)
{ {
if (requests is not { Count: > 0 }) if (requests is not { Count: > 0 })
{ {
@@ -213,7 +203,7 @@ public class DownloaderHelper
var requestConfiguration = new RequestConfiguration() var requestConfiguration = new RequestConfiguration()
{ {
ConnectTimeout = (int)connectTimeout.TotalMilliseconds, ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
Proxy = webProxy, Proxy = webProxy,
KeepAlive = true, KeepAlive = true,
@@ -223,7 +213,7 @@ public class DownloaderHelper
var parallelOptions = new ParallelOptions var parallelOptions = new ParallelOptions
{ {
MaxDegreeOfParallelism = 4, MaxDegreeOfParallelism = 4,
//CancellationToken = cancellationToken, CancellationToken = cancellationToken,
}; };
await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) => await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) =>
@@ -342,4 +332,9 @@ public class DownloaderHelper
return handler; return handler;
} }
private int GetConnectTimeoutMs(bool isProxy)
{
return (int)(isProxy ? Global.ProxyDownloadConnect : Global.DirectDownloadConnect).TotalMilliseconds;
}
} }
+7 -3
View File
@@ -22,7 +22,7 @@ public class HttpClientHelper
this.httpClient = httpClient; this.httpClient = httpClient;
} }
public async Task<string?> TryGetAsync(string url) public async Task<string?> TryGetAsync(string url, CancellationToken cancellationToken = default)
{ {
if (url.IsNullOrEmpty()) if (url.IsNullOrEmpty())
{ {
@@ -31,8 +31,12 @@ public class HttpClientHelper
try try
{ {
var response = await httpClient.GetAsync(url); var response = await httpClient.GetAsync(url, cancellationToken);
return await response.Content.ReadAsStringAsync(); return await response.Content.ReadAsStringAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
} }
catch catch
{ {
@@ -0,0 +1,88 @@
namespace ServiceLib.Helper;
public static class HttpRequestHeadersHelper
{
public static bool TryParse(string? json, out Dictionary<string, string> headers)
{
headers = new(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(json))
{
return true;
}
try
{
using var document = JsonDocument.Parse(json);
if (document.RootElement.ValueKind != JsonValueKind.Object)
{
return false;
}
var parsed = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using var request = new HttpRequestMessage { Content = new ByteArrayContent([]) };
foreach (var property in document.RootElement.EnumerateObject())
{
if (property.Value.ValueKind != JsonValueKind.String
|| !parsed.TryAdd(property.Name, property.Value.GetString()!)
|| !TryAddHeader(request, property.Name, parsed[property.Name]))
{
return false;
}
}
headers = parsed;
return true;
}
catch (Exception ex) when (ex is JsonException or ArgumentException or FormatException)
{
return false;
}
}
public static HttpMessageHandler CreateHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary<string, string>? headers)
{
return headers is { Count: > 0 } ? new RequestHeadersHandler(innerHandler, headers) : innerHandler;
}
private static bool TryAddHeader(HttpRequestMessage request, string name, string value)
{
if (value.Any(c => char.IsControl(c) && c != '\t'))
{
return false;
}
return request.Headers.TryAddWithoutValidation(name, value)
|| request.Content!.Headers.TryAddWithoutValidation(name, value);
}
private sealed class RequestHeadersHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary<string, string> headers)
: DelegatingHandler(innerHandler)
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
using var customHeaders = new HttpRequestMessage { Content = new ByteArrayContent([]) };
foreach (var header in headers)
{
if (!TryAddHeader(customHeaders, header.Key, header.Value))
{
throw new FormatException(ResUI.SubRequestHeadersInvalid);
}
}
// Apply after each downloader's defaults, replacing headers without changing their values.
foreach (var header in customHeaders.Headers.NonValidated)
{
request.Headers.Remove(header.Key);
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
foreach (var header in customHeaders.Content.Headers.NonValidated)
{
request.Content ??= new ByteArrayContent([]);
request.Content.Headers.Remove(header.Key);
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return base.SendAsync(request, cancellationToken);
}
}
}
+16 -3
View File
@@ -75,15 +75,28 @@ public sealed class SQLiteHelper
public async Task DisposeDbConnectionAsync() public async Task DisposeDbConnectionAsync()
{ {
await Task.Factory.StartNew(() => await Task.Run(() =>
{
try
{ {
_db?.Close(); _db?.Close();
_db?.Dispose(); _db?.Dispose();
}
finally
{
_db = null; _db = null;
}
_dbAsync?.GetConnection()?.Close(); try
_dbAsync?.GetConnection()?.Dispose(); {
var conn = _dbAsync?.GetConnection();
conn?.Close();
conn?.Dispose();
}
finally
{
_dbAsync = null; _dbAsync = null;
}
}); });
} }
} }
+8 -10
View File
@@ -28,14 +28,13 @@ public class CertPemManager
/// Get certificate in PEM format from a server with CA pinning validation /// Get certificate in PEM format from a server with CA pinning validation
/// </summary> /// </summary>
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4) List<string>? verifyPeerCertByName = null)
{ {
try try
{ {
var (domain, _, port, _) = Utils.ParseUrl(target); var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource(Global.LocalFetch);
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
@@ -63,8 +62,8 @@ public class CertPemManager
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds")); Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"));
return (null, $"Connection timeout after {timeout} seconds"); return (null, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -77,15 +76,14 @@ public class CertPemManager
/// Get certificate chain in PEM format from a server with CA pinning validation /// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary> /// </summary>
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4) List<string>? verifyPeerCertByName = null)
{ {
var pemList = new List<string>(); var pemList = new List<string>();
try try
{ {
var (domain, _, port, _) = Utils.ParseUrl(target); var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource(Global.LocalFetch);
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
@@ -116,8 +114,8 @@ public class CertPemManager
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds")); Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"));
return (pemList, $"Connection timeout after {timeout} seconds"); return (pemList, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds");
} }
catch (Exception ex) catch (Exception ex)
{ {
+2 -2
View File
@@ -216,7 +216,7 @@ public class CoreManager
await _updateFunc?.Invoke(notify, msg); await _updateFunc?.Invoke(notify, msg);
} }
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000) private static async Task WaitForProxyPort(CoreConfigContext? preContext)
{ {
if (preContext is null) if (preContext is null)
{ {
@@ -227,7 +227,7 @@ public class CoreManager
return; return;
} }
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs)); using var rootCts = new CancellationTokenSource(Global.LocalFetch);
var rootToken = rootCts.Token; var rootToken = rootCts.Token;
var port = preContext.Node.Port; var port = preContext.Node.Port;
@@ -119,7 +119,7 @@ public class GroupProfileManager
p != null && p != null &&
p.IsValid() && p.IsValid() &&
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) && (!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter)) Utils.IsRegexMatch(p.Remarks, extra.Filter)
) )
.ToList() ?? []; .ToList() ?? [];
} }
+43 -58
View File
@@ -2,36 +2,37 @@ namespace ServiceLib.Manager;
public class PacManager public class PacManager
{ {
private static readonly Lazy<PacManager> _instance = new(() => new PacManager()); private const string Tag = "PacManager";
public static PacManager Instance => _instance.Value; private CancellationTokenSource? _cts;
private int _httpPort;
private int _pacPort; private int _pacPort;
private TcpListener? _tcpListener; private TcpListener? _tcpListener;
private byte[] _writeContent; private byte[] _writeContent = [];
private bool _isRunning;
private bool _needRestart = true;
public async Task StartAsync(int httpPort, int pacPort) public async Task StartAsync(int httpPort, int pacPort)
{ {
_needRestart = httpPort != _httpPort || pacPort != _pacPort || !_isRunning; var content = await InitText(httpPort);
_writeContent = content;
_httpPort = httpPort; if (_tcpListener is not null && _pacPort == pacPort)
_pacPort = pacPort;
await InitText();
if (_needRestart)
{ {
return;
}
Stop(); Stop();
RunListener(); var cts = new CancellationTokenSource();
} var listener = TcpListener.Create(pacPort);
listener.Start();
_cts = cts;
_pacPort = pacPort;
_tcpListener = listener;
_ = ListenLoopAsync(listener, cts.Token);
} }
private async Task InitText() private async Task<byte[]> InitText(int httpPort)
{ {
var customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyPacPath; var customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem.CustomSystemProxyPacPath;
var fileName = (customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath)) var fileName = customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath)
? customSystemProxyPacPath ? customSystemProxyPacPath
: Path.Combine(Utils.GetConfigPath(), "pac.txt"); : Path.Combine(Utils.GetConfigPath(), "pac.txt");
@@ -45,7 +46,7 @@ public class PacManager
} }
var pacText = await File.ReadAllTextAsync(fileName); var pacText = await File.ReadAllTextAsync(fileName);
pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;"); pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{httpPort};DIRECT;");
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("HTTP/1.0 200 OK"); sb.AppendLine("HTTP/1.0 200 OK");
@@ -54,59 +55,43 @@ public class PacManager
sb.AppendLine("Content-Length:" + Encoding.UTF8.GetByteCount(pacText)); sb.AppendLine("Content-Length:" + Encoding.UTF8.GetByteCount(pacText));
sb.AppendLine(); sb.AppendLine();
sb.Append(pacText); sb.Append(pacText);
_writeContent = Encoding.UTF8.GetBytes(sb.ToString()); return Encoding.UTF8.GetBytes(sb.ToString());
} }
private void RunListener() private async Task ListenLoopAsync(TcpListener listener, CancellationToken token)
{
_tcpListener = TcpListener.Create(_pacPort);
_isRunning = true;
_tcpListener.Start();
Task.Factory.StartNew(async () =>
{
while (_isRunning)
{ {
var buffer = new byte[1024];
try try
{ {
if (!_tcpListener.Pending()) while (!token.IsCancellationRequested)
{ {
await Task.Delay(10); using var client = await listener.AcceptTcpClientAsync(token).ConfigureAwait(false);
continue; await using var stream = client.GetStream();
_ = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
await stream.WriteAsync(_writeContent, token).ConfigureAwait(false);
await stream.FlushAsync(token).ConfigureAwait(false);
} }
var client = await _tcpListener.AcceptTcpClientAsync();
await Task.Run(() => WriteContent(client));
} }
catch catch (OperationCanceledException) when (token.IsCancellationRequested)
{ {
// ignored
} }
} catch (Exception ex)
}, TaskCreationOptions.LongRunning);
}
private void WriteContent(TcpClient client)
{ {
var stream = client.GetStream(); Logging.SaveLog(Tag, ex);
stream.Write(_writeContent, 0, _writeContent.Length); }
stream.Flush(); finally
{
listener.Stop();
}
} }
public void Stop() public void Stop()
{ {
if (_tcpListener == null) _cts?.Cancel();
{ _tcpListener?.Stop();
return; _cts?.Dispose();
} _cts = null;
try _pacPort = 0;
{
_isRunning = false;
_tcpListener.Stop();
_tcpListener = null; _tcpListener = null;
} }
catch
{
// ignored
}
}
} }
+3 -9
View File
@@ -12,23 +12,17 @@ public class TaskManager
_config = config; _config = config;
_updateFunc = updateFunc; _updateFunc = updateFunc;
_ = Task.Factory.StartNew( Task.Run(ScheduledTasks);
ScheduledTasks,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
} }
private async Task ScheduledTasks() private async Task ScheduledTasks()
{ {
Logging.SaveLog("Setup Scheduled Tasks"); Logging.SaveLog("Setup Scheduled Tasks");
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
var numOfExecuted = 1; var numOfExecuted = 1;
while (true) while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
{ {
//1 minute
await Task.Delay(1000 * 60);
//Execute once 1 minute //Execute once 1 minute
try try
{ {
@@ -213,7 +213,7 @@ public class ClashUIItem
public bool EnableMixinContent { get; set; } public bool EnableMixinContent { get; set; }
public int ProxiesSorting { get; set; } public int ProxiesSorting { get; set; }
public bool ProxiesAutoRefresh { 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 bool ConnectionsAutoRefresh { get; set; }
public int ConnectionsRefreshInterval { get; set; } = 2; public int ConnectionsRefreshInterval { get; set; } = 2;
public List<ColumnItem> ConnectionsColumnItem { get; set; } public List<ColumnItem> ConnectionsColumnItem { get; set; }
@@ -75,8 +75,6 @@ public class Inboundsettings4Ray
public string? address { get; set; } public string? address { get; set; }
public List<UsersItem4Ray>? clients { get; set; }
public string? decryption { get; set; } public string? decryption { get; set; }
public bool? allowTransparent { get; set; } public bool? allowTransparent { get; set; }
@@ -96,21 +94,6 @@ public class Inboundsettings4Ray
public List<string>? dns { get; set; } public List<string>? dns { get; set; }
} }
public class UsersItem4Ray
{
public string? id { get; set; }
public int? alterId { get; set; }
public string? email { get; set; }
public string? security { get; set; }
public string? encryption { get; set; }
public string? flow { get; set; }
}
public class Sniffing4Ray public class Sniffing4Ray
{ {
public bool enabled { get; set; } public bool enabled { get; set; }
@@ -137,10 +120,6 @@ public class Outbounds4Ray
public class Outboundsettings4Ray public class Outboundsettings4Ray
{ {
public List<VnextItem4Ray>? vnext { get; set; }
public List<ServersItem4Ray>? servers { get; set; }
public Response4Ray? response { get; set; } public Response4Ray? response { get; set; }
public int? userLevel { get; set; } public int? userLevel { get; set; }
@@ -174,29 +153,16 @@ public class Outboundsettings4Ray
public int? version { get; set; } public int? version { get; set; }
public List<string>? remoteDNS { get; set; } public List<string>? remoteDNS { get; set; }
}
public class WireguardPeer4Ray public string? id { get; set; }
{
public string endpoint { get; set; }
public string publicKey { get; set; }
public string? preSharedKey { get; set; }
}
public class VnextItem4Ray public int? alterId { get; set; }
{
public string address { get; set; }
public int port { get; set; } public string? security { get; set; }
public List<UsersItem4Ray> users { get; set; } public string? encryption { get; set; }
}
public class ServersItem4Ray public string? flow { get; set; }
{
public string email { get; set; }
public string address { get; set; }
public string? method { get; set; } public string? method { get; set; }
@@ -204,24 +170,14 @@ public class ServersItem4Ray
public string? password { get; set; } public string? password { get; set; }
public int port { get; set; }
public int? level { get; set; }
public string flow { get; set; }
public bool? uot { get; set; } public bool? uot { get; set; }
public List<SocksUsersItem4Ray> users { get; set; }
} }
public class SocksUsersItem4Ray public class WireguardPeer4Ray
{ {
public string user { get; set; } public string endpoint { get; set; }
public string publicKey { get; set; }
public string pass { get; set; } public string? preSharedKey { get; set; }
public int? level { get; set; }
} }
public class Mux4Ray public class Mux4Ray
@@ -13,4 +13,5 @@ public class ClashConnectionModel
public double Time { get; set; } public double Time { get; set; }
public string? Elapsed { get; set; } public string? Elapsed { get; set; }
public string? Chain { get; set; } public string? Chain { get; set; }
public string? ProcessPath { get; set; }
} }
@@ -1,36 +1,37 @@
namespace ServiceLib.Models.Dto; namespace ServiceLib.Models.Dto;
public class ClashConnections public record ClashConnections
{ {
public ulong downloadTotal { get; set; } public ulong downloadTotal { get; init; }
public ulong uploadTotal { get; set; } public ulong uploadTotal { get; init; }
public List<ConnectionItem>? connections { get; set; } public List<ConnectionItem>? connections { get; init; }
} }
public class ConnectionItem public record ConnectionItem
{ {
public string? id { get; set; } public string? id { get; init; }
public MetadataItem? metadata { get; set; } public MetadataItem? metadata { get; init; }
public ulong upload { get; set; } public ulong upload { get; init; }
public ulong download { get; set; } public ulong download { get; init; }
public DateTime start { get; set; } public DateTime start { get; init; }
public List<string>? chains { get; set; } public List<string>? chains { get; init; }
public string? rule { get; set; } public string? rule { get; init; }
public string? rulePayload { get; set; } public string? rulePayload { get; init; }
} }
public class MetadataItem public record MetadataItem
{ {
public string? network { get; set; } public string? network { get; init; }
public string? type { get; set; } public string? type { get; init; }
public string? sourceIP { get; set; } public string? sourceIP { get; init; }
public string? destinationIP { get; set; } public string? destinationIP { get; init; }
public string? sourcePort { get; set; } public string? sourcePort { get; init; }
public string? destinationPort { get; set; } public string? destinationPort { get; init; }
public string? host { get; set; } public string? host { get; init; }
public string? nsMode { get; set; } public string? nsMode { get; init; }
public object? uid { get; set; } public object? uid { get; init; }
public string? process { get; set; } public string? process { get; init; }
public string? processPath { get; set; } public string? processPath { get; init; }
public string? remoteDestination { get; set; } public string? remoteDestination { get; init; }
public string? sniffHost { get; init; }
} }
@@ -3,9 +3,9 @@ namespace ServiceLib.Models.Dto;
[Serializable] [Serializable]
public partial class ClashProxyModel : ReactiveObject 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; } public string? Now { get; set; }
@@ -0,0 +1,3 @@
namespace ServiceLib.Models.Dto;
public sealed record LanguageOption(string Value, string Display);
@@ -16,6 +16,8 @@ public class SubItem
public string UserAgent { get; set; } = string.Empty; public string UserAgent { get; set; } = string.Empty;
public string? RequestHeaders { get; set; }
public int Sort { get; set; } public int Sort { get; set; }
public string? Filter { get; set; } public string? Filter { get; set; }
+46 -10
View File
@@ -555,6 +555,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 HTTP headers (JSON) 的本地化字符串。
/// </summary>
public static string LvRequestHeaders {
get {
return ResourceManager.GetString("LvRequestHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Type 的本地化字符串。 /// 查找类似 Type 的本地化字符串。
/// </summary> /// </summary>
@@ -2140,7 +2149,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Not Support 的本地化字符串。 /// 查找类似 Not Supported 的本地化字符串。
/// </summary> /// </summary>
public static string MsgNotSupport { public static string MsgNotSupport {
get { get {
@@ -2634,6 +2643,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Invalid HTTP headers. Use a JSON object with unique header names and string values. Header names and values must not contain line breaks. 的本地化字符串。
/// </summary>
public static string SubRequestHeadersInvalid {
get {
return ResourceManager.GetString("SubRequestHeadersInvalid", resourceCulture);
}
}
/// <summary>
/// 查找类似 Optional. Example: {&quot;X-hwid&quot;: &quot;my_device&quot;}. Values override default headers for all URLs in this group, including the subscription conversion service. 的本地化字符串。
/// </summary>
public static string SubRequestHeadersTips {
get {
return ResourceManager.GetString("SubRequestHeadersTips", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 For group please leave blank here 的本地化字符串。 /// 查找类似 For group please leave blank here 的本地化字符串。
/// </summary> /// </summary>
@@ -4239,15 +4266,6 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Update via proxy 的本地化字符串。
/// </summary>
public static string TbSettingsEnableUpdateViaProxy {
get {
return ResourceManager.GetString("TbSettingsEnableUpdateViaProxy", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Enable sorting Configurations by drag-n-drop (requires restart) 的本地化字符串。 /// 查找类似 Enable sorting Configurations by drag-n-drop (requires restart) 的本地化字符串。
/// </summary> /// </summary>
@@ -4284,6 +4302,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Update via proxy 的本地化字符串。
/// </summary>
public static string TbSettingsEnableUpdateViaProxy {
get {
return ResourceManager.GetString("TbSettingsEnableUpdateViaProxy", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Exception 的本地化字符串。 /// 查找类似 Exception 的本地化字符串。
/// </summary> /// </summary>
@@ -4995,6 +5022,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Process 的本地化字符串。
/// </summary>
public static string TbSortingProcess {
get {
return ResourceManager.GetString("TbSortingProcess", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Time 的本地化字符串。 /// 查找类似 Time 的本地化字符串。
/// </summary> /// </summary>
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -1783,7 +1783,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Only Check</value> <value>Only Check</value>
</data> </data>
<data name="MsgNotSupport" xml:space="preserve"> <data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value> <value>Not Supported</value>
</data> </data>
<data name="LvTestIpInfo" xml:space="preserve"> <data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value> <value>IP Info</value>
@@ -1887,4 +1887,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSettingsMux4Ray" xml:space="preserve"> <data name="TbSettingsMux4Ray" xml:space="preserve">
<value>Xray Mux setting</value> <value>Xray Mux setting</value>
</data> </data>
<data name="LvRequestHeaders" xml:space="preserve">
<value>HTTP headers (JSON)</value>
</data>
<data name="SubRequestHeadersTips" xml:space="preserve">
<value>Optional. Example: {"X-hwid": "my_device"}. Values override default headers for all URLs in this group, including the subscription conversion service.</value>
</data>
<data name="SubRequestHeadersInvalid" xml:space="preserve">
<value>Invalid HTTP headers. Use a JSON object with unique header names and string values. Header names and values must not contain line breaks.</value>
</data>
<data name="TbSortingProcess" xml:space="preserve">
<value>Process</value>
</data>
</root> </root>
+30
View File
@@ -693,6 +693,9 @@
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve"> <data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>Проверить наличие предварительных обновлений</value> <value>Проверить наличие предварительных обновлений</value>
</data> </data>
<data name="TbSettingsEnableUpdateViaProxy" xml:space="preserve">
<value>Обновлять через прокси</value>
</data>
<data name="TbSettingsException" xml:space="preserve"> <data name="TbSettingsException" xml:space="preserve">
<value>Исключение</value> <value>Исключение</value>
</data> </data>
@@ -1020,6 +1023,15 @@
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve"> <data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
<value>Протокол Mux для sing-box</value> <value>Протокол Mux для sing-box</value>
</data> </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"> <data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Процесс (Linux/Windows)</value> <value>Процесс (Linux/Windows)</value>
</data> </data>
@@ -1869,4 +1881,22 @@
<data name="TbBlockAAAAQueriesTips" xml:space="preserve"> <data name="TbBlockAAAAQueriesTips" xml:space="preserve">
<value>При включении блокирует DNS-запросы IPv6</value> <value>При включении блокирует DNS-запросы IPv6</value>
</data> </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> </root>
+12
View File
@@ -1879,4 +1879,16 @@
<data name="TbSettingsMux4Ray" xml:space="preserve"> <data name="TbSettingsMux4Ray" xml:space="preserve">
<value>Xray Mux 设置</value> <value>Xray Mux 设置</value>
</data> </data>
<data name="LvRequestHeaders" xml:space="preserve">
<value>HTTP 请求头 (JSON)</value>
</data>
<data name="SubRequestHeadersTips" xml:space="preserve">
<value>可选,示例:{"X-hwid": "my_device"}。自定义值覆盖默认请求头,应用于本组所有订阅地址(包括订阅转换服务)。</value>
</data>
<data name="SubRequestHeadersInvalid" xml:space="preserve">
<value>HTTP 请求头无效。请使用 JSON 对象,键名不能重复,值必须为字符串;请求头名称和值不能包含换行符。</value>
</data>
<data name="TbSortingProcess" xml:space="preserve">
<value>进程</value>
</data>
</root> </root>
+36
View File
@@ -1020,6 +1020,15 @@
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve"> <data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
<value>sing-box Mux 多路復用協定</value> <value>sing-box Mux 多路復用協定</value>
</data> </data>
<data name="TbSettingsMux4RayConcurrency" xml:space="preserve">
<value>Xray Mux 並行數</value>
</data>
<data name="TbSettingsMux4RayXudpConcurrency" xml:space="preserve">
<value>Xray Mux XUDP 並行數</value>
</data>
<data name="TbSettingsMux4RayXudpProxyUDP443" xml:space="preserve">
<value>Xray Mux XUDP proxy UDP443</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve"> <data name="TbRoutingRuleProcess" xml:space="preserve">
<value>行程 (Linux/Windows)</value> <value>行程 (Linux/Windows)</value>
</data> </data>
@@ -1864,4 +1873,31 @@
<data name="LvCustomCoreType" xml:space="preserve"> <data name="LvCustomCoreType" xml:space="preserve">
<value>自訂設定核心</value> <value>自訂設定核心</value>
</data> </data>
<data name="TbXrayOnly" xml:space="preserve">
<value>僅 Xray</value>
</data>
<data name="TbBlockAAAAQueries" xml:space="preserve">
<value>封鎖 AAAA 查詢</value>
</data>
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
<value>啟用後將封鎖 IPv6 查詢</value>
</data>
<data name="TbDNS" xml:space="preserve">
<value>DNS</value>
</data>
<data name="TbSettingsMux4Ray" xml:space="preserve">
<value>Xray Mux 設定</value>
</data>
<data name="LvRequestHeaders" xml:space="preserve">
<value>HTTP 請求標頭 (JSON)</value>
</data>
<data name="SubRequestHeadersTips" xml:space="preserve">
<value>可選,範例:{"X-hwid": "my_device"}。自訂值會覆寫預設標頭,套用於本組所有訂閱位址(包括訂閱轉換服務)。</value>
</data>
<data name="SubRequestHeadersInvalid" xml:space="preserve">
<value>HTTP 請求標頭無效。請使用 JSON 物件,鍵名不能重複,值必須為字串;標頭名稱和值不能包含換行字元。</value>
</data>
<data name="TbSortingProcess" xml:space="preserve">
<value>行程</value>
</data>
</root> </root>
+1 -21
View File
@@ -2,28 +2,8 @@
"tag": "proxy", "tag": "proxy",
"protocol": "vmess", "protocol": "vmess",
"settings": { "settings": {
"vnext": [
{
"address": "v2ray.cool", "address": "v2ray.cool",
"port": 10086, "port": 10086
"users": [
{
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
"security": "auto"
}
]
}
],
"servers": [
{
"address": "v2ray.cool",
"method": "chacha20",
"ota": false,
"password": "123456",
"port": 10086,
"level": 1
}
]
}, },
"streamSettings": { "streamSettings": {
"network": "tcp" "network": "tcp"
+3
View File
@@ -62,6 +62,9 @@
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
</Compile> </Compile>
<EmbeddedResource Update="Resx\ResUI.az.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resx\ResUI.fa.resx"> <EmbeddedResource Update="Resx\ResUI.fa.resx">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>PublicResXFileCodeGenerator</Generator> <Generator>PublicResXFileCodeGenerator</Generator>
@@ -365,9 +365,16 @@ public partial class CoreConfigSingboxService
rule4ExpectedIPs = JsonUtils.DeepCopy(rule); rule4ExpectedIPs = JsonUtils.DeepCopy(rule);
rule4ExpectedIPs.geosite = regionGeosite; rule4ExpectedIPs.geosite = regionGeosite;
} }
if (rule.geosite?.Count > 0
|| rule.domain?.Count > 0
|| rule.domain_keyword?.Count > 0
|| rule.domain_regex?.Count > 0
|| rule.domain_suffix?.Count > 0)
{
AddRules(rule, item, directDnsList);
} }
}
if (rule.geosite?.Count > 0 || rule.domain?.Count > 0) else
{ {
AddRules(rule, item, directDnsList); AddRules(rule, item, directDnsList);
} }
@@ -282,9 +282,8 @@ public partial class CoreConfigV2rayService
return false; return false;
} }
var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address var outboundAddress = outbound.settings?.address?.ToString()
?? outbound.settings?.vnext?.FirstOrDefault()?.address ?? outbound.settings?.peers?.FirstOrDefault()?.endpoint
?? outbound.settings?.address?.ToString()
?? string.Empty; ?? string.Empty;
if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase)) if (outboundAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase))
@@ -69,166 +69,88 @@ public partial class CoreConfigV2rayService
{ {
var protocolExtra = _node.GetProtocolExtra(); var protocolExtra = _node.GetProtocolExtra();
var muxEnabled = _node.MuxEnabled ?? false; var muxEnabled = _node.MuxEnabled ?? false;
var outboundSettings = outbound.settings;
switch (_node.ConfigType) switch (_node.ConfigType)
{ {
case EConfigType.VMess: case EConfigType.VMess:
{ {
VnextItem4Ray vnextItem; outboundSettings.address = _node.Address;
if (outbound.settings.vnext.Count <= 0) outboundSettings.port = _node.Port;
{ outboundSettings.id = _node.Password;
vnextItem = new VnextItem4Ray(); outboundSettings.alterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0;
outbound.settings.vnext.Add(vnextItem); outboundSettings.email = Global.UserEMail;
}
else
{
vnextItem = outbound.settings.vnext.First();
}
vnextItem.address = _node.Address;
vnextItem.port = _node.Port;
UsersItem4Ray usersItem;
if (vnextItem.users.Count <= 0)
{
usersItem = new UsersItem4Ray();
vnextItem.users.Add(usersItem);
}
else
{
usersItem = vnextItem.users.First();
}
usersItem.id = _node.Password;
usersItem.alterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0;
usersItem.email = Global.UserEMail;
if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity)) if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity))
{ {
usersItem.security = protocolExtra.VmessSecurity; outboundSettings.security = protocolExtra.VmessSecurity;
} }
else else
{ {
usersItem.security = Global.DefaultSecurity; outboundSettings.security = Global.DefaultSecurity;
} }
FillOutboundMux(outbound, muxEnabled, muxEnabled); FillOutboundMux(outbound, muxEnabled, muxEnabled);
outbound.settings.servers = null;
break; break;
} }
case EConfigType.Shadowsocks: case EConfigType.Shadowsocks:
{ {
ServersItem4Ray serversItem; outboundSettings.address = _node.Address;
if (outbound.settings.servers.Count <= 0) outboundSettings.port = _node.Port;
{ outboundSettings.password = _node.Password;
serversItem = new ServersItem4Ray(); outboundSettings.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod)
outbound.settings.servers.Add(serversItem);
}
else
{
serversItem = outbound.settings.servers.First();
}
serversItem.address = _node.Address;
serversItem.port = _node.Port;
serversItem.password = _node.Password;
serversItem.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod)
? protocolExtra.SsMethod : "none"; ? protocolExtra.SsMethod : "none";
serversItem.uot = protocolExtra.Uot == true ? true : null; outboundSettings.uot = protocolExtra.Uot == true ? true : null;
serversItem.ota = false; outboundSettings.ota = false;
serversItem.level = 1; outboundSettings.level = 1;
FillOutboundMux(outbound); FillOutboundMux(outbound);
outbound.settings.vnext = null;
break; break;
} }
case EConfigType.SOCKS: case EConfigType.SOCKS:
{ {
ServersItem4Ray serversItem; outboundSettings.address = _node.Address;
if (outbound.settings.servers.Count <= 0) outboundSettings.port = _node.Port;
{
serversItem = new ServersItem4Ray();
outbound.settings.servers.Add(serversItem);
}
else
{
serversItem = outbound.settings.servers.First();
}
serversItem.address = _node.Address;
serversItem.port = _node.Port;
serversItem.method = null;
serversItem.password = null;
if (_node.Username.IsNotEmpty() if (_node.Username.IsNotEmpty()
&& _node.Password.IsNotEmpty()) && _node.Password.IsNotEmpty())
{ {
SocksUsersItem4Ray socksUsersItem = new() outboundSettings.user = _node.Username;
{ outboundSettings.pass = _node.Password;
user = _node.Username ?? "", outboundSettings.level = 1;
pass = _node.Password, outboundSettings.email = Global.UserEMail;
level = 1
};
serversItem.users = new List<SocksUsersItem4Ray>() { socksUsersItem };
} }
FillOutboundMux(outbound); FillOutboundMux(outbound);
outbound.settings.vnext = null;
break; break;
} }
case EConfigType.HTTP: case EConfigType.HTTP:
{ {
outbound.settings.address = _node.Address; outboundSettings.address = _node.Address;
outbound.settings.port = _node.Port; outboundSettings.port = _node.Port;
if (protocolExtra.HttpHeaders.IsNotEmpty()) if (protocolExtra.HttpHeaders.IsNotEmpty())
{ {
outbound.settings.headers = JsonUtils.ParseJson(protocolExtra.HttpHeaders); outboundSettings.headers = JsonUtils.ParseJson(protocolExtra.HttpHeaders);
} }
if (_node.Username.IsNotEmpty() if (_node.Username.IsNotEmpty()
&& _node.Password.IsNotEmpty()) && _node.Password.IsNotEmpty())
{ {
outbound.settings.user = _node.Username; outboundSettings.user = _node.Username;
outbound.settings.pass = _node.Password; outboundSettings.pass = _node.Password;
outbound.settings.level = 1; outboundSettings.level = 1;
outbound.settings.email = Global.UserEMail; outboundSettings.email = Global.UserEMail;
} }
FillOutboundMux(outbound); FillOutboundMux(outbound);
outbound.settings.vnext = null;
outbound.settings.servers = null;
break; break;
} }
case EConfigType.VLESS: case EConfigType.VLESS:
{ {
VnextItem4Ray vnextItem; outboundSettings.address = _node.Address;
if (outbound.settings.vnext?.Count <= 0) outboundSettings.port = _node.Port;
{ outboundSettings.id = _node.Password;
vnextItem = new VnextItem4Ray(); outboundSettings.email = Global.UserEMail;
outbound.settings.vnext.Add(vnextItem); outboundSettings.encryption = protocolExtra.VlessEncryption;
}
else
{
vnextItem = outbound.settings.vnext.First();
}
vnextItem.address = _node.Address;
vnextItem.port = _node.Port;
UsersItem4Ray usersItem;
if (vnextItem.users.Count <= 0)
{
usersItem = new UsersItem4Ray();
vnextItem.users.Add(usersItem);
}
else
{
usersItem = vnextItem.users.First();
}
usersItem.id = _node.Password;
usersItem.email = Global.UserEMail;
usersItem.encryption = protocolExtra.VlessEncryption;
if (protocolExtra.Flow.IsNullOrEmpty()) if (protocolExtra.Flow.IsNullOrEmpty())
{ {
@@ -236,46 +158,28 @@ public partial class CoreConfigV2rayService
} }
else else
{ {
usersItem.flow = protocolExtra.Flow; outboundSettings.flow = protocolExtra.Flow;
FillOutboundMux(outbound, false, muxEnabled); FillOutboundMux(outbound, false, muxEnabled);
} }
outbound.settings.servers = null;
break; break;
} }
case EConfigType.Trojan: case EConfigType.Trojan:
{ {
ServersItem4Ray serversItem; outboundSettings.address = _node.Address;
if (outbound.settings.servers.Count <= 0) outboundSettings.port = _node.Port;
{ outboundSettings.password = _node.Password;
serversItem = new ServersItem4Ray();
outbound.settings.servers.Add(serversItem);
}
else
{
serversItem = outbound.settings.servers.First();
}
serversItem.address = _node.Address;
serversItem.port = _node.Port;
serversItem.password = _node.Password;
serversItem.ota = false; outboundSettings.ota = false;
serversItem.level = 1; outboundSettings.level = 1;
FillOutboundMux(outbound); FillOutboundMux(outbound);
outbound.settings.vnext = null;
break; break;
} }
case EConfigType.Hysteria2: case EConfigType.Hysteria2:
{ {
outbound.settings = new() outboundSettings.address = _node.Address;
{ outboundSettings.port = _node.Port;
version = 2, outboundSettings.version = 2;
address = _node.Address,
port = _node.Port,
vnext = null,
servers = null,
};
break; break;
} }
case EConfigType.WireGuard: case EConfigType.WireGuard:
@@ -301,8 +205,6 @@ public partial class CoreConfigV2rayService
peers = [peer], peers = [peer],
}; };
outbound.settings = setting; outbound.settings = setting;
outbound.settings.vnext = null;
outbound.settings.servers = null;
break; break;
} }
} }
+122 -43
View File
@@ -13,29 +13,38 @@ public class DownloadService
public string? AcceptHeader { get; init; } public string? AcceptHeader { get; init; }
public IReadOnlyDictionary<string, string>? RequestHeaders { get; init; }
private static readonly string _tag = "DownloadService"; private static readonly string _tag = "DownloadService";
/// <summary> /// <summary>
/// Downloads data with the specified proxy and reports progress messages. /// Downloads data with the specified proxy and reports progress messages.
/// </summary> /// </summary>
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc) public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, Func<bool, string, Task> updateFunc, CancellationToken cancellationToken = default)
{ {
try try
{ {
var progress = new Progress<string>();
progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}");
await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy, await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy,
url, url,
progress, OnProgress,
downloadTimeout); cancellationToken);
void OnProgress(string message)
{
cancellationToken.ThrowIfCancellationRequested();
updateFunc.Invoke(false, $"{message}");
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
} }
catch (Exception ex) catch (Exception ex)
{ {
await updateFunc?.Invoke(false, ex.Message); await updateFunc.Invoke(false, ex.Message);
if (ex.InnerException != null) if (ex.InnerException != null)
{ {
await updateFunc?.Invoke(false, ex.InnerException.Message); await updateFunc.Invoke(false, ex.InnerException.Message);
} }
} }
return 0; return 0;
@@ -44,23 +53,28 @@ public class DownloadService
/// <summary> /// <summary>
/// Downloads a file and reports progress through events. /// Downloads a file and reports progress through events.
/// </summary> /// </summary>
public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, TimeSpan connectTimeout) public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, CancellationToken cancellationToken = default)
{ {
try try
{ {
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}")); UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}"));
var webProxy = await GetWebProxy(blProxy); var webProxy = await GetWebProxy(blProxy, cancellationToken);
await DownloaderHelper.Instance.DownloadFileAsync(webProxy, await DownloaderHelper.Instance.DownloadFileAsync(webProxy,
request, request,
OnProgress, OnProgress,
connectTimeout); cancellationToken);
void OnProgress(FileDownloadState state) void OnProgress(FileDownloadState state)
{ {
cancellationToken.ThrowIfCancellationRequested();
UpdateCompleted?.Invoke(this, new UpdateResult(state.Completed, $"{Utils.HumanFy((long)state.SpeedBytesPerSecond / 1024)}/s | {Utils.HumanFy(state.DownloadedBytes / 1024)}/{Utils.HumanFy(state.TotalBytes / 1024)}")); UpdateCompleted?.Invoke(this, new UpdateResult(state.Completed, $"{Utils.HumanFy((long)state.SpeedBytesPerSecond / 1024)}/s | {Utils.HumanFy(state.DownloadedBytes / 1024)}/{Utils.HumanFy(state.TotalBytes / 1024)}"));
} }
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
@@ -73,20 +87,21 @@ public class DownloadService
} }
} }
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, TimeSpan connectTimeout) public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, CancellationToken cancellationToken = default)
{ {
try try
{ {
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}")); UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}"));
var webProxy = await GetWebProxy(blProxy); var webProxy = await GetWebProxy(blProxy, cancellationToken);
await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy, await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy,
requests, requests,
OnProgress, OnProgress,
connectTimeout); cancellationToken);
void OnProgress(ReadOnlyMemory<FileDownloadState> states) void OnProgress(ReadOnlyMemory<FileDownloadState> states)
{ {
cancellationToken.ThrowIfCancellationRequested();
var span = states.Span; var span = states.Span;
var completedCount = 0; var completedCount = 0;
var downloadingStates = new List<FileDownloadState>(); var downloadingStates = new List<FileDownloadState>();
@@ -127,6 +142,10 @@ public class DownloadService
UpdateCompleted?.Invoke(this, new UpdateResult(allCompleted, $"{completedCount}/{span.Length} | {Utils.HumanFy((long)totalSpeed / 1024)}/s {Utils.HumanFy(totalDownloadedBytes / 1024)}/{Utils.HumanFy(totalTotalBytes / 1024)} {downloadingFileName}")); UpdateCompleted?.Invoke(this, new UpdateResult(allCompleted, $"{completedCount}/{span.Length} | {Utils.HumanFy((long)totalSpeed / 1024)}/s {Utils.HumanFy(totalDownloadedBytes / 1024)}/{Utils.HumanFy(totalTotalBytes / 1024)} {downloadingFileName}"));
} }
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
@@ -142,12 +161,12 @@ public class DownloadService
/// <summary> /// <summary>
/// Gets redirect target URL without following redirects automatically. /// Gets redirect target URL without following redirects automatically.
/// </summary> /// </summary>
public async Task<string?> UrlRedirectAsync(string url, bool blProxy) public async Task<string?> UrlRedirectAsync(string url, bool blProxy, CancellationToken cancellationToken = default)
{ {
var webRequestHandler = new SocketsHttpHandler var webRequestHandler = new SocketsHttpHandler
{ {
AllowAutoRedirect = false, AllowAutoRedirect = false,
Proxy = await GetWebProxy(blProxy) Proxy = await GetWebProxy(blProxy, cancellationToken)
}; };
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy(); var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null) if (certificateChainPolicy != null)
@@ -157,7 +176,7 @@ public class DownloadService
} }
using var client = new HttpClient(webRequestHandler); using var client = new HttpClient(webRequestHandler);
var response = await client.GetAsync(url); var response = await client.GetAsync(url, cancellationToken);
if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null) if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null)
{ {
return response.Headers.Location.ToString(); return response.Headers.Location.ToString();
@@ -173,26 +192,29 @@ public class DownloadService
/// <summary> /// <summary>
/// Tries to download string content using proxy switch setting. /// Tries to download string content using proxy switch setting.
/// </summary> /// </summary>
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent) public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent, CancellationToken cancellationToken = default)
{ {
var webProxy = await GetWebProxy(blProxy); var webProxy = await GetWebProxy(blProxy, cancellationToken);
return await TryDownloadString(url, webProxy, userAgent); return await TryDownloadString(url, webProxy, userAgent, cancellationToken);
} }
/// <summary> /// <summary>
/// Tries to download string content with a specified proxy. /// Tries to download string content with a specified proxy.
/// </summary> /// </summary>
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent) public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
{ {
var timeout = 15;
try try
{ {
var result1 = await DownloadStringAsync(url, webProxy, userAgent, timeout); var result1 = await DownloadStringAsync(url, webProxy, userAgent, cancellationToken);
if (result1.IsNotEmpty()) if (result1.IsNotEmpty())
{ {
return result1; return result1;
} }
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
@@ -205,12 +227,16 @@ public class DownloadService
try try
{ {
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, timeout); var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, cancellationToken);
if (result2.IsNotEmpty()) if (result2.IsNotEmpty())
{ {
return result2; return result2;
} }
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
@@ -227,16 +253,16 @@ public class DownloadService
/// <summary> /// <summary>
/// Downloads string content via HttpClient. /// Downloads string content via HttpClient.
/// </summary> /// </summary>
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout) private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
{ {
try try
{ {
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var handler = new SocketsHttpHandler var handler = new SocketsHttpHandler
{ {
Proxy = webProxy, Proxy = webProxy,
UseProxy = webProxy != null, UseProxy = webProxy != null,
ConnectTimeout = TimeSpan.FromSeconds(connectTimeout) AutomaticDecompression = DecompressionMethods.All,
ConnectTimeout = webProxy is null ? Global.DirectDownloadConnect : Global.ProxyDownloadConnect,
}; };
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy(); var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null) if (certificateChainPolicy != null)
@@ -245,10 +271,8 @@ public class DownloadService
handler.SslOptions.RemoteCertificateValidationCallback = null; handler.SslOptions.RemoteCertificateValidationCallback = null;
} }
using var client = new HttpClient(handler) using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders));
{ client.Timeout = Timeout.InfiniteTimeSpan;
Timeout = Timeout.InfiniteTimeSpan
};
if (userAgent.IsNullOrEmpty()) if (userAgent.IsNullOrEmpty())
{ {
@@ -267,10 +291,15 @@ public class DownloadService
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo)); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo));
} }
using var cts = new CancellationTokenSource(); using var timeoutCts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout)); timeoutCts.CancelAfter(webProxy is null ? Global.DirectFetch : Global.ProxyFetch);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
return await client.GetStringAsync(url, cts.Token); return await client.GetStringAsync(url, linkedCts.Token);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -288,7 +317,7 @@ public class DownloadService
/// <summary> /// <summary>
/// Downloads string content via DownloaderHelper. /// Downloads string content via DownloaderHelper.
/// </summary> /// </summary>
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout) private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -296,9 +325,13 @@ public class DownloadService
{ {
userAgent = Utils.GetVersion(false); userAgent = Utils.GetVersion(false);
} }
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout); var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, RequestHeaders, AcceptHeader, cancellationToken);
return result; return result;
} }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
@@ -314,14 +347,14 @@ public class DownloadService
/// <summary> /// <summary>
/// Creates local SOCKS proxy when proxy switch is enabled. /// Creates local SOCKS proxy when proxy switch is enabled.
/// </summary> /// </summary>
private async Task<WebProxy?> GetWebProxy(bool blProxy) private async Task<WebProxy?> GetWebProxy(bool blProxy, CancellationToken cancellationToken = default)
{ {
if (!blProxy) if (!blProxy)
{ {
return null; return null;
} }
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
if (await SocketCheck(Global.Loopback, port) == false) if (await SocksPortCheck(Global.Loopback, port, cancellationToken) == false)
{ {
return null; return null;
} }
@@ -332,18 +365,64 @@ public class DownloadService
/// <summary> /// <summary>
/// Checks whether the specified TCP endpoint is reachable. /// Checks whether the specified TCP endpoint is reachable.
/// </summary> /// </summary>
private async Task<bool> SocketCheck(string ip, int port) private async Task<bool> SocksPortCheck(string ip, int port, CancellationToken cancellationToken = default)
{ {
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)
{
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 try
{ {
IPEndPoint point = new(IPAddress.Parse(ip), port); await tcp.ConnectAsync(ip, port, linkedToken);
using Socket? sock = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); var stream = tcp.GetStream();
await sock.ConnectAsync(point);
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; return true;
} }
catch (Exception) }
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; return false;
} }
} }
catch
{
// Ignore other exceptions and continue
}
}
return false;
}
} }
+233 -162
View File
@@ -7,65 +7,127 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private static readonly string _tag = "SpeedtestService"; private static readonly string _tag = "SpeedtestService";
private readonly Config? _config = config; private readonly Config? _config = config;
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc; private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
private static readonly ConcurrentBag<string> _lstExitLoop = []; private readonly Lock _runLock = new();
private readonly List<CancellationTokenSource> _runCtsList = [];
private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize; private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize;
private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1); private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1);
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds) public Task RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationToken ct = default)
{ {
Task.Run(async () => CancellationTokenSource runCts;
lock (_runLock)
{ {
await RunAsync(actionType, selecteds); runCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
await ProfileExManager.Instance.SaveTo();
await UpdateFunc("", ResUI.SpeedtestingCompleted); _runCtsList.Add(runCts);
}); }
return RunLoopAsync(actionType, selecteds, runCts);
} }
public void ExitLoop() public void ExitLoop()
{ {
if (!_lstExitLoop.IsEmpty) var counter = 0;
List<CancellationTokenSource> listToCancel;
lock (_runLock)
{
listToCancel = _runCtsList.ToList();
counter = listToCancel.Count;
}
foreach (var cts in listToCancel)
{
try
{
cts.Cancel();
}
catch (ObjectDisposedException)
{
// Ignored
}
}
if (counter > 0)
{ {
_ = UpdateFunc("", ResUI.SpeedtestingStop); _ = UpdateFunc("", ResUI.SpeedtestingStop);
_lstExitLoop.Clear();
} }
} }
private static bool ShouldStopTest(string exitLoopKey) private async Task RunLoopAsync(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationTokenSource runCts)
{ {
return _lstExitLoop.All(p => p != exitLoopKey); try
{
await RunAsync(actionType, selecteds, runCts.Token);
}
catch (OperationCanceledException) when (runCts.IsCancellationRequested)
{
// Ignored
}
finally
{
try
{
await ProfileExManager.Instance.SaveTo();
}
finally
{
await UpdateFunc("", ResUI.SpeedtestingCompleted);
} }
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds) lock (_runLock)
{ {
var exitLoopKey = Utils.GetGuid(false); _runCtsList.Remove(runCts);
_lstExitLoop.Add(exitLoopKey); }
runCts.Dispose();
}
}
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationToken ct = default)
{
var lstSelected = await GetClearItem(actionType, selecteds); var lstSelected = await GetClearItem(actionType, selecteds);
var completedIds = new ConcurrentDictionary<string, byte>();
try
{
switch (actionType) switch (actionType)
{ {
case ESpeedActionType.Tcping: case ESpeedActionType.Tcping:
await RunTcpingAsync(lstSelected, exitLoopKey); await RunTcpingAsync(lstSelected, completedIds, ct);
break; break;
case ESpeedActionType.Realping: case ESpeedActionType.Realping:
await RunRealPingBatchAsync(lstSelected, exitLoopKey); await RunRealPingBatchAsync(lstSelected, completedIds, 0, ct);
break; break;
case ESpeedActionType.UdpTest: case ESpeedActionType.UdpTest:
await RunUdpTestBatchAsync(lstSelected, exitLoopKey); await RunUdpTestBatchAsync(lstSelected, completedIds, 0, ct);
break; break;
case ESpeedActionType.Speedtest: case ESpeedActionType.Speedtest:
await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey); await RunMixedTestAsync(lstSelected, completedIds, 1, true, ct);
break; break;
case ESpeedActionType.Mixedtest: case ESpeedActionType.Mixedtest:
await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, true, exitLoopKey); await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, true,
ct);
break; 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);
}
}
private async Task<List<ServerTestItem>> GetClearItem(ESpeedActionType actionType, List<ProfileItem> selecteds) private async Task<List<ServerTestItem>> GetClearItem(ESpeedActionType actionType, List<ProfileItem> selecteds)
{ {
@@ -103,29 +165,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
//clear test result //clear test result
foreach (var it in lstSelected) await SetTestResultAsync(lstSelected, actionType, ResUI.Speedtesting).ConfigureAwait(false);
{
switch (actionType)
{
case ESpeedActionType.Tcping:
case ESpeedActionType.Realping:
case ESpeedActionType.UdpTest:
await UpdateFunc(it.IndexId, ResUI.Speedtesting, "");
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
break;
case ESpeedActionType.Speedtest:
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break;
case ESpeedActionType.Mixedtest:
await UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break;
}
}
if (lstSelected.Count > 1 && (actionType == ESpeedActionType.Speedtest || actionType == ESpeedActionType.Mixedtest)) if (lstSelected.Count > 1 && (actionType == ESpeedActionType.Speedtest || actionType == ESpeedActionType.Mixedtest))
{ {
@@ -135,56 +175,70 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return lstSelected; return lstSelected;
} }
private async Task RunTcpingAsync(List<ServerTestItem> selecteds, string exitLoopKey) private async Task SetTestResultAsync(List<ServerTestItem> lstSelected, ESpeedActionType actionType, string message)
{
foreach (var it in lstSelected)
{
switch (actionType)
{
case ESpeedActionType.Tcping:
case ESpeedActionType.Realping:
case ESpeedActionType.UdpTest:
await UpdateFunc(it.IndexId, message, "");
break;
case ESpeedActionType.Speedtest:
await UpdateFunc(it.IndexId, "", message);
break;
case ESpeedActionType.Mixedtest:
await UpdateFunc(it.IndexId, message, message);
break;
}
}
}
private async Task RunTcpingAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{ {
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize); var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
var lstBatch = GetTestBatchItem(selecteds, pageSize); var lstBatch = GetTestBatchItem(selecteds, pageSize);
foreach (var lst in lstBatch) foreach (var lst in lstBatch)
{ {
if (ShouldStopTest(exitLoopKey)) ct.ThrowIfCancellationRequested();
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
List<Task> tasks = []; var parallelOptions = new ParallelOptions
foreach (var it in lst)
{ {
if (ShouldStopTest(exitLoopKey)) CancellationToken = ct,
{ };
return;
}
tasks.Add(Task.Run(async () => await Parallel.ForEachAsync(lst, parallelOptions, async (item, innerCt) =>
{ {
try try
{ {
var responseTime = await GetTcpingTime(it.Address, it.Port); var responseTime = await GetTcpingTime(item.Address, item.Port, innerCt);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(item.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(item.IndexId, responseTime.ToString());
completedIds.TryAdd(item.IndexId, 0);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
})); });
}
await Task.WhenAll(tasks); await Task.Delay(_delayInterval, ct);
if (ShouldStopTest(exitLoopKey))
{
return;
}
await Task.Delay(_delayInterval);
} }
} }
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0) private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected,
ConcurrentDictionary<string, byte> completedIds, int pageSize = 0, CancellationToken ct = default)
{ {
if (pageSize <= 0) if (pageSize <= 0)
{ {
@@ -195,38 +249,35 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
List<ServerTestItem> lstFailed = []; List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest) foreach (var lst in lstTest)
{ {
var ret = await RunRealPingAsync(lst, exitLoopKey); var ret = await RunRealPingAsync(lst, completedIds, ct);
if (ret == false) if (ret == false)
{ {
lstFailed.AddRange(lst); lstFailed.AddRange(lst);
} }
await Task.Delay(_delayInterval); await Task.Delay(_delayInterval, ct);
} }
//Retest the failed part //Retest the failed part
var pageSizeNext = pageSize / 2; var pageSizeNext = pageSize / 2;
if (lstFailed.Count > 0 && pageSizeNext > 0) if (lstFailed.Count > 0 && pageSizeNext > 0)
{ {
if (ShouldStopTest(exitLoopKey)) ct.ThrowIfCancellationRequested();
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount) if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount)
{ {
await RunRealPingBatchAsync(lstFailed, exitLoopKey, pageSizeNext); await RunRealPingBatchAsync(lstFailed, completedIds, pageSizeNext, ct);
} }
else else
{ {
await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, false, exitLoopKey); await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, false, ct);
} }
} }
} }
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds, string exitLoopKey) private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{ {
ProcessService processService = null; ProcessService processService = null;
try try
@@ -236,28 +287,39 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{ {
return false; return false;
} }
await Task.Delay(1000); await Task.Delay(1000, ct);
List<Task> tasks = []; var parallelOptions = new ParallelOptions
foreach (var it in selecteds) {
CancellationToken = ct,
};
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
{ {
if (!it.AllowTest) if (!it.AllowTest)
{ {
await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip); await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
continue; completedIds.TryAdd(it.IndexId, 0);
return;
} }
if (ShouldStopTest(exitLoopKey)) try
{ {
return false; await DoRealPing(it, completedIds, innerCt);
} }
catch (OperationCanceledException) when (ct.IsCancellationRequested)
tasks.Add(Task.Run(async () =>
{ {
await DoRealPing(it); throw;
}));
} }
await Task.WhenAll(tasks); catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
});
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -273,7 +335,8 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return true; return true;
} }
private async Task RunUdpTestBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0) private async Task RunUdpTestBatchAsync(List<ServerTestItem> lstSelected,
ConcurrentDictionary<string, byte> completedIds, int pageSize = 0, CancellationToken ct = default)
{ {
if (pageSize <= 0) if (pageSize <= 0)
{ {
@@ -284,30 +347,27 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
List<ServerTestItem> lstFailed = []; List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest) foreach (var lst in lstTest)
{ {
var ret = await RunUdpTestAsync(lst, exitLoopKey); var ret = await RunUdpTestAsync(lst, completedIds, ct);
if (ret == false) if (ret == false)
{ {
lstFailed.AddRange(lst); lstFailed.AddRange(lst);
} }
await Task.Delay(_delayInterval); await Task.Delay(_delayInterval, ct);
} }
//Retest the failed part //Retest the failed part
if (lstFailed.Count > 0) if (lstFailed.Count > 0)
{ {
if (ShouldStopTest(exitLoopKey)) ct.ThrowIfCancellationRequested();
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
await RunUdpTestAsync(lstFailed, exitLoopKey); await RunUdpTestAsync(lstFailed, completedIds, ct);
} }
} }
private async Task<bool> RunUdpTestAsync(List<ServerTestItem> selecteds, string exitLoopKey) private async Task<bool> RunUdpTestAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{ {
ProcessService processService = null; ProcessService processService = null;
try try
@@ -317,27 +377,39 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{ {
return false; return false;
} }
await Task.Delay(1000); await Task.Delay(1000, ct);
List<Task> tasks = []; var parallelOptions = new ParallelOptions
foreach (var it in selecteds) {
CancellationToken = ct,
};
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
{ {
if (!it.AllowTest) if (!it.AllowTest)
{ {
continue; await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
completedIds.TryAdd(it.IndexId, 0);
return;
} }
if (ShouldStopTest(exitLoopKey)) try
{ {
return false; await DoUdpTest(it, completedIds, innerCt);
} }
catch (OperationCanceledException) when (ct.IsCancellationRequested)
tasks.Add(Task.Run(async () =>
{ {
await DoUdpTest(it); throw;
}));
} }
await Task.WhenAll(tasks); catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
});
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -353,22 +425,22 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return true; return true;
} }
private async Task RunMixedTestAsync(List<ServerTestItem> selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey) private async Task RunMixedTestAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, int concurrencyCount, bool blSpeedTest,
CancellationToken ct = default)
{ {
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
var downloadHandle = new DownloadService(); var downloadHandle = new DownloadService();
List<Task> tasks = [];
foreach (var it in selecteds)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
continue;
}
await concurrencySemaphore.WaitAsync();
tasks.Add(Task.Run(async () => var parallelOptions = new ParallelOptions
{ {
MaxDegreeOfParallelism = concurrencyCount,
CancellationToken = ct,
};
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
{
innerCt.ThrowIfCancellationRequested();
ProcessService processService = null; ProcessService processService = null;
try try
{ {
@@ -379,20 +451,14 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return; return;
} }
await Task.Delay(1000); await Task.Delay(1000, innerCt);
var delay = await DoRealPing(it); var delay = await DoRealPing(it, completedIds, innerCt);
if (blSpeedTest) if (blSpeedTest)
{ {
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
return;
}
if (delay > 0) if (delay > 0)
{ {
await DoSpeedTest(downloadHandle, it); await DoSpeedTest(downloadHandle, it, completedIds, innerCt);
} }
else else
{ {
@@ -400,6 +466,10 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
} }
} }
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
@@ -408,26 +478,24 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{ {
if (processService != null) if (processService != null)
{ {
await processService?.StopAsync(); await processService.StopAsync();
} }
concurrencySemaphore.Release();
} }
})); });
}
await Task.WhenAll(tasks);
} }
private async Task<int> DoRealPing(ServerTestItem it) private async Task<int> DoRealPing(ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{ {
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy); var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, ct);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(it.IndexId, responseTime.ToString());
if (!_config.UiItem.HideColumnIpInfo && responseTime > 0) if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
{ {
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy); var ipInfo = await ConnectionHandler.GetIPInfo(webProxy, ct);
var ipStr = ipInfo?.ToString() ?? Global.None; var ipStr = ipInfo?.ToString() ?? Global.None;
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr); ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
await UpdateIpInfoFunc(it.IndexId, ipStr); await UpdateIpInfoFunc(it.IndexId, ipStr);
@@ -437,17 +505,22 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip); await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
} }
completedIds.TryAdd(it.IndexId, 0);
return responseTime; return responseTime;
} }
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it) private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{ {
await UpdateFunc(it.IndexId, "", ResUI.Speedtesting); await UpdateFunc(it.IndexId, "", ResUI.Speedtesting);
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var url = _config.SpeedTestItem.SpeedTestUrl; var url = _config.SpeedTestItem.SpeedTestUrl;
var timeout = _config.SpeedTestItem.SpeedTestTimeout; var timeout = _config.SpeedTestItem.SpeedTestTimeout;
await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, msg) => using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
var linkedCt = linkedCts.Token;
await downloadHandle.DownloadDataAsync(url, webProxy, async (success, msg) =>
{ {
decimal.TryParse(msg, out var dec); decimal.TryParse(msg, out var dec);
if (dec > 0) if (dec > 0)
@@ -455,34 +528,34 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec); ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec);
} }
await UpdateFunc(it.IndexId, "", msg); await UpdateFunc(it.IndexId, "", msg);
}); }, linkedCt);
completedIds.TryAdd(it.IndexId, 0);
} }
private async Task<int> DoUdpTest(ServerTestItem it) private async Task<int> DoUdpTest(ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{ {
var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl); var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
var responseTime = -1; var responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, ct)).TotalMilliseconds;
try
{
responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, TimeSpan.FromSeconds(5))).TotalMilliseconds;
}
catch
{
// ignored
}
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(it.IndexId, responseTime.ToString());
completedIds.TryAdd(it.IndexId, 0);
return responseTime; return responseTime;
} }
private async Task<int> GetTcpingTime(string url, int port) private async Task<int> GetTcpingTime(string? url, int port, CancellationToken ct = default)
{ {
var responseTime = -1; var responseTime = -1;
if (url.IsNullOrEmpty() || port <= 0)
{
return responseTime;
}
if (!IPAddress.TryParse(url, out var ipAddress)) if (!IPAddress.TryParse(url, out var ipAddress))
{ {
var ipHostInfo = await Dns.GetHostEntryAsync(url); var ipHostInfo = await Dns.GetHostEntryAsync(url, ct);
ipAddress = ipHostInfo.AddressList.First(); ipAddress = ipHostInfo.AddressList.First();
} }
@@ -492,13 +565,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
try try
{ {
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await clientSocket.ConnectAsync(endPoint, cts.Token).ConfigureAwait(false); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
await clientSocket.ConnectAsync(endPoint, linkedCts.Token).ConfigureAwait(false);
responseTime = (int)timer.ElapsedMilliseconds; responseTime = (int)timer.ElapsedMilliseconds;
} }
catch (OperationCanceledException)
{
}
finally finally
{ {
timer.Stop(); timer.Stop();
@@ -5,8 +5,7 @@ namespace ServiceLib.Services.Statistics;
public class StatisticsSingboxService public class StatisticsSingboxService
{ {
private readonly Config _config; private readonly Config _config;
private bool _exitFlag; private CancellationTokenSource? _cts;
private ClientWebSocket? webSocket;
private readonly Func<ServerSpeedItem, Task>? _updateFunc; private readonly Func<ServerSpeedItem, Task>? _updateFunc;
private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic"; private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic";
private static readonly string _tag = "StatisticsSingboxService"; private static readonly string _tag = "StatisticsSingboxService";
@@ -15,40 +14,17 @@ public class StatisticsSingboxService
{ {
_config = config; _config = config;
_updateFunc = updateFunc; _updateFunc = updateFunc;
_exitFlag = false;
_ = Task.Factory.StartNew( Task.Run(Run);
Run,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
private async Task Init()
{
await Task.Delay(5000);
try
{
if (webSocket == null)
{
webSocket = new ClientWebSocket();
await webSocket.ConnectAsync(new Uri(Url), CancellationToken.None);
}
}
catch { }
} }
public void Close() public void Close()
{ {
try try
{ {
_exitFlag = true; _cts?.Cancel();
if (webSocket != null) _cts?.Dispose();
{ _cts = null;
webSocket.Abort();
webSocket = null;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -58,53 +34,63 @@ public class StatisticsSingboxService
private async Task Run() private async Task Run()
{ {
await Init(); Close();
_cts = new CancellationTokenSource();
var token = _cts.Token;
while (!_exitFlag) while (!token.IsCancellationRequested)
{ {
await Task.Delay(1000);
try try
{ {
if (!AppManager.Instance.IsRunningCore(ECoreType.sing_box)) if (!AppManager.Instance.IsRunningCore(ECoreType.sing_box))
{ {
await Task.Delay(1000, token).ConfigureAwait(false);
continue; continue;
} }
if (webSocket != null) using var ws = new ClientWebSocket();
{ await ws.ConnectAsync(new Uri(Url), token).ConfigureAwait(false);
if (webSocket.State is WebSocketState.Aborted or WebSocketState.Closed)
{
webSocket.Abort();
webSocket = null;
await Init();
continue;
}
if (webSocket.State != WebSocketState.Open)
{
continue;
}
var buffer = new byte[1024]; var buffer = new byte[1024];
var res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); while (ws.State == WebSocketState.Open
while (!res.CloseStatus.HasValue) && !token.IsCancellationRequested)
{ {
var result = Encoding.UTF8.GetString(buffer, 0, res.Count); var res = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), token);
if (result.IsNotEmpty()) if (res.MessageType == WebSocketMessageType.Close)
{ {
break;
}
using var ms = new MemoryStream();
ms.Write(buffer, 0, res.Count);
while (!res.EndOfMessage)
{
res = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), token).ConfigureAwait(false);
ms.Write(buffer, 0, res.Count);
}
var result = Encoding.UTF8.GetString(ms.ToArray());
if (!result.IsNotEmpty())
{
continue;
}
ParseOutput(result, out var up, out var down); ParseOutput(result, out var up, out var down);
await _updateFunc?.Invoke(new ServerSpeedItem() if (_updateFunc != null)
{
await _updateFunc.Invoke(new ServerSpeedItem
{ {
ProxyUp = (long)(up / 1000), ProxyUp = (long)(up / 1000),
ProxyDown = (long)(down / 1000) ProxyDown = (long)(down / 1000),
}); }).ConfigureAwait(false);
}
res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
} }
} }
} }
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
break;
}
catch catch
{ {
await Task.Delay(3000, token).ConfigureAwait(false);
} }
} }
} }
@@ -5,33 +5,42 @@ public class StatisticsXrayService
private const long linkBase = 1024; private const long linkBase = 1024;
private ServerSpeedItem _serverSpeedItem = new(); private ServerSpeedItem _serverSpeedItem = new();
private readonly Config _config; private readonly Config _config;
private bool _exitFlag; private CancellationTokenSource? _cts;
private readonly Func<ServerSpeedItem, Task>? _updateFunc; private readonly Func<ServerSpeedItem, Task>? _updateFunc;
private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars"; private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars";
private static readonly string _tag = "StatisticsXrayService";
public StatisticsXrayService(Config config, Func<ServerSpeedItem, Task> updateFunc) public StatisticsXrayService(Config config, Func<ServerSpeedItem, Task> updateFunc)
{ {
_config = config; _config = config;
_updateFunc = updateFunc; _updateFunc = updateFunc;
_exitFlag = false;
_ = Task.Factory.StartNew( Task.Run(Run);
Run,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
} }
public void Close() public void Close()
{ {
_exitFlag = true; try
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
} }
private async Task Run() private async Task Run()
{ {
while (!_exitFlag) Close();
_cts = new CancellationTokenSource();
var token = _cts.Token;
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{ {
await Task.Delay(1000);
try try
{ {
if (AppManager.Instance.RunningCoreType != ECoreType.Xray) if (AppManager.Instance.RunningCoreType != ECoreType.Xray)
@@ -39,16 +48,20 @@ public class StatisticsXrayService
continue; continue;
} }
var result = await HttpClientHelper.Instance.TryGetAsync(Url); var result = await HttpClientHelper.Instance.TryGetAsync(Url, token);
if (result != null) if (result != null)
{ {
var server = ParseOutput(result) ?? new ServerSpeedItem(); var server = ParseOutput(result) ?? new ServerSpeedItem();
await _updateFunc?.Invoke(server); await _updateFunc!.Invoke(server);
} }
} }
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
break;
}
catch catch
{ {
// ignored await Task.Delay(3000, token).ConfigureAwait(false);
} }
} }
} }
+23 -24
View File
@@ -4,10 +4,9 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
{ {
private readonly Config? _config = config; private readonly Config? _config = config;
private readonly Func<bool, string, Task>? _updateFunc = updateFunc; private readonly Func<bool, string, Task>? _updateFunc = updateFunc;
private readonly int _timeout = 30;
private static readonly string _tag = "UpdateService"; private static readonly string _tag = "UpdateService";
public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true) public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{ {
var url = string.Empty; var url = string.Empty;
var fileName = string.Empty; var fileName = string.Empty;
@@ -39,7 +38,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
url = result.Url!; url = result.Url!;
fileName = Utils.GetTempPath(Utils.GetGuid()); fileName = Utils.GetTempPath(Utils.GetGuid());
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, TimeSpan.FromSeconds(_timeout)); await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken);
} }
else else
{ {
@@ -47,7 +46,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
} }
} }
public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true) public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{ {
var url = string.Empty; var url = string.Empty;
var fileName = string.Empty; var fileName = string.Empty;
@@ -89,7 +88,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
url = result.Url!; url = result.Url!;
var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url); var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url);
fileName = Utils.GetTempPath(Utils.GetGuid() + ext); fileName = Utils.GetTempPath(Utils.GetGuid() + ext);
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, TimeSpan.FromSeconds(_timeout)); await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken);
} }
else else
{ {
@@ -100,7 +99,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
} }
} }
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true) public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{ {
if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type)) if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type))
{ {
@@ -109,10 +108,10 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
var downloadHandle = new DownloadService(); var downloadHandle = new DownloadService();
var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease); var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease);
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy); return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy, cancellationToken);
} }
public async Task<List<string>> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true) public async Task<List<string>> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{ {
var msgs = new List<string>(); var msgs = new List<string>();
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes()) foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
@@ -122,7 +121,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
continue; continue;
} }
var result = await CheckHasUpdateOnly(type, preRelease, blProxy); var result = await CheckHasUpdateOnly(type, preRelease, blProxy, cancellationToken);
if (result.Success && result.Version != null) if (result.Success && result.Version != null)
{ {
var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version); var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version);
@@ -137,7 +136,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
return msgs; return msgs;
} }
public async Task UpdateGeoFileAll(bool blProxy = true) public async Task UpdateGeoFileAll(bool blProxy = true, CancellationToken cancellationToken = default)
{ {
var requests = new List<FileDownloadRequest>(); var requests = new List<FileDownloadRequest>();
requests.AddRange(GetGeoFilesRequest()); requests.AddRange(GetGeoFilesRequest());
@@ -145,22 +144,22 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
requests.AddRange(await GetSrsFileAllRequest()); requests.AddRange(await GetSrsFileAllRequest());
// NOTE: srs files are more small, so we reverse the order to ensure a good download experience for the user. // NOTE: srs files are more small, so we reverse the order to ensure a good download experience for the user.
requests.Reverse(); requests.Reverse();
await DownloadGeoFiles(requests, blProxy); await DownloadGeoFiles(requests, blProxy, cancellationToken);
await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
} }
#region CheckUpdate private #region CheckUpdate private
private async Task<UpdateResult> CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy) private async Task<UpdateResult> CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default)
{ {
try try
{ {
var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy); var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy, cancellationToken);
if (!result.Success || result.Version is null) if (!result.Success || result.Version is null)
{ {
return result; return result;
} }
return await ParseDownloadUrl(type, result); return await ParseDownloadUrl(type, result, cancellationToken);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -170,14 +169,14 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
} }
} }
private async Task<UpdateResult> GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy) private async Task<UpdateResult> GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default)
{ {
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type);
var tagName = string.Empty; var tagName = string.Empty;
if (preRelease || coreInfo?.LockedMaxVersion != null) if (preRelease || coreInfo?.LockedMaxVersion != null)
{ {
var url = coreInfo?.ReleaseApiUrl; var url = coreInfo?.ReleaseApiUrl;
var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName); var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName, cancellationToken);
if (result.IsNullOrEmpty()) if (result.IsNullOrEmpty())
{ {
return new UpdateResult(false, ""); return new UpdateResult(false, "");
@@ -209,7 +208,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
else else
{ {
var url = Path.Combine(coreInfo.Url, "latest"); var url = Path.Combine(coreInfo.Url, "latest");
var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy); var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy, cancellationToken);
if (lastUrl == null) if (lastUrl == null)
{ {
return new UpdateResult(false, ""); return new UpdateResult(false, "");
@@ -223,7 +222,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
[GeneratedRegex(@"v?(?<version>\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?)", RegexOptions.IgnoreCase)] [GeneratedRegex(@"v?(?<version>\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?)", RegexOptions.IgnoreCase)]
private static partial Regex SemVerRegex(); private static partial Regex SemVerRegex();
private async Task<SemanticVersion> GetCoreVersion(ECoreType type) private async Task<SemanticVersion> GetCoreVersion(ECoreType type, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -246,7 +245,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
return new SemanticVersion(""); return new SemanticVersion("");
} }
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg); var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg, cancellationToken);
var echo = result ?? ""; var echo = result ?? "";
var version = SemVerRegex().Match(echo).Groups["version"].Value; var version = SemVerRegex().Match(echo).Groups["version"].Value;
return new SemanticVersion(version); return new SemanticVersion(version);
@@ -259,7 +258,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
} }
} }
private async Task<UpdateResult> ParseDownloadUrl(ECoreType type, UpdateResult result) private async Task<UpdateResult> ParseDownloadUrl(ECoreType type, UpdateResult result, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -276,7 +275,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
case ECoreType.v2fly_v5: case ECoreType.v2fly_v5:
case ECoreType.mihomo: case ECoreType.mihomo:
{ {
curVersion = await GetCoreVersion(type); curVersion = await GetCoreVersion(type, cancellationToken);
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v")); message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
url = string.Format(coreUrl, version); url = string.Format(coreUrl, version);
break; break;
@@ -284,7 +283,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
case ECoreType.sing_box: case ECoreType.sing_box:
{ {
curVersion = await GetCoreVersion(type); curVersion = await GetCoreVersion(type, cancellationToken);
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v")); message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v")); url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v"));
break; break;
@@ -531,7 +530,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
}; };
} }
private async Task DownloadGeoFiles(List<FileDownloadRequest> requests, bool blProxy) private async Task DownloadGeoFiles(List<FileDownloadRequest> requests, bool blProxy, CancellationToken cancellationToken = default)
{ {
var tmpFilePathDict = new Dictionary<string, string>(); var tmpFilePathDict = new Dictionary<string, string>();
var tmpFileRequests = new List<FileDownloadRequest>(); var tmpFileRequests = new List<FileDownloadRequest>();
@@ -586,7 +585,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
_ = UpdateFunc(false, args.GetException().Message); _ = UpdateFunc(false, args.GetException().Message);
}; };
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, TimeSpan.FromSeconds(_timeout)); await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, cancellationToken);
} }
#endregion Geo private #endregion Geo private
@@ -42,11 +42,21 @@ public partial class CheckUpdateViewModel : MyReactiveObject
this.WhenAnyValue(x => x.EnableUpdateViaProxy) this.WhenAnyValue(x => x.EnableUpdateViaProxy)
.Subscribe(c => _ = OnUpdateViaProxyChanged()); .Subscribe(c => _ = OnUpdateViaProxyChanged());
RefreshCheckUpdateItems(); AppEvents.HasUpdateNotified
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(bl => RefreshCheckUpdateItems(bl));
RefreshCheckUpdateItems(true);
} }
private void RefreshCheckUpdateItems() private void RefreshCheckUpdateItems(bool hasUpdate)
{ {
if (!hasUpdate)
{
return;
}
var models = CoreInfoManager.Instance.GetCheckUpdateCoreTypes() var models = CoreInfoManager.Instance.GetCheckUpdateCoreTypes()
.Select(t => GetCheckUpdateModel(t)) .Select(t => GetCheckUpdateModel(t))
.ToList(); .ToList();
@@ -24,12 +24,14 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await ClashConnectionClose(true); await ClashConnectionClose(true);
}); });
_ = Task.Factory.StartNew( this.WhenActivated(disposables =>
async () => await GetClashConnectionsTask(), {
CancellationToken.None, var cancelDisposable = new CancellationDisposable();
TaskCreationOptions.LongRunning, cancelDisposable.DisposeWith(disposables);
TaskScheduler.Default var token = cancelDisposable.Token;
);
Task.Run(() => GetClashConnectionsTask(token));
});
} }
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = []; public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
@@ -63,8 +65,20 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
var lstModel = new List<ClashConnectionModel>(); var lstModel = new List<ClashConnectionModel>();
foreach (var item in connections ?? []) foreach (var item in connections ?? [])
{ {
var host = if (item.metadata == null)
$"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}"; {
continue;
}
var dest = item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host;
var hostSb = new StringBuilder();
hostSb.Append(dest);
hostSb.Append($":{item.metadata.destinationPort}");
if (!string.IsNullOrEmpty(item.metadata.sniffHost) &&
dest?.Equals(item.metadata.sniffHost, StringComparison.OrdinalIgnoreCase) == false)
{
hostSb.Append($" ({item.metadata.sniffHost})");
}
var host = hostSb.ToString();
if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter)) if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter))
{ {
continue; continue;
@@ -79,6 +93,7 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds, Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds,
Elapsed = (dtNow - item.start).ToString(@"hh\:mm\:ss"), Elapsed = (dtNow - item.start).ToString(@"hh\:mm\:ss"),
Chain = $"{item.rule} , {string.Join("->", item.chains ?? [])}", Chain = $"{item.rule} , {string.Join("->", item.chains ?? [])}",
ProcessPath = item.metadata.processPath,
}; };
lstModel.Add(model); lstModel.Add(model);
@@ -104,12 +119,14 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await GetClashConnections(); await GetClashConnections();
} }
public async Task GetClashConnectionsTask() public async Task GetClashConnectionsTask(CancellationToken token = default)
{
try
{ {
var numOfExecuted = 1; var numOfExecuted = 1;
while (true) using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{ {
await Task.Delay(1000 * 5);
numOfExecuted++; numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box))) AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
@@ -129,4 +146,13 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await GetClashConnections(); await GetClashConnections();
} }
} }
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception ex)
{
Logging.SaveLog("GetClashConnectionsTask", ex);
}
}
} }
@@ -55,12 +55,14 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand #endregion WhenAnyValue && ReactiveCommand
_ = Task.Factory.StartNew( this.WhenActivated(disposables =>
async () => await GetClashProxiesTask(), {
CancellationToken.None, var cancelDisposable = new CancellationDisposable();
TaskCreationOptions.LongRunning, cancelDisposable.DisposeWith(disposables);
TaskScheduler.Default var token = cancelDisposable.Token;
);
Task.Run(() => GetClashProxiesTask(token));
});
} }
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = []; public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
@@ -101,29 +103,42 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#region task #region task
public async Task GetClashProxiesTask() public async Task GetClashProxiesTask(CancellationToken token = default)
{
try
{ {
var numOfExecuted = 1; var numOfExecuted = 1;
while (true) using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{ {
await Task.Delay(1000 * 60);
numOfExecuted++; numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar && if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box))) AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{ {
continue; continue;
} }
if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0)
if (_config.ClashUIItem.ProxiesRefreshInterval <= 0)
{ {
continue; continue;
} }
if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0)
if (numOfExecuted % _config.ClashUIItem.ProxiesRefreshInterval != 0)
{ {
continue; continue;
} }
await GetClashProxies(); await GetClashProxies();
} }
} }
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception ex)
{
Logging.SaveLog("GetClashProxiesTask", ex);
}
}
#endregion task #endregion task
@@ -269,7 +269,7 @@ public partial class MainWindowViewModel : MyReactiveObject
ProfilesViewModel.RefreshServersRequested ProfilesViewModel.RefreshServersRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.SubscribeAsync(async _ => await RefreshServers()); .SubscribeAsync(async _ => await RefreshServersDispatcherAsync());
var vmReloadRequestedList = new List<IObservable<RxVoid>> var vmReloadRequestedList = new List<IObservable<RxVoid>>
{ {
@@ -400,6 +400,8 @@ public partial class MainWindowViewModel : MyReactiveObject
#region Servers && Groups #region Servers && Groups
private readonly SemaphoreSlim _refreshServersSemaphore = new(1, 1);
private async Task RefreshServers() private async Task RefreshServers()
{ {
await ProfilesViewModel.RefreshServersBiz(); await ProfilesViewModel.RefreshServersBiz();
@@ -411,6 +413,9 @@ public partial class MainWindowViewModel : MyReactiveObject
private async Task RefreshServersDispatcherAsync() private async Task RefreshServersDispatcherAsync()
{ {
//await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler); //await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
await _refreshServersSemaphore.WaitAsync();
try
{
await Signal.FromAsync(async () => await Signal.FromAsync(async () =>
{ {
await RefreshServers(); await RefreshServers();
@@ -419,6 +424,11 @@ public partial class MainWindowViewModel : MyReactiveObject
.SubscribeOn(RxSchedulers.MainThreadScheduler) .SubscribeOn(RxSchedulers.MainThreadScheduler)
.ToTask(); .ToTask();
} }
finally
{
_refreshServersSemaphore.Release();
}
}
private async Task RefreshSubscriptions() private async Task RefreshSubscriptions()
{ {
@@ -670,6 +680,13 @@ public partial class MainWindowViewModel : MyReactiveObject
{ {
SetReloadEnabled(false); SetReloadEnabled(false);
RxSchedulers.MainThreadScheduler.Schedule(() =>
{
if (TabMainSelectedIndex < 0)
{
TabMainSelectedIndex = 0;
}
});
var profileItem = await ConfigHandler.GetDefaultServer(_config); var profileItem = await ConfigHandler.GetDefaultServer(_config);
if (profileItem == null) if (profileItem == null)
{ {
@@ -739,7 +756,10 @@ public partial class MainWindowViewModel : MyReactiveObject
RxSchedulers.MainThreadScheduler.Schedule(() => RxSchedulers.MainThreadScheduler.Schedule(() =>
{ {
ShowClashUI = showClashUI; ShowClashUI = showClashUI;
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0; if (!showClashUI || TabMainSelectedIndex < 0)
{
TabMainSelectedIndex = 0;
}
}); });
} }
+23 -44
View File
@@ -2,12 +2,11 @@ namespace ServiceLib.ViewModels;
public partial class MsgViewModel : MyReactiveObject 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 readonly ConcurrentQueue<string> _queueMsg = new();
private volatile bool _lastMsgFilterNotAvailable; private volatile bool _lastMsgFilterNotAvailable;
private int _showLock = 0; // 0 = unlocked, 1 = locked public int NumMaxMsg => 500;
public int NumMaxMsg { get; } = 500;
[Reactive] [Reactive]
public partial string MsgFilter { get; set; } public partial string MsgFilter { get; set; }
@@ -30,59 +29,39 @@ public partial class MsgViewModel : MyReactiveObject
AppEvents.SendMsgViewRequested AppEvents.SendMsgViewRequested
.AsObservable() .AsObservable()
//.ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(EnqueueQueueMsg);
.Subscribe(content => _ = AppendQueueMsg(content));
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); if (!AutoRefresh || _queueMsg.IsEmpty)
}
private async Task AppendQueueMsg(string msg)
{
if (AutoRefresh == false)
{ {
return; return;
} }
EnqueueQueueMsg(msg);
if (!AppManager.Instance.ShowInTaskbar) if (!AppManager.Instance.ShowInTaskbar)
{ {
return; return;
} }
if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0)
{
return;
}
try
{
await Task.Delay(500).ConfigureAwait(false);
var sb = new StringBuilder(); var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var line)) while (_queueMsg.TryDequeue(out var msg))
{ {
sb.Append(line); sb.Append(msg);
} }
if (sb.Length > 0) if (sb.Length > 0)
{ {
try ShowMsgInteraction.HandleSafe(sb.ToString()).Subscribe();
{
await DispatcherShowMsgInteraction.Handle(sb.ToString());
}
catch
{
_queueMsg.Enqueue(sb.ToString());
}
}
}
finally
{
Interlocked.Exchange(ref _showLock, 0);
} }
} }
@@ -98,7 +77,7 @@ public partial class MsgViewModel : MyReactiveObject
{ {
try try
{ {
if (!Regex.IsMatch(msg, MsgFilter)) if (!Utils.IsRegexMatch(msg, MsgFilter))
{ {
return; return;
} }
@@ -110,11 +89,11 @@ public partial class MsgViewModel : MyReactiveObject
} }
} }
EnqueueWithLimit(msg); var formattedMsg = msg.EndsWith(Environment.NewLine)
if (!msg.EndsWith(Environment.NewLine)) ? msg
{ : msg + Environment.NewLine;
EnqueueWithLimit(Environment.NewLine);
} EnqueueWithLimit(formattedMsg);
} }
private void EnqueueWithLimit(string item) private void EnqueueWithLimit(string item)
@@ -72,11 +72,11 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
// React to ConfigType filter changes // React to ConfigType filter changes
this.WhenAnyValue(x => x.FilterExclude) this.WhenAnyValue(x => x.FilterExclude)
.Skip(1) .Skip(1)
.SubscribeAsync(async _ => await RefreshServersBiz()); .SubscribeAsync(async _ => await RefreshServers());
this.WhenAnyValue(x => x.FilterConfigTypes) this.WhenAnyValue(x => x.FilterConfigTypes)
.Skip(1) .Skip(1)
.SubscribeAsync(async _ => await RefreshServersBiz()); .SubscribeAsync(async _ => await RefreshServers());
#endregion WhenAnyValue && ReactiveCommand #endregion WhenAnyValue && ReactiveCommand
@@ -145,8 +145,14 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
} }
public async Task RefreshServers() public async Task RefreshServers()
{
await Signal.FromAsync(async () =>
{ {
await RefreshServersBiz(); await RefreshServersBiz();
return RxVoid.Default;
})
.SubscribeOn(RxSchedulers.MainThreadScheduler)
.ToTask();
} }
private async Task RefreshServersBiz() private async Task RefreshServersBiz()
@@ -176,7 +182,10 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter) private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
{ {
var lstModel = await AppManager.Instance.ProfileModels(_subIndexId, filter); var lstModel = await AppManager.Instance.ProfileModels(_subIndexId, filter);
var lstProfileExs = await ProfileExManager.Instance.GetProfileExs();
lstModel = (from t in lstModel lstModel = (from t in lstModel
join t3 in lstProfileExs on t.IndexId equals t3.IndexId into t3b
from t33 in t3b.DefaultIfEmpty()
select new ProfileItemModel select new ProfileItemModel
{ {
IndexId = t.IndexId, IndexId = t.IndexId,
@@ -190,6 +199,12 @@ public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
Subid = t.Subid, Subid = t.Subid,
SubRemarks = t.SubRemarks, SubRemarks = t.SubRemarks,
IsActive = t.IndexId == _config.IndexId, IsActive = t.IndexId == _config.IndexId,
Sort = t33?.Sort ?? 0,
Delay = t33?.Delay ?? 0,
Speed = t33?.Speed ?? 0,
DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty,
SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty,
IpInfo = t33?.IpInfo ?? string.Empty,
}).OrderBy(t => t.Sort).ToList(); }).OrderBy(t => t.Sort).ToList();
// Apply ConfigType filter (include or exclude) // Apply ConfigType filter (include or exclude)
@@ -15,7 +15,6 @@ public partial class ProfilesViewModel : MyReactiveObject
#region private prop #region private prop
private List<ProfileItem> _lstProfile;
private string _serverFilter = string.Empty; private string _serverFilter = string.Empty;
private readonly Dictionary<string, bool> _dicHeaderSort = new(); private readonly Dictionary<string, bool> _dicHeaderSort = new();
private SpeedtestService? _speedtestService; private SpeedtestService? _speedtestService;
@@ -362,7 +361,6 @@ public partial class ProfilesViewModel : MyReactiveObject
public async Task RefreshServersBiz() public async Task RefreshServersBiz()
{ {
var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter); var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter);
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
ProfileItems.ReplaceRange(lstModel ?? []); ProfileItems.ReplaceRange(lstModel ?? []);
if (lstModel?.Count > 0) if (lstModel?.Count > 0)
@@ -677,19 +675,15 @@ public partial class ProfilesViewModel : MyReactiveObject
public async Task MoveServer(EMove eMove) public async Task MoveServer(EMove eMove)
{ {
var item = _lstProfile.FirstOrDefault(t => t.IndexId == SelectedProfile.IndexId); var lstProfile = ProfileItems?.Select(t => t.IndexId).ToList() ?? [];
if (item is null) var index = lstProfile.IndexOf(SelectedProfile.IndexId);
if (index < 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return; return;
} }
var index = _lstProfile.IndexOf(item); if (await ConfigHandler.MoveServer(_config, lstProfile, index, eMove) == 0)
if (index < 0)
{
return;
}
if (await ConfigHandler.MoveServer(_config, _lstProfile, index, eMove) == 0)
{ {
await RefreshServers(); await RefreshServers();
} }
@@ -700,7 +694,8 @@ public partial class ProfilesViewModel : MyReactiveObject
var targetIndex = ProfileItems.IndexOf(targetItem); var targetIndex = ProfileItems.IndexOf(targetItem);
if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex) if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex)
{ {
if (await ConfigHandler.MoveServer(_config, _lstProfile, startIndex, EMove.Position, targetIndex) == 0) var lstProfile = ProfileItems?.Select(t => t.IndexId).ToList() ?? [];
if (await ConfigHandler.MoveServer(_config, lstProfile, startIndex, EMove.Position, targetIndex) == 0)
{ {
await RefreshServers(); await RefreshServers();
} }
@@ -77,6 +77,12 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable
} }
} }
if (!HttpRequestHeadersHelper.TryParse(SelectedSource.RequestHeaders, out _))
{
NoticeManager.Instance.Enqueue(ResUI.SubRequestHeadersInvalid);
return;
}
SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null; SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null;
SelectedSource.PrevProfile = PrevProfile; SelectedSource.PrevProfile = PrevProfile;
SelectedSource.NextProfile = NextProfile; SelectedSource.NextProfile = NextProfile;
@@ -93,6 +93,11 @@
Binding="{Binding Type}" Binding="{Binding Type}"
Header="{x:Static resx:ResUI.TbSortingType}" Header="{x:Static resx:ResUI.TbSortingType}"
Tag="Type" /> Tag="Type" />
<DataGridTextColumn
Width="100"
Binding="{Binding ProcessPath}"
Header="{x:Static resx:ResUI.TbSortingProcess}"
Tag="ProcessPath" />
<DataGridTextColumn <DataGridTextColumn
Width="100" Width="100"
Binding="{Binding Elapsed}" Binding="{Binding Elapsed}"
@@ -447,19 +447,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
break; break;
} }
//// workaround
//Task.Run(async () =>
//{
// await Task.Delay(5000);
// Dispatcher.UIThread.Post(() =>
// {
// ViewModel?.TabMainSelectedIndex = 0;
// tabMain.SelectedIndex = 0;
// tabMain1.SelectedIndex = 0;
// tabMain2.SelectedIndex = 0;
// });
//});
RestoreUI(); RestoreUI();
} }
+2 -5
View File
@@ -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.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).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; var msg = interaction.Input;
Dispatcher.UIThread.Post(() => ShowMsg(msg), ShowMsg(msg);
DispatcherPriority.ApplicationIdle);
interaction.SetOutput(RxVoid.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
}); });
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary( TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
@@ -47,7 +47,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
cmbCoreType7.ItemsSource = Global.CoreTypes; cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.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(); cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls; cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls; cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
@@ -2,6 +2,7 @@
x:Class="v2rayN.Desktop.Views.ProfilesSelectWindow" x:Class="v2rayN.Desktop.Views.ProfilesSelectWindow"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="using:v2rayN.Desktop.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dd="clr-namespace:v2rayN.Desktop.DesignData" xmlns:dd="clr-namespace:v2rayN.Desktop.DesignData"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -15,6 +16,10 @@
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.Resources>
<conv:DelayColorConverter x:Key="DelayColorConverter" />
</Window.Resources>
<DockPanel Margin="{StaticResource Margin8}"> <DockPanel Margin="{StaticResource Margin8}">
<StackPanel <StackPanel
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -121,6 +126,26 @@
Binding="{Binding SubRemarks}" Binding="{Binding SubRemarks}"
Header="{x:Static resx:ResUI.LvSubscription}" Header="{x:Static resx:ResUI.LvSubscription}"
Tag="SubRemarks" /> Tag="SubRemarks" />
<DataGridTemplateColumn SortMemberPath="Delay" Tag="Delay">
<DataGridTemplateColumn.Header>
<TextBlock Text="{x:Static resx:ResUI.LvTestDelay}" />
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock
Margin="{StaticResource MarginLr8}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Foreground="{Binding Delay, Converter={StaticResource DelayColorConverter}}"
Text="{Binding Path=DelayVal, Mode=OneWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="100"
Binding="{Binding SpeedVal}"
Header="{x:Static resx:ResUI.LvTestSpeed}"
Tag="Speed" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</DockPanel> </DockPanel>
+35 -13
View File
@@ -186,79 +186,101 @@
Grid.Row="8" Grid.Row="8"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Top"
Text="{x:Static resx:ResUI.LvRequestHeaders}" />
<StackPanel Grid.Row="8" Grid.Column="1">
<TextBox
x:Name="txtRequestHeaders"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
MinLines="4"
MaxLines="8"
Classes="TextArea"
TextWrapping="Wrap"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
<TextBlock
Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.SubRequestHeadersTips}"
TextWrapping="Wrap" />
</StackPanel>
<TextBlock
Grid.Row="9"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvSort}" /> Text="{x:Static resx:ResUI.LvSort}" />
<TextBox <TextBox
x:Name="txtSort" x:Name="txtSort"
Grid.Row="8" Grid.Row="9"
Grid.Column="1" Grid.Column="1"
Width="100" Width="100"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock <TextBlock
Grid.Row="9" Grid.Row="10"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvPrevProfile}" /> Text="{x:Static resx:ResUI.LvPrevProfile}" />
<TextBox <TextBox
x:Name="txtPrevProfile" x:Name="txtPrevProfile"
Grid.Row="9" Grid.Row="10"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" /> PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button <Button
x:Name="btnSelectPrevProfile" x:Name="btnSelectPrevProfile"
Grid.Row="9" Grid.Row="10"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" /> Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock <TextBlock
Grid.Row="10" Grid.Row="11"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvNextProfile}" /> Text="{x:Static resx:ResUI.LvNextProfile}" />
<TextBox <TextBox
x:Name="txtNextProfile" x:Name="txtNextProfile"
Grid.Row="10" Grid.Row="11"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" /> PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button <Button
x:Name="btnSelectNextProfile" x:Name="btnSelectNextProfile"
Grid.Row="10" Grid.Row="11"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" /> Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock <TextBlock
Grid.Row="11" Grid.Row="12"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvCustomCoreType}" /> Text="{x:Static resx:ResUI.LvCustomCoreType}" />
<ComboBox <ComboBox
x:Name="cmbCustomCoreType" x:Name="cmbCustomCoreType"
Grid.Row="11" Grid.Row="12"
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock <TextBlock
Grid.Row="12" Grid.Row="13"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" /> Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
<TextBox <TextBox
x:Name="txtPreSocksPort" x:Name="txtPreSocksPort"
Grid.Row="12" Grid.Row="13"
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -267,14 +289,14 @@
ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}" /> ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}" />
<TextBlock <TextBlock
Grid.Row="13" Grid.Row="14"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvMemo}" /> Text="{x:Static resx:ResUI.LvMemo}" />
<TextBox <TextBox
x:Name="txtMemo" x:Name="txtMemo"
Grid.Row="13" Grid.Row="14"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -22,6 +22,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.RequestHeaders, v => v.txtRequestHeaders.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.SelectedValue).DisposeWith(disposables);
@@ -1,3 +1,4 @@
using Avalonia.Data;
using v2rayN.Desktop.ViewModels; using v2rayN.Desktop.ViewModels;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -14,7 +15,9 @@ public partial class ThemeSettingView : ReactiveUserControl<ThemeSettingViewMode
cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>(); cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>();
cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList(); cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList();
cmbCurrentLanguage.ItemsSource = Global.Languages; cmbCurrentLanguage.ItemsSource = Global.LanguageOptions;
cmbCurrentLanguage.DisplayMemberBinding = new Binding(nameof(LanguageOption.Display));
cmbCurrentLanguage.SelectedValueBinding = new Binding(nameof(LanguageOption.Value));
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
@@ -99,6 +99,11 @@
Binding="{Binding Type}" Binding="{Binding Type}"
ExName="Type" ExName="Type"
Header="{x:Static resx:ResUI.TbSortingType}" /> Header="{x:Static resx:ResUI.TbSortingType}" />
<base:MyDGTextColumn
Width="100"
Binding="{Binding ProcessPath}"
ExName="ProcessPath"
Header="{x:Static resx:ResUI.TbSortingProcess}" />
<base:MyDGTextColumn <base:MyDGTextColumn
Width="100" Width="100"
Binding="{Binding Elapsed}" Binding="{Binding Elapsed}"
+1 -6
View File
@@ -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.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).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; var msg = interaction.Input;
Application.Current?.Dispatcher.Invoke(() =>
{
ShowMsg(msg); ShowMsg(msg);
}, DispatcherPriority.ApplicationIdle);
interaction.SetOutput(RxVoid.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
}); });
btnCopy.Click += menuMsgViewCopyAll_Click; btnCopy.Click += menuMsgViewCopyAll_Click;
@@ -43,7 +43,7 @@ public partial class OptionSettingWindow
cmbCoreType7.ItemsSource = Global.CoreTypes; cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.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(); cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls; cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls; cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:v2rayN.Base" xmlns:base="clr-namespace:v2rayN.Base"
xmlns:conv="clr-namespace:v2rayN.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -17,6 +18,10 @@
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.Resources>
<conv:DelayColorConverter x:Key="DelayColorConverter" />
</Window.Resources>
<DockPanel Margin="{StaticResource Margin8}"> <DockPanel Margin="{StaticResource Margin8}">
<StackPanel <StackPanel
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -147,6 +152,30 @@
Binding="{Binding SubRemarks}" Binding="{Binding SubRemarks}"
ExName="SubRemarks" ExName="SubRemarks"
Header="{x:Static resx:ResUI.LvSubscription}" /> Header="{x:Static resx:ResUI.LvSubscription}" />
<base:MyDGTextColumn
Width="100"
Binding="{Binding DelayVal}"
ExName="Delay"
Header="{x:Static resx:ResUI.LvTestDelay}"
SortMemberPath="Delay">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="Foreground" Value="{Binding Delay, Converter={StaticResource DelayColorConverter}}" />
</Style>
</DataGridTextColumn.ElementStyle>
</base:MyDGTextColumn>
<base:MyDGTextColumn
Width="100"
Binding="{Binding SpeedVal}"
ExName="Speed"
Header="{x:Static resx:ResUI.LvTestSpeed}">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
</DataGridTextColumn.ElementStyle>
</base:MyDGTextColumn>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</DockPanel> </DockPanel>
+36 -13
View File
@@ -65,6 +65,7 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -230,12 +231,34 @@
Grid.Row="8" Grid.Row="8"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Top"
Text="{x:Static resx:ResUI.LvRequestHeaders}" />
<StackPanel Grid.Row="8" Grid.Column="1">
<TextBox
x:Name="txtRequestHeaders"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
MinLines="4"
MaxLines="8"
Style="{StaticResource MyOutlinedTextBox}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
<TextBlock
Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.SubRequestHeadersTips}"
TextWrapping="Wrap" />
</StackPanel>
<TextBlock
Grid.Row="9"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.LvSort}" /> Text="{x:Static resx:ResUI.LvSort}" />
<TextBox <TextBox
x:Name="txtSort" x:Name="txtSort"
Grid.Row="8" Grid.Row="9"
Grid.Column="1" Grid.Column="1"
Width="100" Width="100"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -245,7 +268,7 @@
Style="{StaticResource MyOutlinedTextBox}" /> Style="{StaticResource MyOutlinedTextBox}" />
<TextBlock <TextBlock
Grid.Row="9" Grid.Row="10"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -253,7 +276,7 @@
Text="{x:Static resx:ResUI.LvPrevProfile}" /> Text="{x:Static resx:ResUI.LvPrevProfile}" />
<TextBox <TextBox
x:Name="txtPrevProfile" x:Name="txtPrevProfile"
Grid.Row="9" Grid.Row="10"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -262,7 +285,7 @@
Style="{StaticResource MyOutlinedTextBox}" /> Style="{StaticResource MyOutlinedTextBox}" />
<Button <Button
x:Name="btnSelectPrevProfile" x:Name="btnSelectPrevProfile"
Grid.Row="9" Grid.Row="10"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -270,7 +293,7 @@
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
<TextBlock <TextBlock
Grid.Row="10" Grid.Row="11"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -278,7 +301,7 @@
Text="{x:Static resx:ResUI.LvNextProfile}" /> Text="{x:Static resx:ResUI.LvNextProfile}" />
<TextBox <TextBox
x:Name="txtNextProfile" x:Name="txtNextProfile"
Grid.Row="10" Grid.Row="11"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -287,7 +310,7 @@
Style="{StaticResource MyOutlinedTextBox}" /> Style="{StaticResource MyOutlinedTextBox}" />
<Button <Button
x:Name="btnSelectNextProfile" x:Name="btnSelectNextProfile"
Grid.Row="10" Grid.Row="11"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -295,7 +318,7 @@
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
<TextBlock <TextBlock
Grid.Row="11" Grid.Row="12"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -303,14 +326,14 @@
Text="{x:Static resx:ResUI.LvCustomCoreType}" /> Text="{x:Static resx:ResUI.LvCustomCoreType}" />
<ComboBox <ComboBox
x:Name="cmbCustomCoreType" x:Name="cmbCustomCoreType"
Grid.Row="11" Grid.Row="12"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
MaxDropDownHeight="1000" MaxDropDownHeight="1000"
Style="{StaticResource MyOutlinedTextComboBox}" /> Style="{StaticResource MyOutlinedTextComboBox}" />
<TextBlock <TextBlock
Grid.Row="12" Grid.Row="13"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -318,7 +341,7 @@
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" /> Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
<TextBox <TextBox
x:Name="txtPreSocksPort" x:Name="txtPreSocksPort"
Grid.Row="12" Grid.Row="13"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
@@ -328,7 +351,7 @@
ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" /> ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" />
<TextBlock <TextBlock
Grid.Row="13" Grid.Row="14"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -336,7 +359,7 @@
Text="{x:Static resx:ResUI.LvMemo}" /> Text="{x:Static resx:ResUI.LvMemo}" />
<TextBox <TextBox
x:Name="txtMemo" x:Name="txtMemo"
Grid.Row="13" Grid.Row="14"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -19,6 +19,7 @@ public partial class SubEditWindow
this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Enabled, v => v.togEnable.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.AutoUpdateInterval, v => v.txtAutoUpdateInterval.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.UserAgent, v => v.txtUserAgent.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.RequestHeaders, v => v.txtRequestHeaders.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Sort, v => v.txtSort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Filter, v => v.txtFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.ConvertTarget, v => v.cmbConvertTarget.Text).DisposeWith(disposables);
@@ -81,6 +81,8 @@
Grid.Column="1" Grid.Column="1"
Width="120" Width="120"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
DisplayMemberPath="Display"
SelectedValuePath="Value"
Style="{StaticResource DefComboBox}" /> Style="{StaticResource DefComboBox}" />
</Grid> </Grid>
</StackPanel> </StackPanel>
+2 -2
View File
@@ -14,7 +14,7 @@ public partial class ThemeSettingView
cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>().Take(3).ToList(); cmbCurrentTheme.ItemsSource = Utils.GetEnumNames<ETheme>().Take(3).ToList();
cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList(); cmbCurrentFontSize.ItemsSource = Enumerable.Range(Global.MinFontSize, Global.MinFontSizeCount).ToList();
cmbCurrentLanguage.ItemsSource = Global.Languages; cmbCurrentLanguage.ItemsSource = Global.LanguageOptions;
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
@@ -22,7 +22,7 @@ public partial class ThemeSettingView
this.OneWayBind(ViewModel, vm => vm.Swatches, v => v.cmbSwatches.ItemsSource).DisposeWith(disposables); this.OneWayBind(ViewModel, vm => vm.Swatches, v => v.cmbSwatches.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSwatch, v => v.cmbSwatches.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSwatch, v => v.cmbSwatches.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CurrentFontSize, v => v.cmbCurrentFontSize.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CurrentFontSize, v => v.cmbCurrentFontSize.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CurrentLanguage, v => v.cmbCurrentLanguage.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CurrentLanguage, v => v.cmbCurrentLanguage.SelectedValue).DisposeWith(disposables);
}); });
} }
} }