Compare commits

..

42 Commits

Author SHA1 Message Date
2dust f5747bb321 up 7.25.5 2026-10-05 16:43:51 +08:00
ariansharifi d099e44ff6 Fix subscription custom configs ignoring custom config socks port (#10296)
* Fix subscription custom configs ignoring custom config socks port

Full Xray/sing-box configs imported from a subscription no longer got the
subscription's "Custom config socks port" (SubItem.PreSocksPort) after
eff5845 and 9638b4c. 7.24.4 and earlier copied it onto every imported
config, and the Clash, Hysteria2 and raw-file paths still do.

Without it, GetPreSocksItem builds no pre-socks core for these profiles,
so they cannot be used in TUN mode. Setting the port on the profile is
not a workaround, because every subscription update re-creates the
profiles.

* Add regression test for subscription custom config socks port
2026-10-05 16:36:52 +08:00
DHR60 c63d271d16 Up TUnit.Assertions.Should (#10294) 2026-10-05 16:27:38 +08:00
Miheichev Aleksandr Sergeevich 6152cab494 fix(ui): make cut-off button labels and input hints readable (#10292)
* fix(ui): let update and backup buttons fit their labels

The update dialog gives its two buttons a fixed 100 px width, and the
backup view puts its buttons in a fixed 200 px grid column, so longer
labels are cut off, for example "Only Chec", "Check and" and "Backup
to remote (WebD" in English. Use MinWidth="100" for the update buttons
and an Auto column with MinWidth="200" for the backup buttons: short
labels keep their current size and long ones fit.

The "Automatic update interval (minutes)" label of the subscription
window is cut off the same way between the switch and the interval
field, so let it wrap.

* fix(ui): show cut-off input hints as tooltips

The hints of the previous and next proxy remarks fields in the
subscription window and of the server and log filters are wider than
their fields, so their end is cut off and there is no way to read it.
Show the same text as a tooltip, as the custom config socks port field
of the subscription window already does.

* fix(ui): let dialog buttons and backup labels fit longer translations

The Confirm and Cancel buttons of the editing and settings windows,
and the Reset button of the hotkey window, have a fixed 100 px width,
and the backup view puts its labels in a fixed 300 px column.
Translations longer than the English text are cut off there: the
Russian "Подтвердить" reads "Подтверди", and "Резервное копирование
на удалённый сервер (WebDAV)" loses its end. Use MinWidth="100" for
these buttons, as the message box already does with MinWidth="90",
and an Auto label column with MinWidth="300".
2026-10-05 16:27:19 +08:00
2dust 3187eeef79 Code clean 2026-10-04 11:41:34 +08:00
Miheichev Aleksandr Sergeevich 3364ec0e57 i18n(ru): translate the update dialog buttons in full (#10291)
Add the Russian string for menuCheckAndUpdate, which #10282 added to
ResUI.resx, as "Проверить и обновить". Restore the full Russian text
of menuCheckOnly ("Только проверить") and menuCheckUpdate ("Проверить
обновления"), which were shortened only to fit the fixed 100 px
buttons of the update dialog; they fit once the buttons are sized by
their labels.
2026-10-04 11:08:10 +08:00
dependabot[bot] 23033fa002 Bump TUnit from 1.68.17 to 1.72.16 (#10288)
---
updated-dependencies:
- dependency-name: TUnit
  dependency-version: 1.72.16
  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-10-04 11:07:42 +08:00
Eliot the Cougar 125cbb18a1 Keep Xray startup logs visible in horizontal and vertical layouts (#10272)
* Keep Xray log content visible in separate log layouts

* Revert wpf

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-10-04 11:06:06 +08:00
2dust f85326cf0e Add per-core prerelease update selection
Replaces the global prerelease toggle with per-core prerelease options in both WPF and Avalonia update views, and persists selections via `CheckPreReleaseCoreTypes` in config. Update checks now resolve prerelease mode per selected core (including auto-check flow), and the sing-box max-version lock is removed to allow newer versions.
2026-10-03 14:58:45 +08:00
Tosd 3666fc549d Resx: clarify update button wording (#10282) 2026-10-02 15:08:27 +08:00
Miheichev Aleksandr Sergeevich a147249f95 i18n(ru): translate the Add [MASQUE] menu item (#10276)
Add the Russian string for menuAddMasqueServer, which #10233 added to
ResUI.resx. Without it the Russian UI shows this one item of the
Configuration menu in English. The text follows the other
Add [protocol] items: "Добавить сервер [MASQUE]".
2026-10-02 15:06:49 +08:00
Eliot the Cougar a28f68c3a7 Avoid reentrant log scrolling during Avalonia window dragging (#10271) 2026-10-01 19:55:30 +08:00
Ezra-Y 1a689f869c Fix macOS TUN interface name collisions (#10270) 2026-10-01 19:51:08 +08:00
DHR60 50023a5976 Xray tun features (#10264)
* Xray tun features

* Try sync
2026-10-01 14:55:09 +08:00
DHR60 c659ca8af8 Add MASQUE support (#10233)
* Add MASQUE support

* Add SettingsCoreType
2026-10-01 14:49:45 +08:00
2dust 7d6a967c18 up 7.25.4 2026-09-30 14:38:34 +08:00
2dust 6c2edcb382 Revert "Bump Avalonia.Desktop from 12.1.2 to 12.1.3 (#10216)"
This reverts commit e1cb99cd6e.
2026-09-30 14:34:07 +08:00
2dust 086c991464 up 7.25.3 2026-09-30 09:55:08 +08:00
Alice Apate a9da04acc2 Add support for TLS1.3-only subscription URL for macOS (#10256)
* add support for TLS1.3-only subscription url for macOS

* Update v2rayN.Desktop.csproj

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-09-30 09:54:26 +08:00
2dust e0c790eeb2 Tune speedtest parallelism
Set `MaxDegreeOfParallelism` explicitly to the current selection size in three speed test parallel sections so task fan-out matches the workload. Also wrapped socket connect timing in a catch block that ignores connection exceptions, preventing a failed connect from aborting the flow while still completing cleanup.
https://github.com/2dust/v2rayN/issues/10246
2026-09-28 10:28:32 +08:00
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
110 changed files with 1795 additions and 962 deletions
+1 -1
View File
@@ -146,7 +146,7 @@ jobs:
$xrayTag = "v$xrayVer"
$singTag = "v$singVer"
$xrayUrl = "https://github.com/autorepobot/Xray/releases/download/$xrayTag/Xray-windows-32.zip"
$xrayUrl = "https://github.com/XTLS/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
Write-Host "Bundled Xray version: $xrayVer"
+1 -1
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.25.1</Version>
<Version>7.25.5</Version>
</PropertyGroup>
<PropertyGroup>
+5 -5
View File
@@ -13,22 +13,22 @@
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.2" />
<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="MaterialDesignThemes" Version="5.3.2" />
<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.WPF" Version="24.2.0" />
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
<PackageVersion Include="NLog" Version="6.2.0" />
<PackageVersion Include="NLog" Version="6.2.1" />
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="TUnit" Version="1.66.27" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
<PackageVersion Include="TUnit" Version="1.72.16" />
<PackageVersion Include="TUnit.Assertions.Should" Version="1.72.16-beta" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
@@ -1,8 +1,3 @@
using System.Reflection;
using ServiceLib.Enums;
using ServiceLib.Manager;
using ServiceLib.Models;
namespace ServiceLib.Tests.CoreConfig;
internal static class CoreConfigTestFactory
@@ -716,7 +716,6 @@ public class CoreConfigSingboxServiceTests
}
}
[Test]
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
@@ -51,7 +51,7 @@ public class CoreConfigV2rayServiceTests
.Select(item => item!.GetValue<string>())
.Should().BeEquivalentTo(["a=1", "b=2"]);
}
[Test]
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
{
@@ -0,0 +1,42 @@
using ServiceLib.Tests.CoreConfig;
namespace ServiceLib.Tests.Handler;
public class ConfigHandlerCustomSubscriptionTests
{
private const string FullXrayConfigs = """
[
{"remarks":"custom-1","inbounds":[{"tag":"socks","listen":"127.0.0.1","port":10808,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],"outbounds":[{"tag":"proxy","protocol":"vless","settings":{"vnext":[{"address":"127.0.0.1","port":443,"users":[{"id":"00000000-0000-0000-0000-000000000001","encryption":"none"}]}]},"streamSettings":{"network":"tcp"}}],"routing":{"domainStrategy":"AsIs","rules":[]}},
{"remarks":"custom-2","inbounds":[{"tag":"socks","listen":"127.0.0.1","port":10808,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],"outbounds":[{"tag":"proxy","protocol":"vless","settings":{"vnext":[{"address":"127.0.0.1","port":443,"users":[{"id":"00000000-0000-0000-0000-000000000001","encryption":"none"}]}]},"streamSettings":{"network":"tcp"}}],"routing":{"domainStrategy":"AsIs","rules":[]}}
]
""";
[Test]
public async Task AddBatchServers_FullConfigsFromSubscription_InheritSubscriptionPreSocksPort()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
SQLiteHelper.Instance.CreateTable<SubItem>();
SQLiteHelper.Instance.CreateTable<ProfileItem>();
var subId = $"sub-{Guid.NewGuid():N}";
await SQLiteHelper.Instance.ReplaceAsync(new SubItem
{
Id = subId,
Remarks = "sub",
Url = "http://127.0.0.1/sub",
PreSocksPort = 10808,
});
var count = await ConfigHandler.AddBatchServers(config, FullXrayConfigs, subId, true);
var profiles = await AppManager.Instance.ProfileItems(subId) ?? [];
await count.Should().BeEqualTo(2);
await profiles.Should().HaveCount(2);
foreach (var profile in profiles)
{
await profile.ConfigType.Should().BeEqualTo(EConfigType.Custom);
await profile.PreSocksPort.Should().BeEqualTo(10808);
}
}
}
@@ -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}");
}
}
+120 -99
View File
@@ -2,16 +2,21 @@ namespace ServiceLib.UdpTest;
public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable
{
private bool _initialized;
private IPEndPoint _relayEndPoint;
private TcpClient _tcpClient;
private UdpClient _udpClient;
private IPEndPoint _relayEndPoint;
private bool _initialized = false;
public void Dispose()
{
_tcpClient?.Dispose();
_udpClient?.Dispose();
}
/// <summary>
/// Send UDP data to a remote endpoint (IP address)
/// Send UDP data to a remote endpoint (IP address)
/// </summary>
public async Task SendAsync(IPEndPoint remote, byte[] data)
public async Task SendAsync(IPEndPoint remote, byte[] data, CancellationToken ct = default)
{
var addrData = new Socks5AddressData
{
@@ -19,19 +24,20 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
? Socks5AddressData.AddrTypeIPv4
: Socks5AddressData.AddrTypeIPv6,
Host = remote.Address.ToString(),
Port = (ushort)remote.Port
Port = (ushort)remote.Port,
};
var packet = BuildSocks5UdpPacket(addrData, data);
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
}
/// <summary>
/// Send UDP data to a remote endpoint (domain name or IP address)
/// Send UDP data to a remote endpoint (domain name or IP address)
/// </summary>
/// <param name="host">Domain name or IP address</param>
/// <param name="port">Port number</param>
/// <param name="data">Data to send</param>
public async Task SendAsync(string host, ushort port, byte[] data)
/// <param name="ct">Cancellation token</param>
public async Task SendAsync(string host, ushort port, byte[] data, CancellationToken ct = default)
{
var addrData = new Socks5AddressData();
@@ -53,11 +59,12 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
addrData.Port = port;
var packet = BuildSocks5UdpPacket(addrData, data);
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
// await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
}
/// <summary>
/// Receive UDP data from remote endpoint
/// Receive UDP data from remote endpoint
/// </summary>
/// <param name="cancellationToken">Cancellation token to cancel the receive operation</param>
/// <returns>Remote endpoint information and received data</returns>
@@ -69,16 +76,6 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
return (remote, payload);
}
/// <summary>
/// Represents a remote endpoint that can be either an IP address or a domain name
/// </summary>
public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
{
public string Host { get; set; } = host;
public ushort Port { get; set; } = port;
public bool IsDomain { get; set; } = isDomain;
}
private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data)
{
using var ms = new MemoryStream();
@@ -104,6 +101,11 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
throw new ArgumentException("Invalid SOCKS5 UDP packet: too short");
}
if (packet[0] != 0x00 || packet[1] != 0x00)
{
throw new ArgumentException("Invalid SOCKS5 UDP packet: RSV field must be 0");
}
var offset = 0;
// RSV (2 bytes) - Reserved field, skip
@@ -196,83 +198,34 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
return (remote, data);
}
public void Dispose()
/// <summary>
/// Represents a remote endpoint that can be either an IP address or a domain name
/// </summary>
public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
{
_tcpClient.Dispose();
_udpClient.Dispose();
public string Host { get; set; } = host;
public ushort Port { get; set; } = port;
public bool IsDomain { get; set; } = isDomain;
}
#region SOCKS5 Connection Handling
private const byte Socks5Version = 0x05;
private const byte SocksCmdUdpAssociate = 0x03;
public async Task<bool> EstablishUdpAssociationAsync(CancellationToken cancellationToken)
{
if (_initialized)
{
Dispose();
_initialized = false;
}
_udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
_tcpClient = new TcpClient();
try
{
await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
}
catch (SocketException)
{
return false;
}
var tcpControlStream = _tcpClient.GetStream();
byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
var handshakeResponse = new byte[2];
if (await tcpControlStream.ReadAsync(handshakeResponse, cancellationToken).ConfigureAwait(false) < 2 ||
handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
{
return false;
}
var clientAddrForSocks = new Socks5AddressData
{
AddressType = Socks5AddressData.AddrTypeIPv4,
Host = "0.0.0.0",
Port = 0
};
using var udpAssociateReqMs = new MemoryStream();
udpAssociateReqMs.WriteByte(Socks5Version);
udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
udpAssociateReqMs.WriteByte(0x00);
udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
var verRepRsv = new byte[3];
if (await tcpControlStream.ReadAsync(verRepRsv, cancellationToken).ConfigureAwait(false) < 3 ||
verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
{
return false;
}
var proxyRelaySocksAddr =
await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
{
return false;
}
_relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
_initialized = true;
return true;
}
#endregion SOCKS5 Connection Handling
#region SOCKS5 Address Handling
private static async Task<bool> TryReadExactlyAsync(
Stream stream,
Memory<byte> buffer,
CancellationToken cancellationToken)
{
try
{
await stream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false);
return true;
}
catch (EndOfStreamException)
{
return false;
}
}
private class Socks5AddressData
{
public const byte AddrTypeIPv4 = 0x01;
@@ -343,7 +296,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
var typeByte = new byte[1];
try
{
if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
if (!await TryReadExactlyAsync(stream, typeByte, ct).ConfigureAwait(false))
{
return null;
}
@@ -353,7 +306,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
{
case AddrTypeIPv4:
var ipv4Bytes = new byte[4];
if (await stream.ReadAsync(ipv4Bytes.AsMemory(0, 4), ct).ConfigureAwait(false) < 4)
if (!await TryReadExactlyAsync(stream, ipv4Bytes, ct).ConfigureAwait(false))
{
return null;
}
@@ -363,7 +316,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
case AddrTypeDomain:
var lenByte = new byte[1];
if (await stream.ReadAsync(lenByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
if (!await TryReadExactlyAsync(stream, lenByte, ct).ConfigureAwait(false))
{
return null;
}
@@ -375,8 +328,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
else
{
var domainBytes = new byte[lenByte[0]];
if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct)
.ConfigureAwait(false) < domainBytes.Length)
if (!await TryReadExactlyAsync(stream, domainBytes, ct).ConfigureAwait(false))
{
return null;
}
@@ -388,7 +340,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
case AddrTypeIPv6:
var ipv6Bytes = new byte[16];
if (await stream.ReadAsync(ipv6Bytes.AsMemory(0, 16), ct).ConfigureAwait(false) < 16)
if (!await TryReadExactlyAsync(stream, ipv6Bytes, ct).ConfigureAwait(false))
{
return null;
}
@@ -401,7 +353,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
}
var portBytes = new byte[2];
if (await stream.ReadAsync(portBytes.AsMemory(0, 2), ct).ConfigureAwait(false) < 2)
if (!await TryReadExactlyAsync(stream, portBytes, ct).ConfigureAwait(false))
{
return null;
}
@@ -417,4 +369,73 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
}
#endregion SOCKS5 Address Handling
#region SOCKS5 Connection Handling
private const byte Socks5Version = 0x05;
private const byte SocksCmdUdpAssociate = 0x03;
public async Task<bool> EstablishUdpAssociationAsync(CancellationToken cancellationToken)
{
if (_initialized)
{
Dispose();
_initialized = false;
}
_udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
_tcpClient = new TcpClient();
try
{
await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
}
catch (SocketException)
{
return false;
}
var tcpControlStream = _tcpClient.GetStream();
byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
var handshakeResponse = new byte[2];
if (!await TryReadExactlyAsync(tcpControlStream, handshakeResponse, cancellationToken).ConfigureAwait(false) ||
handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
{
return false;
}
var clientAddrForSocks = new Socks5AddressData
{
AddressType = Socks5AddressData.AddrTypeIPv4,
Host = "0.0.0.0",
Port = 0,
};
using var udpAssociateReqMs = new MemoryStream();
udpAssociateReqMs.WriteByte(Socks5Version);
udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
udpAssociateReqMs.WriteByte(0x00);
udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
var verRepRsv = new byte[3];
if (!await TryReadExactlyAsync(tcpControlStream, verRepRsv, cancellationToken).ConfigureAwait(false) ||
verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
{
return false;
}
var proxyRelaySocksAddr =
await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
{
return false;
}
_relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
_initialized = true;
return true;
}
#endregion SOCKS5 Connection Handling
}
@@ -13,7 +13,7 @@ public class DnsService : IUdpTest
// Question: www.google.com, Type A, Class IN
0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F,
0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00,
0x00, 0x01, 0x00, 0x01
0x00, 0x01, 0x00, 0x01,
];
public byte[] BuildUdpRequestPacket()
@@ -17,13 +17,13 @@ public class McBeService : IUdpTest
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
// Client GUID (random 16 bytes)
0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E,
0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B
0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B,
];
private static readonly byte[] McBeMagicBytes =
[
0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
];
private static readonly List<string> ValidGameModes =
@@ -31,7 +31,7 @@ public class McBeService : IUdpTest
"Survival",
"Creative",
"Adventure",
"Spectator"
"Spectator",
];
public byte[] BuildUdpRequestPacket()
@@ -61,6 +61,10 @@ public class McBeService : IUdpTest
return false; // Magic bytes do not match
}
var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]);
if (mcbeResponseBytes.Length < 35 + stringLength)
{
return false; // Not enough data for the string
}
var stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray());
var stringParts = stringData.Split(';');
// check Game Mode str
@@ -31,10 +31,7 @@ public class StunService : IUdpTest
if (stunResponseBytes.Length >= 2)
{
var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1];
if (messageType is 0x0101 or 0x0111)
{
return true;
}
return messageType is 0x0101 or 0x0111;
}
return true;
+22 -19
View File
@@ -5,7 +5,6 @@ namespace ServiceLib.UdpTest;
public class UdpTestService
{
private const string DefaultUdpTestType = "ntp";
private readonly IUdpTest _udpTest;
private static readonly IReadOnlyDictionary<string, Func<IUdpTest>> UdpTestFactories =
new Dictionary<string, Func<IUdpTest>>(StringComparer.OrdinalIgnoreCase)
@@ -16,6 +15,8 @@ public class UdpTestService
["mcbe"] = () => new McBeService(),
};
private readonly IUdpTest _udpTest;
private UdpTestService(IUdpTest udpTest)
{
_udpTest = udpTest;
@@ -88,24 +89,26 @@ public class UdpTestService
return (targetServerHost, _udpTest.GetDefaultTargetPort());
}
public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port, TimeSpan operationTimeout)
public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port,
CancellationToken ct = default)
{
using var cts = new CancellationTokenSource(operationTimeout);
var cancellationToken = cts.Token;
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
var linkedCt = linkedCts.Token;
var udpRequestPacket = _udpTest.BuildUdpRequestPacket();
if (udpRequestPacket == null || udpRequestPacket.Length == 0)
{
throw new InvalidOperationException("Failed to build UDP request packet.");
}
using var channel = new Socks5UdpChannel("127.0.0.1", socks5Port);
if (!await channel.EstablishUdpAssociationAsync(cancellationToken).ConfigureAwait(false))
if (!await channel.EstablishUdpAssociationAsync(linkedCt).ConfigureAwait(false))
{
throw new Exception("Failed to establish UDP association with SOCKS5 proxy.");
}
var (targetHost, targetPort) = ParseHostAndPort(targetServerHost);
byte[] udpReceiveResult = null;
byte[]? validUdpReceiveResult = null;
// Get minimum round trip time from two attempts
var roundTripTime = TimeSpan.MaxValue;
@@ -116,11 +119,15 @@ public class UdpTestService
{
var stopwatch = new Stopwatch();
stopwatch.Start();
await channel.SendAsync(targetHost, targetPort, udpRequestPacket).ConfigureAwait(false);
var (_, receiveResult) = await channel.ReceiveAsync(cancellationToken).ConfigureAwait(false);
await channel.SendAsync(targetHost, targetPort, udpRequestPacket, linkedCt).ConfigureAwait(false);
var (_, receiveResult) = await channel.ReceiveAsync(linkedCt).ConfigureAwait(false);
stopwatch.Stop();
udpReceiveResult = receiveResult;
if (!_udpTest.VerifyAndExtractUdpResponse(receiveResult))
{
continue;
}
validUdpReceiveResult = receiveResult;
var currentRoundTripTime = stopwatch.Elapsed;
if (currentRoundTripTime < roundTripTime)
@@ -128,6 +135,10 @@ public class UdpTestService
roundTripTime = currentRoundTripTime;
}
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
throw;
}
catch
{
if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
@@ -137,18 +148,10 @@ public class UdpTestService
}
}
if ((udpReceiveResult?.Length ?? 0) < 4 + 1 + 4 + 2)
{
throw new Exception("Received NTP response is too short.");
}
if (udpReceiveResult != null && _udpTest.VerifyAndExtractUdpResponse(udpReceiveResult))
if (validUdpReceiveResult != null)
{
return roundTripTime;
}
else
{
throw new Exception("Failed to verify and extract UDP response.");
}
throw new Exception("Failed to verify and extract UDP response.");
}
}
@@ -8,8 +8,13 @@ public class BulkObservableCollection<T> : ObservableCollection<T>
{
}
public BulkObservableCollection(IEnumerable<T> collection) : base(collection) { }
public BulkObservableCollection(List<T> list) : base(list) { }
public BulkObservableCollection(IEnumerable<T> collection) : base(collection)
{
}
public BulkObservableCollection(List<T> list) : base(list)
{
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
+3 -1
View File
@@ -1,6 +1,8 @@
namespace ServiceLib.Base;
public class MyReactiveObject : ReactiveObject
public class MyReactiveObject : ReactiveObject, IActivatableViewModel
{
protected static Config? _config;
public ViewModelActivator Activator { get; } = new();
}
+13 -19
View File
@@ -136,29 +136,23 @@ public static class Extension
.Replace("\n", replacement);
}
public static async Task<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction,
TInput input,
public static IObservable<TOutput> HandleSafe<TInput, TOutput>(
this Interaction<TInput, TOutput> interaction, TInput input,
TOutput defaultValue = default!,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
try
{
return await interaction.Handle(input);
}
catch (UnhandledInteractionException<TInput, TOutput> ex)
{
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}";
Logging.SaveLog(title, ex);
return defaultValue;
}
catch (Exception ex)
{
var title = $"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}";
Logging.SaveLog(title, ex);
return defaultValue;
}
return Signal.Defer(() => interaction.Handle(input))
.Catch<TOutput, UnhandledInteractionException<TInput, TOutput>>(ex =>
{
Logging.SaveLog($"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}", ex);
return Signal.Return(defaultValue);
})
.Catch<TOutput, Exception>(ex =>
{
Logging.SaveLog($"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}", ex);
return Signal.Return(defaultValue);
});
}
}
+63 -4
View File
@@ -501,6 +501,31 @@ public class Utils
};
}
public static List<(string, string)> ParseHeaders(string? headers)
{
var result = new List<(string, string)>();
if (headers.IsNullOrEmpty())
{
return result;
}
var lines = headers.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var parts = line.Split(new[] { ':' }, 2);
if (parts.Length != 2)
{
continue;
}
var key = parts[0].Trim();
var value = parts[1].Trim();
if (!string.IsNullOrEmpty(key))
{
result.Add((key, value));
}
}
return result;
}
#endregion Conversion Functions
#region Data Checks
@@ -726,6 +751,40 @@ public class Utils
return false;
}
/// <summary>
/// Regex match with a timeout guard. Filter patterns can come from user
/// input or subscription content while the tested text (remarks, log
/// messages) is attacker-influenced, so an evil pattern like (a+)+$
/// would otherwise hang the caller (ReDoS). On timeout or invalid
/// pattern, fail open (return true) so no node/message is silently
/// dropped; the incident is logged.
/// </summary>
public static bool IsRegexMatch(string? input, string? pattern, int timeoutSeconds = 2)
{
if (pattern.IsNullOrEmpty())
{
return true;
}
if (input.IsNullOrEmpty())
{
return false;
}
try
{
return Regex.IsMatch(input, pattern, RegexOptions.None, TimeSpan.FromSeconds(timeoutSeconds));
}
catch (RegexMatchTimeoutException ex)
{
Logging.SaveLog("IsRegexMatch timeout", ex);
return true;
}
catch (ArgumentException ex)
{
Logging.SaveLog("IsRegexMatch invalid pattern", ex);
return true;
}
}
#endregion Data Checks
#region Speed Test
@@ -1018,12 +1077,12 @@ public class Utils
return new Dictionary<string, string>();
}
public static async Task<string?> GetCliWrapOutput(string filePath, string? arg)
public static async Task<string?> GetCliWrapOutput(string filePath, string? arg, CancellationToken cancellationToken = default)
{
return await GetCliWrapOutput(filePath, arg != null ? new List<string>() { arg } : null);
return await GetCliWrapOutput(filePath, arg != null ? new List<string>() { arg } : null, cancellationToken);
}
public static async Task<string?> GetCliWrapOutput(string filePath, IEnumerable<string>? args)
public static async Task<string?> GetCliWrapOutput(string filePath, IEnumerable<string>? args, CancellationToken cancellationToken = default)
{
try
{
@@ -1040,7 +1099,7 @@ public class Utils
}
}
var result = await cmd.ExecuteBufferedAsync();
var result = await cmd.ExecuteBufferedAsync(cancellationToken);
if (result.IsSuccess)
{
return result.StandardOutput ?? "";
+1
View File
@@ -15,6 +15,7 @@ public enum EConfigType
Anytls = 11,
Naive = 12,
Outbound = 13,
MASQUE = 14,
PolicyGroup = 101,
ProxyChain = 102,
}
+10 -1
View File
@@ -92,10 +92,16 @@ public class Global
public const string XrayLocalAsset = "XRAY_LOCATION_ASSET";
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
public const int SpeedTestPageSize = 1000;
public const int SpeedTestConcurrencyCountMin = 10;
public const string LinuxBash = "/bin/bash";
public const string StringTrue = "true";
public const string StringFalse = "false";
public const int SqliteMaxBatchSize = 10000;
public static readonly TimeSpan LocalFetch = TimeSpan.FromSeconds(5);
public static readonly TimeSpan DirectFetch = TimeSpan.FromSeconds(10);
public static readonly TimeSpan ProxyFetch = TimeSpan.FromSeconds(30);
public static readonly TimeSpan DirectDownloadConnect = TimeSpan.FromSeconds(5);
public static readonly TimeSpan ProxyDownloadConnect = TimeSpan.FromSeconds(10);
public const string SingboxDirectDNSTagPrefix = "direct-dns-";
public const string SingboxRemoteDNSTagPrefix = "remote-dns-";
@@ -255,7 +261,8 @@ public class Global
{ EConfigType.TUIC, "tuic" },
{ EConfigType.WireGuard, "wireguard" },
{ EConfigType.Anytls, "anytls" },
{ EConfigType.Naive, "naive" }
{ EConfigType.Naive, "naive" },
{ EConfigType.MASQUE, "masque" },
};
public static readonly List<string> VmessSecurities =
@@ -365,6 +372,7 @@ public class Global
EConfigType.Trojan,
EConfigType.Hysteria2,
EConfigType.WireGuard,
EConfigType.MASQUE,
EConfigType.SOCKS,
EConfigType.HTTP,
];
@@ -380,6 +388,7 @@ public class Global
EConfigType.Anytls,
EConfigType.Naive,
EConfigType.WireGuard,
EConfigType.MASQUE,
EConfigType.SOCKS,
EConfigType.HTTP,
];
+5 -5
View File
@@ -6,11 +6,6 @@ global using System.Diagnostics;
global using System.Net;
global using System.Net.NetworkInformation;
global using System.Net.Sockets;
global using ReactiveUI.Primitives;
global using ReactiveUI.Primitives.Concurrency;
global using ReactiveUI.Primitives.Extensions;
global using ReactiveUI.Primitives.Disposables;
global using ReactiveUI.Primitives.Signals;
global using System.Reflection;
global using System.Runtime.InteropServices;
global using System.Runtime.Versioning;
@@ -22,6 +17,11 @@ global using System.Text.Json.Nodes;
global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions;
global using ReactiveUI;
global using ReactiveUI.Primitives;
global using ReactiveUI.Primitives.Concurrency;
global using ReactiveUI.Primitives.Disposables;
global using ReactiveUI.Primitives.Extensions;
global using ReactiveUI.Primitives.Signals;
global using ReactiveUI.SourceGenerators;
global using ServiceLib.Base;
global using ServiceLib.Common;
@@ -49,6 +49,7 @@ public class CoreConfigContextBuilder
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
IsWindows = Utils.IsWindows(),
IsMacOS = Utils.IsMacOS(),
IsLinux = Utils.IsLinux(),
HasGlobalIPv6Address = Utils.HasGlobalIPv6Address(),
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
};
+33 -9
View File
@@ -136,10 +136,7 @@ public static class ConfigHandler
{
config.SpeedTestItem.SpeedPingTestUrl = Global.SpeedPingTestUrls.First();
}
if (config.SpeedTestItem.MixedConcurrencyCount < 1)
{
config.SpeedTestItem.MixedConcurrencyCount = 5;
}
config.SpeedTestItem.MixedConcurrencyCount = Math.Max(config.SpeedTestItem.MixedConcurrencyCount, Global.SpeedTestConcurrencyCountMin);
if (config.SpeedTestItem.UdpTestTarget.IsNullOrEmpty())
{
config.SpeedTestItem.UdpTestTarget = Global.UdpTestTargets.First();
@@ -295,6 +292,7 @@ public static class ConfigHandler
EConfigType.WireGuard => await AddWireguardServer(config, item),
EConfigType.Anytls => await AddAnytlsServer(config, item),
EConfigType.Naive => await AddNaiveServer(config, item),
EConfigType.MASQUE => await AddMasqueServer(config, item),
_ => -1,
};
return ret;
@@ -995,6 +993,29 @@ public static class ConfigHandler
return 0;
}
public static async Task<int> AddMasqueServer(Config config, ProfileItem profileItem, bool toFile = true)
{
profileItem.ConfigType = EConfigType.MASQUE;
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Username = profileItem.Username.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty;
profileItem.Network = string.Empty;
profileItem.AllowInsecure = string.Empty;
if (profileItem.StreamSecurity.IsNullOrEmpty())
{
profileItem.StreamSecurity = Global.StreamSecurity;
}
if (profileItem.Password.IsNullOrEmpty())
{
return -1;
}
await AddServerCommon(config, profileItem, toFile);
return 0;
}
/// <summary>
/// Sort the server list by the specified column
/// Updates the sort order in the profile extension data
@@ -1528,7 +1549,7 @@ public static class ConfigHandler
p != null &&
p.IsValid() &&
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
Utils.IsRegexMatch(p.Remarks, extraItem.Filter)
)
.ToList() ?? [];
if (matchedChildProfiles.Count == 0)
@@ -1667,7 +1688,7 @@ public static class ConfigHandler
//exist sub items //filter
if (isSub && subid.IsNotEmpty() && subFilter.IsNotEmpty())
{
if (!Regex.IsMatch(profileItem.Remarks, subFilter))
if (!Utils.IsRegexMatch(profileItem.Remarks, subFilter))
{
continue;
}
@@ -1687,6 +1708,7 @@ public static class ConfigHandler
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
EConfigType.MASQUE => await AddMasqueServer(config, profileItem, false),
_ => -1,
};
@@ -1756,7 +1778,7 @@ public static class ConfigHandler
}
if (lstProfiles.Count > 0)
{
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub, subItem);
if (count > 0)
{
return count;
@@ -1802,7 +1824,7 @@ public static class ConfigHandler
if (lstProfiles?.Count > 0)
{
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub, subItem);
if (count > 0)
{
return count;
@@ -1816,7 +1838,8 @@ public static class ConfigHandler
Config config,
List<ProfileItem> lstProfiles,
string subid,
bool isSub)
bool isSub,
SubItem? subItem)
{
var count = 0;
foreach (var it in lstProfiles)
@@ -1826,6 +1849,7 @@ public static class ConfigHandler
if (it.ConfigType == EConfigType.Custom)
{
it.PreSocksPort = subItem?.PreSocksPort;
if (await AddCustomServer(config, it, true) == 0)
{
count++;
+19 -8
View File
@@ -66,34 +66,41 @@ public static class ConnectionHandler
/// <summary>
/// Measures response time by sending HTTP requests through proxy.
/// </summary>
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout = 9)
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, CancellationToken cancellationToken = default)
{
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
var responseTime = -1;
try
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout));
using var timeoutCts = new CancellationTokenSource();
timeoutCts.CancelAfter(Global.LocalFetch);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
var linkedToken = linkedCts.Token;
using var client = new HttpClient(new SocketsHttpHandler()
{
Proxy = webProxy,
UseProxy = webProxy != null,
ConnectTimeout = TimeSpan.FromSeconds(3)
ConnectTimeout = Global.LocalFetch,
});
List<int> oneTime = [];
for (var i = 0; i < 2; i++)
{
var timer = Stopwatch.StartNew();
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
await client.GetAsync(url, linkedToken).ConfigureAwait(false);
timer.Stop();
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
await Task.Delay(100, cts.Token);
await Task.Delay(100, linkedToken);
}
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch
{
// Ignore
}
return responseTime;
}
@@ -101,7 +108,7 @@ public static class ConnectionHandler
/// <summary>
/// Gets IP and country information through specified proxy.
/// </summary>
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy)
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy, CancellationToken cancellationToken = default)
{
try
{
@@ -112,7 +119,7 @@ public static class ConnectionHandler
}
var downloadHandle = new DownloadService();
var result = await downloadHandle.TryDownloadString(url, webProxy, "");
var result = await downloadHandle.TryDownloadString(url, webProxy, "", cancellationToken);
if (result == null)
{
return null;
@@ -129,6 +136,10 @@ public static class ConnectionHandler
return new IpInfoResult(country, ip);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch
{
return null;
@@ -24,6 +24,24 @@ public class WireguardFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
var finalmaskDecoded = GetQueryDecoded(query, "fm");
if (finalmaskDecoded.IsNotEmpty())
{
var node = JsonUtils.ParseJson(finalmaskDecoded);
item.Finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
: finalmaskDecoded;
}
else
{
item.Finalmask = string.Empty;
}
item.SetProtocolExtra(item.GetProtocolExtra() with
{
WgPublicKey = GetQueryDecoded(query, "publickey"),
@@ -76,6 +94,19 @@ public class WireguardFmt : BaseFmt
{
dicQuery.Add("dns", Utils.UrlEncode(protoExtra.WgDns));
}
if (item.Finalmask.IsNotEmpty())
{
var node = JsonUtils.ParseJson(item.Finalmask);
var finalmask = node != null
? JsonUtils.Serialize(node, new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
: item.Finalmask;
dicQuery.Add("fm", Utils.UrlEncode(finalmask));
}
return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
}
@@ -3,6 +3,7 @@ namespace ServiceLib.Handler.SysProxy;
public static class 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)
{
@@ -56,7 +57,7 @@ public static class SysProxyHandler
if (type != ESysProxyType.Pac && Utils.IsWindows())
{
PacManager.Instance.Stop();
_pacManager.Value.Stop();
}
}
catch (Exception ex)
@@ -110,7 +111,7 @@ public static class SysProxyHandler
private static async Task SetWindowsProxyPac(int port)
{
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}";
ProxySettingWindows.SetProxy(strProxy, "", 4);
}
+45 -52
View File
@@ -8,16 +8,14 @@ public class DownloaderHelper
private static readonly Lazy<DownloaderHelper> _instance = new(() => new());
public static DownloaderHelper Instance => _instance.Value;
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout,
IReadOnlyDictionary<string, string>? requestHeaders = null, string? acceptHeader = null)
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent,
IReadOnlyDictionary<string, string>? requestHeaders = null, string? acceptHeader = null, CancellationToken cancellationToken = default)
{
if (url.IsNullOrEmpty())
{
return null;
}
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
Uri uri = new(url);
//Authorization Header
var headers = new WebHeaderCollection();
@@ -31,12 +29,11 @@ public class DownloaderHelper
Headers = headers,
Accept = acceptHeader,
UserAgent = userAgent,
ConnectTimeout = connectTimeout * 1000,
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration()
{
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => HttpRequestHeadersHelper.CreateHandler(GetSocketsHttpHandler(requestConfiguration), requestHeaders),
@@ -51,31 +48,26 @@ public class DownloaderHelper
}
};
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
using StreamReader reader = new(stream);
return await reader.ReadToEndAsync(cts.Token);
return await reader.ReadToEndAsync(cancellationToken);
}
public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, IProgress<string> progress, int timeout)
public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, Action<string> onProgress, CancellationToken cancellationToken = default)
{
if (url.IsNullOrEmpty())
{
throw new ArgumentNullException(nameof(url));
}
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var requestConfiguration = new RequestConfiguration()
{
ConnectTimeout = connectTimeout * 1000,
ConnectTimeout = GetConnectTimeoutMs(true),
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration()
{
BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2,
RequestConfiguration = requestConfiguration,
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
@@ -88,49 +80,45 @@ public class DownloaderHelper
downloader.DownloadProgressChanged += (sender, value) =>
{
if (progress != null && value.BytesPerSecondSpeed > 0)
if (!(value.BytesPerSecondSpeed > 0))
{
hasValue = true;
if (value.BytesPerSecondSpeed > maxSpeed)
{
maxSpeed = value.BytesPerSecondSpeed;
}
return;
}
hasValue = true;
if (value.BytesPerSecondSpeed > maxSpeed)
{
maxSpeed = value.BytesPerSecondSpeed;
}
var ts = DateTime.Now - lastUpdateTime;
if (ts.TotalMilliseconds >= 1000)
{
lastUpdateTime = DateTime.Now;
var speed = (maxSpeed / 1000 / 1000).ToString("#0.0");
progress.Report(speed);
}
var ts = DateTime.Now - lastUpdateTime;
if (ts.TotalMilliseconds >= 1000)
{
lastUpdateTime = DateTime.Now;
var speed = (maxSpeed / 1000 / 1000).ToString("#0.0");
onProgress.Invoke(speed);
}
};
downloader.DownloadFileCompleted += (sender, value) =>
{
if (progress != null)
if (hasValue && maxSpeed > 0)
{
if (hasValue && maxSpeed > 0)
{
var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0");
progress.Report(finalSpeed);
}
else if (value.Error != null)
{
progress.Report(value.Error?.Message);
}
else
{
progress.Report("0");
}
var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0");
onProgress.Invoke(finalSpeed);
}
else if (value.Error != null)
{
onProgress.Invoke(value.Error?.Message);
}
else
{
onProgress.Invoke("0");
}
};
//progress.Report("......");
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
//progress.Invoke("......");
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
}
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
if (request.FilePath.IsNullOrEmpty())
@@ -147,9 +135,9 @@ public class DownloaderHelper
Request = request,
};
var requestConfiguration = new RequestConfiguration()
var requestConfiguration = new RequestConfiguration
{
ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
Proxy = webProxy,
};
var downloadOpt = new DownloadConfiguration()
@@ -196,7 +184,7 @@ public class DownloaderHelper
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken);
}
public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, CancellationToken cancellationToken = default)
{
if (requests is not { Count: > 0 })
{
@@ -215,7 +203,7 @@ public class DownloaderHelper
var requestConfiguration = new RequestConfiguration()
{
ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
Proxy = webProxy,
KeepAlive = true,
@@ -225,7 +213,7 @@ public class DownloaderHelper
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = 4,
//CancellationToken = cancellationToken,
CancellationToken = cancellationToken,
};
await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) =>
@@ -344,4 +332,9 @@ public class DownloaderHelper
return handler;
}
private int GetConnectTimeoutMs(bool isProxy)
{
return (int)(isProxy ? Global.ProxyDownloadConnect : Global.DirectDownloadConnect).TotalMilliseconds;
}
}
+7 -3
View File
@@ -22,7 +22,7 @@ public class HttpClientHelper
this.httpClient = httpClient;
}
public async Task<string?> TryGetAsync(string url)
public async Task<string?> TryGetAsync(string url, CancellationToken cancellationToken = default)
{
if (url.IsNullOrEmpty())
{
@@ -31,8 +31,12 @@ public class HttpClientHelper
try
{
var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
var response = await httpClient.GetAsync(url, cancellationToken);
return await response.Content.ReadAsStringAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch
{
+20 -7
View File
@@ -75,15 +75,28 @@ public sealed class SQLiteHelper
public async Task DisposeDbConnectionAsync()
{
await Task.Factory.StartNew(() =>
await Task.Run(() =>
{
_db?.Close();
_db?.Dispose();
_db = null;
try
{
_db?.Close();
_db?.Dispose();
}
finally
{
_db = null;
}
_dbAsync?.GetConnection()?.Close();
_dbAsync?.GetConnection()?.Dispose();
_dbAsync = null;
try
{
var conn = _dbAsync?.GetConnection();
conn?.Close();
conn?.Dispose();
}
finally
{
_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
/// </summary>
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4)
List<string>? verifyPeerCertByName = null)
{
try
{
var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var cts = new CancellationTokenSource(Global.LocalFetch);
using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
@@ -63,8 +62,8 @@ public class CertPemManager
}
catch (OperationCanceledException)
{
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
return (null, $"Connection timeout after {timeout} seconds");
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"));
return (null, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds");
}
catch (Exception ex)
{
@@ -77,15 +76,14 @@ public class CertPemManager
/// Get certificate chain in PEM format from a server with CA pinning validation
/// </summary>
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
List<string>? verifyPeerCertByName = null, int timeout = 4)
List<string>? verifyPeerCertByName = null)
{
var pemList = new List<string>();
try
{
var (domain, _, port, _) = Utils.ParseUrl(target);
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var cts = new CancellationTokenSource(Global.LocalFetch);
using var client = new TcpClient();
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
@@ -116,8 +114,8 @@ public class CertPemManager
}
catch (OperationCanceledException)
{
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
return (pemList, $"Connection timeout after {timeout} seconds");
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"));
return (pemList, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds");
}
catch (Exception ex)
{
+1 -1
View File
@@ -229,7 +229,7 @@ public sealed class CoreInfoManager
DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz",
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz",
LockedMaxVersion = new SemanticVersion(1, 14, int.MaxValue),
//LockedMaxVersion = new SemanticVersion(1, 14, int.MaxValue),
Match = "sing-box",
VersionArg = "version",
},
+2 -2
View File
@@ -216,7 +216,7 @@ public class CoreManager
await _updateFunc?.Invoke(notify, msg);
}
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000)
private static async Task WaitForProxyPort(CoreConfigContext? preContext)
{
if (preContext is null)
{
@@ -227,7 +227,7 @@ public class CoreManager
return;
}
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs));
using var rootCts = new CancellationTokenSource(Global.LocalFetch);
var rootToken = rootCts.Token;
var port = preContext.Node.Port;
@@ -119,7 +119,7 @@ public class GroupProfileManager
p != null &&
p.IsValid() &&
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
Utils.IsRegexMatch(p.Remarks, extra.Filter)
)
.ToList() ?? [];
}
+49 -64
View File
@@ -2,36 +2,37 @@ namespace ServiceLib.Manager;
public class PacManager
{
private static readonly Lazy<PacManager> _instance = new(() => new PacManager());
public static PacManager Instance => _instance.Value;
private int _httpPort;
private const string Tag = "PacManager";
private CancellationTokenSource? _cts;
private int _pacPort;
private TcpListener? _tcpListener;
private byte[] _writeContent;
private bool _isRunning;
private bool _needRestart = true;
private byte[] _writeContent = [];
public async Task StartAsync(int httpPort, int pacPort)
{
_needRestart = httpPort != _httpPort || pacPort != _pacPort || !_isRunning;
var content = await InitText(httpPort);
_writeContent = content;
_httpPort = httpPort;
_pacPort = pacPort;
await InitText();
if (_needRestart)
if (_tcpListener is not null && _pacPort == pacPort)
{
Stop();
RunListener();
return;
}
Stop();
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 fileName = (customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath))
var customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem.CustomSystemProxyPacPath;
var fileName = customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath)
? customSystemProxyPacPath
: Path.Combine(Utils.GetConfigPath(), "pac.txt");
@@ -45,7 +46,7 @@ public class PacManager
}
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();
sb.AppendLine("HTTP/1.0 200 OK");
@@ -54,59 +55,43 @@ public class PacManager
sb.AppendLine("Content-Length:" + Encoding.UTF8.GetByteCount(pacText));
sb.AppendLine();
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 () =>
var buffer = new byte[1024];
try
{
while (_isRunning)
while (!token.IsCancellationRequested)
{
try
{
if (!_tcpListener.Pending())
{
await Task.Delay(10);
continue;
}
var client = await _tcpListener.AcceptTcpClientAsync();
await Task.Run(() => WriteContent(client));
}
catch
{
// ignored
}
using var client = await listener.AcceptTcpClientAsync(token).ConfigureAwait(false);
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);
}
}, TaskCreationOptions.LongRunning);
}
private void WriteContent(TcpClient client)
{
var stream = client.GetStream();
stream.Write(_writeContent, 0, _writeContent.Length);
stream.Flush();
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logging.SaveLog(Tag, ex);
}
finally
{
listener.Stop();
}
}
public void Stop()
{
if (_tcpListener == null)
{
return;
}
try
{
_isRunning = false;
_tcpListener.Stop();
_tcpListener = null;
}
catch
{
// ignored
}
_cts?.Cancel();
_tcpListener?.Stop();
_cts?.Dispose();
_cts = null;
_pacPort = 0;
_tcpListener = null;
}
}
+4 -10
View File
@@ -12,23 +12,17 @@ public class TaskManager
_config = config;
_updateFunc = updateFunc;
_ = Task.Factory.StartNew(
ScheduledTasks,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
Task.Run(ScheduledTasks);
}
private async Task ScheduledTasks()
{
Logging.SaveLog("Setup Scheduled Tasks");
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
var numOfExecuted = 1;
while (true)
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
{
//1 minute
await Task.Delay(1000 * 60);
//Execute once 1 minute
try
{
@@ -140,7 +134,7 @@ public class TaskManager
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.CheckPreReleaseUpdate, _config.CheckUpdateItem.UpdateViaProxy);
var msgs = await updateService.CheckHasUpdateOnlyAll(_config.CheckUpdateItem.UpdateViaProxy);
foreach (var msg in msgs)
{
await _updateFunc?.Invoke(false, msg);
@@ -213,7 +213,7 @@ public class ClashUIItem
public bool EnableMixinContent { get; set; }
public int ProxiesSorting { get; set; }
public bool ProxiesAutoRefresh { get; set; }
public int ProxiesAutoDelayTestInterval { get; set; } = 10;
public int ProxiesRefreshInterval { get; set; } = 2;
public bool ConnectionsAutoRefresh { get; set; }
public int ConnectionsRefreshInterval { get; set; } = 2;
public List<ColumnItem> ConnectionsColumnItem { get; set; }
@@ -242,9 +242,9 @@ public class WebDavItem
[Serializable]
public class CheckUpdateItem
{
public bool CheckPreReleaseUpdate { get; set; }
public bool UpdateViaProxy { get; set; } = true;
public List<string>? SelectedCoreTypes { get; set; }
public List<string>? CheckPreReleaseCoreTypes { get; set; }
}
[Serializable]
@@ -24,6 +24,7 @@ public record CoreConfigContext
public bool IsWindows { get; init; }
public bool IsMacOS { get; init; }
public bool IsLinux { get; init; }
// Defaults to true so that a context built without this flag keeps routing IPv6 into the
// tunnel; only a positive detection of the host having no global IPv6 address turns it off.
@@ -128,8 +128,6 @@ public class User4Sbox
public class Outbound4Sbox : BaseServer4Sbox
{
public string? server { get; set; }
public int? server_port { get; set; }
public List<string>? server_ports { get; set; }
public string? uuid { get; set; }
public string? security { get; set; }
@@ -171,6 +169,10 @@ public class Endpoints4Sbox : BaseServer4Sbox
public string? udp_timeout { get; set; }
public int? workers { get; set; }
public List<Peer4Sbox> peers { get; set; }
public string? username { get; set; }
public string? password { get; set; }
public string? path { get; set; }
public Dictionary<string, List<string>>? headers { get; set; }
}
public class Peer4Sbox
@@ -271,10 +273,8 @@ public class Server4Sbox : BaseServer4Sbox
public string? inet4_range { get; set; }
public string? inet6_range { get; set; }
public string? client_subnet { get; set; }
public string? server { get; set; }
public new string? domain_resolver { get; set; }
[JsonPropertyName("interface")] public string? Interface { get; set; }
public int? server_port { get; set; }
public string? path { get; set; }
public Headers4Sbox? headers { get; set; }
@@ -359,4 +359,6 @@ public abstract class BaseServer4Sbox : DialFields4Sbox
{
public string type { get; set; }
public string tag { get; set; }
public string? server { get; set; }
public int? server_port { get; set; }
}
@@ -92,6 +92,10 @@ public class Inboundsettings4Ray
public string? autoOutboundsInterface { get; set; }
public List<string>? dns { get; set; }
public bool? autoSystemDnsToGateway { get; set; }
public List<string>? autoSystemWfpBlockLeak { get; set; }
}
public class Sniffing4Ray
@@ -315,6 +319,7 @@ public class StreamSettings4Ray
public GrpcSettings4Ray? grpcSettings { get; set; }
public HysteriaSettings4Ray? hysteriaSettings { get; set; }
public MasqueSettings4Ray? masqueSettings { get; set; }
public object? finalmask { get; set; }
@@ -443,6 +448,12 @@ public class HysteriaSettings4Ray
public string? auth { get; set; }
}
public class MasqueSettings4Ray
{
public string? path { get; set; }
public Dictionary<string, string>? headers { get; set; }
}
public class UdpHop4Ray
{
public string? ports { get; set; }
@@ -3,6 +3,8 @@ namespace ServiceLib.Models.Dto;
public partial class CheckUpdateModel : ReactiveObject
{
public bool? IsSelected { get; set; }
public bool IsCheckPreRelease { get; set; }
public bool ShowCheckPreRelease { get; set; }
public ECoreType? CoreType { get; set; }
[Reactive] public partial string? Remarks { get; set; }
public string? FileName { get; set; }
@@ -3,9 +3,9 @@ namespace ServiceLib.Models.Dto;
[Serializable]
public partial class ClashProxyModel : ReactiveObject
{
public required string Name { get; set; }
public string Name { get; set; }
public required string Type { get; set; }
public string Type { get; set; }
public string? Now { get; set; }
@@ -46,6 +46,10 @@ public record ProtocolExtraItem
public int? InsecureConcurrency { get; init; }
public bool? NaiveQuic { get; init; }
// MASQUE
public string? MasquePath { get; init; }
public string? MasqueHeaders { get; init; }
// group profile
public string? GroupType { get; init; }
public string? ChildItems { get; init; }
+19 -1
View File
@@ -789,6 +789,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Add [MASQUE] 的本地化字符串。
/// </summary>
public static string menuAddMasqueServer {
get {
return ResourceManager.GetString("menuAddMasqueServer", resourceCulture);
}
}
/// <summary>
/// 查找类似 Add [NaïveProxy] 的本地化字符串。
/// </summary>
@@ -924,6 +933,15 @@ namespace ServiceLib.Resx {
}
}
/// <summary>
/// 查找类似 Check and Update 的本地化字符串。
/// </summary>
public static string menuCheckAndUpdate {
get {
return ResourceManager.GetString("menuCheckAndUpdate", resourceCulture);
}
}
/// <summary>
/// 查找类似 Only Check 的本地化字符串。
/// </summary>
@@ -2149,7 +2167,7 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Not Support 的本地化字符串。
/// 查找类似 Not Supported 的本地化字符串。
/// </summary>
public static string MsgNotSupport {
get {
+8 -2
View File
@@ -399,6 +399,9 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>Filter, press Enter to execute</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>Check and Update</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>Check Update</value>
</data>
@@ -1783,7 +1786,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>Only Check</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Not Support</value>
<value>Not Supported</value>
</data>
<data name="LvTestIpInfo" xml:space="preserve">
<value>IP Info</value>
@@ -1899,4 +1902,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbSortingProcess" xml:space="preserve">
<value>Process</value>
</data>
</root>
<data name="menuAddMasqueServer" xml:space="preserve">
<value>Add [MASQUE]</value>
</data>
</root>
+38 -2
View File
@@ -399,8 +399,11 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>Фильтр, нажмите Enter для выполнения</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>Проверить и обновить</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>Обновить</value>
<value>Проверить обновления</value>
</data>
<data name="menuClose" xml:space="preserve">
<value>Закрыть</value>
@@ -693,6 +696,9 @@
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
<value>Проверить наличие предварительных обновлений</value>
</data>
<data name="TbSettingsEnableUpdateViaProxy" xml:space="preserve">
<value>Обновлять через прокси</value>
</data>
<data name="TbSettingsException" xml:space="preserve">
<value>Исключение</value>
</data>
@@ -1020,6 +1026,15 @@
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
<value>Протокол Mux для sing-box</value>
</data>
<data name="TbSettingsMux4RayConcurrency" xml:space="preserve">
<value>Лимит дочерних соединений Mux для Xray</value>
</data>
<data name="TbSettingsMux4RayXudpConcurrency" xml:space="preserve">
<value>Лимит дочерних XUDP-соединений Mux для Xray</value>
</data>
<data name="TbSettingsMux4RayXudpProxyUDP443" xml:space="preserve">
<value>Обработка UDP443 (QUIC) в Mux для Xray</value>
</data>
<data name="TbRoutingRuleProcess" xml:space="preserve">
<value>Процесс (Linux/Windows)</value>
</data>
@@ -1768,7 +1783,7 @@
<value>Доступна новая версия {0}: {1}</value>
</data>
<data name="menuCheckOnly" xml:space="preserve">
<value>Проверить</value>
<value>Только проверить</value>
</data>
<data name="MsgNotSupport" xml:space="preserve">
<value>Не поддерживается</value>
@@ -1869,4 +1884,25 @@
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
<value>При включении блокирует DNS-запросы IPv6</value>
</data>
<data name="TbDNS" xml:space="preserve">
<value>DNS</value>
</data>
<data name="TbSettingsMux4Ray" xml:space="preserve">
<value>Настройки Mux для Xray</value>
</data>
<data name="LvRequestHeaders" xml:space="preserve">
<value>HTTP-заголовки (JSON)</value>
</data>
<data name="SubRequestHeadersTips" xml:space="preserve">
<value>Необязательно. Пример: {"X-hwid": "my_device"}. Заданные значения переопределяют заголовки по умолчанию для всех URL этой группы, включая сервис конвертации подписок</value>
</data>
<data name="SubRequestHeadersInvalid" xml:space="preserve">
<value>Некорректные HTTP-заголовки. Используйте JSON-объект с уникальными именами заголовков и строковыми значениями. Имена и значения заголовков не должны содержать переносов строк</value>
</data>
<data name="TbSortingProcess" xml:space="preserve">
<value>Процесс</value>
</data>
<data name="menuAddMasqueServer" xml:space="preserve">
<value>Добавить сервер [MASQUE]</value>
</data>
</root>
+7 -1
View File
@@ -399,6 +399,9 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>过滤器,按回车执行</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>检查并更新</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>检查更新</value>
</data>
@@ -1891,4 +1894,7 @@
<data name="TbSortingProcess" xml:space="preserve">
<value>进程</value>
</data>
</root>
<data name="menuAddMasqueServer" xml:space="preserve">
<value>添加 [MASQUE]</value>
</data>
</root>
@@ -399,6 +399,9 @@
<data name="MsgServerTitle" xml:space="preserve">
<value>過濾器,按 Enter 執行</value>
</data>
<data name="menuCheckAndUpdate" xml:space="preserve">
<value>檢查並更新</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>檢查更新</value>
</data>
+1 -7
View File
@@ -3,13 +3,7 @@
"protocol": "vmess",
"settings": {
"address": "v2ray.cool",
"port": 10086,
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
"security": "auto",
"method": "chacha20",
"ota": false,
"password": "123456",
"level": 1
"port": 10086
},
"streamSettings": {
"network": "tcp"
@@ -365,9 +365,16 @@ public partial class CoreConfigSingboxService
rule4ExpectedIPs = JsonUtils.DeepCopy(rule);
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);
}
@@ -63,7 +63,7 @@ public partial class CoreConfigSingboxService
}
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
tunInbound.interface_name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "singbox_tun";
tunInbound.interface_name = context.IsMacOS ? null : "singbox_tun";
tunInbound.mtu = _config.TunModeItem.Mtu;
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute;
@@ -77,7 +77,7 @@ public partial class CoreConfigSingboxService
}
}
if (_node.ConfigType == EConfigType.WireGuard)
if (_node.ConfigType is EConfigType.WireGuard or EConfigType.MASQUE)
{
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
FillEndpoint(endpoint);
@@ -350,13 +350,13 @@ public partial class CoreConfigSingboxService
{
var protocolExtra = _node.GetProtocolExtra();
endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"];
endpoint.type = Global.ProtocolTypes[_node.ConfigType];
switch (_node.ConfigType)
{
case EConfigType.WireGuard:
{
endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"];
var peer = new Peer4Sbox
{
public_key = protocolExtra.WgPublicKey ?? string.Empty,
@@ -371,6 +371,28 @@ public partial class CoreConfigSingboxService
endpoint.peers = [peer];
break;
}
case EConfigType.MASQUE:
{
endpoint.type = "masque-client";
endpoint.server = _node.Address;
endpoint.server_port = _node.Port;
endpoint.username = _node.Username;
endpoint.password = _node.Password;
endpoint.path = protocolExtra?.MasquePath.NullIfEmpty();
if (!string.IsNullOrEmpty(protocolExtra?.MasqueHeaders))
{
var headers = Utils.ParseHeaders(protocolExtra.MasqueHeaders)
.GroupBy(x => x.Item1, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => g.Select(x => x.Item2).ToList(),
StringComparer.OrdinalIgnoreCase
);
endpoint.headers = headers.Count > 0 ? headers : null;
}
FillOutboundTls(endpoint);
break;
}
}
}
catch (Exception ex)
@@ -402,7 +424,7 @@ public partial class CoreConfigSingboxService
}
}
private void FillOutboundTls(Outbound4Sbox outbound)
private void FillOutboundTls(BaseServer4Sbox server)
{
try
{
@@ -476,7 +498,7 @@ public partial class CoreConfigSingboxService
{
tls.ech = ech;
}
outbound.tls = tls;
server.tls = tls;
}
catch (Exception ex)
{
@@ -90,6 +90,15 @@ public partial class CoreConfigV2rayService
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
tunInbound.sniffing.routeOnly = true;
if (context.IsWindows && _config.TunModeItem.StrictRoute == true)
{
tunInbound.settings.autoSystemWfpBlockLeak = ["dns", "misconfigtun"];
}
if (context.IsLinux)
{
tunInbound.settings.autoSystemDnsToGateway = true;
}
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{
var wholeInternet = IPNetwork2.Parse("0.0.0.0/0");
@@ -182,6 +182,12 @@ public partial class CoreConfigV2rayService
outboundSettings.version = 2;
break;
}
case EConfigType.MASQUE:
{
outboundSettings.address = _node.Address;
outboundSettings.port = _node.Port;
break;
}
case EConfigType.WireGuard:
{
var address = _node.Address;
@@ -257,8 +263,13 @@ public partial class CoreConfigV2rayService
{
network = "hysteria";
}
else if (_node.ConfigType == EConfigType.MASQUE)
{
network = "masque";
}
streamSettings.network = network;
var transport = _node.GetTransportExtra();
var protocolExtra = _node.GetProtocolExtra();
var host = string.Empty;
var path = string.Empty;
var kcpSeed = string.Empty;
@@ -502,7 +513,6 @@ public partial class CoreConfigV2rayService
break;
case "hysteria":
var protocolExtra = _node.GetProtocolExtra();
var ports = protocolExtra?.Ports;
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
? su
@@ -574,6 +584,32 @@ public partial class CoreConfigV2rayService
streamSettings.finalmask = hy2Finalmask;
break;
case "masque":
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var authValue = $"Basic {Utils.Base64Encode($"{_node.Username}:{_node.Password}")}";
headers.Add("Authorization", authValue);
if (!string.IsNullOrEmpty(protocolExtra?.MasqueHeaders))
{
var userHeaders = Utils.ParseHeaders(protocolExtra.MasqueHeaders)
.GroupBy(x => x.Item1, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => string.Join(", ", g.Select(x => x.Item2)),
StringComparer.OrdinalIgnoreCase
);
if (userHeaders.TryGetValue("Authorization", out var userAuthValue))
{
headers["Authorization"] = userAuthValue;
}
headers = userHeaders;
}
streamSettings.masqueSettings = new()
{
path = protocolExtra?.MasquePath.NullIfEmpty(),
headers = headers.Count > 0 ? headers : null,
};
break;
default:
// raw
if (headerType == Global.RawHeaderHttp)
+124 -48
View File
@@ -20,24 +20,31 @@ public class DownloadService
/// <summary>
/// Downloads data with the specified proxy and reports progress messages.
/// </summary>
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, Func<bool, string, Task> updateFunc, CancellationToken cancellationToken = default)
{
try
{
var progress = new Progress<string>();
progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}");
await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy,
url,
progress,
downloadTimeout);
OnProgress,
cancellationToken);
void OnProgress(string message)
{
cancellationToken.ThrowIfCancellationRequested();
updateFunc.Invoke(false, $"{message}");
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
await updateFunc?.Invoke(false, ex.Message);
await updateFunc.Invoke(false, ex.Message);
if (ex.InnerException != null)
{
await updateFunc?.Invoke(false, ex.InnerException.Message);
await updateFunc.Invoke(false, ex.InnerException.Message);
}
}
return 0;
@@ -46,23 +53,28 @@ public class DownloadService
/// <summary>
/// Downloads a file and reports progress through events.
/// </summary>
public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, TimeSpan connectTimeout)
public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, CancellationToken cancellationToken = default)
{
try
{
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}"));
var webProxy = await GetWebProxy(blProxy);
var webProxy = await GetWebProxy(blProxy, cancellationToken);
await DownloaderHelper.Instance.DownloadFileAsync(webProxy,
request,
OnProgress,
connectTimeout);
cancellationToken);
void OnProgress(FileDownloadState state)
{
cancellationToken.ThrowIfCancellationRequested();
UpdateCompleted?.Invoke(this, new UpdateResult(state.Completed, $"{Utils.HumanFy((long)state.SpeedBytesPerSecond / 1024)}/s | {Utils.HumanFy(state.DownloadedBytes / 1024)}/{Utils.HumanFy(state.TotalBytes / 1024)}"));
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -75,20 +87,21 @@ public class DownloadService
}
}
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, TimeSpan connectTimeout)
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, CancellationToken cancellationToken = default)
{
try
{
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}"));
var webProxy = await GetWebProxy(blProxy);
var webProxy = await GetWebProxy(blProxy, cancellationToken);
await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy,
requests,
OnProgress,
connectTimeout);
cancellationToken);
void OnProgress(ReadOnlyMemory<FileDownloadState> states)
{
cancellationToken.ThrowIfCancellationRequested();
var span = states.Span;
var completedCount = 0;
var downloadingStates = new List<FileDownloadState>();
@@ -129,6 +142,10 @@ public class DownloadService
UpdateCompleted?.Invoke(this, new UpdateResult(allCompleted, $"{completedCount}/{span.Length} | {Utils.HumanFy((long)totalSpeed / 1024)}/s {Utils.HumanFy(totalDownloadedBytes / 1024)}/{Utils.HumanFy(totalTotalBytes / 1024)} {downloadingFileName}"));
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -144,12 +161,12 @@ public class DownloadService
/// <summary>
/// Gets redirect target URL without following redirects automatically.
/// </summary>
public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
public async Task<string?> UrlRedirectAsync(string url, bool blProxy, CancellationToken cancellationToken = default)
{
var webRequestHandler = new SocketsHttpHandler
{
AllowAutoRedirect = false,
Proxy = await GetWebProxy(blProxy)
Proxy = await GetWebProxy(blProxy, cancellationToken)
};
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null)
@@ -159,7 +176,7 @@ public class DownloadService
}
using var client = new HttpClient(webRequestHandler);
var response = await client.GetAsync(url);
var response = await client.GetAsync(url, cancellationToken);
if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null)
{
return response.Headers.Location.ToString();
@@ -175,26 +192,29 @@ public class DownloadService
/// <summary>
/// Tries to download string content using proxy switch setting.
/// </summary>
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent, CancellationToken cancellationToken = default)
{
var webProxy = await GetWebProxy(blProxy);
return await TryDownloadString(url, webProxy, userAgent);
var webProxy = await GetWebProxy(blProxy, cancellationToken);
return await TryDownloadString(url, webProxy, userAgent, cancellationToken);
}
/// <summary>
/// Tries to download string content with a specified proxy.
/// </summary>
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
{
var timeout = 15;
try
{
var result1 = await DownloadStringAsync(url, webProxy, userAgent, timeout);
var result1 = await DownloadStringAsync(url, webProxy, userAgent, cancellationToken);
if (result1.IsNotEmpty())
{
return result1;
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -207,12 +227,16 @@ public class DownloadService
try
{
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, timeout);
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, cancellationToken);
if (result2.IsNotEmpty())
{
return result2;
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -229,17 +253,16 @@ public class DownloadService
/// <summary>
/// Downloads string content via HttpClient.
/// </summary>
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
{
try
{
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var handler = new SocketsHttpHandler
{
Proxy = webProxy,
UseProxy = webProxy != null,
AutomaticDecompression = DecompressionMethods.All,
ConnectTimeout = TimeSpan.FromSeconds(connectTimeout)
ConnectTimeout = webProxy is null ? Global.DirectDownloadConnect : Global.ProxyDownloadConnect,
};
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null)
@@ -248,10 +271,8 @@ public class DownloadService
handler.SslOptions.RemoteCertificateValidationCallback = null;
}
using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders))
{
Timeout = Timeout.InfiniteTimeSpan
};
using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders));
client.Timeout = Timeout.InfiniteTimeSpan;
if (userAgent.IsNullOrEmpty())
{
@@ -270,10 +291,15 @@ public class DownloadService
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo));
}
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var timeoutCts = new CancellationTokenSource();
timeoutCts.CancelAfter(webProxy is null ? Global.DirectFetch : Global.ProxyFetch);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
return await client.GetStringAsync(url, cts.Token);
return await client.GetStringAsync(url, linkedCts.Token);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
@@ -291,7 +317,7 @@ public class DownloadService
/// <summary>
/// Downloads string content via DownloaderHelper.
/// </summary>
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout)
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
{
try
{
@@ -299,9 +325,13 @@ public class DownloadService
{
userAgent = Utils.GetVersion(false);
}
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout, RequestHeaders, AcceptHeader);
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, RequestHeaders, AcceptHeader, cancellationToken);
return result;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
@@ -317,14 +347,14 @@ public class DownloadService
/// <summary>
/// Creates local SOCKS proxy when proxy switch is enabled.
/// </summary>
private async Task<WebProxy?> GetWebProxy(bool blProxy)
private async Task<WebProxy?> GetWebProxy(bool blProxy, CancellationToken cancellationToken = default)
{
if (!blProxy)
{
return null;
}
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
if (await SocketCheck(Global.Loopback, port) == false)
if (await SocksPortCheck(Global.Loopback, port, cancellationToken) == false)
{
return null;
}
@@ -335,18 +365,64 @@ public class DownloadService
/// <summary>
/// Checks whether the specified TCP endpoint is reachable.
/// </summary>
private async Task<bool> SocketCheck(string ip, int port)
private async Task<bool> SocksPortCheck(string ip, int port, CancellationToken cancellationToken = default)
{
try
using var rootTimeOutCts = new CancellationTokenSource(Global.LocalFetch);
using var rootCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, rootTimeOutCts.Token);
var rootToken = rootCts.Token;
// SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth)
ReadOnlyMemory<byte> greeting = new byte[] { 0x05, 0x01, 0x00 };
var buf = new byte[2];
while (!rootToken.IsCancellationRequested)
{
IPEndPoint point = new(IPAddress.Parse(ip), port);
using Socket? sock = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync(point);
return true;
}
catch (Exception)
{
return false;
using var tcp = new TcpClient();
using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token);
var linkedToken = linkedCts.Token;
try
{
await tcp.ConnectAsync(ip, port, linkedToken);
var stream = tcp.GetStream();
await stream.WriteAsync(greeting, linkedToken);
var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken);
// Server selection: VER=5, METHOD=0x00 — proxy is fully ready
if (read == 2 && buf[0] == 0x05)
{
return true;
}
}
catch (OperationCanceledException)
{
if (!rootToken.IsCancellationRequested)
{
continue;
}
Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready.");
return false;
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
{
// Connection refused, proxy not ready yet, wait 50ms before retrying
try
{
await Task.Delay(50, rootToken);
}
catch (OperationCanceledException)
{
Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready.");
return false;
}
}
catch
{
// Ignore other exceptions and continue
}
}
return false;
}
}
+287 -209
View File
@@ -7,63 +7,125 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private static readonly string _tag = "SpeedtestService";
private readonly Config? _config = config;
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
private static readonly ConcurrentBag<string> _lstExitLoop = [];
private readonly Lock _runLock = new();
private readonly List<CancellationTokenSource> _runCtsList = [];
private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize;
private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1);
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds)
public Task RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationToken ct = default)
{
Task.Run(async () =>
CancellationTokenSource runCts;
lock (_runLock)
{
await RunAsync(actionType, selecteds);
await ProfileExManager.Instance.SaveTo();
await UpdateFunc("", ResUI.SpeedtestingCompleted);
});
runCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_runCtsList.Add(runCts);
}
return RunLoopAsync(actionType, selecteds, runCts);
}
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);
_lstExitLoop.Clear();
}
}
private static bool ShouldStopTest(string exitLoopKey)
private async Task RunLoopAsync(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationTokenSource runCts)
{
return _lstExitLoop.All(p => p != exitLoopKey);
try
{
await RunAsync(actionType, selecteds, runCts.Token);
}
catch (OperationCanceledException) when (runCts.IsCancellationRequested)
{
// Ignored
}
finally
{
try
{
await ProfileExManager.Instance.SaveTo();
}
finally
{
await UpdateFunc("", ResUI.SpeedtestingCompleted);
}
lock (_runLock)
{
_runCtsList.Remove(runCts);
}
runCts.Dispose();
}
}
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds)
private async Task RunAsync(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationToken ct = default)
{
var exitLoopKey = Utils.GetGuid(false);
_lstExitLoop.Add(exitLoopKey);
var lstSelected = await GetClearItem(actionType, selecteds);
var completedIds = new ConcurrentDictionary<string, byte>();
switch (actionType)
try
{
case ESpeedActionType.Tcping:
await RunTcpingAsync(lstSelected, exitLoopKey);
break;
switch (actionType)
{
case ESpeedActionType.Tcping:
await RunTcpingAsync(lstSelected, completedIds, ct);
break;
case ESpeedActionType.Realping:
await RunRealPingBatchAsync(lstSelected, exitLoopKey);
break;
case ESpeedActionType.Realping:
await RunRealPingBatchAsync(lstSelected, completedIds, 0, ct);
break;
case ESpeedActionType.UdpTest:
await RunUdpTestBatchAsync(lstSelected, exitLoopKey);
break;
case ESpeedActionType.UdpTest:
await RunUdpTestBatchAsync(lstSelected, completedIds, 0, ct);
break;
case ESpeedActionType.Speedtest:
await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey);
break;
case ESpeedActionType.Speedtest:
await RunMixedTestAsync(lstSelected, completedIds, 1, true, ct);
break;
case ESpeedActionType.Mixedtest:
await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, true, exitLoopKey);
break;
case ESpeedActionType.Mixedtest:
await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, true,
ct);
break;
}
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
_ = UpdateFunc("", ResUI.SpeedtestingStop);
await SetTestResultAsync(lstSelected.Where(it => !completedIds.ContainsKey(it.IndexId)).ToList(),
actionType, ResUI.SpeedtestingSkip).ConfigureAwait(false);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
_ = UpdateFunc("", ex.Message);
}
}
@@ -103,29 +165,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
}
//clear test result
foreach (var it in lstSelected)
{
switch (actionType)
{
case ESpeedActionType.Tcping:
case ESpeedActionType.Realping:
case ESpeedActionType.UdpTest:
await UpdateFunc(it.IndexId, ResUI.Speedtesting, "");
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
break;
case ESpeedActionType.Speedtest:
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break;
case ESpeedActionType.Mixedtest:
await UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break;
}
}
await SetTestResultAsync(lstSelected, actionType, ResUI.Speedtesting).ConfigureAwait(false);
if (lstSelected.Count > 1 && (actionType == ESpeedActionType.Speedtest || actionType == ESpeedActionType.Mixedtest))
{
@@ -135,56 +175,71 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return lstSelected;
}
private async Task RunTcpingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
private async Task SetTestResultAsync(List<ServerTestItem> lstSelected, ESpeedActionType actionType, string message)
{
foreach (var it in lstSelected)
{
switch (actionType)
{
case ESpeedActionType.Tcping:
case ESpeedActionType.Realping:
case ESpeedActionType.UdpTest:
await UpdateFunc(it.IndexId, message, "");
break;
case ESpeedActionType.Speedtest:
await UpdateFunc(it.IndexId, "", message);
break;
case ESpeedActionType.Mixedtest:
await UpdateFunc(it.IndexId, message, message);
break;
}
}
}
private async Task RunTcpingAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
var lstBatch = GetTestBatchItem(selecteds, pageSize);
foreach (var lst in lstBatch)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
ct.ThrowIfCancellationRequested();
List<Task> tasks = [];
foreach (var it in lst)
var parallelOptions = new ParallelOptions
{
if (ShouldStopTest(exitLoopKey))
MaxDegreeOfParallelism = lst.Count,
CancellationToken = ct,
};
await Parallel.ForEachAsync(lst, parallelOptions, async (item, innerCt) =>
{
try
{
return;
var responseTime = await GetTcpingTime(item.Address, item.Port, innerCt);
ProfileExManager.Instance.SetTestDelay(item.IndexId, responseTime);
await UpdateFunc(item.IndexId, responseTime.ToString());
completedIds.TryAdd(item.IndexId, 0);
}
tasks.Add(Task.Run(async () =>
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
try
{
var responseTime = await GetTcpingTime(it.Address, it.Port);
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
});
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}));
}
await Task.WhenAll(tasks);
if (ShouldStopTest(exitLoopKey))
{
return;
}
await Task.Delay(_delayInterval);
await Task.Delay(_delayInterval, ct);
}
}
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected,
ConcurrentDictionary<string, byte> completedIds, int pageSize = 0, CancellationToken ct = default)
{
if (pageSize <= 0)
{
@@ -195,38 +250,35 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest)
{
var ret = await RunRealPingAsync(lst, exitLoopKey);
var ret = await RunRealPingAsync(lst, completedIds, ct);
if (ret == false)
{
lstFailed.AddRange(lst);
}
await Task.Delay(_delayInterval);
await Task.Delay(_delayInterval, ct);
}
//Retest the failed part
var pageSizeNext = pageSize / 2;
if (lstFailed.Count > 0 && pageSizeNext > 0)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
ct.ThrowIfCancellationRequested();
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount)
{
await RunRealPingBatchAsync(lstFailed, exitLoopKey, pageSizeNext);
await RunRealPingBatchAsync(lstFailed, completedIds, pageSizeNext, ct);
}
else
{
await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, false, exitLoopKey);
await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, false, ct);
}
}
}
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
ProcessService processService = null;
try
@@ -236,28 +288,40 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
return false;
}
await Task.Delay(1000);
await Task.Delay(1000, ct);
List<Task> tasks = [];
foreach (var it in selecteds)
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = selecteds.Count,
CancellationToken = ct,
};
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
{
if (!it.AllowTest)
{
await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
continue;
completedIds.TryAdd(it.IndexId, 0);
return;
}
if (ShouldStopTest(exitLoopKey))
try
{
return false;
await DoRealPing(it, completedIds, innerCt);
}
tasks.Add(Task.Run(async () =>
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
await DoRealPing(it);
}));
}
await Task.WhenAll(tasks);
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
});
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
@@ -273,7 +337,8 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return true;
}
private async Task RunUdpTestBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
private async Task RunUdpTestBatchAsync(List<ServerTestItem> lstSelected,
ConcurrentDictionary<string, byte> completedIds, int pageSize = 0, CancellationToken ct = default)
{
if (pageSize <= 0)
{
@@ -284,30 +349,27 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
List<ServerTestItem> lstFailed = [];
foreach (var lst in lstTest)
{
var ret = await RunUdpTestAsync(lst, exitLoopKey);
var ret = await RunUdpTestAsync(lst, completedIds, ct);
if (ret == false)
{
lstFailed.AddRange(lst);
}
await Task.Delay(_delayInterval);
await Task.Delay(_delayInterval, ct);
}
//Retest the failed part
if (lstFailed.Count > 0)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
ct.ThrowIfCancellationRequested();
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
await RunUdpTestAsync(lstFailed, exitLoopKey);
await RunUdpTestAsync(lstFailed, completedIds, ct);
}
}
private async Task<bool> RunUdpTestAsync(List<ServerTestItem> selecteds, string exitLoopKey)
private async Task<bool> RunUdpTestAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
ProcessService processService = null;
try
@@ -317,27 +379,40 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
{
return false;
}
await Task.Delay(1000);
await Task.Delay(1000, ct);
List<Task> tasks = [];
foreach (var it in selecteds)
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = selecteds.Count,
CancellationToken = ct,
};
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
{
if (!it.AllowTest)
{
continue;
await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
completedIds.TryAdd(it.IndexId, 0);
return;
}
if (ShouldStopTest(exitLoopKey))
try
{
return false;
await DoUdpTest(it, completedIds, innerCt);
}
tasks.Add(Task.Run(async () =>
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
await DoUdpTest(it);
}));
}
await Task.WhenAll(tasks);
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
});
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
@@ -353,81 +428,77 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
return true;
}
private async Task RunMixedTestAsync(List<ServerTestItem> selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey)
private async Task RunMixedTestAsync(List<ServerTestItem> selecteds,
ConcurrentDictionary<string, byte> completedIds, int concurrencyCount, bool blSpeedTest,
CancellationToken ct = default)
{
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
var downloadHandle = new DownloadService();
List<Task> tasks = [];
foreach (var it in selecteds)
var parallelOptions = new ParallelOptions
{
if (ShouldStopTest(exitLoopKey))
MaxDegreeOfParallelism = concurrencyCount,
CancellationToken = ct,
};
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
{
innerCt.ThrowIfCancellationRequested();
ProcessService processService = null;
try
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
continue;
processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
if (processService is null)
{
await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
return;
}
await Task.Delay(1000, innerCt);
var delay = await DoRealPing(it, completedIds, innerCt);
if (blSpeedTest)
{
if (delay > 0)
{
await DoSpeedTest(downloadHandle, it, completedIds, innerCt);
}
else
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
}
}
}
await concurrencySemaphore.WaitAsync();
tasks.Add(Task.Run(async () =>
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
ProcessService processService = null;
try
throw;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
finally
{
if (processService != null)
{
processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
if (processService is null)
{
await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
return;
}
await Task.Delay(1000);
var delay = await DoRealPing(it);
if (blSpeedTest)
{
if (ShouldStopTest(exitLoopKey))
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
return;
}
if (delay > 0)
{
await DoSpeedTest(downloadHandle, it);
}
else
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
}
}
await processService.StopAsync();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
finally
{
if (processService != null)
{
await processService?.StopAsync();
}
concurrencySemaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
}
});
}
private async Task<int> DoRealPing(ServerTestItem it)
private async Task<int> DoRealPing(ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy);
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, ct);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
{
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy);
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy, ct);
var ipStr = ipInfo?.ToString() ?? Global.None;
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
await UpdateIpInfoFunc(it.IndexId, ipStr);
@@ -437,17 +508,22 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
}
completedIds.TryAdd(it.IndexId, 0);
return responseTime;
}
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it)
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
await UpdateFunc(it.IndexId, "", ResUI.Speedtesting);
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var url = _config.SpeedTestItem.SpeedTestUrl;
var timeout = _config.SpeedTestItem.SpeedTestTimeout;
await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, msg) =>
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
var linkedCt = linkedCts.Token;
await downloadHandle.DownloadDataAsync(url, webProxy, async (success, msg) =>
{
decimal.TryParse(msg, out var dec);
if (dec > 0)
@@ -455,34 +531,34 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec);
}
await UpdateFunc(it.IndexId, "", msg);
});
}, linkedCt);
completedIds.TryAdd(it.IndexId, 0);
}
private async Task<int> DoUdpTest(ServerTestItem it)
private async Task<int> DoUdpTest(ServerTestItem it,
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
{
var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
var responseTime = -1;
try
{
responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, TimeSpan.FromSeconds(5))).TotalMilliseconds;
}
catch
{
// ignored
}
var responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, ct)).TotalMilliseconds;
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString());
completedIds.TryAdd(it.IndexId, 0);
return responseTime;
}
private async Task<int> GetTcpingTime(string url, int port)
private async Task<int> GetTcpingTime(string? url, int port, CancellationToken ct = default)
{
var responseTime = -1;
if (url.IsNullOrEmpty() || port <= 0)
{
return responseTime;
}
if (!IPAddress.TryParse(url, out var ipAddress))
{
var ipHostInfo = await Dns.GetHostEntryAsync(url);
var ipHostInfo = await Dns.GetHostEntryAsync(url, ct);
ipAddress = ipHostInfo.AddressList.First();
}
@@ -492,12 +568,14 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
var timer = Stopwatch.StartNew();
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await clientSocket.ConnectAsync(endPoint, cts.Token).ConfigureAwait(false);
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
await clientSocket.ConnectAsync(endPoint, linkedCts.Token).ConfigureAwait(false);
responseTime = (int)timer.ElapsedMilliseconds;
}
catch (OperationCanceledException)
catch
{
// Ignore
}
finally
{
@@ -5,8 +5,7 @@ namespace ServiceLib.Services.Statistics;
public class StatisticsSingboxService
{
private readonly Config _config;
private bool _exitFlag;
private ClientWebSocket? webSocket;
private CancellationTokenSource? _cts;
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic";
private static readonly string _tag = "StatisticsSingboxService";
@@ -15,40 +14,17 @@ public class StatisticsSingboxService
{
_config = config;
_updateFunc = updateFunc;
_exitFlag = false;
_ = Task.Factory.StartNew(
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 { }
Task.Run(Run);
}
public void Close()
{
try
{
_exitFlag = true;
if (webSocket != null)
{
webSocket.Abort();
webSocket = null;
}
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
catch (Exception ex)
{
@@ -58,53 +34,63 @@ public class StatisticsSingboxService
private async Task Run()
{
await Init();
Close();
_cts = new CancellationTokenSource();
var token = _cts.Token;
while (!_exitFlag)
while (!token.IsCancellationRequested)
{
await Task.Delay(1000);
try
{
if (!AppManager.Instance.IsRunningCore(ECoreType.sing_box))
{
await Task.Delay(1000, token).ConfigureAwait(false);
continue;
}
if (webSocket != null)
using var ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri(Url), token).ConfigureAwait(false);
var buffer = new byte[1024];
while (ws.State == WebSocketState.Open
&& !token.IsCancellationRequested)
{
if (webSocket.State is WebSocketState.Aborted or WebSocketState.Closed)
var res = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), token);
if (res.MessageType == WebSocketMessageType.Close)
{
webSocket.Abort();
webSocket = null;
await Init();
continue;
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);
}
if (webSocket.State != WebSocketState.Open)
var result = Encoding.UTF8.GetString(ms.ToArray());
if (!result.IsNotEmpty())
{
continue;
}
ParseOutput(result, out var up, out var down);
var buffer = new byte[1024];
var res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!res.CloseStatus.HasValue)
if (_updateFunc != null)
{
var result = Encoding.UTF8.GetString(buffer, 0, res.Count);
if (result.IsNotEmpty())
await _updateFunc.Invoke(new ServerSpeedItem
{
ParseOutput(result, out var up, out var down);
await _updateFunc?.Invoke(new ServerSpeedItem()
{
ProxyUp = (long)(up / 1000),
ProxyDown = (long)(down / 1000)
});
}
res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
ProxyUp = (long)(up / 1000),
ProxyDown = (long)(down / 1000),
}).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
break;
}
catch
{
await Task.Delay(3000, token).ConfigureAwait(false);
}
}
}
@@ -5,33 +5,42 @@ public class StatisticsXrayService
private const long linkBase = 1024;
private ServerSpeedItem _serverSpeedItem = new();
private readonly Config _config;
private bool _exitFlag;
private CancellationTokenSource? _cts;
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
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)
{
_config = config;
_updateFunc = updateFunc;
_exitFlag = false;
_ = Task.Factory.StartNew(
Run,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
Task.Run(Run);
}
public void Close()
{
_exitFlag = true;
try
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
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
{
if (AppManager.Instance.RunningCoreType != ECoreType.Xray)
@@ -39,16 +48,20 @@ public class StatisticsXrayService
continue;
}
var result = await HttpClientHelper.Instance.TryGetAsync(Url);
var result = await HttpClientHelper.Instance.TryGetAsync(Url, token);
if (result != null)
{
var server = ParseOutput(result) ?? new ServerSpeedItem();
await _updateFunc?.Invoke(server);
await _updateFunc!.Invoke(server);
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
break;
}
catch
{
// ignored
await Task.Delay(3000, token).ConfigureAwait(false);
}
}
}
+24 -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 Func<bool, string, Task>? _updateFunc = updateFunc;
private readonly int _timeout = 30;
private static readonly string _tag = "UpdateService";
public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true)
public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{
var url = string.Empty;
var fileName = string.Empty;
@@ -39,7 +38,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
url = result.Url!;
fileName = Utils.GetTempPath(Utils.GetGuid());
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, TimeSpan.FromSeconds(_timeout));
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken);
}
else
{
@@ -47,7 +46,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
}
}
public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true)
public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{
var url = string.Empty;
var fileName = string.Empty;
@@ -89,7 +88,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
url = result.Url!;
var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url);
fileName = Utils.GetTempPath(Utils.GetGuid() + ext);
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, TimeSpan.FromSeconds(_timeout));
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken);
}
else
{
@@ -100,7 +99,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
}
}
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true)
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
{
if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type))
{
@@ -109,10 +108,10 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
var downloadHandle = new DownloadService();
var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease);
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy);
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy, cancellationToken);
}
public async Task<List<string>> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true)
public async Task<List<string>> CheckHasUpdateOnlyAll(bool blProxy = true, CancellationToken cancellationToken = default)
{
var msgs = new List<string>();
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
@@ -122,7 +121,8 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
continue;
}
var result = await CheckHasUpdateOnly(type, preRelease, blProxy);
var preRelease = _config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(type.ToString()) ?? false;
var result = await CheckHasUpdateOnly(type, preRelease, blProxy, cancellationToken);
if (result.Success && result.Version != null)
{
var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version);
@@ -137,7 +137,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
return msgs;
}
public async Task UpdateGeoFileAll(bool blProxy = true)
public async Task UpdateGeoFileAll(bool blProxy = true, CancellationToken cancellationToken = default)
{
var requests = new List<FileDownloadRequest>();
requests.AddRange(GetGeoFilesRequest());
@@ -145,22 +145,22 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
requests.AddRange(await GetSrsFileAllRequest());
// NOTE: srs files are more small, so we reverse the order to ensure a good download experience for the user.
requests.Reverse();
await DownloadGeoFiles(requests, blProxy);
await DownloadGeoFiles(requests, blProxy, cancellationToken);
await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
}
#region CheckUpdate private
private async Task<UpdateResult> CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy)
private async Task<UpdateResult> CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default)
{
try
{
var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy);
var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy, cancellationToken);
if (!result.Success || result.Version is null)
{
return result;
}
return await ParseDownloadUrl(type, result);
return await ParseDownloadUrl(type, result, cancellationToken);
}
catch (Exception ex)
{
@@ -170,14 +170,14 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
}
}
private async Task<UpdateResult> GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy)
private async Task<UpdateResult> GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default)
{
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type);
var tagName = string.Empty;
if (preRelease || coreInfo?.LockedMaxVersion != null)
{
var url = coreInfo?.ReleaseApiUrl;
var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName);
var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName, cancellationToken);
if (result.IsNullOrEmpty())
{
return new UpdateResult(false, "");
@@ -209,7 +209,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
else
{
var url = Path.Combine(coreInfo.Url, "latest");
var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy);
var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy, cancellationToken);
if (lastUrl == null)
{
return new UpdateResult(false, "");
@@ -223,7 +223,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
[GeneratedRegex(@"v?(?<version>\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?)", RegexOptions.IgnoreCase)]
private static partial Regex SemVerRegex();
private async Task<SemanticVersion> GetCoreVersion(ECoreType type)
private async Task<SemanticVersion> GetCoreVersion(ECoreType type, CancellationToken cancellationToken = default)
{
try
{
@@ -246,7 +246,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
return new SemanticVersion("");
}
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg);
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg, cancellationToken);
var echo = result ?? "";
var version = SemVerRegex().Match(echo).Groups["version"].Value;
return new SemanticVersion(version);
@@ -259,7 +259,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
}
}
private async Task<UpdateResult> ParseDownloadUrl(ECoreType type, UpdateResult result)
private async Task<UpdateResult> ParseDownloadUrl(ECoreType type, UpdateResult result, CancellationToken cancellationToken = default)
{
try
{
@@ -276,7 +276,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
case ECoreType.v2fly_v5:
case ECoreType.mihomo:
{
curVersion = await GetCoreVersion(type);
curVersion = await GetCoreVersion(type, cancellationToken);
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
url = string.Format(coreUrl, version);
break;
@@ -284,7 +284,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
case ECoreType.sing_box:
{
curVersion = await GetCoreVersion(type);
curVersion = await GetCoreVersion(type, cancellationToken);
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v"));
break;
@@ -531,7 +531,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
};
}
private async Task DownloadGeoFiles(List<FileDownloadRequest> requests, bool blProxy)
private async Task DownloadGeoFiles(List<FileDownloadRequest> requests, bool blProxy, CancellationToken cancellationToken = default)
{
var tmpFilePathDict = new Dictionary<string, string>();
var tmpFileRequests = new List<FileDownloadRequest>();
@@ -586,7 +586,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
_ = UpdateFunc(false, args.GetException().Message);
};
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, TimeSpan.FromSeconds(_timeout));
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, cancellationToken);
}
#endregion Geo private
@@ -97,6 +97,12 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
[Reactive]
public partial int GeckoMaxPacketSize { get; set; }
[Reactive]
public partial string MasquePath { get; set; }
[Reactive]
public partial string MasqueHeaders { get; set; }
[Reactive]
public partial string RawHeaderType { get; set; }
@@ -322,6 +328,8 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty;
GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
MasquePath = protocolExtra.MasquePath ?? string.Empty;
MasqueHeaders = protocolExtra.MasqueHeaders ?? string.Empty;
RawHeaderType = transport.RawHeaderType ?? Global.None;
Host = transport.Host ?? string.Empty;
@@ -443,6 +451,8 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
Hy2RealmUrl = realm?.ToUri().NullIfEmpty(),
GeckoMinPacketSize = GeckoMinPacketSize > 0 ? GeckoMinPacketSize.ToString() : null,
GeckoMaxPacketSize = GeckoMaxPacketSize > 0 ? GeckoMaxPacketSize.ToString() : null,
MasquePath = MasquePath.NullIfEmpty(),
MasqueHeaders = MasqueHeaders.NullIfEmpty(),
});
SelectedSource.SetTransportExtra(transport);
@@ -12,7 +12,7 @@ public partial class CheckUpdateViewModel : MyReactiveObject
public BulkObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = [];
public ReactiveCommand<RxVoid, RxVoid> CheckUpdateCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> CheckOnlyCmd { get; }
[Reactive] public partial bool EnableCheckPreReleaseUpdate { get; set; }
[Reactive] public partial bool EnableUpdateViaProxy { get; set; }
public CheckUpdateViewModel()
@@ -33,20 +33,26 @@ public partial class CheckUpdateViewModel : MyReactiveObject
_ = UpdateView(_v2rayN, ex.Message);
});
EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate;
EnableUpdateViaProxy = _config.CheckUpdateItem.UpdateViaProxy;
this.WhenAnyValue(x => x.EnableCheckPreReleaseUpdate)
.SubscribeAsync(async _ => await OnCheckPreReleaseUpdateChanged());
this.WhenAnyValue(x => x.EnableUpdateViaProxy)
.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()
.Select(t => GetCheckUpdateModel(t))
.ToList();
@@ -63,6 +69,8 @@ public partial class CheckUpdateViewModel : MyReactiveObject
return new()
{
IsSelected = false,
IsCheckPreRelease = false,
ShowCheckPreRelease = false,
CoreType = coreType,
IsGeoFile = false,
Remarks = ResUI.menuCheckUpdate + $" ({ResUI.MsgNotSupport})",
@@ -73,6 +81,8 @@ public partial class CheckUpdateViewModel : MyReactiveObject
return new()
{
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType.ToString()) ?? true,
IsCheckPreRelease = _config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(coreType.ToString()) ?? false,
ShowCheckPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(coreType, true),
CoreType = coreType,
IsGeoFile = false,
Remarks = lastResult ?? ResUI.menuCheckUpdate,
@@ -84,22 +94,14 @@ public partial class CheckUpdateViewModel : MyReactiveObject
return new()
{
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(_geo) ?? true,
IsCheckPreRelease = false,
ShowCheckPreRelease = false,
CoreType = null,
IsGeoFile = true,
Remarks = ResUI.menuCheckUpdate,
};
}
private async Task OnCheckPreReleaseUpdateChanged()
{
if (_config.CheckUpdateItem.CheckPreReleaseUpdate == EnableCheckPreReleaseUpdate)
{
return;
}
_config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate;
await SaveSelectedCoreTypes();
}
private async Task OnUpdateViaProxyChanged()
{
if (_config.CheckUpdateItem.UpdateViaProxy == EnableUpdateViaProxy)
@@ -117,6 +119,11 @@ public partial class CheckUpdateViewModel : MyReactiveObject
.Select(t => t.CoreTypeForStorage)
.ToList();
_config.CheckUpdateItem.CheckPreReleaseCoreTypes =
CheckUpdateModels.Where(t => t.IsCheckPreRelease == true)
.Select(t => t.CoreTypeForStorage)
.ToList();
await ConfigHandler.SaveConfig(_config);
}
@@ -157,7 +164,7 @@ public partial class CheckUpdateViewModel : MyReactiveObject
}
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
var result = await updateService.CheckHasUpdateOnly(item.CoreType.Value, EnableCheckPreReleaseUpdate, EnableUpdateViaProxy);
var result = await updateService.CheckHasUpdateOnly(item.CoreType.Value, item.IsCheckPreRelease, EnableUpdateViaProxy);
if (result.Success && result.Version != null)
{
await UpdateView(item.CoreType, string.Format(ResUI.MsgCheckUpdateHasNewVersion, item.CoreType, result.Version));
@@ -203,15 +210,11 @@ public partial class CheckUpdateViewModel : MyReactiveObject
await UpdateView(_v2rayN, ResUI.MsgNotSupport);
continue;
}
await CheckUpdateN(EnableCheckPreReleaseUpdate);
}
else if (item.CoreType is ECoreType.Xray or ECoreType.sing_box)
{
await CheckUpdateCore(item, EnableCheckPreReleaseUpdate);
await CheckUpdateN(item.IsCheckPreRelease);
}
else if (item.CoreType.HasValue)
{
await CheckUpdateCore(item, false);
await CheckUpdateCore(item, item.IsCheckPreRelease);
}
}
@@ -24,12 +24,14 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await ClashConnectionClose(true);
});
_ = Task.Factory.StartNew(
async () => await GetClashConnectionsTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
this.WhenActivated(disposables =>
{
var cancelDisposable = new CancellationDisposable();
cancelDisposable.DisposeWith(disposables);
var token = cancelDisposable.Token;
Task.Run(() => GetClashConnectionsTask(token));
});
}
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
@@ -117,29 +119,40 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
await GetClashConnections();
}
public async Task GetClashConnectionsTask()
public async Task GetClashConnectionsTask(CancellationToken token = default)
{
var numOfExecuted = 1;
while (true)
try
{
await Task.Delay(1000 * 5);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
var numOfExecuted = 1;
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
continue;
}
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{
continue;
}
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{
continue;
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
{
continue;
}
await GetClashConnections();
}
await GetClashConnections();
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception ex)
{
Logging.SaveLog("GetClashConnectionsTask", ex);
}
}
}
@@ -55,12 +55,14 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand
_ = Task.Factory.StartNew(
async () => await GetClashProxiesTask(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
this.WhenActivated(disposables =>
{
var cancelDisposable = new CancellationDisposable();
cancelDisposable.DisposeWith(disposables);
var token = cancelDisposable.Token;
Task.Run(() => GetClashProxiesTask(token));
});
}
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
@@ -101,27 +103,40 @@ public partial class ClashProxiesViewModel : MyReactiveObject
#region task
public async Task GetClashProxiesTask()
public async Task GetClashProxiesTask(CancellationToken token = default)
{
var numOfExecuted = 1;
while (true)
try
{
await Task.Delay(1000 * 60);
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
var numOfExecuted = 1;
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
continue;
numOfExecuted++;
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
{
continue;
}
if (_config.ClashUIItem.ProxiesRefreshInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ProxiesRefreshInterval != 0)
{
continue;
}
await GetClashProxies();
}
if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0)
{
continue;
}
if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0)
{
continue;
}
await GetClashProxies();
}
catch (OperationCanceledException)
{
// Ignored
}
catch (Exception ex)
{
Logging.SaveLog("GetClashProxiesTask", ex);
}
}
@@ -32,6 +32,7 @@ public partial class MainWindowViewModel : MyReactiveObject
public ReactiveCommand<RxVoid, RxVoid> AddWireguardServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddAnytlsServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddNaiveServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddMasqueServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddCustomServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddCustomOutboundServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddPolicyGroupServerCmd { get; }
@@ -140,6 +141,10 @@ public partial class MainWindowViewModel : MyReactiveObject
{
await AddServerAsync(EConfigType.Naive);
});
AddMasqueServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(EConfigType.MASQUE);
});
AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(EConfigType.Custom);
@@ -680,6 +685,13 @@ public partial class MainWindowViewModel : MyReactiveObject
{
SetReloadEnabled(false);
RxSchedulers.MainThreadScheduler.Schedule(() =>
{
if (TabMainSelectedIndex < 0)
{
TabMainSelectedIndex = 0;
}
});
var profileItem = await ConfigHandler.GetDefaultServer(_config);
if (profileItem == null)
{
+25 -46
View File
@@ -2,12 +2,11 @@ namespace ServiceLib.ViewModels;
public partial class MsgViewModel : MyReactiveObject
{
public Interaction<string, RxVoid> DispatcherShowMsgInteraction { get; } = new();
public Interaction<string, RxVoid> ShowMsgInteraction { get; } = new();
private readonly ConcurrentQueue<string> _queueMsg = new();
private volatile bool _lastMsgFilterNotAvailable;
private int _showLock = 0; // 0 = unlocked, 1 = locked
public int NumMaxMsg { get; } = 500;
public int NumMaxMsg => 500;
[Reactive]
public partial string MsgFilter { get; set; }
@@ -30,59 +29,39 @@ public partial class MsgViewModel : MyReactiveObject
AppEvents.SendMsgViewRequested
.AsObservable()
//.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(content => _ = AppendQueueMsg(content));
.Subscribe(EnqueueQueueMsg);
this.WhenActivated(disposables =>
{
Signal.Every(TimeSpan.FromSeconds(1))
.Where(_ => AutoRefresh && AppManager.Instance.ShowInTaskbar)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(_ => FlushQueueToView())
.DisposeWith(disposables);
});
}
public void FlushQueueMsg()
private void FlushQueueToView()
{
_ = AppendQueueMsg(string.Empty);
}
private async Task AppendQueueMsg(string msg)
{
if (AutoRefresh == false)
if (!AutoRefresh || _queueMsg.IsEmpty)
{
return;
}
EnqueueQueueMsg(msg);
if (!AppManager.Instance.ShowInTaskbar)
{
return;
}
if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0)
var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var msg))
{
return;
sb.Append(msg);
}
try
if (sb.Length > 0)
{
await Task.Delay(500).ConfigureAwait(false);
var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var line))
{
sb.Append(line);
}
if (sb.Length > 0)
{
try
{
await DispatcherShowMsgInteraction.Handle(sb.ToString());
}
catch
{
_queueMsg.Enqueue(sb.ToString());
}
}
}
finally
{
Interlocked.Exchange(ref _showLock, 0);
ShowMsgInteraction.HandleSafe(sb.ToString()).Subscribe();
}
}
@@ -98,7 +77,7 @@ public partial class MsgViewModel : MyReactiveObject
{
try
{
if (!Regex.IsMatch(msg, MsgFilter))
if (!Utils.IsRegexMatch(msg, MsgFilter))
{
return;
}
@@ -110,11 +89,11 @@ public partial class MsgViewModel : MyReactiveObject
}
}
EnqueueWithLimit(msg);
if (!msg.EndsWith(Environment.NewLine))
{
EnqueueWithLimit(Environment.NewLine);
}
var formattedMsg = msg.EndsWith(Environment.NewLine)
? msg
: msg + Environment.NewLine;
EnqueueWithLimit(formattedMsg);
}
private void EnqueueWithLimit(string item)
@@ -113,6 +113,7 @@ public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
[Reactive] public partial string CoreType6 { get; set; }
[Reactive] public partial string CoreType7 { get; set; }
[Reactive] public partial string CoreType9 { get; set; }
[Reactive] public partial string CoreType14 { get; set; }
#endregion CoreType
@@ -283,6 +284,10 @@ public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
case 9:
CoreType9 = type;
break;
case 14:
CoreType14 = type;
break;
}
});
await Task.CompletedTask;
@@ -451,6 +456,10 @@ public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
type = CoreType9;
break;
case 14:
type = CoreType14;
break;
default:
continue;
}
@@ -24,12 +24,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -23,12 +23,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -24,12 +24,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -783,6 +783,70 @@
HorizontalAlignment="Left" />
</Grid>
<Grid
x:Name="gridMasque"
Grid.Row="2"
ColumnDefinitions="300,Auto"
IsVisible="False"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbUsername}" />
<TextBox
x:Name="txtId14"
Grid.Row="1"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbId3}" />
<TextBox
x:Name="txtSecurity14"
Grid.Row="2"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPath}" />
<TextBox
x:Name="txtPath14"
Grid.Row="3"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="4"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbHttpOutboundHeaders}" />
<TextBox
x:Name="txtHttpHeaders14"
Grid.Row="4"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
AcceptsReturn="True"
Classes="TextArea"
PlaceholderText="Header: Value"
TextWrapping="NoWrap" />
</Grid>
<Separator
x:Name="sepa2"
Grid.Row="3"
@@ -141,6 +141,13 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(currentTypeDisposables);
break;
case EConfigType.MASQUE:
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasquePath, v => v.txtPath14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasqueHeaders, v => v.txtHttpHeaders14.Text).DisposeWith(currentTypeDisposables);
break;
}
})
.DisposeWith(disposables);
@@ -194,7 +201,6 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
}
private void InitializeData(ProfileItem profileItem)
@@ -284,6 +290,14 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
break;
case EConfigType.MASQUE:
gridMasque.IsVisible = true;
sepa2.IsVisible = false;
gridTransport.IsVisible = false;
cmbFingerprint.IsEnabled = false;
cmbAlpn.IsEnabled = false;
break;
}
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
cmbStreamSecurity.SelectedItem = profileItem.StreamSecurity;
@@ -30,10 +30,11 @@
VerticalAlignment="Center"
Theme="{DynamicResource CardBorder}">
<Grid
Margin="{StaticResource Margin4}"
ColumnDefinitions="300,200"
RowDefinitions="Auto,Auto,Auto,Auto">
<Grid Margin="{StaticResource Margin4}" RowDefinitions="Auto,Auto,Auto,Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="300" />
<ColumnDefinition Width="Auto" MinWidth="200" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
@@ -79,10 +80,11 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Theme="{DynamicResource CardBorder}">
<Grid
Margin="{StaticResource Margin4}"
ColumnDefinitions="300,200"
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<Grid Margin="{StaticResource Margin4}" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="300" />
<ColumnDefinition Width="Auto" MinWidth="200" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Row="0"
Grid.Column="0"
@@ -35,27 +35,17 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsEnableCheckPreReleaseUpdate}" />
<ToggleSwitch
x:Name="togEnableCheckPreReleaseUpdate"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<Button
x:Name="btnCheckOnly"
Width="100"
MinWidth="100"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.menuCheckOnly}" />
<Button
x:Name="btnCheckUpdate"
Width="100"
MinWidth="100"
Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.menuCheckUpdate}" />
Content="{x:Static resx:ResUI.menuCheckAndUpdate}" />
</StackPanel>
</Border>
@@ -75,8 +65,8 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Panel Width="500" VerticalAlignment="Center">
<Grid ColumnDefinitions="1*,1*,3*" RowDefinitions="Auto">
<Panel Width="680" VerticalAlignment="Center">
<Grid ColumnDefinitions="1*,3*,1*,3*" RowDefinitions="Auto">
<ToggleSwitch
x:Name="togAutoRefresh"
Grid.Column="0"
@@ -84,13 +74,31 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{Binding IsSelected}" />
<TextBlock
<StackPanel
Grid.Column="1"
Margin="{StaticResource Margin4}"
IsVisible="{Binding ShowCheckPreRelease}"
Orientation="Horizontal">
<ToggleSwitch
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
IsChecked="{Binding IsCheckPreRelease}"
OffContent=""
OnContent="" />
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsEnableCheckPreReleaseUpdate}" />
</StackPanel>
<TextBlock
Grid.Column="2"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding CoreTypeForStorage}" />
<TextBlock
Grid.Column="2"
Grid.Column="3"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding Remarks}"
@@ -10,7 +10,6 @@ public partial class CheckUpdateView : ReactiveUserControl<CheckUpdateViewModel>
{
this.OneWayBind(ViewModel, vm => vm.CheckUpdateModels, v => v.lstCheckUpdates.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCheckPreReleaseUpdate, v => v.togEnableCheckPreReleaseUpdate.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableUpdateViaProxy, v => v.togEnableUpdateViaProxy.IsChecked).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckOnlyCmd, v => v.btnCheckOnly).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables);
@@ -24,12 +24,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -24,12 +24,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -23,17 +23,17 @@
Orientation="Horizontal">
<Button
x:Name="btnReset"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbReset}" />
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -18,6 +18,15 @@
ShowInTaskbar="True"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.Styles>
<!-- Keep the log selected; hide only the strip when it has no other tabs. -->
<Style Selector="TabControl.logPane /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="IsVisible" Value="{Binding ShowClashUI}" />
</Style>
<Style Selector="TabControl.logPane /template/ Border#PART_BorderSeparator">
<Setter Property="IsVisible" Value="{Binding ShowClashUI}" />
</Style>
</Window.Styles>
<dialogHost:DialogHost CloseOnClickAway="True" DisableOpeningAnimation="True">
<DockPanel>
<DockPanel Margin="{StaticResource Margin8}" DockPanel.Dock="Top">
@@ -45,6 +54,7 @@
<MenuItem x:Name="menuAddTrojanServer" Header="{x:Static resx:ResUI.menuAddTrojanServer}" />
<MenuItem x:Name="menuAddHysteria2Server" Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
<MenuItem x:Name="menuAddWireguardServer" Header="{x:Static resx:ResUI.menuAddWireguardServer}" />
<MenuItem x:Name="menuAddMasqueServer" Header="{x:Static resx:ResUI.menuAddMasqueServer}" />
<MenuItem x:Name="menuAddSocksServer" Header="{x:Static resx:ResUI.menuAddSocksServer}" />
<MenuItem x:Name="menuAddHttpServer" Header="{x:Static resx:ResUI.menuAddHttpServer}" />
<Separator />
@@ -123,6 +133,7 @@
<TabControl
x:Name="tabMain"
Grid.Column="2"
Classes="logPane"
HorizontalContentAlignment="Left">
<TabItem x:Name="tabMsgView" Header="{x:Static resx:ResUI.MsgInformationTitle}" />
<TabItem x:Name="tabClashProxies" Header="{x:Static resx:ResUI.TbProxies}" />
@@ -138,6 +149,7 @@
<TabControl
x:Name="tabMain1"
Grid.Row="2"
Classes="logPane"
TabStripPlacement="Left">
<TabItem x:Name="tabMsgView1" Header="{x:Static resx:ResUI.MsgInformationTitle}" />
<TabItem x:Name="tabClashProxies1" Header="{x:Static resx:ResUI.TbProxies}" />
@@ -46,6 +46,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddMasqueServerCmd, v => v.menuAddMasqueServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomOutboundServerCmd, v => v.menuAddCustomOutboundServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
@@ -418,7 +419,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.OneWayBind(ViewModel, vm => vm.MsgViewModel, v => v.tabMsgView.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashProxiesViewModel, v => v.tabClashProxies.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashConnectionsViewModel, v => v.tabClashConnections.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections.IsVisible).DisposeWith(currentLayoutDisposables);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain.SelectedIndex).DisposeWith(currentLayoutDisposables);
@@ -429,7 +429,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.OneWayBind(ViewModel, vm => vm.MsgViewModel, v => v.tabMsgView1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashProxiesViewModel, v => v.tabClashProxies1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ClashConnectionsViewModel, v => v.tabClashConnections1.Content).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabMsgView1.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashProxies1.IsVisible).DisposeWith(currentLayoutDisposables);
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections1.IsVisible).DisposeWith(currentLayoutDisposables);
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain1.SelectedIndex).DisposeWith(currentLayoutDisposables);
+2 -1
View File
@@ -23,7 +23,8 @@
Width="200"
Margin="{StaticResource MarginLr8}"
VerticalContentAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.MsgFilterTitle}" />
PlaceholderText="{x:Static resx:ResUI.MsgFilterTitle}"
ToolTip.Tip="{x:Static resx:ResUI.MsgFilterTitle}" />
<Button
x:Name="btnCopy"
+10 -8
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.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction =>
ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
{
var msg = interaction.Input;
Dispatcher.UIThread.Post(() => ShowMsg(msg),
DispatcherPriority.ApplicationIdle);
ShowMsg(msg);
interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
});
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
@@ -50,9 +47,14 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
if (togScrollToEnd.IsChecked ?? true)
{
txtMsg.ScrollToEnd();
Dispatcher.UIThread.Invoke(() =>
txtMsg.ScrollTo(txtMsg.LineCount, 0, AvaloniaEdit.Rendering.VisualYPosition.TextBottom, txtMsg.Bounds.Height, 0),
DispatcherPriority.Background);
// Let layout finish without a nested dispatcher loop during a native window drag.
Dispatcher.UIThread.Post(() =>
{
if (togScrollToEnd.IsChecked ?? true)
{
txtMsg.ScrollTo(txtMsg.LineCount, 0, AvaloniaEdit.Rendering.VisualYPosition.TextBottom, txtMsg.Bounds.Height, 0);
}
}, DispatcherPriority.Background);
}
}
@@ -23,12 +23,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -1192,7 +1192,7 @@
<Grid
Margin="{StaticResource Margin4}"
ColumnDefinitions="Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock
Grid.Row="1"
Grid.Column="0"
@@ -1296,6 +1296,19 @@
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="9"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="MASQUE" />
<ComboBox
x:Name="cmbCoreType14"
Grid.Row="9"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}" />
</Grid>
</TabItem>
</TabControl>
@@ -46,8 +46,9 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
cmbCoreType6.ItemsSource = Global.CoreTypes;
cmbCoreType7.ItemsSource = Global.CoreTypes;
cmbCoreType9.ItemsSource = Global.CoreTypes;
cmbCoreType14.ItemsSource = Global.CoreTypes;
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(2, 7).ToList();
cmbMixedConcurrencyCount.ItemsSource = Enumerable.Range(Global.SpeedTestConcurrencyCountMin, 20).ToList();
cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
@@ -145,6 +146,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.CoreType6, v => v.cmbCoreType6.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType7, v => v.cmbCoreType7.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType9, v => v.cmbCoreType9.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType14, v => v.cmbCoreType14.SelectedValue).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
@@ -28,12 +28,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -72,7 +72,8 @@
Margin="{StaticResource MarginLr4}"
VerticalContentAlignment="Center"
Text="{Binding ServerFilter, Mode=TwoWay}"
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}" />
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}"
ToolTip.Tip="{x:Static resx:ResUI.MsgServerTitle}" />
</WrapPanel>
<!-- Profiles grid -->
@@ -66,7 +66,8 @@
Width="200"
Margin="{StaticResource MarginLr4}"
VerticalContentAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}" />
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}"
ToolTip.Tip="{x:Static resx:ResUI.MsgServerTitle}" />
<Button
x:Name="btnAutofitColumnWidth"
@@ -439,6 +439,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
private static readonly DataFormat<ProfileItemModel> LstProfilesRowFormat =
DataFormat.CreateInProcessFormat<ProfileItemModel>("LstProfilesRow");
private (Point, PointerPressedEventArgs)? _dragStartPoint;
private void LstProfiles_PointerPressed(object? sender, PointerPressedEventArgs e)
@@ -208,12 +208,12 @@
</StackPanel>
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -30,12 +30,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -22,12 +22,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -136,7 +136,8 @@
Margin="{StaticResource Margin4}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvAutoUpdateInterval}" />
Text="{x:Static resx:ResUI.LvAutoUpdateInterval}"
TextWrapping="Wrap" />
</DockPanel>
<TextBlock
@@ -230,7 +231,8 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}"
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button
x:Name="btnSelectPrevProfile"
Grid.Row="10"
@@ -250,7 +252,8 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}"
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button
x:Name="btnSelectNextProfile"
Grid.Row="11"
@@ -20,12 +20,12 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLr8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="True" />
@@ -10,6 +10,10 @@
<CETCompat>false</CETCompat>
</PropertyGroup>
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-x64' Or '$(RuntimeIdentifier)' == 'osx-arm64'">
<RuntimeHostConfigurationOption Include="System.Net.Security.UseNetworkFramework" Value="true" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.AvaloniaEdit" />
<PackageReference Include="Avalonia.Controls.DataGrid">
@@ -27,13 +27,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
+2 -2
View File
@@ -25,13 +25,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
+85 -2
View File
@@ -33,13 +33,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -1000,6 +1000,89 @@
HorizontalAlignment="Left" />
</Grid>
<Grid
x:Name="gridMasque"
Grid.Row="2"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbUsername}" />
<TextBox
x:Name="txtId14"
Grid.Row="1"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbId3}" />
<TextBox
x:Name="txtSecurity14"
Grid.Row="2"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbPath}" />
<TextBox
x:Name="txtPath14"
Grid.Row="3"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
Style="{StaticResource DefTextBox}" />
<TextBlock
Grid.Row="4"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbHttpOutboundHeaders}" />
<TextBox
x:Name="txtHeaders14"
Grid.Row="4"
Grid.Column="1"
Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
materialDesign:HintAssist.Hint="Header: Value"
AcceptsReturn="True"
MaxLines="12"
MinLines="3"
Style="{StaticResource MyOutlinedTextBox}"
TextWrapping="Wrap" />
</Grid>
<Separator
x:Name="sepa2"
Grid.Row="3"
@@ -139,6 +139,13 @@ public partial class AddServerWindow
this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(currentTypeDisposables);
break;
case EConfigType.MASQUE:
this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasquePath, v => v.txtPath14.Text).DisposeWith(currentTypeDisposables);
this.Bind(ViewModel, vm => vm.MasqueHeaders, v => v.txtHeaders14.Text).DisposeWith(currentTypeDisposables);
break;
}
})
.DisposeWith(disposables);
@@ -284,6 +291,14 @@ public partial class AddServerWindow
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
break;
case EConfigType.MASQUE:
gridMasque.Visibility = Visibility.Visible;
sepa2.Visibility = Visibility.Collapsed;
gridTransport.Visibility = Visibility.Collapsed;
cmbFingerprint.IsEnabled = false;
cmbAlpn.IsEnabled = false;
break;
}
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
cmbStreamSecurity.SelectedItem = profileItem.StreamSecurity;
@@ -40,8 +40,8 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="Auto" MinWidth="300" />
<ColumnDefinition Width="Auto" MinWidth="200" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
@@ -98,8 +98,8 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="Auto" MinWidth="300" />
<ColumnDefinition Width="Auto" MinWidth="200" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock
+29 -17
View File
@@ -14,7 +14,9 @@
x:TypeArguments="vms:CheckUpdateViewModel"
Style="{StaticResource ViewGlobal}"
mc:Ignorable="d">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter" />
</UserControl.Resources>
<DockPanel Margin="{StaticResource Margin8}">
<StackPanel
Margin="{StaticResource Margin8}"
@@ -32,28 +34,18 @@
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<TextBlock
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsEnableCheckPreReleaseUpdate}" />
<ToggleButton
x:Name="togEnableCheckPreReleaseUpdate"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<Button
x:Name="btnCheckOnly"
Width="100"
MinWidth="100"
Margin="{StaticResource Margin8}"
Content="{x:Static resx:ResUI.menuCheckOnly}"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCheckUpdate"
Width="100"
MinWidth="100"
Margin="{StaticResource Margin8}"
Content="{x:Static resx:ResUI.menuCheckUpdate}"
Content="{x:Static resx:ResUI.menuCheckAndUpdate}"
IsDefault="True"
Style="{StaticResource DefButton}" />
</StackPanel>
@@ -71,7 +63,7 @@
<ListView.ItemTemplate>
<DataTemplate>
<Border
Width="500"
Width="660"
Margin="{StaticResource Margin8}"
Padding="0"
VerticalAlignment="Center">
@@ -81,6 +73,7 @@
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
@@ -91,12 +84,31 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsChecked="{Binding IsSelected}" />
<TextBlock
<StackPanel
Grid.Column="1"
Margin="{StaticResource MarginLeftRight4}"
Orientation="Horizontal"
Visibility="{Binding ShowCheckPreRelease, Converter={StaticResource BoolToVisConverter}}">
<ToggleButton
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left"
IsChecked="{Binding IsCheckPreRelease}" />
<TextBlock
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSettingsEnableCheckPreReleaseUpdate}" />
</StackPanel>
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource ListItemTitle}"
Text="{Binding CoreTypeForStorage}" />
<TextBlock
Grid.Column="2"
Grid.Column="3"
VerticalAlignment="Center"
Style="{StaticResource ListItemSubTitle}"
Text="{Binding Remarks}"
TextWrapping="WrapWithOverflow" />
@@ -10,7 +10,6 @@ public partial class CheckUpdateView
{
this.OneWayBind(ViewModel, vm => vm.CheckUpdateModels, v => v.lstCheckUpdates.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableCheckPreReleaseUpdate, v => v.togEnableCheckPreReleaseUpdate.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableUpdateViaProxy, v => v.togEnableUpdateViaProxy.IsChecked).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckOnlyCmd, v => v.btnCheckOnly).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables);
+2 -2
View File
@@ -25,13 +25,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -25,13 +25,13 @@
Orientation="Horizontal">
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
@@ -25,19 +25,19 @@
Orientation="Horizontal">
<Button
x:Name="btnReset"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbReset}"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnSave"
Width="100"
MinWidth="100"
Content="{x:Static resx:ResUI.TbConfirm}"
IsDefault="True"
Style="{StaticResource DefButton}" />
<Button
x:Name="btnCancel"
Width="100"
MinWidth="100"
Margin="{StaticResource MarginLeftRight8}"
Content="{x:Static resx:ResUI.TbCancel}"
IsCancel="true"
+4
View File
@@ -112,6 +112,10 @@
x:Name="menuAddWireguardServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddWireguardServer}" />
<MenuItem
x:Name="menuAddMasqueServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAddMasqueServer}" />
<MenuItem
x:Name="menuAddSocksServer"
Height="{StaticResource MenuItemHeight}"
+1
View File
@@ -46,6 +46,7 @@ public partial class MainWindow
this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddMasqueServerCmd, v => v.menuAddMasqueServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomOutboundServerCmd, v => v.menuAddCustomOutboundServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);

Some files were not shown because too many files have changed in this diff Show More