mirror of
https://github.com/2dust/v2rayN.git
synced 2026-10-21 12:44:59 +03:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94e9a3ca02 | |||
| 57f17d5939 | |||
| 66dd7da6e6 | |||
| 5e0ea3275d | |||
| 673283da4b | |||
| 1f083adbe4 | |||
| 80ab14d057 | |||
| 95f438abc9 | |||
| f1023e08e1 | |||
| 1883178b5f | |||
| 1cde521d75 | |||
| 2407d44f4f | |||
| fbf822d74c | |||
| 4869a6cbba | |||
| b501efbc45 | |||
| 5ea8ae6420 | |||
| fa9d4a4769 | |||
| 771ef3192a | |||
| f5747bb321 | |||
| d099e44ff6 | |||
| c63d271d16 | |||
| 6152cab494 | |||
| 3187eeef79 | |||
| 3364ec0e57 | |||
| 23033fa002 | |||
| 125cbb18a1 | |||
| f85326cf0e | |||
| 3666fc549d | |||
| a147249f95 | |||
| a28f68c3a7 | |||
| 1a689f869c | |||
| 50023a5976 | |||
| c659ca8af8 | |||
| 7d6a967c18 | |||
| 6c2edcb382 | |||
| 086c991464 | |||
| a9da04acc2 | |||
| e0c790eeb2 | |||
| d433aa3b04 | |||
| 91a7ed65d5 | |||
| 984e685ab3 | |||
| 29f99ceacc | |||
| 93d8174dbe | |||
| fa1e201c76 | |||
| e1cb99cd6e | |||
| 28139853a8 | |||
| 5d9718a0bb | |||
| 740729c847 | |||
| 13cd2ef80d | |||
| be3566df2f | |||
| 6e9ec6cbee | |||
| e6e95e00d7 | |||
| 2fbe81041b | |||
| 05901b5673 | |||
| bdd0ee1620 | |||
| 00d1001def | |||
| 21bafe52e0 | |||
| cef40d38ee | |||
| 4f5a6be12d | |||
| 7674d7e472 |
@@ -0,0 +1,923 @@
|
|||||||
|
name: build v2rayN.WebAPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
description: 'Release tag (for example 7.x.y). Empty builds a development API package.'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
type: string
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'v2rayN/v2rayN.WebAPI/**'
|
||||||
|
- 'v2rayN/ServiceLib/**'
|
||||||
|
- 'v2rayN/ServiceLib.Tests/**'
|
||||||
|
- 'v2rayN/Directory.Build.*'
|
||||||
|
- 'v2rayN/Directory.Packages.props'
|
||||||
|
- 'v2rayN/v2rayN.slnx'
|
||||||
|
- 'global.json'
|
||||||
|
- '.github/workflows/build-WebAPI.yml'
|
||||||
|
- '.github/workflows/release-WebAPI.yml'
|
||||||
|
- '.github/workflows/build-all.yml'
|
||||||
|
- '.github/workflows/upload-sign.yml'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'v2rayN/v2rayN.WebAPI/**'
|
||||||
|
- 'v2rayN/ServiceLib/**'
|
||||||
|
- 'v2rayN/ServiceLib.Tests/**'
|
||||||
|
- 'v2rayN/Directory.Build.*'
|
||||||
|
- 'v2rayN/Directory.Packages.props'
|
||||||
|
- 'v2rayN/v2rayN.slnx'
|
||||||
|
- 'global.json'
|
||||||
|
- '.github/workflows/build-WebAPI.yml'
|
||||||
|
- '.github/workflows/release-WebAPI.yml'
|
||||||
|
- '.github/workflows/build-all.yml'
|
||||||
|
- '.github/workflows/upload-sign.yml'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
identity:
|
||||||
|
name: Resolve API build identity
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.resolve.outputs.version }}
|
||||||
|
tag: ${{ steps.resolve.outputs.tag }}
|
||||||
|
release: ${{ steps.resolve.outputs.release }}
|
||||||
|
build_date: ${{ steps.resolve.outputs.build_date }}
|
||||||
|
steps:
|
||||||
|
- name: Resolve release identity
|
||||||
|
id: resolve
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
if [[ -z "$RELEASE_TAG" ]]; then
|
||||||
|
{
|
||||||
|
echo "version=0.0.0-dev"
|
||||||
|
echo "tag="
|
||||||
|
echo "release=false"
|
||||||
|
echo "build_date=$build_date"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
[[ "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
|
||||||
|
echo "Invalid release tag: $RELEASE_TAG" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
{
|
||||||
|
echo "version=$RELEASE_TAG"
|
||||||
|
echo "tag=$RELEASE_TAG"
|
||||||
|
echo "release=true"
|
||||||
|
echo "build_date=$build_date"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
verify:
|
||||||
|
name: Backend, ServiceLib, package smoke
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
env:
|
||||||
|
TUNIT_MAX_PARALLEL_TESTS: 1
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: actions/setup-dotnet@v6
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.x'
|
||||||
|
- name: Verify API build, backend and ServiceLib tests, native startup and package boundaries
|
||||||
|
run: bash v2rayN/v2rayN.WebAPI/Scripts/verify.sh "$RUNNER_TEMP/v2rayN.WebAPI-verify"
|
||||||
|
- name: Check patch whitespace
|
||||||
|
if: github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || '' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
base_sha="$BASE_SHA"
|
||||||
|
if [[ -z "$base_sha" ]]; then
|
||||||
|
base_sha="$(git rev-parse HEAD^)"
|
||||||
|
fi
|
||||||
|
if [[ "$base_sha" =~ ^0+$ ]]; then
|
||||||
|
empty_tree="$(git hash-object -t tree -w /dev/null)"
|
||||||
|
git diff --check "$empty_tree" HEAD
|
||||||
|
else
|
||||||
|
git diff --check "$base_sha...HEAD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
windows:
|
||||||
|
name: Windows WebAPI build and runtime smoke
|
||||||
|
needs: identity
|
||||||
|
runs-on: windows-latest
|
||||||
|
env:
|
||||||
|
TUNIT_MAX_PARALLEL_TESTS: 1
|
||||||
|
WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||||
|
WEB_COMMIT: ${{ github.sha }}
|
||||||
|
WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||||
|
WEB_REPOSITORY: ${{ github.repository }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: actions/setup-dotnet@v6
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.x'
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Restore WebAPI
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet restore v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj
|
||||||
|
- name: Restore WebAPI.Tests
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet restore v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj
|
||||||
|
- name: Restore ServiceLib.Tests
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet restore v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj
|
||||||
|
- name: Build WebAPI
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet build v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj --configuration Release --no-restore
|
||||||
|
- name: Run WebAPI.Tests
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet test --project v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj --configuration Release --no-restore
|
||||||
|
- name: Run ServiceLib.Tests
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet test --project v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj --configuration Release --no-restore
|
||||||
|
- name: Publish win-x64 self-contained single-file WebAPI
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||||
|
dotnet publish v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj `
|
||||||
|
--configuration Release `
|
||||||
|
--runtime win-x64 `
|
||||||
|
--self-contained true `
|
||||||
|
-p:PublishSingleFile=true `
|
||||||
|
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||||
|
-p:WebVersion=$env:WEB_VERSION `
|
||||||
|
-p:WebCommit=$env:WEB_COMMIT `
|
||||||
|
-p:WebBuildDate=$env:WEB_BUILD_DATE `
|
||||||
|
-p:WebRepository=$env:WEB_REPOSITORY `
|
||||||
|
--output $publishDirectory
|
||||||
|
$identity = [ordered]@{
|
||||||
|
product = 'v2rayN.WebAPI'
|
||||||
|
version = $env:WEB_VERSION
|
||||||
|
commit = $env:WEB_COMMIT
|
||||||
|
buildDate = $env:WEB_BUILD_DATE
|
||||||
|
rid = 'win-x64'
|
||||||
|
}
|
||||||
|
$json = $identity | ConvertTo-Json -Compress
|
||||||
|
[IO.File]::WriteAllText((Join-Path $publishDirectory 'v2rayN.WebAPI.build.json'), $json, [Text.UTF8Encoding]::new($false))
|
||||||
|
- name: Verify single-file publish output
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||||
|
$executable = Join-Path $publishDirectory 'v2rayN.WebAPI.exe'
|
||||||
|
if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) {
|
||||||
|
throw "dotnet publish did not produce $executable"
|
||||||
|
}
|
||||||
|
if ((Get-Item -LiteralPath $executable).Length -eq 0) {
|
||||||
|
throw "Published executable is empty: $executable"
|
||||||
|
}
|
||||||
|
$looseDlls = @(Get-ChildItem -LiteralPath $publishDirectory -Filter '*.dll' -File -Recurse)
|
||||||
|
if ($looseDlls.Count -gt 0) {
|
||||||
|
throw "Single-file publish left loose DLLs: $($looseDlls.FullName -join ', ')"
|
||||||
|
}
|
||||||
|
- name: Smoke-test first startup, health endpoint and graceful shutdown
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||||
|
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $publishDirectory
|
||||||
|
- name: Publish previous Windows fixture for update regression
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-previous'
|
||||||
|
dotnet publish v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj `
|
||||||
|
--configuration Release `
|
||||||
|
--runtime win-x64 `
|
||||||
|
--self-contained true `
|
||||||
|
-p:PublishSingleFile=true `
|
||||||
|
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||||
|
-p:WebVersion=0.0.1-regression `
|
||||||
|
-p:WebCommit=$env:WEB_COMMIT `
|
||||||
|
-p:WebBuildDate=$env:WEB_BUILD_DATE `
|
||||||
|
-p:WebRepository=$env:WEB_REPOSITORY `
|
||||||
|
--output $publishDirectory
|
||||||
|
$identity = [ordered]@{
|
||||||
|
product = 'v2rayN.WebAPI'
|
||||||
|
version = '0.0.1-regression'
|
||||||
|
commit = $env:WEB_COMMIT
|
||||||
|
buildDate = $env:WEB_BUILD_DATE
|
||||||
|
rid = 'win-x64'
|
||||||
|
}
|
||||||
|
$json = $identity | ConvertTo-Json -Compress
|
||||||
|
[IO.File]::WriteAllText((Join-Path $publishDirectory 'v2rayN.WebAPI.build.json'), $json, [Text.UTF8Encoding]::new($false))
|
||||||
|
- name: Add the matching Windows Core bundle
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$bundle = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-64.zip'
|
||||||
|
$expanded = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-64-core'
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||||
|
$previousDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-previous'
|
||||||
|
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-windows-64.zip' -OutFile $bundle
|
||||||
|
Expand-Archive -LiteralPath $bundle -DestinationPath $expanded -Force
|
||||||
|
$sourceBin = Join-Path $expanded 'v2rayN-windows-64\bin'
|
||||||
|
if (-not (Test-Path -LiteralPath $sourceBin -PathType Container)) {
|
||||||
|
throw "Windows Core bundle is missing $sourceBin"
|
||||||
|
}
|
||||||
|
foreach ($directory in @($publishDirectory, $previousDirectory)) {
|
||||||
|
$destinationBin = Join-Path $directory 'bin'
|
||||||
|
New-Item -ItemType Directory -Path $destinationBin -Force | Out-Null
|
||||||
|
Copy-Item -Path (Join-Path $sourceBin '*') -Destination $destinationBin -Recurse -Force
|
||||||
|
}
|
||||||
|
foreach ($directory in @($publishDirectory, $previousDirectory)) {
|
||||||
|
$checkBin = Join-Path $directory 'bin'
|
||||||
|
foreach ($relative in @('xray\xray.exe','sing_box\sing-box.exe','mihomo\mihomo.exe','geoip.dat','geosite.dat','geoip.metadb','Country.mmdb')) {
|
||||||
|
$required = Join-Path $checkBin $relative
|
||||||
|
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Missing Windows Core asset: $required" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$candidateBin = Join-Path $publishDirectory 'bin'
|
||||||
|
& (Join-Path $candidateBin 'xray\xray.exe') -version
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "xray.exe smoke failed: $LASTEXITCODE" }
|
||||||
|
& (Join-Path $candidateBin 'sing_box\sing-box.exe') version
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "sing-box.exe smoke failed: $LASTEXITCODE" }
|
||||||
|
& (Join-Path $candidateBin 'mihomo\mihomo.exe') -v
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "mihomo.exe smoke failed: $LASTEXITCODE" }
|
||||||
|
- name: Smoke-test Windows backup restore handoff and Core intent recovery
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 `
|
||||||
|
-Mode restore `
|
||||||
|
-PublishDirectory (Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64')
|
||||||
|
- name: Smoke-test Windows self-update replacement and rollback
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 `
|
||||||
|
-Mode update `
|
||||||
|
-PreviousPublishDirectory (Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-previous') `
|
||||||
|
-CandidatePublishDirectory (Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64')
|
||||||
|
- name: Package and smoke-test Windows full-install and app-only ZIPs
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||||
|
$dist = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-dist'
|
||||||
|
python v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid win-x64 --publish $publishDirectory --dist $dist
|
||||||
|
$fullZip = Join-Path $dist 'v2rayN-windows-64-WebAPI.zip'
|
||||||
|
$appOnlyZip = Join-Path $dist 'v2rayN-windows-64-WebAPI-update.zip'
|
||||||
|
if (-not (Test-Path -LiteralPath $fullZip -PathType Leaf) -or -not (Test-Path -LiteralPath $appOnlyZip -PathType Leaf)) {
|
||||||
|
throw 'Windows release ZIPs were not created.'
|
||||||
|
}
|
||||||
|
$extracted = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-64-WebAPI-smoke'
|
||||||
|
Expand-Archive -LiteralPath $fullZip -DestinationPath $extracted -Force
|
||||||
|
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $extracted
|
||||||
|
"Windows full-install ZIP: $fullZip"
|
||||||
|
"Windows app-only update ZIP: $appOnlyZip"
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
with:
|
||||||
|
name: v2rayN.WebAPI-win-x64
|
||||||
|
path: ${{ runner.temp }}/v2rayN.WebAPI-win-x64-dist/*.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# The self-update replacement smoke stays on win-x64: the updater logic is RID-generic
|
||||||
|
# and the per-RID PE/ELF checks are covered by unit tests. This job proves the win-arm64
|
||||||
|
# build, first startup, health endpoint and graceful shutdown on native ARM64 Windows.
|
||||||
|
windows-arm64:
|
||||||
|
name: Windows ARM64 WebAPI build and runtime smoke
|
||||||
|
needs: identity
|
||||||
|
runs-on: windows-11-arm
|
||||||
|
env:
|
||||||
|
TUNIT_MAX_PARALLEL_TESTS: 1
|
||||||
|
WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||||
|
WEB_COMMIT: ${{ github.sha }}
|
||||||
|
WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||||
|
WEB_REPOSITORY: ${{ github.repository }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: actions/setup-dotnet@v6
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.x'
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Restore WebAPI
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet restore v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj
|
||||||
|
- name: Restore WebAPI.Tests
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet restore v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj
|
||||||
|
- name: Restore ServiceLib.Tests
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet restore v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj
|
||||||
|
- name: Build WebAPI
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet build v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj --configuration Release --no-restore
|
||||||
|
- name: Run WebAPI.Tests
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet test --project v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj --configuration Release --no-restore
|
||||||
|
- name: Run ServiceLib.Tests
|
||||||
|
shell: pwsh
|
||||||
|
run: dotnet test --project v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj --configuration Release --no-restore
|
||||||
|
- name: Publish win-arm64 self-contained single-file WebAPI
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
|
||||||
|
dotnet publish v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj `
|
||||||
|
--configuration Release `
|
||||||
|
--runtime win-arm64 `
|
||||||
|
--self-contained true `
|
||||||
|
-p:PublishSingleFile=true `
|
||||||
|
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||||
|
-p:WebVersion=$env:WEB_VERSION `
|
||||||
|
-p:WebCommit=$env:WEB_COMMIT `
|
||||||
|
-p:WebBuildDate=$env:WEB_BUILD_DATE `
|
||||||
|
-p:WebRepository=$env:WEB_REPOSITORY `
|
||||||
|
--output $publishDirectory
|
||||||
|
$identity = [ordered]@{
|
||||||
|
product = 'v2rayN.WebAPI'
|
||||||
|
version = $env:WEB_VERSION
|
||||||
|
commit = $env:WEB_COMMIT
|
||||||
|
buildDate = $env:WEB_BUILD_DATE
|
||||||
|
rid = 'win-arm64'
|
||||||
|
}
|
||||||
|
$json = $identity | ConvertTo-Json -Compress
|
||||||
|
[IO.File]::WriteAllText((Join-Path $publishDirectory 'v2rayN.WebAPI.build.json'), $json, [Text.UTF8Encoding]::new($false))
|
||||||
|
- name: Smoke-test first startup, health endpoint and graceful shutdown
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
|
||||||
|
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $publishDirectory
|
||||||
|
- name: Add the matching Windows ARM64 Core bundle
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$bundle = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-arm64.zip'
|
||||||
|
$expanded = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-arm64-core'
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
|
||||||
|
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-windows-arm64.zip' -OutFile $bundle
|
||||||
|
Expand-Archive -LiteralPath $bundle -DestinationPath $expanded -Force
|
||||||
|
$sourceBin = Join-Path $expanded 'v2rayN-windows-arm64\bin'
|
||||||
|
if (-not (Test-Path -LiteralPath $sourceBin -PathType Container)) {
|
||||||
|
throw "Windows ARM64 Core bundle is missing $sourceBin"
|
||||||
|
}
|
||||||
|
$destinationBin = Join-Path $publishDirectory 'bin'
|
||||||
|
New-Item -ItemType Directory -Path $destinationBin -Force | Out-Null
|
||||||
|
Copy-Item -Path (Join-Path $sourceBin '*') -Destination $destinationBin -Recurse -Force
|
||||||
|
foreach ($relative in @('xray\xray.exe','sing_box\sing-box.exe','mihomo\mihomo.exe','geoip.dat','geosite.dat','geoip.metadb','Country.mmdb')) {
|
||||||
|
$required = Join-Path $destinationBin $relative
|
||||||
|
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Missing Windows ARM64 Core asset: $required" }
|
||||||
|
}
|
||||||
|
& (Join-Path $destinationBin 'xray\xray.exe') -version
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "xray.exe smoke failed: $LASTEXITCODE" }
|
||||||
|
& (Join-Path $destinationBin 'sing_box\sing-box.exe') version
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "sing-box.exe smoke failed: $LASTEXITCODE" }
|
||||||
|
& (Join-Path $destinationBin 'mihomo\mihomo.exe') -v
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "mihomo.exe smoke failed: $LASTEXITCODE" }
|
||||||
|
- name: Package Windows ARM64 full-install and app-only ZIPs
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
|
||||||
|
$dist = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64-dist'
|
||||||
|
python v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid win-arm64 --publish $publishDirectory --dist $dist
|
||||||
|
$fullZip = Join-Path $dist 'v2rayN-windows-arm64-WebAPI.zip'
|
||||||
|
$appOnlyZip = Join-Path $dist 'v2rayN-windows-arm64-WebAPI-update.zip'
|
||||||
|
if (-not (Test-Path -LiteralPath $fullZip -PathType Leaf) -or -not (Test-Path -LiteralPath $appOnlyZip -PathType Leaf)) {
|
||||||
|
throw 'Windows ARM64 release ZIPs were not created.'
|
||||||
|
}
|
||||||
|
$extracted = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-arm64-WebAPI-smoke'
|
||||||
|
Expand-Archive -LiteralPath $fullZip -DestinationPath $extracted -Force
|
||||||
|
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $extracted
|
||||||
|
"Windows ARM64 full-install ZIP: $fullZip"
|
||||||
|
"Windows ARM64 app-only update ZIP: $appOnlyZip"
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
with:
|
||||||
|
name: v2rayN.WebAPI-win-arm64
|
||||||
|
path: ${{ runner.temp }}/v2rayN.WebAPI-win-arm64-dist/*.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
native:
|
||||||
|
name: Native API build (${{ matrix.rid }})
|
||||||
|
needs: identity
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Development builds keep x64 coverage; releases also validate ARM64.
|
||||||
|
# Only tested Linux RIDs are enabled; native RID/runner pairs are independent
|
||||||
|
# of Desktop workflows and the container platform matrix.
|
||||||
|
include: >-
|
||||||
|
${{ fromJSON(needs.identity.outputs.release == 'true'
|
||||||
|
&& '[{"rid":"linux-x64","runner":"ubuntu-24.04"},{"rid":"linux-arm64","runner":"ubuntu-24.04-arm"}]'
|
||||||
|
|| '[{"rid":"linux-x64","runner":"ubuntu-24.04"}]') }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
env:
|
||||||
|
RID: ${{ matrix.rid }}
|
||||||
|
V2RAYN_WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||||
|
V2RAYN_WEB_REPOSITORY: ${{ github.repository }}
|
||||||
|
V2RAYN_WEB_COMMIT: ${{ github.sha }}
|
||||||
|
V2RAYN_WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: actions/setup-dotnet@v6
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.x'
|
||||||
|
- name: Publish native API
|
||||||
|
run: bash v2rayN/v2rayN.WebAPI/Scripts/publish-native.sh "$RID"
|
||||||
|
- name: Verify native executable
|
||||||
|
run: test -x "v2rayN/v2rayN.WebAPI/publish/$RID/v2rayN.WebAPI"
|
||||||
|
- name: Add the matching Linux Core bundle
|
||||||
|
if: needs.identity.outputs.release == 'true'
|
||||||
|
run: sh v2rayN/v2rayN.WebAPI/Scripts/fetch-core-bundle.sh "$RID" "v2rayN/v2rayN.WebAPI/publish/$RID/bin"
|
||||||
|
- name: Package API install and self-update ZIPs
|
||||||
|
if: needs.identity.outputs.release == 'true'
|
||||||
|
run: python3 v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid "$RID" --publish "v2rayN/v2rayN.WebAPI/publish/$RID" --dist dist
|
||||||
|
- name: Smoke-test bundled Cores
|
||||||
|
if: needs.identity.outputs.release == 'true'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
publish="v2rayN/v2rayN.WebAPI/publish/$RID"
|
||||||
|
"$publish/bin/xray/xray" -version
|
||||||
|
"$publish/bin/sing_box/sing-box" version
|
||||||
|
"$publish/bin/mihomo/mihomo" -v
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
if: needs.identity.outputs.release == 'true'
|
||||||
|
with:
|
||||||
|
name: v2rayN.WebAPI-${{ matrix.rid }}
|
||||||
|
path: dist/*.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# linux-riscv64 runs natively on the RISE RISC-V runners with the community
|
||||||
|
# xujiegb/dotnet-riscv SDK; single-file publish is verified to work there.
|
||||||
|
native-riscv64:
|
||||||
|
name: Native API build (linux-riscv64)
|
||||||
|
needs: identity
|
||||||
|
if: needs.identity.outputs.release == 'true'
|
||||||
|
runs-on: ubuntu-24.04-riscv
|
||||||
|
container: debian:13
|
||||||
|
env:
|
||||||
|
RID: linux-riscv64
|
||||||
|
V2RAYN_WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||||
|
V2RAYN_WEB_REPOSITORY: ${{ github.repository }}
|
||||||
|
V2RAYN_WEB_COMMIT: ${{ github.sha }}
|
||||||
|
V2RAYN_WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||||
|
steps:
|
||||||
|
- name: Prepare tools (Debian)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 libicu76 || \
|
||||||
|
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 libicu72
|
||||||
|
- name: Checkout repo
|
||||||
|
shell: bash
|
||||||
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rm -rf ./*
|
||||||
|
git init .
|
||||||
|
git config --global --add safe.directory "$PWD"
|
||||||
|
git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||||
|
git fetch --depth=1 origin "${GITHUB_SHA}"
|
||||||
|
git checkout FETCH_HEAD
|
||||||
|
- name: Install community .NET SDK (linux-riscv64)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
SDK=dotnet-sdk-10.0.111-linux-riscv64.tar.gz
|
||||||
|
curl -fL "https://github.com/xujiegb/dotnet-riscv/releases/download/10.0.111/$SDK" -o /tmp/$SDK
|
||||||
|
mkdir -p "$HOME/.dotnet"
|
||||||
|
tar -C "$HOME/.dotnet" -xzf /tmp/$SDK
|
||||||
|
echo "DOTNET_ROOT=$HOME/.dotnet" >> "$GITHUB_ENV"
|
||||||
|
echo "$HOME/.dotnet" >> "$GITHUB_PATH"
|
||||||
|
export DOTNET_ROOT="$HOME/.dotnet"
|
||||||
|
export PATH="$DOTNET_ROOT:$PATH"
|
||||||
|
dotnet --info
|
||||||
|
- name: Publish native API
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
export DOTNET_ROOT="$HOME/.dotnet"
|
||||||
|
export PATH="$DOTNET_ROOT:$PATH"
|
||||||
|
bash v2rayN/v2rayN.WebAPI/Scripts/publish-native.sh "$RID"
|
||||||
|
test -x "v2rayN/v2rayN.WebAPI/publish/$RID/v2rayN.WebAPI"
|
||||||
|
- name: Add the matching Linux Core bundle
|
||||||
|
shell: bash
|
||||||
|
run: sh v2rayN/v2rayN.WebAPI/Scripts/fetch-core-bundle.sh "$RID" "v2rayN/v2rayN.WebAPI/publish/$RID/bin"
|
||||||
|
- name: Package API install and self-update ZIPs
|
||||||
|
shell: bash
|
||||||
|
run: python3 v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid "$RID" --publish "v2rayN/v2rayN.WebAPI/publish/$RID" --dist dist
|
||||||
|
- name: Smoke-test bundled Cores
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
publish="v2rayN/v2rayN.WebAPI/publish/$RID"
|
||||||
|
"$publish/bin/xray/xray" -version
|
||||||
|
"$publish/bin/sing_box/sing-box" version
|
||||||
|
"$publish/bin/mihomo/mihomo" -v
|
||||||
|
- name: Smoke-test the published API (native riscv64)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
publish="v2rayN/v2rayN.WebAPI/publish/$RID"
|
||||||
|
"$publish/v2rayN.WebAPI" --help | head -n 5
|
||||||
|
export V2RAYN_WEB_API_KEY=ci-smoke-key-0123456789abcdef
|
||||||
|
export ASPNETCORE_URLS=http://127.0.0.1:5080
|
||||||
|
cd "$publish"
|
||||||
|
./v2rayN.WebAPI --foreground --no-open > /tmp/smoke.log 2>&1 &
|
||||||
|
app_pid=$!
|
||||||
|
echo "$app_pid" > /tmp/smoke.pid
|
||||||
|
echo "riscv64 API smoke application PID: $app_pid"
|
||||||
|
ok=0
|
||||||
|
for i in $(seq 1 90); do
|
||||||
|
if curl --noproxy '*' -fsS http://127.0.0.1:5080/api/health >/dev/null 2>&1; then ok=1; break; fi
|
||||||
|
if ! kill -0 "$app_pid" 2>/dev/null; then break; fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ "$ok" != 1 ]; then
|
||||||
|
echo 'riscv64 API smoke did not become healthy'
|
||||||
|
tail -n 60 /tmp/smoke.log || true
|
||||||
|
kill -TERM "$app_pid" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
kill -KILL "$app_pid" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
kill -TERM "$app_pid"
|
||||||
|
stopped=0
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if ! kill -0 "$app_pid" 2>/dev/null; then stopped=1; break; fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ "$stopped" != 1 ]; then
|
||||||
|
echo 'riscv64 API smoke did not exit within 30s after SIGTERM'
|
||||||
|
tail -n 60 /tmp/smoke.log || true
|
||||||
|
kill -KILL "$app_pid" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
set +e
|
||||||
|
wait "$app_pid"
|
||||||
|
exit_code=$?
|
||||||
|
set -e
|
||||||
|
if [ "$exit_code" -ne 0 ]; then
|
||||||
|
echo "riscv64 API smoke exited with status $exit_code after SIGTERM"
|
||||||
|
tail -n 60 /tmp/smoke.log || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo 'linux-riscv64 native API smoke OK'
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: v2rayN.WebAPI-linux-riscv64
|
||||||
|
path: dist/*.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# linux-loongarch64 publishes inside the upstream QEMU loong64 VM with the official
|
||||||
|
# Loongson .NET SDK; the VM runs the native smoke before the host packages the ZIPs.
|
||||||
|
native-loong64:
|
||||||
|
name: Native API build (linux-loongarch64)
|
||||||
|
needs: identity
|
||||||
|
if: needs.identity.outputs.release == 'true'
|
||||||
|
runs-on: ubuntu-26.04-arm
|
||||||
|
env:
|
||||||
|
RID: linux-loongarch64
|
||||||
|
V2RAYN_WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||||
|
V2RAYN_WEB_REPOSITORY: ${{ github.repository }}
|
||||||
|
V2RAYN_WEB_COMMIT: ${{ github.sha }}
|
||||||
|
V2RAYN_WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||||
|
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
|
||||||
|
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd
|
||||||
|
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
|
||||||
|
QCOW2_IMAGE: debian13-loong64.qcow2
|
||||||
|
EFI_CODE: edk2-loongarch64-code.fd
|
||||||
|
EFI_VARS: edk2-loongarch64-vars.fd
|
||||||
|
QEMU_VERSION: 10.2.4
|
||||||
|
steps:
|
||||||
|
- name: Prepare host tools
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
|
||||||
|
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
|
||||||
|
virtiofsd --version || virtiofsd --help | head
|
||||||
|
- name: Host clone repo
|
||||||
|
shell: bash
|
||||||
|
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rm -rf repo
|
||||||
|
git init repo
|
||||||
|
git -C repo config --global --add safe.directory "$PWD/repo" || true
|
||||||
|
git -C repo remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||||
|
git -C repo fetch --depth=1 origin "${GITHUB_SHA}"
|
||||||
|
git -C repo checkout FETCH_HEAD
|
||||||
|
- name: Download QEMU prebuild
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
|
||||||
|
tar -xzf qemu-linux-arm64-v8.2.tar.gz
|
||||||
|
mkdir -p "$HOME/qemu-install"
|
||||||
|
rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
|
||||||
|
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
|
||||||
|
- name: Download loong64 qcow2 and EFI firmware
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
|
||||||
|
wget -O "$EFI_CODE" "$EFI_CODE_URL"
|
||||||
|
wget -O "$EFI_VARS" "$EFI_VARS_URL"
|
||||||
|
qemu-img info "$QCOW2_IMAGE"
|
||||||
|
- name: Stage the VM build script and the matching Core bundle
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cat > webapi-loong.env <<EOF
|
||||||
|
WEB_VERSION=${V2RAYN_WEB_VERSION}
|
||||||
|
WEB_REPOSITORY=${V2RAYN_WEB_REPOSITORY}
|
||||||
|
WEB_COMMIT=${V2RAYN_WEB_COMMIT}
|
||||||
|
WEB_BUILD_DATE=${V2RAYN_WEB_BUILD_DATE}
|
||||||
|
EOF
|
||||||
|
cat > webapi-loong-run.sh <<'SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set +e
|
||||||
|
set -o pipefail
|
||||||
|
source /workspace/webapi-loong.env
|
||||||
|
export DOTNET_ROOT=/root/.dotnet
|
||||||
|
export PATH="$DOTNET_ROOT:$PATH"
|
||||||
|
mkdir -p /root/.dotnet
|
||||||
|
SDK=dotnet-sdk-10.0.111-linux-loongarch64.tar.gz
|
||||||
|
curl -fL "https://github.com/loongson/dotnet/releases/download/v10.0.111-loongarch64/$SDK" -o /tmp/$SDK || exit 9
|
||||||
|
tar -C /root/.dotnet -xzf /tmp/$SDK || exit 9
|
||||||
|
dotnet --info >/dev/null 2>&1 || exit 9
|
||||||
|
cd /root/v2rayN-src || exit 9
|
||||||
|
mkdir -p /workspace/publish/linux-loongarch64
|
||||||
|
V2RAYN_WEB_VERSION="$WEB_VERSION" V2RAYN_WEB_REPOSITORY="$WEB_REPOSITORY" V2RAYN_WEB_COMMIT="$WEB_COMMIT" V2RAYN_WEB_BUILD_DATE="$WEB_BUILD_DATE" \
|
||||||
|
bash v2rayN/v2rayN.WebAPI/Scripts/publish-native.sh linux-loongarch64 /workspace/publish/linux-loongarch64 2>&1 | tee /tmp/publish.log
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
mkdir -p /workspace/publish/linux-loongarch64/bin
|
||||||
|
cp -a /workspace/core-bundle/. /workspace/publish/linux-loongarch64/bin/
|
||||||
|
# Smoke a VM-local copy: the shared publish tree must stay exactly the packaging
|
||||||
|
# input, without runtime lock/data files owned by the VM user.
|
||||||
|
rm -rf /root/websmoke
|
||||||
|
cp -a /workspace/publish/linux-loongarch64 /root/websmoke
|
||||||
|
cd /root/websmoke || exit 9
|
||||||
|
./bin/xray/xray -version >/dev/null || rc=9
|
||||||
|
./bin/sing_box/sing-box version >/dev/null || rc=9
|
||||||
|
./bin/mihomo/mihomo -v >/dev/null || rc=9
|
||||||
|
./v2rayN.WebAPI --help >/dev/null || rc=9
|
||||||
|
V2RAYN_WEB_API_KEY=ci-smoke-key-0123456789abcdef ASPNETCORE_URLS=http://127.0.0.1:5080 ./v2rayN.WebAPI --foreground --no-open > /tmp/smoke.log 2>&1 &
|
||||||
|
APP=$!
|
||||||
|
echo "$APP" > /tmp/smoke.pid
|
||||||
|
ok=0
|
||||||
|
for i in $(seq 1 90); do
|
||||||
|
if curl --noproxy '*' -fsS http://127.0.0.1:5080/api/health >/dev/null 2>&1; then ok=1; break; fi
|
||||||
|
if ! kill -0 "$APP" 2>/dev/null; then break; fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ "$ok" != 1 ]; then
|
||||||
|
echo 'loongarch64 API smoke did not become healthy'
|
||||||
|
tail -n 60 /tmp/smoke.log
|
||||||
|
kill -KILL "$APP" 2>/dev/null || true
|
||||||
|
rc=9
|
||||||
|
fi
|
||||||
|
if [ "$rc" = 0 ]; then
|
||||||
|
kill -TERM "$APP"
|
||||||
|
exited=0
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if ! kill -0 "$APP" 2>/dev/null; then exited=1; break; fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ "$exited" != 1 ]; then
|
||||||
|
echo 'loongarch64 API smoke did not exit within 60s after SIGTERM'
|
||||||
|
tail -n 60 /tmp/smoke.log
|
||||||
|
kill -KILL "$APP" 2>/dev/null || true
|
||||||
|
rc=9
|
||||||
|
else
|
||||||
|
wait "$APP"
|
||||||
|
app_rc=$?
|
||||||
|
if [ "$app_rc" != 0 ]; then
|
||||||
|
echo "loongarch64 API smoke exited with status $app_rc after SIGTERM"
|
||||||
|
tail -n 60 /tmp/smoke.log
|
||||||
|
rc=9
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$rc" != 0 ]; then echo '=== publish.log ==='; tail -n 120 /tmp/publish.log 2>/dev/null; fi
|
||||||
|
echo "__WEBAPI_DONE__$rc"
|
||||||
|
SCRIPT
|
||||||
|
bash repo/v2rayN/v2rayN.WebAPI/Scripts/fetch-core-bundle.sh "$RID" "$GITHUB_WORKSPACE/core-bundle"
|
||||||
|
ls -la "$GITHUB_WORKSPACE/core-bundle" | head -n 15
|
||||||
|
- name: Build WebAPI loongarch64 in the VM (publish + native smoke)
|
||||||
|
shell: bash
|
||||||
|
timeout-minutes: 180
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE/dist"
|
||||||
|
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-webapi-loongarch64.sock"
|
||||||
|
rm -f "$VIRTIOFSD_SOCKET"
|
||||||
|
|
||||||
|
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
|
||||||
|
|
||||||
|
VIRTIOFSD_PID=$!
|
||||||
|
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
|
||||||
|
|
||||||
|
for i in {1..100}; do
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
|
||||||
|
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
|
||||||
|
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
|
||||||
|
expect <<'EOF'
|
||||||
|
log_user 1
|
||||||
|
set timeout 10800
|
||||||
|
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
|
||||||
|
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
|
||||||
|
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
|
||||||
|
set qcow2 $env(QCOW2_IMAGE)
|
||||||
|
set efi_code $env(EFI_CODE)
|
||||||
|
set efi_vars $env(EFI_VARS)
|
||||||
|
proc wait_prompt {} {
|
||||||
|
expect {
|
||||||
|
-re "__CI_PROMPT__# " {}
|
||||||
|
timeout { exit 1 }
|
||||||
|
eof { exit 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proc run_cmd {cmd} {
|
||||||
|
send -- "$cmd\r"
|
||||||
|
wait_prompt
|
||||||
|
}
|
||||||
|
spawn $qemu_bin \
|
||||||
|
-L $qemu_rom_dir \
|
||||||
|
-machine virt \
|
||||||
|
-accel tcg,thread=multi,tb-size=2048 \
|
||||||
|
-cpu la464 \
|
||||||
|
-m 9216 \
|
||||||
|
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
|
||||||
|
-numa node,memdev=mem0 \
|
||||||
|
-smp 4 \
|
||||||
|
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
|
||||||
|
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
|
||||||
|
-device virtio-blk-pci,drive=hd0,bootindex=1 \
|
||||||
|
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
|
||||||
|
-netdev user,id=net0 \
|
||||||
|
-device virtio-net-pci-non-transitional,netdev=net0 \
|
||||||
|
-chardev socket,id=charfs0,path=$virtiofs_socket \
|
||||||
|
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
|
||||||
|
-display none \
|
||||||
|
-serial stdio \
|
||||||
|
-monitor none
|
||||||
|
expect -re "login:|debian login:"
|
||||||
|
send -- "root\r"
|
||||||
|
expect -re "Password:|密码:|密码:"
|
||||||
|
send -- "password\r"
|
||||||
|
expect {
|
||||||
|
-re "# " {}
|
||||||
|
timeout { exit 1 }
|
||||||
|
eof { exit 1 }
|
||||||
|
}
|
||||||
|
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
|
||||||
|
wait_prompt
|
||||||
|
run_cmd "mkdir -p /workspace"
|
||||||
|
run_cmd "mount -t virtiofs workspace /workspace"
|
||||||
|
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
|
||||||
|
run_cmd "dhclient \$IFACE || true"
|
||||||
|
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
|
||||||
|
run_cmd "apt-get update || apt-get update || apt-get update"
|
||||||
|
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 libicu76 || DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1"
|
||||||
|
run_cmd "rm -rf /root/v2rayN-src"
|
||||||
|
run_cmd "rsync -a /workspace/repo/ /root/v2rayN-src/"
|
||||||
|
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
|
||||||
|
send -- "bash /workspace/webapi-loong-run.sh\r"
|
||||||
|
expect {
|
||||||
|
-re "__WEBAPI_DONE__0" { send -- "poweroff\r" }
|
||||||
|
-re "__WEBAPI_DONE__" { exit 1 }
|
||||||
|
timeout { exit 1 }
|
||||||
|
eof { exit 1 }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
- name: Verify the loongarch64 publish tree
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
publish="$GITHUB_WORKSPACE/publish/linux-loongarch64"
|
||||||
|
test -x "$publish/v2rayN.WebAPI"
|
||||||
|
test -f "$publish/v2rayN.WebAPI.build.json"
|
||||||
|
unreadable="$(find "$publish" ! -readable -print -quit)"
|
||||||
|
if [ -n "$unreadable" ]; then
|
||||||
|
echo "The loongarch64 publish tree contains unreadable entries:"
|
||||||
|
find "$publish" ! -readable -print | head -n 20
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "loongarch64 publish tree is complete and readable"
|
||||||
|
- name: Package API install and self-update ZIPs
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 repo/v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid "$RID" --publish "$GITHUB_WORKSPACE/publish/linux-loongarch64" --dist "$GITHUB_WORKSPACE/dist"
|
||||||
|
ls -la "$GITHUB_WORKSPACE/dist"
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: v2rayN.WebAPI-linux-loongarch64
|
||||||
|
path: dist/*.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
container:
|
||||||
|
name: API Containerfile (${{ matrix.rid }})
|
||||||
|
needs: identity
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Development builds keep amd64 coverage; releases also validate ARM64.
|
||||||
|
include: >-
|
||||||
|
${{ fromJSON(needs.identity.outputs.release == 'true'
|
||||||
|
&& '[{"rid":"linux-x64","platform":"linux/amd64"},{"rid":"linux-arm64","platform":"linux/arm64"}]'
|
||||||
|
|| '[{"rid":"linux-x64","platform":"linux/amd64"}]') }}
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Build API container (no push)
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
env:
|
||||||
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./v2rayN/v2rayN.WebAPI/Containerfile
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
tags: v2rayn-webapi:ci
|
||||||
|
build-args: |
|
||||||
|
RID=${{ matrix.rid }}
|
||||||
|
WEB_VERSION=${{ needs.identity.outputs.version }}
|
||||||
|
WEB_REPOSITORY=${{ github.repository }}
|
||||||
|
WEB_COMMIT=${{ github.sha }}
|
||||||
|
WEB_BUILD_DATE=${{ needs.identity.outputs.build_date }}
|
||||||
|
push: false
|
||||||
|
|
||||||
|
release-assets:
|
||||||
|
name: Assemble API release assets
|
||||||
|
if: needs.identity.outputs.release == 'true'
|
||||||
|
needs: [identity, verify, windows, windows-arm64, native, native-riscv64, native-loong64, container]
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
pattern: v2rayN.WebAPI-*
|
||||||
|
path: release-assets
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Build and verify WebAPI-update.json
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ needs.identity.outputs.tag }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 v2rayN/v2rayN.WebAPI/Scripts/web-update-manifest.py write \
|
||||||
|
--version "$RELEASE_TAG" \
|
||||||
|
--repository "${{ github.repository }}" \
|
||||||
|
--commit "${{ github.sha }}" \
|
||||||
|
--build-date "${{ needs.identity.outputs.build_date }}" \
|
||||||
|
--dist release-assets \
|
||||||
|
--output release-assets/WebAPI-update.json
|
||||||
|
python3 v2rayN/v2rayN.WebAPI/Scripts/web-update-manifest.py verify \
|
||||||
|
--manifest release-assets/WebAPI-update.json \
|
||||||
|
--dist release-assets \
|
||||||
|
--repository "${{ github.repository }}"
|
||||||
|
- name: Smoke-test full API ZIP
|
||||||
|
run: bash v2rayN/v2rayN.WebAPI/Tests/Integration/test-full-zip-runtime.sh release-assets/v2rayN-linux-64-WebAPI.zip
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: WebAPI-release-packages
|
||||||
|
path: release-assets/*.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: WebAPI-release-manifest
|
||||||
|
path: release-assets/WebAPI-update.json
|
||||||
|
if-no-files-found: error
|
||||||
@@ -20,6 +20,13 @@ jobs:
|
|||||||
secrets:
|
secrets:
|
||||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
release-WebAPI:
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
uses: ./.github/workflows/release-WebAPI.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
update:
|
update:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ jobs:
|
|||||||
$xrayTag = "v$xrayVer"
|
$xrayTag = "v$xrayVer"
|
||||||
$singTag = "v$singVer"
|
$singTag = "v$singVer"
|
||||||
|
|
||||||
$xrayUrl = "https://github.com/autorepobot/Xray/releases/download/$xrayTag/Xray-windows-32.zip"
|
$xrayUrl = "https://github.com/XTLS/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
|
||||||
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
|
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
|
||||||
|
|
||||||
Write-Host "Bundled Xray version: $xrayVer"
|
Write-Host "Bundled Xray version: $xrayVer"
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: release v2rayN.WebAPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
GPG_PRIVATE_KEY:
|
||||||
|
required: false
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
description: 'Release tag (for example 7.x.y).'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: ./.github/workflows/build-WebAPI.yml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
|
||||||
|
release-web-packages:
|
||||||
|
name: sign and upload Web API packages
|
||||||
|
# Upload through the shared signing flow, exactly like the desktop packages.
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
needs: build
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
artifact_name: WebAPI-release-packages
|
||||||
|
file_pattern: 'v2rayN-*-WebAPI*.zip'
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
|
|
||||||
|
release-web-manifest:
|
||||||
|
name: sign and upload Web update manifest
|
||||||
|
if: inputs.release_tag != ''
|
||||||
|
# Advertise the update only after its packages and manifest have been uploaded.
|
||||||
|
needs: [build, release-web-packages]
|
||||||
|
uses: ./.github/workflows/upload-sign.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ inputs.release_tag }}
|
||||||
|
artifact_name: WebAPI-release-manifest
|
||||||
|
file_pattern: 'WebAPI-update.json'
|
||||||
|
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||||
@@ -19,7 +19,9 @@ jobs:
|
|||||||
$wingetPackage = "2dust.v2rayN"
|
$wingetPackage = "2dust.v2rayN"
|
||||||
$gitToken = "${{ secrets.PT_WINGET }}"
|
$gitToken = "${{ secrets.PT_WINGET }}"
|
||||||
|
|
||||||
$github = Invoke-RestMethod -uri "https://api.github.com/repos/2dust/v2rayN/releases"
|
$headers = @{ Authorization = "Bearer $gitToken"; Accept = "application/vnd.github+json" }
|
||||||
|
|
||||||
|
$github = Invoke-RestMethod -Uri "https://api.github.com/repos/2dust/v2rayN/releases" -Headers $headers
|
||||||
|
|
||||||
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
|
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
|
||||||
|
|
||||||
|
|||||||
@@ -676,6 +676,15 @@ EOF
|
|||||||
find "$stage/opt/v2rayN" -type f -exec chmod 0644 {} +
|
find "$stage/opt/v2rayN" -type f -exec chmod 0644 {} +
|
||||||
[[ -f "$stage/opt/v2rayN/v2rayN" ]] && chmod 0755 "$stage/opt/v2rayN/v2rayN" || true
|
[[ -f "$stage/opt/v2rayN/v2rayN" ]] && chmod 0755 "$stage/opt/v2rayN/v2rayN" || true
|
||||||
|
|
||||||
|
# Core binaries must be executable before installation: ordinary users cannot
|
||||||
|
# chmod the root-owned files installed by dpkg.
|
||||||
|
local core
|
||||||
|
for core in xray/xray sing_box/sing-box mihomo/mihomo; do
|
||||||
|
if [[ -f "$stage/opt/v2rayN/bin/$core" ]]; then
|
||||||
|
chmod 0755 "$stage/opt/v2rayN/bin/$core"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb"
|
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb"
|
||||||
dpkg-deb --root-owner-group --build "$stage" "$deb_out"
|
dpkg-deb --root-owner-group --build "$stage" "$deb_out"
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ internal class UpgradeApp
|
|||||||
var thisAppOldFile = $"{Utils.GetExePath()}.tmp";
|
var thisAppOldFile = $"{Utils.GetExePath()}.tmp";
|
||||||
File.Delete(thisAppOldFile);
|
File.Delete(thisAppOldFile);
|
||||||
var splitKey = "/";
|
var splitKey = "/";
|
||||||
|
var allowedBaseDir = Utils.StartupPath();
|
||||||
|
var pathComparison = Utils.GetPathComparison();
|
||||||
|
|
||||||
using var archive = ZipFile.OpenRead(fileName);
|
using var archive = ZipFile.OpenRead(fileName);
|
||||||
foreach (var entry in archive.Entries)
|
foreach (var entry in archive.Entries)
|
||||||
@@ -65,16 +67,21 @@ internal class UpgradeApp
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fullName = string.Join(splitKey, lst[1..lst.Length]);
|
var fullName = string.Join(splitKey, lst[1..lst.Length]);
|
||||||
|
var entryOutputPath = Utils.GetPath(fullName);
|
||||||
|
if (!Utils.IsPathUnderDirectory(allowedBaseDir, entryOutputPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{Resx.Resource.FailedUpgrade} blocked potential path traversal: {entry.FullName}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(Utils.GetExePath(), Utils.GetPath(fullName), StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(Utils.GetExePath(), entryOutputPath, pathComparison))
|
||||||
{
|
{
|
||||||
File.Move(Utils.GetExePath(), thisAppOldFile);
|
File.Move(Utils.GetExePath(), thisAppOldFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryOutputPath = Utils.GetPath(fullName);
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(entryOutputPath)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(entryOutputPath)!);
|
||||||
//In the bin folder, if the file already exists, it will be skipped
|
//In the bin folder, if the file already exists, it will be skipped
|
||||||
if (fullName.StartsWith("bin") && File.Exists(entryOutputPath))
|
if (fullName.StartsWith("bin", pathComparison) && File.Exists(entryOutputPath))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,28 @@ internal class Utils
|
|||||||
return Path.Combine(startupPath, fileName);
|
return Path.Combine(startupPath, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsPathUnderDirectory(string baseDir, string targetPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(baseDir) || string.IsNullOrWhiteSpace(targetPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseFull = Path.GetFullPath(baseDir);
|
||||||
|
var targetFull = Path.GetFullPath(targetPath);
|
||||||
|
|
||||||
|
baseFull = baseFull.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
|
return targetFull.StartsWith(baseFull, GetPathComparison());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StringComparison GetPathComparison()
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsWindows()
|
||||||
|
? StringComparison.OrdinalIgnoreCase
|
||||||
|
: StringComparison.Ordinal;
|
||||||
|
}
|
||||||
|
|
||||||
public static string V2rayN => "v2rayN";
|
public static string V2rayN => "v2rayN";
|
||||||
|
|
||||||
public static void StartV2RayN()
|
public static void StartV2RayN()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>7.25.1</Version>
|
<Version>7.25.6</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -13,22 +13,22 @@
|
|||||||
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
||||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.2" />
|
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.2" />
|
||||||
<PackageVersion Include="CliWrap" Version="3.10.5" />
|
<PackageVersion Include="CliWrap" Version="3.10.5" />
|
||||||
<PackageVersion Include="Downloader" Version="5.9.6" />
|
<PackageVersion Include="Downloader" Version="5.10.1" />
|
||||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||||
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
||||||
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
||||||
<PackageVersion Include="ReactiveUI" Version="24.2.0" />
|
<PackageVersion Include="ReactiveUI" Version="24.3.0" />
|
||||||
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
|
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
|
||||||
<PackageVersion Include="ReactiveUI.WPF" Version="24.2.0" />
|
<PackageVersion Include="ReactiveUI.WPF" Version="24.2.0" />
|
||||||
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
|
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
|
||||||
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
|
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
|
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
|
||||||
<PackageVersion Include="NLog" Version="6.2.0" />
|
<PackageVersion Include="NLog" Version="6.2.1" />
|
||||||
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
||||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
||||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||||
<PackageVersion Include="TUnit" Version="1.66.27" />
|
<PackageVersion Include="TUnit" Version="1.75.0" />
|
||||||
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
|
<PackageVersion Include="TUnit.Assertions.Should" Version="1.72.16-beta" />
|
||||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||||
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
||||||
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace ServiceLib.Tests.Common;
|
||||||
|
|
||||||
|
public class UnixFileModeTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task AlreadyExecutableSystemBinary_ShouldNotRequireOwnership()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// /bin/sh is executable but owned by root on the Unix CI runners.
|
||||||
|
// A regular user must not need chmod permission just to launch it.
|
||||||
|
var mode = File.GetUnixFileMode("/bin/sh");
|
||||||
|
await Utils.SetUnixFileMode("/bin/sh").Should().BeTrue();
|
||||||
|
await File.GetUnixFileMode("/bin/sh").Should().BeEqualTo(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task NonExecutableOwnedFile_ShouldGainExecuteBitsWithoutLosingOtherPermissions()
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = Path.GetTempFileName();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead;
|
||||||
|
File.SetUnixFileMode(path, mode);
|
||||||
|
|
||||||
|
await Utils.SetUnixFileMode(path).Should().BeTrue();
|
||||||
|
await File.GetUnixFileMode(path).Should().BeEqualTo(mode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
using System.Reflection;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Manager;
|
|
||||||
using ServiceLib.Models;
|
|
||||||
|
|
||||||
namespace ServiceLib.Tests.CoreConfig;
|
namespace ServiceLib.Tests.CoreConfig;
|
||||||
|
|
||||||
internal static class CoreConfigTestFactory
|
internal static class CoreConfigTestFactory
|
||||||
|
|||||||
@@ -716,7 +716,6 @@ public class CoreConfigSingboxServiceTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
|
public async Task GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class CoreConfigV2rayServiceTests
|
|||||||
.Select(item => item!.GetValue<string>())
|
.Select(item => item!.GetValue<string>())
|
||||||
.Should().BeEquivalentTo(["a=1", "b=2"]);
|
.Should().BeEquivalentTo(["a=1", "b=2"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
|
public async Task GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
|
||||||
{
|
{
|
||||||
@@ -587,13 +587,11 @@ public class CoreConfigV2rayServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[Arguments(true)]
|
public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6()
|
||||||
[Arguments(false)]
|
|
||||||
public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6(bool enableIPv6Address)
|
|
||||||
{
|
{
|
||||||
// A host without a global IPv6 address has no IPv6 traffic that could bypass the tunnel,
|
// A host without a global IPv6 address has no IPv6 traffic that could bypass the tunnel,
|
||||||
// while ::/0 would pull IPv6 attempts into a tunnel they cannot leave.
|
// while ::/0 would pull IPv6 attempts into a tunnel they cannot leave.
|
||||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
|
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: false);
|
||||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
@@ -611,6 +609,26 @@ public class CoreConfigV2rayServiceTests
|
|||||||
await ipv6Routes.Should().BeEmpty();
|
await ipv6Routes.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6WhenEnabledWithoutGlobalIPv6()
|
||||||
|
{
|
||||||
|
// Hosts behind NAT66 hold only ULA addresses, so EnableIPv6Address must still route ::/0.
|
||||||
|
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: true);
|
||||||
|
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||||
|
|
||||||
|
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||||
|
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray, hasGlobalIPv6Address: false);
|
||||||
|
|
||||||
|
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||||
|
|
||||||
|
await result.Success.Should().BeTrue();
|
||||||
|
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||||
|
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||||
|
|
||||||
|
await tunInbound.Should().NotBeNull();
|
||||||
|
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("::/0");
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldSkipIPv6RangesWithoutGlobalIPv6()
|
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldSkipIPv6RangesWithoutGlobalIPv6()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
namespace ServiceLib.Tests.Manager;
|
||||||
|
|
||||||
|
public class ProfileExManagerConcurrencyTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task SetTestDelay_FromManyThreads_ShouldNotThrow()
|
||||||
|
{
|
||||||
|
var manager = CreateManager();
|
||||||
|
var exceptions = new List<Exception>();
|
||||||
|
var threads = Environment.ProcessorCount * 2;
|
||||||
|
|
||||||
|
//Hammer the same manager from several streams at once: the queue is mutated while
|
||||||
|
//other threads keep pushing into it
|
||||||
|
var barrier = new Barrier(threads);
|
||||||
|
Parallel.For(0, threads, t =>
|
||||||
|
{
|
||||||
|
barrier.SignalAndWait();
|
||||||
|
for (var i = 0; i < 5000; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
manager.SetTestDelay($"index{i % 500}", i);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lock (exceptions)
|
||||||
|
{
|
||||||
|
exceptions.Add(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await exceptions.Count.Should().BeEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task SetTestDelay_FromManyThreads_ShouldNotLoseQueuedIds()
|
||||||
|
{
|
||||||
|
var manager = CreateManager();
|
||||||
|
var ids = 500;
|
||||||
|
|
||||||
|
Parallel.For(0, Environment.ProcessorCount * 2, t =>
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 5000; i++)
|
||||||
|
{
|
||||||
|
manager.SetTestDelay($"id{i % ids}", i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var drained = Drain(manager);
|
||||||
|
await drained.Count.Should().BeGreaterThanOrEqualTo(ids);
|
||||||
|
await drained.Distinct().Count().Should().BeEqualTo(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task SetTestDelay_ShouldCreateOneEntryPerIndex()
|
||||||
|
{
|
||||||
|
var manager = CreateManager();
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(Enumerable.Range(0, 500), new ParallelOptions { MaxDegreeOfParallelism = 32 },
|
||||||
|
async (i, ct) =>
|
||||||
|
{
|
||||||
|
manager.SetTestDelay($"same{i % 10}", i);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
var items = await manager.GetProfileExs();
|
||||||
|
await items.Count.Should().BeEqualTo(10);
|
||||||
|
await items.Select(t => t.IndexId).Distinct().Count().Should().BeEqualTo(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileExManager CreateManager()
|
||||||
|
{
|
||||||
|
return (ProfileExManager)Activator.CreateInstance(typeof(ProfileExManager), nonPublic: true)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> Drain(ProfileExManager manager)
|
||||||
|
{
|
||||||
|
var queue = (System.Collections.Concurrent.ConcurrentQueue<string>)typeof(ProfileExManager)
|
||||||
|
.GetField("_queIndexIds", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
|
.GetValue(manager)!;
|
||||||
|
|
||||||
|
var result = new List<string>();
|
||||||
|
while (queue.TryDequeue(out var id))
|
||||||
|
{
|
||||||
|
result.Add(id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
namespace ServiceLib.Tests.Services;
|
||||||
|
|
||||||
|
public class SpeedtestServiceBatchTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
[Arguments(0)]
|
||||||
|
[Arguments(1000)]
|
||||||
|
public async Task EmptySelection_ShouldReturnNoBatches(int pageSize)
|
||||||
|
{
|
||||||
|
await GetBatches([], pageSize).Count.Should().BeEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments(0)]
|
||||||
|
[Arguments(-1)]
|
||||||
|
public async Task NonPositivePageSize_ShouldKeepEveryEligibleProfile(int pageSize)
|
||||||
|
{
|
||||||
|
var profiles = CreateProfiles();
|
||||||
|
var batches = GetBatches(profiles, pageSize);
|
||||||
|
|
||||||
|
await batches.Count.Should().BeEqualTo(profiles.Count);
|
||||||
|
await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count);
|
||||||
|
foreach (var profile in profiles)
|
||||||
|
{
|
||||||
|
await batches.SelectMany(batch => batch).Count(item => ReferenceEquals(item, profile)).Should().BeEqualTo(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task NormalBatches_ShouldRespectPageSizeAndKeepCoreTypesSeparate()
|
||||||
|
{
|
||||||
|
var profiles = CreateProfiles();
|
||||||
|
var batches = GetBatches(profiles, 2);
|
||||||
|
|
||||||
|
await batches.Count.Should().BeEqualTo(3);
|
||||||
|
await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count);
|
||||||
|
foreach (var batch in batches)
|
||||||
|
{
|
||||||
|
await (batch.Count > 0 && batch.Count <= 2).Should().BeTrue();
|
||||||
|
await batch.Select(item => item.CoreType).Distinct().Count().Should().BeEqualTo(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ServerTestItem> CreateProfiles() =>
|
||||||
|
[
|
||||||
|
new() { CoreType = ECoreType.Xray },
|
||||||
|
new() { CoreType = ECoreType.Xray },
|
||||||
|
new() { CoreType = ECoreType.Xray },
|
||||||
|
new() { CoreType = ECoreType.sing_box },
|
||||||
|
new() { CoreType = ECoreType.sing_box }
|
||||||
|
];
|
||||||
|
|
||||||
|
private static List<List<ServerTestItem>> GetBatches(List<ServerTestItem> profiles, int pageSize)
|
||||||
|
{
|
||||||
|
var service = new SpeedtestService(new Config { SpeedTestItem = new() }, _ => Task.CompletedTask);
|
||||||
|
var method = typeof(SpeedtestService).GetMethod("GetTestBatchItem", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
return (List<List<ServerTestItem>>)method.Invoke(service, [profiles, pageSize])!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,21 @@ namespace ServiceLib.UdpTest;
|
|||||||
|
|
||||||
public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable
|
public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable
|
||||||
{
|
{
|
||||||
|
private bool _initialized;
|
||||||
|
private IPEndPoint _relayEndPoint;
|
||||||
private TcpClient _tcpClient;
|
private TcpClient _tcpClient;
|
||||||
private UdpClient _udpClient;
|
private UdpClient _udpClient;
|
||||||
private IPEndPoint _relayEndPoint;
|
|
||||||
|
|
||||||
private bool _initialized = false;
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_tcpClient?.Dispose();
|
||||||
|
_udpClient?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send UDP data to a remote endpoint (IP address)
|
/// Send UDP data to a remote endpoint (IP address)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task SendAsync(IPEndPoint remote, byte[] data)
|
public async Task SendAsync(IPEndPoint remote, byte[] data, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var addrData = new Socks5AddressData
|
var addrData = new Socks5AddressData
|
||||||
{
|
{
|
||||||
@@ -19,19 +24,20 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
? Socks5AddressData.AddrTypeIPv4
|
? Socks5AddressData.AddrTypeIPv4
|
||||||
: Socks5AddressData.AddrTypeIPv6,
|
: Socks5AddressData.AddrTypeIPv6,
|
||||||
Host = remote.Address.ToString(),
|
Host = remote.Address.ToString(),
|
||||||
Port = (ushort)remote.Port
|
Port = (ushort)remote.Port,
|
||||||
};
|
};
|
||||||
var packet = BuildSocks5UdpPacket(addrData, data);
|
var packet = BuildSocks5UdpPacket(addrData, data);
|
||||||
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
|
await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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>
|
/// </summary>
|
||||||
/// <param name="host">Domain name or IP address</param>
|
/// <param name="host">Domain name or IP address</param>
|
||||||
/// <param name="port">Port number</param>
|
/// <param name="port">Port number</param>
|
||||||
/// <param name="data">Data to send</param>
|
/// <param name="data">Data to send</param>
|
||||||
public async Task SendAsync(string host, ushort port, byte[] data)
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
public async Task SendAsync(string host, ushort port, byte[] data, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var addrData = new Socks5AddressData();
|
var addrData = new Socks5AddressData();
|
||||||
|
|
||||||
@@ -53,11 +59,12 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
addrData.Port = port;
|
addrData.Port = port;
|
||||||
|
|
||||||
var packet = BuildSocks5UdpPacket(addrData, data);
|
var packet = BuildSocks5UdpPacket(addrData, data);
|
||||||
await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
|
// await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
|
||||||
|
await _udpClient.SendAsync(packet.AsMemory(), _relayEndPoint, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Receive UDP data from remote endpoint
|
/// Receive UDP data from remote endpoint
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">Cancellation token to cancel the receive operation</param>
|
/// <param name="cancellationToken">Cancellation token to cancel the receive operation</param>
|
||||||
/// <returns>Remote endpoint information and received data</returns>
|
/// <returns>Remote endpoint information and received data</returns>
|
||||||
@@ -69,16 +76,6 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
return (remote, payload);
|
return (remote, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a remote endpoint that can be either an IP address or a domain name
|
|
||||||
/// </summary>
|
|
||||||
public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
|
|
||||||
{
|
|
||||||
public string Host { get; set; } = host;
|
|
||||||
public ushort Port { get; set; } = port;
|
|
||||||
public bool IsDomain { get; set; } = isDomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data)
|
private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data)
|
||||||
{
|
{
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
@@ -104,6 +101,11 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
throw new ArgumentException("Invalid SOCKS5 UDP packet: too short");
|
throw new ArgumentException("Invalid SOCKS5 UDP packet: too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (packet[0] != 0x00 || packet[1] != 0x00)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid SOCKS5 UDP packet: RSV field must be 0");
|
||||||
|
}
|
||||||
|
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
|
|
||||||
// RSV (2 bytes) - Reserved field, skip
|
// RSV (2 bytes) - Reserved field, skip
|
||||||
@@ -196,83 +198,34 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
return (remote, data);
|
return (remote, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
/// <summary>
|
||||||
|
/// Represents a remote endpoint that can be either an IP address or a domain name
|
||||||
|
/// </summary>
|
||||||
|
public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
|
||||||
{
|
{
|
||||||
_tcpClient.Dispose();
|
public string Host { get; set; } = host;
|
||||||
_udpClient.Dispose();
|
public ushort Port { get; set; } = port;
|
||||||
|
public bool IsDomain { get; set; } = isDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region SOCKS5 Connection Handling
|
|
||||||
|
|
||||||
private const byte Socks5Version = 0x05;
|
|
||||||
private const byte SocksCmdUdpAssociate = 0x03;
|
|
||||||
|
|
||||||
public async Task<bool> EstablishUdpAssociationAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_initialized)
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
_initialized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
|
|
||||||
_tcpClient = new TcpClient();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (SocketException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tcpControlStream = _tcpClient.GetStream();
|
|
||||||
|
|
||||||
byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
|
|
||||||
await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
|
|
||||||
var handshakeResponse = new byte[2];
|
|
||||||
if (await tcpControlStream.ReadAsync(handshakeResponse, cancellationToken).ConfigureAwait(false) < 2 ||
|
|
||||||
handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var clientAddrForSocks = new Socks5AddressData
|
|
||||||
{
|
|
||||||
AddressType = Socks5AddressData.AddrTypeIPv4,
|
|
||||||
Host = "0.0.0.0",
|
|
||||||
Port = 0
|
|
||||||
};
|
|
||||||
using var udpAssociateReqMs = new MemoryStream();
|
|
||||||
udpAssociateReqMs.WriteByte(Socks5Version);
|
|
||||||
udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
|
|
||||||
udpAssociateReqMs.WriteByte(0x00);
|
|
||||||
udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
|
|
||||||
await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var verRepRsv = new byte[3];
|
|
||||||
if (await tcpControlStream.ReadAsync(verRepRsv, cancellationToken).ConfigureAwait(false) < 3 ||
|
|
||||||
verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxyRelaySocksAddr =
|
|
||||||
await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
|
|
||||||
_initialized = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion SOCKS5 Connection Handling
|
|
||||||
|
|
||||||
#region SOCKS5 Address Handling
|
#region SOCKS5 Address Handling
|
||||||
|
|
||||||
|
private static async Task<bool> TryReadExactlyAsync(
|
||||||
|
Stream stream,
|
||||||
|
Memory<byte> buffer,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await stream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (EndOfStreamException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class Socks5AddressData
|
private class Socks5AddressData
|
||||||
{
|
{
|
||||||
public const byte AddrTypeIPv4 = 0x01;
|
public const byte AddrTypeIPv4 = 0x01;
|
||||||
@@ -343,7 +296,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
var typeByte = new byte[1];
|
var typeByte = new byte[1];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
|
if (!await TryReadExactlyAsync(stream, typeByte, ct).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -353,7 +306,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
{
|
{
|
||||||
case AddrTypeIPv4:
|
case AddrTypeIPv4:
|
||||||
var ipv4Bytes = new byte[4];
|
var ipv4Bytes = new byte[4];
|
||||||
if (await stream.ReadAsync(ipv4Bytes.AsMemory(0, 4), ct).ConfigureAwait(false) < 4)
|
if (!await TryReadExactlyAsync(stream, ipv4Bytes, ct).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -363,7 +316,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
|
|
||||||
case AddrTypeDomain:
|
case AddrTypeDomain:
|
||||||
var lenByte = new byte[1];
|
var lenByte = new byte[1];
|
||||||
if (await stream.ReadAsync(lenByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
|
if (!await TryReadExactlyAsync(stream, lenByte, ct).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -375,8 +328,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var domainBytes = new byte[lenByte[0]];
|
var domainBytes = new byte[lenByte[0]];
|
||||||
if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct)
|
if (!await TryReadExactlyAsync(stream, domainBytes, ct).ConfigureAwait(false))
|
||||||
.ConfigureAwait(false) < domainBytes.Length)
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -388,7 +340,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
|
|
||||||
case AddrTypeIPv6:
|
case AddrTypeIPv6:
|
||||||
var ipv6Bytes = new byte[16];
|
var ipv6Bytes = new byte[16];
|
||||||
if (await stream.ReadAsync(ipv6Bytes.AsMemory(0, 16), ct).ConfigureAwait(false) < 16)
|
if (!await TryReadExactlyAsync(stream, ipv6Bytes, ct).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -401,7 +353,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
}
|
}
|
||||||
|
|
||||||
var portBytes = new byte[2];
|
var portBytes = new byte[2];
|
||||||
if (await stream.ReadAsync(portBytes.AsMemory(0, 2), ct).ConfigureAwait(false) < 2)
|
if (!await TryReadExactlyAsync(stream, portBytes, ct).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -417,4 +369,73 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion SOCKS5 Address Handling
|
#endregion SOCKS5 Address Handling
|
||||||
|
|
||||||
|
#region SOCKS5 Connection Handling
|
||||||
|
|
||||||
|
private const byte Socks5Version = 0x05;
|
||||||
|
private const byte SocksCmdUdpAssociate = 0x03;
|
||||||
|
|
||||||
|
public async Task<bool> EstablishUdpAssociationAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
_initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
|
||||||
|
_tcpClient = new TcpClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tcpControlStream = _tcpClient.GetStream();
|
||||||
|
|
||||||
|
byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
|
||||||
|
await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
|
||||||
|
var handshakeResponse = new byte[2];
|
||||||
|
if (!await TryReadExactlyAsync(tcpControlStream, handshakeResponse, cancellationToken).ConfigureAwait(false) ||
|
||||||
|
handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientAddrForSocks = new Socks5AddressData
|
||||||
|
{
|
||||||
|
AddressType = Socks5AddressData.AddrTypeIPv4,
|
||||||
|
Host = "0.0.0.0",
|
||||||
|
Port = 0,
|
||||||
|
};
|
||||||
|
using var udpAssociateReqMs = new MemoryStream();
|
||||||
|
udpAssociateReqMs.WriteByte(Socks5Version);
|
||||||
|
udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
|
||||||
|
udpAssociateReqMs.WriteByte(0x00);
|
||||||
|
udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
|
||||||
|
await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var verRepRsv = new byte[3];
|
||||||
|
if (!await TryReadExactlyAsync(tcpControlStream, verRepRsv, cancellationToken).ConfigureAwait(false) ||
|
||||||
|
verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyRelaySocksAddr =
|
||||||
|
await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
|
||||||
|
_initialized = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion SOCKS5 Connection Handling
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class DnsService : IUdpTest
|
|||||||
// Question: www.google.com, Type A, Class IN
|
// Question: www.google.com, Type A, Class IN
|
||||||
0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F,
|
0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F,
|
||||||
0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00,
|
0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00,
|
||||||
0x00, 0x01, 0x00, 0x01
|
0x00, 0x01, 0x00, 0x01,
|
||||||
];
|
];
|
||||||
|
|
||||||
public byte[] BuildUdpRequestPacket()
|
public byte[] BuildUdpRequestPacket()
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ public class McBeService : IUdpTest
|
|||||||
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
|
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
|
||||||
// Client GUID (random 16 bytes)
|
// Client GUID (random 16 bytes)
|
||||||
0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E,
|
0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E,
|
||||||
0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B
|
0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B,
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly byte[] McBeMagicBytes =
|
private static readonly byte[] McBeMagicBytes =
|
||||||
[
|
[
|
||||||
0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
|
0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
|
||||||
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78
|
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly List<string> ValidGameModes =
|
private static readonly List<string> ValidGameModes =
|
||||||
@@ -31,7 +31,7 @@ public class McBeService : IUdpTest
|
|||||||
"Survival",
|
"Survival",
|
||||||
"Creative",
|
"Creative",
|
||||||
"Adventure",
|
"Adventure",
|
||||||
"Spectator"
|
"Spectator",
|
||||||
];
|
];
|
||||||
|
|
||||||
public byte[] BuildUdpRequestPacket()
|
public byte[] BuildUdpRequestPacket()
|
||||||
@@ -61,6 +61,10 @@ public class McBeService : IUdpTest
|
|||||||
return false; // Magic bytes do not match
|
return false; // Magic bytes do not match
|
||||||
}
|
}
|
||||||
var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]);
|
var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]);
|
||||||
|
if (mcbeResponseBytes.Length < 35 + stringLength)
|
||||||
|
{
|
||||||
|
return false; // Not enough data for the string
|
||||||
|
}
|
||||||
var stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray());
|
var stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray());
|
||||||
var stringParts = stringData.Split(';');
|
var stringParts = stringData.Split(';');
|
||||||
// check Game Mode str
|
// check Game Mode str
|
||||||
|
|||||||
@@ -31,10 +31,7 @@ public class StunService : IUdpTest
|
|||||||
if (stunResponseBytes.Length >= 2)
|
if (stunResponseBytes.Length >= 2)
|
||||||
{
|
{
|
||||||
var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1];
|
var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1];
|
||||||
if (messageType is 0x0101 or 0x0111)
|
return messageType is 0x0101 or 0x0111;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace ServiceLib.UdpTest;
|
|||||||
public class UdpTestService
|
public class UdpTestService
|
||||||
{
|
{
|
||||||
private const string DefaultUdpTestType = "ntp";
|
private const string DefaultUdpTestType = "ntp";
|
||||||
private readonly IUdpTest _udpTest;
|
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<string, Func<IUdpTest>> UdpTestFactories =
|
private static readonly IReadOnlyDictionary<string, Func<IUdpTest>> UdpTestFactories =
|
||||||
new Dictionary<string, Func<IUdpTest>>(StringComparer.OrdinalIgnoreCase)
|
new Dictionary<string, Func<IUdpTest>>(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -16,6 +15,8 @@ public class UdpTestService
|
|||||||
["mcbe"] = () => new McBeService(),
|
["mcbe"] = () => new McBeService(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly IUdpTest _udpTest;
|
||||||
|
|
||||||
private UdpTestService(IUdpTest udpTest)
|
private UdpTestService(IUdpTest udpTest)
|
||||||
{
|
{
|
||||||
_udpTest = udpTest;
|
_udpTest = udpTest;
|
||||||
@@ -88,24 +89,26 @@ public class UdpTestService
|
|||||||
return (targetServerHost, _udpTest.GetDefaultTargetPort());
|
return (targetServerHost, _udpTest.GetDefaultTargetPort());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port, TimeSpan operationTimeout)
|
public async Task<TimeSpan> SendUdpRequestAsync(string targetServerHost, int socks5Port,
|
||||||
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource(operationTimeout);
|
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
var cancellationToken = cts.Token;
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
|
||||||
|
var linkedCt = linkedCts.Token;
|
||||||
var udpRequestPacket = _udpTest.BuildUdpRequestPacket();
|
var udpRequestPacket = _udpTest.BuildUdpRequestPacket();
|
||||||
if (udpRequestPacket == null || udpRequestPacket.Length == 0)
|
if (udpRequestPacket == null || udpRequestPacket.Length == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to build UDP request packet.");
|
throw new InvalidOperationException("Failed to build UDP request packet.");
|
||||||
}
|
}
|
||||||
using var channel = new Socks5UdpChannel("127.0.0.1", socks5Port);
|
using var channel = new Socks5UdpChannel("127.0.0.1", socks5Port);
|
||||||
if (!await channel.EstablishUdpAssociationAsync(cancellationToken).ConfigureAwait(false))
|
if (!await channel.EstablishUdpAssociationAsync(linkedCt).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
throw new Exception("Failed to establish UDP association with SOCKS5 proxy.");
|
throw new Exception("Failed to establish UDP association with SOCKS5 proxy.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var (targetHost, targetPort) = ParseHostAndPort(targetServerHost);
|
var (targetHost, targetPort) = ParseHostAndPort(targetServerHost);
|
||||||
|
|
||||||
byte[] udpReceiveResult = null;
|
byte[]? validUdpReceiveResult = null;
|
||||||
|
|
||||||
// Get minimum round trip time from two attempts
|
// Get minimum round trip time from two attempts
|
||||||
var roundTripTime = TimeSpan.MaxValue;
|
var roundTripTime = TimeSpan.MaxValue;
|
||||||
@@ -116,11 +119,15 @@ public class UdpTestService
|
|||||||
{
|
{
|
||||||
var stopwatch = new Stopwatch();
|
var stopwatch = new Stopwatch();
|
||||||
stopwatch.Start();
|
stopwatch.Start();
|
||||||
await channel.SendAsync(targetHost, targetPort, udpRequestPacket).ConfigureAwait(false);
|
await channel.SendAsync(targetHost, targetPort, udpRequestPacket, linkedCt).ConfigureAwait(false);
|
||||||
var (_, receiveResult) = await channel.ReceiveAsync(cancellationToken).ConfigureAwait(false);
|
var (_, receiveResult) = await channel.ReceiveAsync(linkedCt).ConfigureAwait(false);
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
|
|
||||||
udpReceiveResult = receiveResult;
|
if (!_udpTest.VerifyAndExtractUdpResponse(receiveResult))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validUdpReceiveResult = receiveResult;
|
||||||
|
|
||||||
var currentRoundTripTime = stopwatch.Elapsed;
|
var currentRoundTripTime = stopwatch.Elapsed;
|
||||||
if (currentRoundTripTime < roundTripTime)
|
if (currentRoundTripTime < roundTripTime)
|
||||||
@@ -128,6 +135,10 @@ public class UdpTestService
|
|||||||
roundTripTime = currentRoundTripTime;
|
roundTripTime = currentRoundTripTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
|
if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
|
||||||
@@ -137,18 +148,10 @@ public class UdpTestService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((udpReceiveResult?.Length ?? 0) < 4 + 1 + 4 + 2)
|
if (validUdpReceiveResult != null)
|
||||||
{
|
|
||||||
throw new Exception("Received NTP response is too short.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (udpReceiveResult != null && _udpTest.VerifyAndExtractUdpResponse(udpReceiveResult))
|
|
||||||
{
|
{
|
||||||
return roundTripTime;
|
return roundTripTime;
|
||||||
}
|
}
|
||||||
else
|
throw new Exception("Failed to verify and extract UDP response.");
|
||||||
{
|
|
||||||
throw new Exception("Failed to verify and extract UDP response.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ public class BulkObservableCollection<T> : ObservableCollection<T>
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public BulkObservableCollection(IEnumerable<T> collection) : base(collection) { }
|
public BulkObservableCollection(IEnumerable<T> collection) : base(collection)
|
||||||
public BulkObservableCollection(List<T> list) : base(list) { }
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulkObservableCollection(List<T> list) : base(list)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
namespace ServiceLib.Base;
|
namespace ServiceLib.Base;
|
||||||
|
|
||||||
public class MyReactiveObject : ReactiveObject
|
public class MyReactiveObject : ReactiveObject, IActivatableViewModel
|
||||||
{
|
{
|
||||||
protected static Config? _config;
|
protected static Config? _config;
|
||||||
|
|
||||||
|
public ViewModelActivator Activator { get; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,29 +136,23 @@ public static class Extension
|
|||||||
.Replace("\n", replacement);
|
.Replace("\n", replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<TOutput> HandleSafe<TInput, TOutput>(
|
public static IObservable<TOutput> HandleSafe<TInput, TOutput>(
|
||||||
this Interaction<TInput, TOutput> interaction,
|
this Interaction<TInput, TOutput> interaction, TInput input,
|
||||||
TInput input,
|
|
||||||
TOutput defaultValue = default!,
|
TOutput defaultValue = default!,
|
||||||
[CallerMemberName] string memberName = "",
|
[CallerMemberName] string memberName = "",
|
||||||
[CallerFilePath] string filePath = "",
|
[CallerFilePath] string filePath = "",
|
||||||
[CallerLineNumber] int lineNumber = 0)
|
[CallerLineNumber] int lineNumber = 0)
|
||||||
{
|
{
|
||||||
try
|
return Signal.Defer(() => interaction.Handle(input))
|
||||||
{
|
.Catch<TOutput, UnhandledInteractionException<TInput, TOutput>>(ex =>
|
||||||
return await interaction.Handle(input);
|
{
|
||||||
}
|
Logging.SaveLog($"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}", ex);
|
||||||
catch (UnhandledInteractionException<TInput, TOutput> ex)
|
return Signal.Return(defaultValue);
|
||||||
{
|
})
|
||||||
var title = $"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}";
|
.Catch<TOutput, Exception>(ex =>
|
||||||
Logging.SaveLog(title, ex);
|
{
|
||||||
return defaultValue;
|
Logging.SaveLog($"Exception occurred while handling interaction in {memberName} at {filePath}:{lineNumber}, input: {input}", ex);
|
||||||
}
|
return Signal.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
#endregion Conversion Functions
|
||||||
|
|
||||||
#region Data Checks
|
#region Data Checks
|
||||||
@@ -726,6 +751,40 @@ public class Utils
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regex match with a timeout guard. Filter patterns can come from user
|
||||||
|
/// input or subscription content while the tested text (remarks, log
|
||||||
|
/// messages) is attacker-influenced, so an evil pattern like (a+)+$
|
||||||
|
/// would otherwise hang the caller (ReDoS). On timeout or invalid
|
||||||
|
/// pattern, fail open (return true) so no node/message is silently
|
||||||
|
/// dropped; the incident is logged.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsRegexMatch(string? input, string? pattern, int timeoutSeconds = 2)
|
||||||
|
{
|
||||||
|
if (pattern.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (input.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Regex.IsMatch(input, pattern, RegexOptions.None, TimeSpan.FromSeconds(timeoutSeconds));
|
||||||
|
}
|
||||||
|
catch (RegexMatchTimeoutException ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog("IsRegexMatch timeout", ex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog("IsRegexMatch invalid pattern", ex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Data Checks
|
#endregion Data Checks
|
||||||
|
|
||||||
#region Speed Test
|
#region Speed Test
|
||||||
@@ -1018,12 +1077,12 @@ public class Utils
|
|||||||
return new Dictionary<string, string>();
|
return new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<string?> GetCliWrapOutput(string filePath, string? arg)
|
public static async Task<string?> GetCliWrapOutput(string filePath, string? arg, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await GetCliWrapOutput(filePath, arg != null ? new List<string>() { arg } : null);
|
return await GetCliWrapOutput(filePath, arg != null ? new List<string>() { arg } : null, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<string?> GetCliWrapOutput(string filePath, IEnumerable<string>? args)
|
public static async Task<string?> GetCliWrapOutput(string filePath, IEnumerable<string>? args, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -1040,7 +1099,7 @@ public class Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await cmd.ExecuteBufferedAsync();
|
var result = await cmd.ExecuteBufferedAsync(cancellationToken);
|
||||||
if (result.IsSuccess)
|
if (result.IsSuccess)
|
||||||
{
|
{
|
||||||
return result.StandardOutput ?? "";
|
return result.StandardOutput ?? "";
|
||||||
@@ -1382,7 +1441,11 @@ public class Utils
|
|||||||
if (File.Exists(fileName))
|
if (File.Exists(fileName))
|
||||||
{
|
{
|
||||||
var currentMode = File.GetUnixFileMode(fileName);
|
var currentMode = File.GetUnixFileMode(fileName);
|
||||||
File.SetUnixFileMode(fileName, currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
|
var executableMode = currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
|
||||||
|
if (currentMode != executableMode)
|
||||||
|
{
|
||||||
|
File.SetUnixFileMode(fileName, executableMode);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public enum EConfigType
|
|||||||
Anytls = 11,
|
Anytls = 11,
|
||||||
Naive = 12,
|
Naive = 12,
|
||||||
Outbound = 13,
|
Outbound = 13,
|
||||||
|
MASQUE = 14,
|
||||||
PolicyGroup = 101,
|
PolicyGroup = 101,
|
||||||
ProxyChain = 102,
|
ProxyChain = 102,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,10 +92,16 @@ public class Global
|
|||||||
public const string XrayLocalAsset = "XRAY_LOCATION_ASSET";
|
public const string XrayLocalAsset = "XRAY_LOCATION_ASSET";
|
||||||
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
|
public const string XrayLocalCert = "XRAY_LOCATION_CERT";
|
||||||
public const int SpeedTestPageSize = 1000;
|
public const int SpeedTestPageSize = 1000;
|
||||||
|
public const int SpeedTestConcurrencyCountMin = 10;
|
||||||
public const string LinuxBash = "/bin/bash";
|
public const string LinuxBash = "/bin/bash";
|
||||||
public const string StringTrue = "true";
|
public const string StringTrue = "true";
|
||||||
public const string StringFalse = "false";
|
public const string StringFalse = "false";
|
||||||
public const int SqliteMaxBatchSize = 10000;
|
public const int SqliteMaxBatchSize = 10000;
|
||||||
|
public static readonly TimeSpan LocalFetch = TimeSpan.FromSeconds(5);
|
||||||
|
public static readonly TimeSpan DirectFetch = TimeSpan.FromSeconds(10);
|
||||||
|
public static readonly TimeSpan ProxyFetch = TimeSpan.FromSeconds(30);
|
||||||
|
public static readonly TimeSpan DirectDownloadConnect = TimeSpan.FromSeconds(5);
|
||||||
|
public static readonly TimeSpan ProxyDownloadConnect = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
public const string SingboxDirectDNSTagPrefix = "direct-dns-";
|
public const string SingboxDirectDNSTagPrefix = "direct-dns-";
|
||||||
public const string SingboxRemoteDNSTagPrefix = "remote-dns-";
|
public const string SingboxRemoteDNSTagPrefix = "remote-dns-";
|
||||||
@@ -255,7 +261,8 @@ public class Global
|
|||||||
{ EConfigType.TUIC, "tuic" },
|
{ EConfigType.TUIC, "tuic" },
|
||||||
{ EConfigType.WireGuard, "wireguard" },
|
{ EConfigType.WireGuard, "wireguard" },
|
||||||
{ EConfigType.Anytls, "anytls" },
|
{ EConfigType.Anytls, "anytls" },
|
||||||
{ EConfigType.Naive, "naive" }
|
{ EConfigType.Naive, "naive" },
|
||||||
|
{ EConfigType.MASQUE, "masque" },
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<string> VmessSecurities =
|
public static readonly List<string> VmessSecurities =
|
||||||
@@ -365,6 +372,7 @@ public class Global
|
|||||||
EConfigType.Trojan,
|
EConfigType.Trojan,
|
||||||
EConfigType.Hysteria2,
|
EConfigType.Hysteria2,
|
||||||
EConfigType.WireGuard,
|
EConfigType.WireGuard,
|
||||||
|
EConfigType.MASQUE,
|
||||||
EConfigType.SOCKS,
|
EConfigType.SOCKS,
|
||||||
EConfigType.HTTP,
|
EConfigType.HTTP,
|
||||||
];
|
];
|
||||||
@@ -380,6 +388,7 @@ public class Global
|
|||||||
EConfigType.Anytls,
|
EConfigType.Anytls,
|
||||||
EConfigType.Naive,
|
EConfigType.Naive,
|
||||||
EConfigType.WireGuard,
|
EConfigType.WireGuard,
|
||||||
|
EConfigType.MASQUE,
|
||||||
EConfigType.SOCKS,
|
EConfigType.SOCKS,
|
||||||
EConfigType.HTTP,
|
EConfigType.HTTP,
|
||||||
];
|
];
|
||||||
@@ -647,10 +656,10 @@ public class Global
|
|||||||
{ ECoreType.Xray, "XTLS/Xray-core" },
|
{ ECoreType.Xray, "XTLS/Xray-core" },
|
||||||
{ ECoreType.sing_box, "SagerNet/sing-box" },
|
{ ECoreType.sing_box, "SagerNet/sing-box" },
|
||||||
{ ECoreType.mihomo, "MetaCubeX/mihomo" },
|
{ ECoreType.mihomo, "MetaCubeX/mihomo" },
|
||||||
{ ECoreType.hysteria, "apernet/hysteria" },
|
{ ECoreType.hysteria, "HyNetworks/hysteria" },
|
||||||
{ ECoreType.hysteria2, "apernet/hysteria" },
|
{ ECoreType.hysteria2, "HyNetworks/hysteria" },
|
||||||
{ ECoreType.naiveproxy, "klzgrad/naiveproxy" },
|
{ ECoreType.naiveproxy, "klzgrad/naiveproxy" },
|
||||||
{ ECoreType.tuic, "EAimTY/tuic" },
|
{ ECoreType.tuic, "tuic-protocol/tuic" },
|
||||||
{ ECoreType.juicity, "juicity/juicity" },
|
{ ECoreType.juicity, "juicity/juicity" },
|
||||||
{ ECoreType.brook, "txthinking/brook" },
|
{ ECoreType.brook, "txthinking/brook" },
|
||||||
{ ECoreType.overtls, "ShadowsocksR-Live/overtls" },
|
{ ECoreType.overtls, "ShadowsocksR-Live/overtls" },
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ global using System.Diagnostics;
|
|||||||
global using System.Net;
|
global using System.Net;
|
||||||
global using System.Net.NetworkInformation;
|
global using System.Net.NetworkInformation;
|
||||||
global using System.Net.Sockets;
|
global using System.Net.Sockets;
|
||||||
global using ReactiveUI.Primitives;
|
|
||||||
global using ReactiveUI.Primitives.Concurrency;
|
|
||||||
global using ReactiveUI.Primitives.Extensions;
|
|
||||||
global using ReactiveUI.Primitives.Disposables;
|
|
||||||
global using ReactiveUI.Primitives.Signals;
|
|
||||||
global using System.Reflection;
|
global using System.Reflection;
|
||||||
global using System.Runtime.InteropServices;
|
global using System.Runtime.InteropServices;
|
||||||
global using System.Runtime.Versioning;
|
global using System.Runtime.Versioning;
|
||||||
@@ -22,6 +17,11 @@ global using System.Text.Json.Nodes;
|
|||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
global using System.Text.RegularExpressions;
|
global using System.Text.RegularExpressions;
|
||||||
global using ReactiveUI;
|
global using ReactiveUI;
|
||||||
|
global using ReactiveUI.Primitives;
|
||||||
|
global using ReactiveUI.Primitives.Concurrency;
|
||||||
|
global using ReactiveUI.Primitives.Disposables;
|
||||||
|
global using ReactiveUI.Primitives.Extensions;
|
||||||
|
global using ReactiveUI.Primitives.Signals;
|
||||||
global using ReactiveUI.SourceGenerators;
|
global using ReactiveUI.SourceGenerators;
|
||||||
global using ServiceLib.Base;
|
global using ServiceLib.Base;
|
||||||
global using ServiceLib.Common;
|
global using ServiceLib.Common;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public class CoreConfigContextBuilder
|
|||||||
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
|
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
|
||||||
IsWindows = Utils.IsWindows(),
|
IsWindows = Utils.IsWindows(),
|
||||||
IsMacOS = Utils.IsMacOS(),
|
IsMacOS = Utils.IsMacOS(),
|
||||||
|
IsLinux = Utils.IsLinux(),
|
||||||
HasGlobalIPv6Address = Utils.HasGlobalIPv6Address(),
|
HasGlobalIPv6Address = Utils.HasGlobalIPv6Address(),
|
||||||
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
|
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -136,10 +136,7 @@ public static class ConfigHandler
|
|||||||
{
|
{
|
||||||
config.SpeedTestItem.SpeedPingTestUrl = Global.SpeedPingTestUrls.First();
|
config.SpeedTestItem.SpeedPingTestUrl = Global.SpeedPingTestUrls.First();
|
||||||
}
|
}
|
||||||
if (config.SpeedTestItem.MixedConcurrencyCount < 1)
|
config.SpeedTestItem.MixedConcurrencyCount = Math.Max(config.SpeedTestItem.MixedConcurrencyCount, Global.SpeedTestConcurrencyCountMin);
|
||||||
{
|
|
||||||
config.SpeedTestItem.MixedConcurrencyCount = 5;
|
|
||||||
}
|
|
||||||
if (config.SpeedTestItem.UdpTestTarget.IsNullOrEmpty())
|
if (config.SpeedTestItem.UdpTestTarget.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
config.SpeedTestItem.UdpTestTarget = Global.UdpTestTargets.First();
|
config.SpeedTestItem.UdpTestTarget = Global.UdpTestTargets.First();
|
||||||
@@ -295,6 +292,7 @@ public static class ConfigHandler
|
|||||||
EConfigType.WireGuard => await AddWireguardServer(config, item),
|
EConfigType.WireGuard => await AddWireguardServer(config, item),
|
||||||
EConfigType.Anytls => await AddAnytlsServer(config, item),
|
EConfigType.Anytls => await AddAnytlsServer(config, item),
|
||||||
EConfigType.Naive => await AddNaiveServer(config, item),
|
EConfigType.Naive => await AddNaiveServer(config, item),
|
||||||
|
EConfigType.MASQUE => await AddMasqueServer(config, item),
|
||||||
_ => -1,
|
_ => -1,
|
||||||
};
|
};
|
||||||
return ret;
|
return ret;
|
||||||
@@ -995,6 +993,29 @@ public static class ConfigHandler
|
|||||||
return 0;
|
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>
|
/// <summary>
|
||||||
/// Sort the server list by the specified column
|
/// Sort the server list by the specified column
|
||||||
/// Updates the sort order in the profile extension data
|
/// Updates the sort order in the profile extension data
|
||||||
@@ -1528,7 +1549,7 @@ public static class ConfigHandler
|
|||||||
p != null &&
|
p != null &&
|
||||||
p.IsValid() &&
|
p.IsValid() &&
|
||||||
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||||
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
|
Utils.IsRegexMatch(p.Remarks, extraItem.Filter)
|
||||||
)
|
)
|
||||||
.ToList() ?? [];
|
.ToList() ?? [];
|
||||||
if (matchedChildProfiles.Count == 0)
|
if (matchedChildProfiles.Count == 0)
|
||||||
@@ -1667,7 +1688,7 @@ public static class ConfigHandler
|
|||||||
//exist sub items //filter
|
//exist sub items //filter
|
||||||
if (isSub && subid.IsNotEmpty() && subFilter.IsNotEmpty())
|
if (isSub && subid.IsNotEmpty() && subFilter.IsNotEmpty())
|
||||||
{
|
{
|
||||||
if (!Regex.IsMatch(profileItem.Remarks, subFilter))
|
if (!Utils.IsRegexMatch(profileItem.Remarks, subFilter))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1687,6 +1708,7 @@ public static class ConfigHandler
|
|||||||
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
|
EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
|
||||||
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
|
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
|
||||||
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
|
EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
|
||||||
|
EConfigType.MASQUE => await AddMasqueServer(config, profileItem, false),
|
||||||
_ => -1,
|
_ => -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1756,7 +1778,7 @@ public static class ConfigHandler
|
|||||||
}
|
}
|
||||||
if (lstProfiles.Count > 0)
|
if (lstProfiles.Count > 0)
|
||||||
{
|
{
|
||||||
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub, subItem);
|
||||||
if (count > 0)
|
if (count > 0)
|
||||||
{
|
{
|
||||||
return count;
|
return count;
|
||||||
@@ -1802,7 +1824,7 @@ public static class ConfigHandler
|
|||||||
|
|
||||||
if (lstProfiles?.Count > 0)
|
if (lstProfiles?.Count > 0)
|
||||||
{
|
{
|
||||||
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub, subItem);
|
||||||
if (count > 0)
|
if (count > 0)
|
||||||
{
|
{
|
||||||
return count;
|
return count;
|
||||||
@@ -1816,7 +1838,8 @@ public static class ConfigHandler
|
|||||||
Config config,
|
Config config,
|
||||||
List<ProfileItem> lstProfiles,
|
List<ProfileItem> lstProfiles,
|
||||||
string subid,
|
string subid,
|
||||||
bool isSub)
|
bool isSub,
|
||||||
|
SubItem? subItem)
|
||||||
{
|
{
|
||||||
var count = 0;
|
var count = 0;
|
||||||
foreach (var it in lstProfiles)
|
foreach (var it in lstProfiles)
|
||||||
@@ -1826,6 +1849,7 @@ public static class ConfigHandler
|
|||||||
|
|
||||||
if (it.ConfigType == EConfigType.Custom)
|
if (it.ConfigType == EConfigType.Custom)
|
||||||
{
|
{
|
||||||
|
it.PreSocksPort = subItem?.PreSocksPort;
|
||||||
if (await AddCustomServer(config, it, true) == 0)
|
if (await AddCustomServer(config, it, true) == 0)
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
|
|||||||
@@ -66,34 +66,41 @@ public static class ConnectionHandler
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Measures response time by sending HTTP requests through proxy.
|
/// Measures response time by sending HTTP requests through proxy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, int downloadTimeout = 9)
|
public static async Task<int> GetRealPingTime(IWebProxy? webProxy, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
var url = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl;
|
||||||
var responseTime = -1;
|
var responseTime = -1;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource();
|
using var timeoutCts = new CancellationTokenSource();
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout));
|
timeoutCts.CancelAfter(Global.LocalFetch);
|
||||||
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||||
|
var linkedToken = linkedCts.Token;
|
||||||
using var client = new HttpClient(new SocketsHttpHandler()
|
using var client = new HttpClient(new SocketsHttpHandler()
|
||||||
{
|
{
|
||||||
Proxy = webProxy,
|
Proxy = webProxy,
|
||||||
UseProxy = webProxy != null,
|
UseProxy = webProxy != null,
|
||||||
ConnectTimeout = TimeSpan.FromSeconds(3)
|
ConnectTimeout = Global.LocalFetch,
|
||||||
});
|
});
|
||||||
|
|
||||||
List<int> oneTime = [];
|
List<int> oneTime = [];
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var timer = Stopwatch.StartNew();
|
var timer = Stopwatch.StartNew();
|
||||||
await client.GetAsync(url, cts.Token).ConfigureAwait(false);
|
await client.GetAsync(url, linkedToken).ConfigureAwait(false);
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
|
oneTime.Add((int)timer.Elapsed.TotalMilliseconds);
|
||||||
await Task.Delay(100, cts.Token);
|
await Task.Delay(100, linkedToken);
|
||||||
}
|
}
|
||||||
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
|
responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
// Ignore
|
||||||
}
|
}
|
||||||
return responseTime;
|
return responseTime;
|
||||||
}
|
}
|
||||||
@@ -101,7 +108,7 @@ public static class ConnectionHandler
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets IP and country information through specified proxy.
|
/// Gets IP and country information through specified proxy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy)
|
public static async Task<IpInfoResult?> GetIPInfo(IWebProxy? webProxy, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -112,7 +119,7 @@ public static class ConnectionHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
var downloadHandle = new DownloadService();
|
var downloadHandle = new DownloadService();
|
||||||
var result = await downloadHandle.TryDownloadString(url, webProxy, "");
|
var result = await downloadHandle.TryDownloadString(url, webProxy, "", cancellationToken);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -129,6 +136,10 @@ public static class ConnectionHandler
|
|||||||
|
|
||||||
return new IpInfoResult(country, ip);
|
return new IpInfoResult(country, ip);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ public class WireguardFmt : BaseFmt
|
|||||||
|
|
||||||
var query = Utils.ParseQueryString(url.Query);
|
var query = Utils.ParseQueryString(url.Query);
|
||||||
|
|
||||||
|
var finalmaskDecoded = GetQueryDecoded(query, "fm");
|
||||||
|
if (finalmaskDecoded.IsNotEmpty())
|
||||||
|
{
|
||||||
|
var node = JsonUtils.ParseJson(finalmaskDecoded);
|
||||||
|
item.Finalmask = node != null
|
||||||
|
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
|
})
|
||||||
|
: finalmaskDecoded;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item.Finalmask = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
item.SetProtocolExtra(item.GetProtocolExtra() with
|
item.SetProtocolExtra(item.GetProtocolExtra() with
|
||||||
{
|
{
|
||||||
WgPublicKey = GetQueryDecoded(query, "publickey"),
|
WgPublicKey = GetQueryDecoded(query, "publickey"),
|
||||||
@@ -76,6 +94,19 @@ public class WireguardFmt : BaseFmt
|
|||||||
{
|
{
|
||||||
dicQuery.Add("dns", Utils.UrlEncode(protoExtra.WgDns));
|
dicQuery.Add("dns", Utils.UrlEncode(protoExtra.WgDns));
|
||||||
}
|
}
|
||||||
|
if (item.Finalmask.IsNotEmpty())
|
||||||
|
{
|
||||||
|
var node = JsonUtils.ParseJson(item.Finalmask);
|
||||||
|
var finalmask = node != null
|
||||||
|
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = false,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
|
})
|
||||||
|
: item.Finalmask;
|
||||||
|
dicQuery.Add("fm", Utils.UrlEncode(finalmask));
|
||||||
|
}
|
||||||
return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
|
return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ namespace ServiceLib.Handler.SysProxy;
|
|||||||
public static class SysProxyHandler
|
public static class SysProxyHandler
|
||||||
{
|
{
|
||||||
private static readonly string _tag = "SysProxyHandler";
|
private static readonly string _tag = "SysProxyHandler";
|
||||||
|
private static readonly Lazy<PacManager> _pacManager = new(() => new PacManager());
|
||||||
|
|
||||||
public static async Task<bool> UpdateSysProxy(Config config, bool forceDisable)
|
public static async Task<bool> UpdateSysProxy(Config config, bool forceDisable)
|
||||||
{
|
{
|
||||||
@@ -56,7 +57,7 @@ public static class SysProxyHandler
|
|||||||
|
|
||||||
if (type != ESysProxyType.Pac && Utils.IsWindows())
|
if (type != ESysProxyType.Pac && Utils.IsWindows())
|
||||||
{
|
{
|
||||||
PacManager.Instance.Stop();
|
_pacManager.Value.Stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -110,7 +111,7 @@ public static class SysProxyHandler
|
|||||||
private static async Task SetWindowsProxyPac(int port)
|
private static async Task SetWindowsProxyPac(int port)
|
||||||
{
|
{
|
||||||
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);
|
var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac);
|
||||||
await PacManager.Instance.StartAsync(port, portPac);
|
await _pacManager.Value.StartAsync(port, portPac);
|
||||||
var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}";
|
var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}";
|
||||||
ProxySettingWindows.SetProxy(strProxy, "", 4);
|
ProxySettingWindows.SetProxy(strProxy, "", 4);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ public class DownloaderHelper
|
|||||||
private static readonly Lazy<DownloaderHelper> _instance = new(() => new());
|
private static readonly Lazy<DownloaderHelper> _instance = new(() => new());
|
||||||
public static DownloaderHelper Instance => _instance.Value;
|
public static DownloaderHelper Instance => _instance.Value;
|
||||||
|
|
||||||
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout,
|
public async Task<string?> DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent,
|
||||||
IReadOnlyDictionary<string, string>? requestHeaders = null, string? acceptHeader = null)
|
IReadOnlyDictionary<string, string>? requestHeaders = null, string? acceptHeader = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (url.IsNullOrEmpty())
|
if (url.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
|
|
||||||
|
|
||||||
Uri uri = new(url);
|
Uri uri = new(url);
|
||||||
//Authorization Header
|
//Authorization Header
|
||||||
var headers = new WebHeaderCollection();
|
var headers = new WebHeaderCollection();
|
||||||
@@ -31,12 +29,11 @@ public class DownloaderHelper
|
|||||||
Headers = headers,
|
Headers = headers,
|
||||||
Accept = acceptHeader,
|
Accept = acceptHeader,
|
||||||
UserAgent = userAgent,
|
UserAgent = userAgent,
|
||||||
ConnectTimeout = connectTimeout * 1000,
|
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
|
||||||
Proxy = webProxy
|
Proxy = webProxy
|
||||||
};
|
};
|
||||||
var downloadOpt = new DownloadConfiguration()
|
var downloadOpt = new DownloadConfiguration()
|
||||||
{
|
{
|
||||||
BlockTimeout = timeout * 1000,
|
|
||||||
MaxTryAgainOnFailure = 2,
|
MaxTryAgainOnFailure = 2,
|
||||||
RequestConfiguration = requestConfiguration,
|
RequestConfiguration = requestConfiguration,
|
||||||
CustomHttpMessageHandlerFactory = () => HttpRequestHeadersHelper.CreateHandler(GetSocketsHttpHandler(requestConfiguration), requestHeaders),
|
CustomHttpMessageHandlerFactory = () => HttpRequestHeadersHelper.CreateHandler(GetSocketsHttpHandler(requestConfiguration), requestHeaders),
|
||||||
@@ -51,31 +48,26 @@ public class DownloaderHelper
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
|
||||||
|
|
||||||
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
|
|
||||||
using StreamReader reader = new(stream);
|
using StreamReader reader = new(stream);
|
||||||
|
|
||||||
return await reader.ReadToEndAsync(cts.Token);
|
return await reader.ReadToEndAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, IProgress<string> progress, int timeout)
|
public async Task DownloadDataAsync4Speed(IWebProxy webProxy, string url, Action<string> onProgress, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (url.IsNullOrEmpty())
|
if (url.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(url));
|
throw new ArgumentNullException(nameof(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
|
|
||||||
var requestConfiguration = new RequestConfiguration()
|
var requestConfiguration = new RequestConfiguration()
|
||||||
{
|
{
|
||||||
ConnectTimeout = connectTimeout * 1000,
|
ConnectTimeout = GetConnectTimeoutMs(true),
|
||||||
Proxy = webProxy
|
Proxy = webProxy
|
||||||
};
|
};
|
||||||
var downloadOpt = new DownloadConfiguration()
|
var downloadOpt = new DownloadConfiguration()
|
||||||
{
|
{
|
||||||
BlockTimeout = timeout * 1000,
|
|
||||||
MaxTryAgainOnFailure = 2,
|
MaxTryAgainOnFailure = 2,
|
||||||
RequestConfiguration = requestConfiguration,
|
RequestConfiguration = requestConfiguration,
|
||||||
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
|
||||||
@@ -88,49 +80,45 @@ public class DownloaderHelper
|
|||||||
|
|
||||||
downloader.DownloadProgressChanged += (sender, value) =>
|
downloader.DownloadProgressChanged += (sender, value) =>
|
||||||
{
|
{
|
||||||
if (progress != null && value.BytesPerSecondSpeed > 0)
|
if (!(value.BytesPerSecondSpeed > 0))
|
||||||
{
|
{
|
||||||
hasValue = true;
|
return;
|
||||||
if (value.BytesPerSecondSpeed > maxSpeed)
|
}
|
||||||
{
|
hasValue = true;
|
||||||
maxSpeed = value.BytesPerSecondSpeed;
|
if (value.BytesPerSecondSpeed > maxSpeed)
|
||||||
}
|
{
|
||||||
|
maxSpeed = value.BytesPerSecondSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
var ts = DateTime.Now - lastUpdateTime;
|
var ts = DateTime.Now - lastUpdateTime;
|
||||||
if (ts.TotalMilliseconds >= 1000)
|
if (ts.TotalMilliseconds >= 1000)
|
||||||
{
|
{
|
||||||
lastUpdateTime = DateTime.Now;
|
lastUpdateTime = DateTime.Now;
|
||||||
var speed = (maxSpeed / 1000 / 1000).ToString("#0.0");
|
var speed = (maxSpeed / 1000 / 1000).ToString("#0.0");
|
||||||
progress.Report(speed);
|
onProgress.Invoke(speed);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
downloader.DownloadFileCompleted += (sender, value) =>
|
downloader.DownloadFileCompleted += (sender, value) =>
|
||||||
{
|
{
|
||||||
if (progress != null)
|
if (hasValue && maxSpeed > 0)
|
||||||
{
|
{
|
||||||
if (hasValue && maxSpeed > 0)
|
var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0");
|
||||||
{
|
onProgress.Invoke(finalSpeed);
|
||||||
var finalSpeed = (maxSpeed / 1000 / 1000).ToString("#0.0");
|
}
|
||||||
progress.Report(finalSpeed);
|
else if (value.Error != null)
|
||||||
}
|
{
|
||||||
else if (value.Error != null)
|
onProgress.Invoke(value.Error?.Message);
|
||||||
{
|
}
|
||||||
progress.Report(value.Error?.Message);
|
else
|
||||||
}
|
{
|
||||||
else
|
onProgress.Invoke("0");
|
||||||
{
|
|
||||||
progress.Report("0");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
//progress.Report("......");
|
//progress.Invoke("......");
|
||||||
using var cts = new CancellationTokenSource();
|
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cancellationToken);
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
|
||||||
await using var stream = await downloader.DownloadFileTaskAsync(address: url, cts.Token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
|
public async Task DownloadFileAsync(IWebProxy? webProxy, FileDownloadRequest request, Action<FileDownloadState> onProgress, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
if (request.FilePath.IsNullOrEmpty())
|
if (request.FilePath.IsNullOrEmpty())
|
||||||
@@ -147,9 +135,9 @@ public class DownloaderHelper
|
|||||||
Request = request,
|
Request = request,
|
||||||
};
|
};
|
||||||
|
|
||||||
var requestConfiguration = new RequestConfiguration()
|
var requestConfiguration = new RequestConfiguration
|
||||||
{
|
{
|
||||||
ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
|
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
|
||||||
Proxy = webProxy,
|
Proxy = webProxy,
|
||||||
};
|
};
|
||||||
var downloadOpt = new DownloadConfiguration()
|
var downloadOpt = new DownloadConfiguration()
|
||||||
@@ -196,7 +184,7 @@ public class DownloaderHelper
|
|||||||
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken);
|
await downloader.DownloadFileTaskAsync(request.FileUrl, request.FilePath, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, TimeSpan connectTimeout, CancellationToken cancellationToken = default)
|
public async Task DownloadSmallFilesAsync(IWebProxy? webProxy, List<FileDownloadRequest> requests, Action<ReadOnlyMemory<FileDownloadState>> onProgress, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (requests is not { Count: > 0 })
|
if (requests is not { Count: > 0 })
|
||||||
{
|
{
|
||||||
@@ -215,7 +203,7 @@ public class DownloaderHelper
|
|||||||
|
|
||||||
var requestConfiguration = new RequestConfiguration()
|
var requestConfiguration = new RequestConfiguration()
|
||||||
{
|
{
|
||||||
ConnectTimeout = (int)connectTimeout.TotalMilliseconds,
|
ConnectTimeout = GetConnectTimeoutMs(webProxy != null),
|
||||||
Proxy = webProxy,
|
Proxy = webProxy,
|
||||||
|
|
||||||
KeepAlive = true,
|
KeepAlive = true,
|
||||||
@@ -225,7 +213,7 @@ public class DownloaderHelper
|
|||||||
var parallelOptions = new ParallelOptions
|
var parallelOptions = new ParallelOptions
|
||||||
{
|
{
|
||||||
MaxDegreeOfParallelism = 4,
|
MaxDegreeOfParallelism = 4,
|
||||||
//CancellationToken = cancellationToken,
|
CancellationToken = cancellationToken,
|
||||||
};
|
};
|
||||||
|
|
||||||
await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) =>
|
await Parallel.ForEachAsync(Enumerable.Range(0, requests.Count), parallelOptions, async (index, parallelCancellationToken) =>
|
||||||
@@ -344,4 +332,9 @@ public class DownloaderHelper
|
|||||||
|
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int GetConnectTimeoutMs(bool isProxy)
|
||||||
|
{
|
||||||
|
return (int)(isProxy ? Global.ProxyDownloadConnect : Global.DirectDownloadConnect).TotalMilliseconds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class HttpClientHelper
|
|||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string?> TryGetAsync(string url)
|
public async Task<string?> TryGetAsync(string url, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (url.IsNullOrEmpty())
|
if (url.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -31,8 +31,12 @@ public class HttpClientHelper
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await httpClient.GetAsync(url);
|
var response = await httpClient.GetAsync(url, cancellationToken);
|
||||||
return await response.Content.ReadAsStringAsync();
|
return await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,15 +75,28 @@ public sealed class SQLiteHelper
|
|||||||
|
|
||||||
public async Task DisposeDbConnectionAsync()
|
public async Task DisposeDbConnectionAsync()
|
||||||
{
|
{
|
||||||
await Task.Factory.StartNew(() =>
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
_db?.Close();
|
try
|
||||||
_db?.Dispose();
|
{
|
||||||
_db = null;
|
_db?.Close();
|
||||||
|
_db?.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_db = null;
|
||||||
|
}
|
||||||
|
|
||||||
_dbAsync?.GetConnection()?.Close();
|
try
|
||||||
_dbAsync?.GetConnection()?.Dispose();
|
{
|
||||||
_dbAsync = null;
|
var conn = _dbAsync?.GetConnection();
|
||||||
|
conn?.Close();
|
||||||
|
conn?.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dbAsync = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,13 @@ public class CertPemManager
|
|||||||
/// Get certificate in PEM format from a server with CA pinning validation
|
/// Get certificate in PEM format from a server with CA pinning validation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
|
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName,
|
||||||
List<string>? verifyPeerCertByName = null, int timeout = 4)
|
List<string>? verifyPeerCertByName = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (domain, _, port, _) = Utils.ParseUrl(target);
|
var (domain, _, port, _) = Utils.ParseUrl(target);
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource(Global.LocalFetch);
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
|
||||||
|
|
||||||
using var client = new TcpClient();
|
using var client = new TcpClient();
|
||||||
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
||||||
@@ -63,8 +62,8 @@ public class CertPemManager
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
|
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"));
|
||||||
return (null, $"Connection timeout after {timeout} seconds");
|
return (null, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -77,15 +76,14 @@ public class CertPemManager
|
|||||||
/// Get certificate chain in PEM format from a server with CA pinning validation
|
/// Get certificate chain in PEM format from a server with CA pinning validation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
|
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName,
|
||||||
List<string>? verifyPeerCertByName = null, int timeout = 4)
|
List<string>? verifyPeerCertByName = null)
|
||||||
{
|
{
|
||||||
var pemList = new List<string>();
|
var pemList = new List<string>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (domain, _, port, _) = Utils.ParseUrl(target);
|
var (domain, _, port, _) = Utils.ParseUrl(target);
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource(Global.LocalFetch);
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
|
||||||
|
|
||||||
using var client = new TcpClient();
|
using var client = new TcpClient();
|
||||||
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token);
|
||||||
@@ -116,8 +114,8 @@ public class CertPemManager
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds"));
|
Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds"));
|
||||||
return (pemList, $"Connection timeout after {timeout} seconds");
|
return (pemList, $"Connection timeout after {Global.LocalFetch.TotalSeconds} seconds");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ public sealed class CoreInfoManager
|
|||||||
DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz",
|
DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz",
|
||||||
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
|
DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz",
|
||||||
DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.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",
|
Match = "sing-box",
|
||||||
VersionArg = "version",
|
VersionArg = "version",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ public class CoreManager
|
|||||||
await _updateFunc?.Invoke(notify, msg);
|
await _updateFunc?.Invoke(notify, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task WaitForProxyPort(CoreConfigContext? preContext, int timeoutMs = 5000)
|
private static async Task WaitForProxyPort(CoreConfigContext? preContext)
|
||||||
{
|
{
|
||||||
if (preContext is null)
|
if (preContext is null)
|
||||||
{
|
{
|
||||||
@@ -227,7 +227,7 @@ public class CoreManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var rootCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs));
|
using var rootCts = new CancellationTokenSource(Global.LocalFetch);
|
||||||
var rootToken = rootCts.Token;
|
var rootToken = rootCts.Token;
|
||||||
|
|
||||||
var port = preContext.Node.Port;
|
var port = preContext.Node.Port;
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ public class GroupProfileManager
|
|||||||
p != null &&
|
p != null &&
|
||||||
p.IsValid() &&
|
p.IsValid() &&
|
||||||
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||||
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
|
Utils.IsRegexMatch(p.Remarks, extra.Filter)
|
||||||
)
|
)
|
||||||
.ToList() ?? [];
|
.ToList() ?? [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,36 +2,37 @@ namespace ServiceLib.Manager;
|
|||||||
|
|
||||||
public class PacManager
|
public class PacManager
|
||||||
{
|
{
|
||||||
private static readonly Lazy<PacManager> _instance = new(() => new PacManager());
|
private const string Tag = "PacManager";
|
||||||
public static PacManager Instance => _instance.Value;
|
private CancellationTokenSource? _cts;
|
||||||
|
|
||||||
private int _httpPort;
|
|
||||||
private int _pacPort;
|
private int _pacPort;
|
||||||
private TcpListener? _tcpListener;
|
private TcpListener? _tcpListener;
|
||||||
private byte[] _writeContent;
|
private byte[] _writeContent = [];
|
||||||
private bool _isRunning;
|
|
||||||
private bool _needRestart = true;
|
|
||||||
|
|
||||||
public async Task StartAsync(int httpPort, int pacPort)
|
public async Task StartAsync(int httpPort, int pacPort)
|
||||||
{
|
{
|
||||||
_needRestart = httpPort != _httpPort || pacPort != _pacPort || !_isRunning;
|
var content = await InitText(httpPort);
|
||||||
|
_writeContent = content;
|
||||||
|
|
||||||
_httpPort = httpPort;
|
if (_tcpListener is not null && _pacPort == pacPort)
|
||||||
_pacPort = pacPort;
|
|
||||||
|
|
||||||
await InitText();
|
|
||||||
|
|
||||||
if (_needRestart)
|
|
||||||
{
|
{
|
||||||
Stop();
|
return;
|
||||||
RunListener();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem.CustomSystemProxyPacPath;
|
||||||
var fileName = (customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath))
|
var fileName = customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath)
|
||||||
? customSystemProxyPacPath
|
? customSystemProxyPacPath
|
||||||
: Path.Combine(Utils.GetConfigPath(), "pac.txt");
|
: Path.Combine(Utils.GetConfigPath(), "pac.txt");
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ public class PacManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pacText = await File.ReadAllTextAsync(fileName);
|
var pacText = await File.ReadAllTextAsync(fileName);
|
||||||
pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;");
|
pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{httpPort};DIRECT;");
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("HTTP/1.0 200 OK");
|
sb.AppendLine("HTTP/1.0 200 OK");
|
||||||
@@ -54,59 +55,43 @@ public class PacManager
|
|||||||
sb.AppendLine("Content-Length:" + Encoding.UTF8.GetByteCount(pacText));
|
sb.AppendLine("Content-Length:" + Encoding.UTF8.GetByteCount(pacText));
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.Append(pacText);
|
sb.Append(pacText);
|
||||||
_writeContent = Encoding.UTF8.GetBytes(sb.ToString());
|
return Encoding.UTF8.GetBytes(sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RunListener()
|
private async Task ListenLoopAsync(TcpListener listener, CancellationToken token)
|
||||||
{
|
{
|
||||||
_tcpListener = TcpListener.Create(_pacPort);
|
var buffer = new byte[1024];
|
||||||
_isRunning = true;
|
try
|
||||||
_tcpListener.Start();
|
|
||||||
Task.Factory.StartNew(async () =>
|
|
||||||
{
|
{
|
||||||
while (_isRunning)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
using var client = await listener.AcceptTcpClientAsync(token).ConfigureAwait(false);
|
||||||
{
|
await using var stream = client.GetStream();
|
||||||
if (!_tcpListener.Pending())
|
_ = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
|
||||||
{
|
await stream.WriteAsync(_writeContent, token).ConfigureAwait(false);
|
||||||
await Task.Delay(10);
|
await stream.FlushAsync(token).ConfigureAwait(false);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = await _tcpListener.AcceptTcpClientAsync();
|
|
||||||
await Task.Run(() => WriteContent(client));
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, TaskCreationOptions.LongRunning);
|
}
|
||||||
}
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
private void WriteContent(TcpClient client)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
var stream = client.GetStream();
|
{
|
||||||
stream.Write(_writeContent, 0, _writeContent.Length);
|
Logging.SaveLog(Tag, ex);
|
||||||
stream.Flush();
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listener.Stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
if (_tcpListener == null)
|
_cts?.Cancel();
|
||||||
{
|
_tcpListener?.Stop();
|
||||||
return;
|
_cts?.Dispose();
|
||||||
}
|
_cts = null;
|
||||||
try
|
_pacPort = 0;
|
||||||
{
|
_tcpListener = null;
|
||||||
_isRunning = false;
|
|
||||||
_tcpListener.Stop();
|
|
||||||
_tcpListener = null;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ public class ProfileExManager
|
|||||||
{
|
{
|
||||||
private static readonly Lazy<ProfileExManager> _instance = new(() => new());
|
private static readonly Lazy<ProfileExManager> _instance = new(() => new());
|
||||||
private ConcurrentBag<ProfileExItem> _lstProfileEx = [];
|
private ConcurrentBag<ProfileExItem> _lstProfileEx = [];
|
||||||
private readonly Queue<string> _queIndexIds = new();
|
private readonly ConcurrentQueue<string> _queIndexIds = new();
|
||||||
|
private readonly Lock _profileExLock = new();
|
||||||
public static ProfileExManager Instance => _instance.Value;
|
public static ProfileExManager Instance => _instance.Value;
|
||||||
private static readonly string _tag = "ProfileExHandler";
|
private static readonly string _tag = "ProfileExHandler";
|
||||||
|
|
||||||
@@ -27,29 +28,39 @@ public class ProfileExManager
|
|||||||
{
|
{
|
||||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem where indexId not in ( select indexId from ProfileItem )");
|
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem where indexId not in ( select indexId from ProfileItem )");
|
||||||
|
|
||||||
_lstProfileEx = new(await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync());
|
var lstProfileEx = new ConcurrentBag<ProfileExItem>(await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync());
|
||||||
|
lock (_profileExLock)
|
||||||
|
{
|
||||||
|
_lstProfileEx = lstProfileEx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void IndexIdEnqueue(string indexId)
|
private void IndexIdEnqueue(string indexId)
|
||||||
{
|
{
|
||||||
if (indexId.IsNotEmpty() && !_queIndexIds.Contains(indexId))
|
if (indexId.IsNotEmpty())
|
||||||
{
|
{
|
||||||
|
//Speed tests touch this from many threads; duplicates are harmless here since
|
||||||
|
//the save loop just writes the same row more than once
|
||||||
_queIndexIds.Enqueue(indexId);
|
_queIndexIds.Enqueue(indexId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveQueueIndexIds()
|
private async Task SaveQueueIndexIds()
|
||||||
{
|
{
|
||||||
var cnt = _queIndexIds.Count;
|
var lstIds = new List<string>();
|
||||||
if (cnt > 0)
|
while (_queIndexIds.TryDequeue(out var id))
|
||||||
|
{
|
||||||
|
lstIds.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lstIds.Count > 0)
|
||||||
{
|
{
|
||||||
var lstExists = await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync();
|
var lstExists = await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync();
|
||||||
List<ProfileExItem> lstInserts = [];
|
List<ProfileExItem> lstInserts = [];
|
||||||
List<ProfileExItem> lstUpdates = [];
|
List<ProfileExItem> lstUpdates = [];
|
||||||
|
|
||||||
for (var i = 0; i < cnt; i++)
|
foreach (var id in lstIds)
|
||||||
{
|
{
|
||||||
var id = _queIndexIds.Dequeue();
|
|
||||||
var item = lstExists.FirstOrDefault(t => t.IndexId == id);
|
var item = lstExists.FirstOrDefault(t => t.IndexId == id);
|
||||||
var itemNew = _lstProfileEx?.FirstOrDefault(t => t.IndexId == id);
|
var itemNew = _lstProfileEx?.FirstOrDefault(t => t.IndexId == id);
|
||||||
if (itemNew is null)
|
if (itemNew is null)
|
||||||
@@ -103,13 +114,26 @@ public class ProfileExManager
|
|||||||
|
|
||||||
private ProfileExItem GetProfileExItem(string? indexId)
|
private ProfileExItem GetProfileExItem(string? indexId)
|
||||||
{
|
{
|
||||||
return _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId) ?? AddProfileEx(indexId);
|
var profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId);
|
||||||
|
if (profileEx is not null)
|
||||||
|
{
|
||||||
|
return profileEx;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_profileExLock)
|
||||||
|
{
|
||||||
|
profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId);
|
||||||
|
return profileEx ?? AddProfileEx(indexId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ClearAll()
|
public async Task ClearAll()
|
||||||
{
|
{
|
||||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
|
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
|
||||||
_lstProfileEx = [];
|
lock (_profileExLock)
|
||||||
|
{
|
||||||
|
_lstProfileEx = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveTo()
|
public async Task SaveTo()
|
||||||
|
|||||||
@@ -12,23 +12,17 @@ public class TaskManager
|
|||||||
_config = config;
|
_config = config;
|
||||||
_updateFunc = updateFunc;
|
_updateFunc = updateFunc;
|
||||||
|
|
||||||
_ = Task.Factory.StartNew(
|
Task.Run(ScheduledTasks);
|
||||||
ScheduledTasks,
|
|
||||||
CancellationToken.None,
|
|
||||||
TaskCreationOptions.LongRunning,
|
|
||||||
TaskScheduler.Default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ScheduledTasks()
|
private async Task ScheduledTasks()
|
||||||
{
|
{
|
||||||
Logging.SaveLog("Setup Scheduled Tasks");
|
Logging.SaveLog("Setup Scheduled Tasks");
|
||||||
|
|
||||||
|
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
|
||||||
var numOfExecuted = 1;
|
var numOfExecuted = 1;
|
||||||
while (true)
|
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
//1 minute
|
|
||||||
await Task.Delay(1000 * 60);
|
|
||||||
|
|
||||||
//Execute once 1 minute
|
//Execute once 1 minute
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -140,7 +134,7 @@ public class TaskManager
|
|||||||
|
|
||||||
var updateService = new UpdateService(_config, async (success, msg) => await Task.CompletedTask);
|
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)
|
foreach (var msg in msgs)
|
||||||
{
|
{
|
||||||
await _updateFunc?.Invoke(false, msg);
|
await _updateFunc?.Invoke(false, msg);
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ public class ClashUIItem
|
|||||||
public bool EnableMixinContent { get; set; }
|
public bool EnableMixinContent { get; set; }
|
||||||
public int ProxiesSorting { get; set; }
|
public int ProxiesSorting { get; set; }
|
||||||
public bool ProxiesAutoRefresh { get; set; }
|
public bool ProxiesAutoRefresh { get; set; }
|
||||||
public int ProxiesAutoDelayTestInterval { get; set; } = 10;
|
public int ProxiesRefreshInterval { get; set; } = 2;
|
||||||
public bool ConnectionsAutoRefresh { get; set; }
|
public bool ConnectionsAutoRefresh { get; set; }
|
||||||
public int ConnectionsRefreshInterval { get; set; } = 2;
|
public int ConnectionsRefreshInterval { get; set; } = 2;
|
||||||
public List<ColumnItem> ConnectionsColumnItem { get; set; }
|
public List<ColumnItem> ConnectionsColumnItem { get; set; }
|
||||||
@@ -242,9 +242,9 @@ public class WebDavItem
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class CheckUpdateItem
|
public class CheckUpdateItem
|
||||||
{
|
{
|
||||||
public bool CheckPreReleaseUpdate { get; set; }
|
|
||||||
public bool UpdateViaProxy { get; set; } = true;
|
public bool UpdateViaProxy { get; set; } = true;
|
||||||
public List<string>? SelectedCoreTypes { get; set; }
|
public List<string>? SelectedCoreTypes { get; set; }
|
||||||
|
public List<string>? CheckPreReleaseCoreTypes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public record CoreConfigContext
|
|||||||
|
|
||||||
public bool IsWindows { get; init; }
|
public bool IsWindows { get; init; }
|
||||||
public bool IsMacOS { 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
|
// 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.
|
// 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 class Outbound4Sbox : BaseServer4Sbox
|
||||||
{
|
{
|
||||||
public string? server { get; set; }
|
|
||||||
public int? server_port { get; set; }
|
|
||||||
public List<string>? server_ports { get; set; }
|
public List<string>? server_ports { get; set; }
|
||||||
public string? uuid { get; set; }
|
public string? uuid { get; set; }
|
||||||
public string? security { get; set; }
|
public string? security { get; set; }
|
||||||
@@ -171,6 +169,10 @@ public class Endpoints4Sbox : BaseServer4Sbox
|
|||||||
public string? udp_timeout { get; set; }
|
public string? udp_timeout { get; set; }
|
||||||
public int? workers { get; set; }
|
public int? workers { get; set; }
|
||||||
public List<Peer4Sbox> peers { 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
|
public class Peer4Sbox
|
||||||
@@ -271,10 +273,8 @@ public class Server4Sbox : BaseServer4Sbox
|
|||||||
public string? inet4_range { get; set; }
|
public string? inet4_range { get; set; }
|
||||||
public string? inet6_range { get; set; }
|
public string? inet6_range { get; set; }
|
||||||
public string? client_subnet { get; set; }
|
public string? client_subnet { get; set; }
|
||||||
public string? server { get; set; }
|
|
||||||
public new string? domain_resolver { get; set; }
|
public new string? domain_resolver { get; set; }
|
||||||
[JsonPropertyName("interface")] public string? Interface { get; set; }
|
[JsonPropertyName("interface")] public string? Interface { get; set; }
|
||||||
public int? server_port { get; set; }
|
|
||||||
public string? path { get; set; }
|
public string? path { get; set; }
|
||||||
public Headers4Sbox? headers { get; set; }
|
public Headers4Sbox? headers { get; set; }
|
||||||
|
|
||||||
@@ -359,4 +359,6 @@ public abstract class BaseServer4Sbox : DialFields4Sbox
|
|||||||
{
|
{
|
||||||
public string type { get; set; }
|
public string type { get; set; }
|
||||||
public string tag { 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 string? autoOutboundsInterface { get; set; }
|
||||||
|
|
||||||
public List<string>? dns { get; set; }
|
public List<string>? dns { get; set; }
|
||||||
|
|
||||||
|
public bool? autoSystemDnsToGateway { get; set; }
|
||||||
|
|
||||||
|
public List<string>? autoSystemWfpBlockLeak { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Sniffing4Ray
|
public class Sniffing4Ray
|
||||||
@@ -315,6 +319,7 @@ public class StreamSettings4Ray
|
|||||||
public GrpcSettings4Ray? grpcSettings { get; set; }
|
public GrpcSettings4Ray? grpcSettings { get; set; }
|
||||||
|
|
||||||
public HysteriaSettings4Ray? hysteriaSettings { get; set; }
|
public HysteriaSettings4Ray? hysteriaSettings { get; set; }
|
||||||
|
public MasqueSettings4Ray? masqueSettings { get; set; }
|
||||||
|
|
||||||
public object? finalmask { get; set; }
|
public object? finalmask { get; set; }
|
||||||
|
|
||||||
@@ -443,6 +448,12 @@ public class HysteriaSettings4Ray
|
|||||||
public string? auth { get; set; }
|
public string? auth { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class MasqueSettings4Ray
|
||||||
|
{
|
||||||
|
public string? path { get; set; }
|
||||||
|
public Dictionary<string, string>? headers { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class UdpHop4Ray
|
public class UdpHop4Ray
|
||||||
{
|
{
|
||||||
public string? ports { get; set; }
|
public string? ports { get; set; }
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ namespace ServiceLib.Models.Dto;
|
|||||||
public partial class CheckUpdateModel : ReactiveObject
|
public partial class CheckUpdateModel : ReactiveObject
|
||||||
{
|
{
|
||||||
public bool? IsSelected { get; set; }
|
public bool? IsSelected { get; set; }
|
||||||
|
public bool IsCheckPreRelease { get; set; }
|
||||||
|
public bool ShowCheckPreRelease { get; set; }
|
||||||
public ECoreType? CoreType { get; set; }
|
public ECoreType? CoreType { get; set; }
|
||||||
[Reactive] public partial string? Remarks { get; set; }
|
[Reactive] public partial string? Remarks { get; set; }
|
||||||
public string? FileName { get; set; }
|
public string? FileName { get; set; }
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ namespace ServiceLib.Models.Dto;
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public partial class ClashProxyModel : ReactiveObject
|
public partial class ClashProxyModel : ReactiveObject
|
||||||
{
|
{
|
||||||
public required string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public required string Type { get; set; }
|
public string Type { get; set; }
|
||||||
|
|
||||||
public string? Now { get; set; }
|
public string? Now { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ public record ProtocolExtraItem
|
|||||||
public int? InsecureConcurrency { get; init; }
|
public int? InsecureConcurrency { get; init; }
|
||||||
public bool? NaiveQuic { get; init; }
|
public bool? NaiveQuic { get; init; }
|
||||||
|
|
||||||
|
// MASQUE
|
||||||
|
public string? MasquePath { get; init; }
|
||||||
|
public string? MasqueHeaders { get; init; }
|
||||||
|
|
||||||
// group profile
|
// group profile
|
||||||
public string? GroupType { get; init; }
|
public string? GroupType { get; init; }
|
||||||
public string? ChildItems { get; init; }
|
public string? ChildItems { get; init; }
|
||||||
|
|||||||
Generated
+19
-1
@@ -789,6 +789,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Add [MASQUE] 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string menuAddMasqueServer {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("menuAddMasqueServer", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Add [NaïveProxy] 的本地化字符串。
|
/// 查找类似 Add [NaïveProxy] 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -924,6 +933,15 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 Check and Update 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
public static string menuCheckAndUpdate {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("menuCheckAndUpdate", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Only Check 的本地化字符串。
|
/// 查找类似 Only Check 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2149,7 +2167,7 @@ namespace ServiceLib.Resx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 Not Support 的本地化字符串。
|
/// 查找类似 Not Supported 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string MsgNotSupport {
|
public static string MsgNotSupport {
|
||||||
get {
|
get {
|
||||||
|
|||||||
@@ -399,6 +399,9 @@
|
|||||||
<data name="MsgServerTitle" xml:space="preserve">
|
<data name="MsgServerTitle" xml:space="preserve">
|
||||||
<value>Filter, press Enter to execute</value>
|
<value>Filter, press Enter to execute</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuCheckAndUpdate" xml:space="preserve">
|
||||||
|
<value>Check and Update</value>
|
||||||
|
</data>
|
||||||
<data name="menuCheckUpdate" xml:space="preserve">
|
<data name="menuCheckUpdate" xml:space="preserve">
|
||||||
<value>Check Update</value>
|
<value>Check Update</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1783,7 +1786,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||||||
<value>Only Check</value>
|
<value>Only Check</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>Not Support</value>
|
<value>Not Supported</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LvTestIpInfo" xml:space="preserve">
|
<data name="LvTestIpInfo" xml:space="preserve">
|
||||||
<value>IP Info</value>
|
<value>IP Info</value>
|
||||||
@@ -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">
|
<data name="TbSortingProcess" xml:space="preserve">
|
||||||
<value>Process</value>
|
<value>Process</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<data name="menuAddMasqueServer" xml:space="preserve">
|
||||||
|
<value>Add [MASQUE]</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -399,8 +399,11 @@
|
|||||||
<data name="MsgServerTitle" xml:space="preserve">
|
<data name="MsgServerTitle" xml:space="preserve">
|
||||||
<value>Фильтр, нажмите Enter для выполнения</value>
|
<value>Фильтр, нажмите Enter для выполнения</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuCheckAndUpdate" xml:space="preserve">
|
||||||
|
<value>Проверить и обновить</value>
|
||||||
|
</data>
|
||||||
<data name="menuCheckUpdate" xml:space="preserve">
|
<data name="menuCheckUpdate" xml:space="preserve">
|
||||||
<value>Обновить</value>
|
<value>Проверить обновления</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuClose" xml:space="preserve">
|
<data name="menuClose" xml:space="preserve">
|
||||||
<value>Закрыть</value>
|
<value>Закрыть</value>
|
||||||
@@ -693,6 +696,9 @@
|
|||||||
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
|
<data name="TbSettingsEnableCheckPreReleaseUpdate" xml:space="preserve">
|
||||||
<value>Проверить наличие предварительных обновлений</value>
|
<value>Проверить наличие предварительных обновлений</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsEnableUpdateViaProxy" xml:space="preserve">
|
||||||
|
<value>Обновлять через прокси</value>
|
||||||
|
</data>
|
||||||
<data name="TbSettingsException" xml:space="preserve">
|
<data name="TbSettingsException" xml:space="preserve">
|
||||||
<value>Исключение</value>
|
<value>Исключение</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1020,6 +1026,15 @@
|
|||||||
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
|
<data name="TbSettingsMux4SboxProtocol" xml:space="preserve">
|
||||||
<value>Протокол Mux для sing-box</value>
|
<value>Протокол Mux для sing-box</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbSettingsMux4RayConcurrency" xml:space="preserve">
|
||||||
|
<value>Лимит дочерних соединений Mux для Xray</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsMux4RayXudpConcurrency" xml:space="preserve">
|
||||||
|
<value>Лимит дочерних XUDP-соединений Mux для Xray</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsMux4RayXudpProxyUDP443" xml:space="preserve">
|
||||||
|
<value>Обработка UDP443 (QUIC) в Mux для Xray</value>
|
||||||
|
</data>
|
||||||
<data name="TbRoutingRuleProcess" xml:space="preserve">
|
<data name="TbRoutingRuleProcess" xml:space="preserve">
|
||||||
<value>Процесс (Linux/Windows)</value>
|
<value>Процесс (Linux/Windows)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1768,7 +1783,7 @@
|
|||||||
<value>Доступна новая версия {0}: {1}</value>
|
<value>Доступна новая версия {0}: {1}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="menuCheckOnly" xml:space="preserve">
|
<data name="menuCheckOnly" xml:space="preserve">
|
||||||
<value>Проверить</value>
|
<value>Только проверить</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MsgNotSupport" xml:space="preserve">
|
<data name="MsgNotSupport" xml:space="preserve">
|
||||||
<value>Не поддерживается</value>
|
<value>Не поддерживается</value>
|
||||||
@@ -1869,4 +1884,25 @@
|
|||||||
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
|
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
|
||||||
<value>При включении блокирует DNS-запросы IPv6</value>
|
<value>При включении блокирует DNS-запросы IPv6</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TbDNS" xml:space="preserve">
|
||||||
|
<value>DNS</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSettingsMux4Ray" xml:space="preserve">
|
||||||
|
<value>Настройки Mux для Xray</value>
|
||||||
|
</data>
|
||||||
|
<data name="LvRequestHeaders" xml:space="preserve">
|
||||||
|
<value>HTTP-заголовки (JSON)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubRequestHeadersTips" xml:space="preserve">
|
||||||
|
<value>Необязательно. Пример: {"X-hwid": "my_device"}. Заданные значения переопределяют заголовки по умолчанию для всех URL этой группы, включая сервис конвертации подписок</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubRequestHeadersInvalid" xml:space="preserve">
|
||||||
|
<value>Некорректные HTTP-заголовки. Используйте JSON-объект с уникальными именами заголовков и строковыми значениями. Имена и значения заголовков не должны содержать переносов строк</value>
|
||||||
|
</data>
|
||||||
|
<data name="TbSortingProcess" xml:space="preserve">
|
||||||
|
<value>Процесс</value>
|
||||||
|
</data>
|
||||||
|
<data name="menuAddMasqueServer" xml:space="preserve">
|
||||||
|
<value>Добавить сервер [MASQUE]</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -399,6 +399,9 @@
|
|||||||
<data name="MsgServerTitle" xml:space="preserve">
|
<data name="MsgServerTitle" xml:space="preserve">
|
||||||
<value>过滤器,按回车执行</value>
|
<value>过滤器,按回车执行</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuCheckAndUpdate" xml:space="preserve">
|
||||||
|
<value>检查并更新</value>
|
||||||
|
</data>
|
||||||
<data name="menuCheckUpdate" xml:space="preserve">
|
<data name="menuCheckUpdate" xml:space="preserve">
|
||||||
<value>检查更新</value>
|
<value>检查更新</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1891,4 +1894,7 @@
|
|||||||
<data name="TbSortingProcess" xml:space="preserve">
|
<data name="TbSortingProcess" xml:space="preserve">
|
||||||
<value>进程</value>
|
<value>进程</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<data name="menuAddMasqueServer" xml:space="preserve">
|
||||||
|
<value>添加 [MASQUE]</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -399,6 +399,9 @@
|
|||||||
<data name="MsgServerTitle" xml:space="preserve">
|
<data name="MsgServerTitle" xml:space="preserve">
|
||||||
<value>過濾器,按 Enter 執行</value>
|
<value>過濾器,按 Enter 執行</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="menuCheckAndUpdate" xml:space="preserve">
|
||||||
|
<value>檢查並更新</value>
|
||||||
|
</data>
|
||||||
<data name="menuCheckUpdate" xml:space="preserve">
|
<data name="menuCheckUpdate" xml:space="preserve">
|
||||||
<value>檢查更新</value>
|
<value>檢查更新</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -3,13 +3,7 @@
|
|||||||
"protocol": "vmess",
|
"protocol": "vmess",
|
||||||
"settings": {
|
"settings": {
|
||||||
"address": "v2ray.cool",
|
"address": "v2ray.cool",
|
||||||
"port": 10086,
|
"port": 10086
|
||||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
|
||||||
"security": "auto",
|
|
||||||
"method": "chacha20",
|
|
||||||
"ota": false,
|
|
||||||
"password": "123456",
|
|
||||||
"level": 1
|
|
||||||
},
|
},
|
||||||
"streamSettings": {
|
"streamSettings": {
|
||||||
"network": "tcp"
|
"network": "tcp"
|
||||||
|
|||||||
@@ -300,40 +300,6 @@ AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
|
|||||||
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
|
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe
|
|
||||||
MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0
|
|
||||||
ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe
|
|
||||||
Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw
|
|
||||||
IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL
|
|
||||||
SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF
|
|
||||||
AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH
|
|
||||||
SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh
|
|
||||||
ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X
|
|
||||||
DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1
|
|
||||||
TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ
|
|
||||||
fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA
|
|
||||||
sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU
|
|
||||||
WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS
|
|
||||||
nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH
|
|
||||||
dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip
|
|
||||||
NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC
|
|
||||||
AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF
|
|
||||||
MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH
|
|
||||||
ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB
|
|
||||||
uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl
|
|
||||||
PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP
|
|
||||||
JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/
|
|
||||||
gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2
|
|
||||||
j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6
|
|
||||||
5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB
|
|
||||||
o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS
|
|
||||||
/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z
|
|
||||||
Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE
|
|
||||||
W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D
|
|
||||||
hNQ+IIX3Sj0rnP0qCglN6oH4EZw=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF
|
MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF
|
||||||
MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD
|
MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD
|
||||||
@@ -732,20 +698,6 @@ LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
|
|||||||
4uJEvlz36hz1
|
4uJEvlz36hz1
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD
|
|
||||||
VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
|
|
||||||
A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
|
|
||||||
WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
|
|
||||||
IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
|
|
||||||
AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G
|
|
||||||
jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2
|
|
||||||
4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
|
|
||||||
BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7
|
|
||||||
VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm
|
|
||||||
ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
|
MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
|
||||||
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
|
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
|
||||||
@@ -805,38 +757,6 @@ HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/
|
|||||||
bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm
|
bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw
|
|
||||||
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
|
|
||||||
MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
|
|
||||||
MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
|
|
||||||
Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA
|
|
||||||
A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt
|
|
||||||
nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY
|
|
||||||
6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu
|
|
||||||
MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k
|
|
||||||
RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg
|
|
||||||
f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV
|
|
||||||
+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo
|
|
||||||
dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW
|
|
||||||
Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa
|
|
||||||
G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq
|
|
||||||
gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID
|
|
||||||
AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
|
|
||||||
FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H
|
|
||||||
vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8
|
|
||||||
0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC
|
|
||||||
B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u
|
|
||||||
NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg
|
|
||||||
yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev
|
|
||||||
HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6
|
|
||||||
xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR
|
|
||||||
TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg
|
|
||||||
JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV
|
|
||||||
7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl
|
|
||||||
6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
|
MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
|
||||||
BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
|
BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
|
||||||
@@ -1189,31 +1109,6 @@ AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB
|
|||||||
RA+GsCyRxj3qrg+E
|
RA+GsCyRxj3qrg+E
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG
|
|
||||||
EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3
|
|
||||||
MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl
|
|
||||||
cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR
|
|
||||||
dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB
|
|
||||||
pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM
|
|
||||||
b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm
|
|
||||||
aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz
|
|
||||||
IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
|
||||||
MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT
|
|
||||||
lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz
|
|
||||||
AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5
|
|
||||||
VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG
|
|
||||||
ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2
|
|
||||||
BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG
|
|
||||||
AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M
|
|
||||||
U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh
|
|
||||||
bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C
|
|
||||||
+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
|
|
||||||
bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F
|
|
||||||
uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2
|
|
||||||
XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt
|
MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt
|
||||||
MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg
|
MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg
|
||||||
|
|||||||
@@ -365,9 +365,16 @@ public partial class CoreConfigSingboxService
|
|||||||
rule4ExpectedIPs = JsonUtils.DeepCopy(rule);
|
rule4ExpectedIPs = JsonUtils.DeepCopy(rule);
|
||||||
rule4ExpectedIPs.geosite = regionGeosite;
|
rule4ExpectedIPs.geosite = regionGeosite;
|
||||||
}
|
}
|
||||||
|
if (rule.geosite?.Count > 0
|
||||||
|
|| rule.domain?.Count > 0
|
||||||
|
|| rule.domain_keyword?.Count > 0
|
||||||
|
|| rule.domain_regex?.Count > 0
|
||||||
|
|| rule.domain_suffix?.Count > 0)
|
||||||
|
{
|
||||||
|
AddRules(rule, item, directDnsList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (rule.geosite?.Count > 0 || rule.domain?.Count > 0)
|
|
||||||
{
|
{
|
||||||
AddRules(rule, item, directDnsList);
|
AddRules(rule, item, directDnsList);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public partial class CoreConfigSingboxService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
|
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.mtu = _config.TunModeItem.Mtu;
|
||||||
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
|
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
|
||||||
tunInbound.strict_route = _config.TunModeItem.StrictRoute;
|
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);
|
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
|
||||||
FillEndpoint(endpoint);
|
FillEndpoint(endpoint);
|
||||||
@@ -350,13 +350,15 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
var protocolExtra = _node.GetProtocolExtra();
|
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];
|
endpoint.type = Global.ProtocolTypes[_node.ConfigType];
|
||||||
|
|
||||||
switch (_node.ConfigType)
|
switch (_node.ConfigType)
|
||||||
{
|
{
|
||||||
case EConfigType.WireGuard:
|
case EConfigType.WireGuard:
|
||||||
{
|
{
|
||||||
|
endpoint.server = null;
|
||||||
|
endpoint.server_port = null;
|
||||||
|
endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"];
|
||||||
var peer = new Peer4Sbox
|
var peer = new Peer4Sbox
|
||||||
{
|
{
|
||||||
public_key = protocolExtra.WgPublicKey ?? string.Empty,
|
public_key = protocolExtra.WgPublicKey ?? string.Empty,
|
||||||
@@ -371,6 +373,28 @@ public partial class CoreConfigSingboxService
|
|||||||
endpoint.peers = [peer];
|
endpoint.peers = [peer];
|
||||||
break;
|
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)
|
catch (Exception ex)
|
||||||
@@ -402,7 +426,7 @@ public partial class CoreConfigSingboxService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FillOutboundTls(Outbound4Sbox outbound)
|
private void FillOutboundTls(BaseServer4Sbox server)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -476,7 +500,7 @@ public partial class CoreConfigSingboxService
|
|||||||
{
|
{
|
||||||
tls.ech = ech;
|
tls.ech = ech;
|
||||||
}
|
}
|
||||||
outbound.tls = tls;
|
server.tls = tls;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -67,12 +67,10 @@ public partial class CoreConfigV2rayService
|
|||||||
|
|
||||||
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
|
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
|
||||||
tunInbound.settings.gateway = [address];
|
tunInbound.settings.gateway = [address];
|
||||||
// Route both families into the tunnel regardless of EnableIPv6Address. That option only
|
// Without ::/0, IPv6 bypasses the tunnel. Skip it only when the host has no global IPv6
|
||||||
// controls whether the interface gets an IPv6 address; leaving ::/0 out of the routing
|
// address and EnableIPv6Address is off; NAT66 hosts have only ULA addresses.
|
||||||
// table makes IPv6 follow the system default route and bypass the tunnel entirely.
|
var routeIPv6 = _config.TunModeItem.EnableIPv6Address || context.HasGlobalIPv6Address;
|
||||||
// A host without a global IPv6 address is the exception: it has nothing to leak,
|
tunInbound.settings.autoSystemRoutingTable = routeIPv6
|
||||||
// and IPv6 sent into the tunnel would have no way back out.
|
|
||||||
tunInbound.settings.autoSystemRoutingTable = context.HasGlobalIPv6Address
|
|
||||||
? ["0.0.0.0/0", "::/0"]
|
? ["0.0.0.0/0", "::/0"]
|
||||||
: ["0.0.0.0/0"];
|
: ["0.0.0.0/0"];
|
||||||
if (_config.TunModeItem.EnableIPv6Address == true)
|
if (_config.TunModeItem.EnableIPv6Address == true)
|
||||||
@@ -90,6 +88,15 @@ public partial class CoreConfigV2rayService
|
|||||||
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
|
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
|
||||||
tunInbound.sniffing.routeOnly = true;
|
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 })
|
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
|
||||||
{
|
{
|
||||||
var wholeInternet = IPNetwork2.Parse("0.0.0.0/0");
|
var wholeInternet = IPNetwork2.Parse("0.0.0.0/0");
|
||||||
@@ -99,7 +106,7 @@ public partial class CoreConfigV2rayService
|
|||||||
.Where(x => x != null).ToList();
|
.Where(x => x != null).ToList();
|
||||||
|
|
||||||
var includeList = new List<IPNetwork2> { wholeInternet };
|
var includeList = new List<IPNetwork2> { wholeInternet };
|
||||||
var includeListV6 = context.HasGlobalIPv6Address
|
var includeListV6 = routeIPv6
|
||||||
? new List<IPNetwork2> { wholeInternetV6 }
|
? new List<IPNetwork2> { wholeInternetV6 }
|
||||||
: new List<IPNetwork2>();
|
: new List<IPNetwork2>();
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,12 @@ public partial class CoreConfigV2rayService
|
|||||||
outboundSettings.version = 2;
|
outboundSettings.version = 2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case EConfigType.MASQUE:
|
||||||
|
{
|
||||||
|
outboundSettings.address = _node.Address;
|
||||||
|
outboundSettings.port = _node.Port;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case EConfigType.WireGuard:
|
case EConfigType.WireGuard:
|
||||||
{
|
{
|
||||||
var address = _node.Address;
|
var address = _node.Address;
|
||||||
@@ -257,8 +263,13 @@ public partial class CoreConfigV2rayService
|
|||||||
{
|
{
|
||||||
network = "hysteria";
|
network = "hysteria";
|
||||||
}
|
}
|
||||||
|
else if (_node.ConfigType == EConfigType.MASQUE)
|
||||||
|
{
|
||||||
|
network = "masque";
|
||||||
|
}
|
||||||
streamSettings.network = network;
|
streamSettings.network = network;
|
||||||
var transport = _node.GetTransportExtra();
|
var transport = _node.GetTransportExtra();
|
||||||
|
var protocolExtra = _node.GetProtocolExtra();
|
||||||
var host = string.Empty;
|
var host = string.Empty;
|
||||||
var path = string.Empty;
|
var path = string.Empty;
|
||||||
var kcpSeed = string.Empty;
|
var kcpSeed = string.Empty;
|
||||||
@@ -502,7 +513,6 @@ public partial class CoreConfigV2rayService
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "hysteria":
|
case "hysteria":
|
||||||
var protocolExtra = _node.GetProtocolExtra();
|
|
||||||
var ports = protocolExtra?.Ports;
|
var ports = protocolExtra?.Ports;
|
||||||
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
|
int? upMbps = protocolExtra?.UpMbps is { } su and >= 0
|
||||||
? su
|
? su
|
||||||
@@ -574,6 +584,32 @@ public partial class CoreConfigV2rayService
|
|||||||
streamSettings.finalmask = hy2Finalmask;
|
streamSettings.finalmask = hy2Finalmask;
|
||||||
break;
|
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:
|
default:
|
||||||
// raw
|
// raw
|
||||||
if (headerType == Global.RawHeaderHttp)
|
if (headerType == Global.RawHeaderHttp)
|
||||||
|
|||||||
@@ -20,24 +20,31 @@ public class DownloadService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads data with the specified proxy and reports progress messages.
|
/// Downloads data with the specified proxy and reports progress messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
|
public async Task<int> DownloadDataAsync(string url, IWebProxy webProxy, Func<bool, string, Task> updateFunc, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var progress = new Progress<string>();
|
|
||||||
progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}");
|
|
||||||
|
|
||||||
await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy,
|
await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy,
|
||||||
url,
|
url,
|
||||||
progress,
|
OnProgress,
|
||||||
downloadTimeout);
|
cancellationToken);
|
||||||
|
|
||||||
|
void OnProgress(string message)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
updateFunc.Invoke(false, $"{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await updateFunc?.Invoke(false, ex.Message);
|
await updateFunc.Invoke(false, ex.Message);
|
||||||
if (ex.InnerException != null)
|
if (ex.InnerException != null)
|
||||||
{
|
{
|
||||||
await updateFunc?.Invoke(false, ex.InnerException.Message);
|
await updateFunc.Invoke(false, ex.InnerException.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -46,23 +53,28 @@ public class DownloadService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads a file and reports progress through events.
|
/// Downloads a file and reports progress through events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, TimeSpan connectTimeout)
|
public async Task DownloadFileAsync(FileDownloadRequest request, bool blProxy, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}"));
|
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} {request.FileUrl}"));
|
||||||
|
|
||||||
var webProxy = await GetWebProxy(blProxy);
|
var webProxy = await GetWebProxy(blProxy, cancellationToken);
|
||||||
await DownloaderHelper.Instance.DownloadFileAsync(webProxy,
|
await DownloaderHelper.Instance.DownloadFileAsync(webProxy,
|
||||||
request,
|
request,
|
||||||
OnProgress,
|
OnProgress,
|
||||||
connectTimeout);
|
cancellationToken);
|
||||||
|
|
||||||
void OnProgress(FileDownloadState state)
|
void OnProgress(FileDownloadState state)
|
||||||
{
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
UpdateCompleted?.Invoke(this, new UpdateResult(state.Completed, $"{Utils.HumanFy((long)state.SpeedBytesPerSecond / 1024)}/s | {Utils.HumanFy(state.DownloadedBytes / 1024)}/{Utils.HumanFy(state.TotalBytes / 1024)}"));
|
UpdateCompleted?.Invoke(this, new UpdateResult(state.Completed, $"{Utils.HumanFy((long)state.SpeedBytesPerSecond / 1024)}/s | {Utils.HumanFy(state.DownloadedBytes / 1024)}/{Utils.HumanFy(state.TotalBytes / 1024)}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logging.SaveLog(_tag, ex);
|
Logging.SaveLog(_tag, ex);
|
||||||
@@ -75,20 +87,21 @@ public class DownloadService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, TimeSpan connectTimeout)
|
public async Task DownloadSmallFilesAsync(List<FileDownloadRequest> requests, bool blProxy, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}"));
|
UpdateCompleted?.Invoke(this, new UpdateResult(false, $"{ResUI.Downloading} 0/{requests.Count}"));
|
||||||
|
|
||||||
var webProxy = await GetWebProxy(blProxy);
|
var webProxy = await GetWebProxy(blProxy, cancellationToken);
|
||||||
await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy,
|
await DownloaderHelper.Instance.DownloadSmallFilesAsync(webProxy,
|
||||||
requests,
|
requests,
|
||||||
OnProgress,
|
OnProgress,
|
||||||
connectTimeout);
|
cancellationToken);
|
||||||
|
|
||||||
void OnProgress(ReadOnlyMemory<FileDownloadState> states)
|
void OnProgress(ReadOnlyMemory<FileDownloadState> states)
|
||||||
{
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
var span = states.Span;
|
var span = states.Span;
|
||||||
var completedCount = 0;
|
var completedCount = 0;
|
||||||
var downloadingStates = new List<FileDownloadState>();
|
var downloadingStates = new List<FileDownloadState>();
|
||||||
@@ -129,6 +142,10 @@ public class DownloadService
|
|||||||
UpdateCompleted?.Invoke(this, new UpdateResult(allCompleted, $"{completedCount}/{span.Length} | {Utils.HumanFy((long)totalSpeed / 1024)}/s {Utils.HumanFy(totalDownloadedBytes / 1024)}/{Utils.HumanFy(totalTotalBytes / 1024)} {downloadingFileName}"));
|
UpdateCompleted?.Invoke(this, new UpdateResult(allCompleted, $"{completedCount}/{span.Length} | {Utils.HumanFy((long)totalSpeed / 1024)}/s {Utils.HumanFy(totalDownloadedBytes / 1024)}/{Utils.HumanFy(totalTotalBytes / 1024)} {downloadingFileName}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logging.SaveLog(_tag, ex);
|
Logging.SaveLog(_tag, ex);
|
||||||
@@ -144,12 +161,12 @@ public class DownloadService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets redirect target URL without following redirects automatically.
|
/// Gets redirect target URL without following redirects automatically.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<string?> UrlRedirectAsync(string url, bool blProxy)
|
public async Task<string?> UrlRedirectAsync(string url, bool blProxy, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var webRequestHandler = new SocketsHttpHandler
|
var webRequestHandler = new SocketsHttpHandler
|
||||||
{
|
{
|
||||||
AllowAutoRedirect = false,
|
AllowAutoRedirect = false,
|
||||||
Proxy = await GetWebProxy(blProxy)
|
Proxy = await GetWebProxy(blProxy, cancellationToken)
|
||||||
};
|
};
|
||||||
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
||||||
if (certificateChainPolicy != null)
|
if (certificateChainPolicy != null)
|
||||||
@@ -159,7 +176,7 @@ public class DownloadService
|
|||||||
}
|
}
|
||||||
using var client = new HttpClient(webRequestHandler);
|
using var client = new HttpClient(webRequestHandler);
|
||||||
|
|
||||||
var response = await client.GetAsync(url);
|
var response = await client.GetAsync(url, cancellationToken);
|
||||||
if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null)
|
if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null)
|
||||||
{
|
{
|
||||||
return response.Headers.Location.ToString();
|
return response.Headers.Location.ToString();
|
||||||
@@ -175,26 +192,29 @@ public class DownloadService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to download string content using proxy switch setting.
|
/// Tries to download string content using proxy switch setting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent)
|
public async Task<string?> TryDownloadString(string url, bool blProxy, string userAgent, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var webProxy = await GetWebProxy(blProxy);
|
var webProxy = await GetWebProxy(blProxy, cancellationToken);
|
||||||
return await TryDownloadString(url, webProxy, userAgent);
|
return await TryDownloadString(url, webProxy, userAgent, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to download string content with a specified proxy.
|
/// Tries to download string content with a specified proxy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent)
|
public async Task<string?> TryDownloadString(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var timeout = 15;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result1 = await DownloadStringAsync(url, webProxy, userAgent, timeout);
|
var result1 = await DownloadStringAsync(url, webProxy, userAgent, cancellationToken);
|
||||||
if (result1.IsNotEmpty())
|
if (result1.IsNotEmpty())
|
||||||
{
|
{
|
||||||
return result1;
|
return result1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logging.SaveLog(_tag, ex);
|
Logging.SaveLog(_tag, ex);
|
||||||
@@ -207,12 +227,16 @@ public class DownloadService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, timeout);
|
var result2 = await DownloadStringViaDownloader(url, webProxy, userAgent, cancellationToken);
|
||||||
if (result2.IsNotEmpty())
|
if (result2.IsNotEmpty())
|
||||||
{
|
{
|
||||||
return result2;
|
return result2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logging.SaveLog(_tag, ex);
|
Logging.SaveLog(_tag, ex);
|
||||||
@@ -229,17 +253,16 @@ public class DownloadService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads string content via HttpClient.
|
/// Downloads string content via HttpClient.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, int timeout)
|
private async Task<string?> DownloadStringAsync(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
|
|
||||||
var handler = new SocketsHttpHandler
|
var handler = new SocketsHttpHandler
|
||||||
{
|
{
|
||||||
Proxy = webProxy,
|
Proxy = webProxy,
|
||||||
UseProxy = webProxy != null,
|
UseProxy = webProxy != null,
|
||||||
AutomaticDecompression = DecompressionMethods.All,
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
ConnectTimeout = TimeSpan.FromSeconds(connectTimeout)
|
ConnectTimeout = webProxy is null ? Global.DirectDownloadConnect : Global.ProxyDownloadConnect,
|
||||||
};
|
};
|
||||||
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
|
||||||
if (certificateChainPolicy != null)
|
if (certificateChainPolicy != null)
|
||||||
@@ -248,10 +271,8 @@ public class DownloadService
|
|||||||
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
handler.SslOptions.RemoteCertificateValidationCallback = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders))
|
using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders));
|
||||||
{
|
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||||
Timeout = Timeout.InfiniteTimeSpan
|
|
||||||
};
|
|
||||||
|
|
||||||
if (userAgent.IsNullOrEmpty())
|
if (userAgent.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
@@ -270,10 +291,15 @@ public class DownloadService
|
|||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo));
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var cts = new CancellationTokenSource();
|
using var timeoutCts = new CancellationTokenSource();
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
|
timeoutCts.CancelAfter(webProxy is null ? Global.DirectFetch : Global.ProxyFetch);
|
||||||
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||||
|
|
||||||
return await client.GetStringAsync(url, cts.Token);
|
return await client.GetStringAsync(url, linkedCts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -291,7 +317,7 @@ public class DownloadService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads string content via DownloaderHelper.
|
/// Downloads string content via DownloaderHelper.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, int timeout)
|
private async Task<string?> DownloadStringViaDownloader(string url, IWebProxy? webProxy, string userAgent, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -299,9 +325,13 @@ public class DownloadService
|
|||||||
{
|
{
|
||||||
userAgent = Utils.GetVersion(false);
|
userAgent = Utils.GetVersion(false);
|
||||||
}
|
}
|
||||||
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout, RequestHeaders, AcceptHeader);
|
var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, RequestHeaders, AcceptHeader, cancellationToken);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logging.SaveLog(_tag, ex);
|
Logging.SaveLog(_tag, ex);
|
||||||
@@ -317,14 +347,14 @@ public class DownloadService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates local SOCKS proxy when proxy switch is enabled.
|
/// Creates local SOCKS proxy when proxy switch is enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<WebProxy?> GetWebProxy(bool blProxy)
|
private async Task<WebProxy?> GetWebProxy(bool blProxy, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!blProxy)
|
if (!blProxy)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||||
if (await SocketCheck(Global.Loopback, port) == false)
|
if (await SocksPortCheck(Global.Loopback, port, cancellationToken) == false)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -335,18 +365,64 @@ public class DownloadService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether the specified TCP endpoint is reachable.
|
/// Checks whether the specified TCP endpoint is reachable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<bool> SocketCheck(string ip, int port)
|
private async Task<bool> SocksPortCheck(string ip, int port, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
using var rootTimeOutCts = new CancellationTokenSource(Global.LocalFetch);
|
||||||
|
using var rootCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, rootTimeOutCts.Token);
|
||||||
|
var rootToken = rootCts.Token;
|
||||||
|
|
||||||
|
// SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth)
|
||||||
|
ReadOnlyMemory<byte> greeting = new byte[] { 0x05, 0x01, 0x00 };
|
||||||
|
var buf = new byte[2];
|
||||||
|
|
||||||
|
while (!rootToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
IPEndPoint point = new(IPAddress.Parse(ip), port);
|
using var tcp = new TcpClient();
|
||||||
using Socket? sock = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
|
||||||
await sock.ConnectAsync(point);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token);
|
||||||
return true;
|
var linkedToken = linkedCts.Token;
|
||||||
}
|
try
|
||||||
catch (Exception)
|
{
|
||||||
{
|
await tcp.ConnectAsync(ip, port, linkedToken);
|
||||||
return false;
|
var stream = tcp.GetStream();
|
||||||
|
|
||||||
|
await stream.WriteAsync(greeting, linkedToken);
|
||||||
|
|
||||||
|
var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken);
|
||||||
|
|
||||||
|
// Server selection: VER=5, METHOD=0x00 — proxy is fully ready
|
||||||
|
if (read == 2 && buf[0] == 0x05)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (!rootToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
|
||||||
|
{
|
||||||
|
// Connection refused, proxy not ready yet, wait 50ms before retrying
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(50, rootToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logging.SaveLog($"SocksPortCheck Timeout waiting for proxy port {port} to be ready.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore other exceptions and continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,63 +7,125 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
private static readonly string _tag = "SpeedtestService";
|
private static readonly string _tag = "SpeedtestService";
|
||||||
private readonly Config? _config = config;
|
private readonly Config? _config = config;
|
||||||
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
|
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
|
||||||
private static readonly ConcurrentBag<string> _lstExitLoop = [];
|
private readonly Lock _runLock = new();
|
||||||
|
private readonly List<CancellationTokenSource> _runCtsList = [];
|
||||||
private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize;
|
private readonly int _speedTestPageSize = config.SpeedTestItem.SpeedTestPageSize ?? Global.SpeedTestPageSize;
|
||||||
private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1);
|
private readonly TimeSpan _delayInterval = TimeSpan.FromSeconds(config.SpeedTestItem.SpeedTestDelayInterval ?? 1);
|
||||||
|
|
||||||
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds)
|
public Task RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
Task.Run(async () =>
|
CancellationTokenSource runCts;
|
||||||
|
|
||||||
|
lock (_runLock)
|
||||||
{
|
{
|
||||||
await RunAsync(actionType, selecteds);
|
runCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
await ProfileExManager.Instance.SaveTo();
|
|
||||||
await UpdateFunc("", ResUI.SpeedtestingCompleted);
|
_runCtsList.Add(runCts);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return RunLoopAsync(actionType, selecteds, runCts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExitLoop()
|
public void ExitLoop()
|
||||||
{
|
{
|
||||||
if (!_lstExitLoop.IsEmpty)
|
var counter = 0;
|
||||||
|
List<CancellationTokenSource> listToCancel;
|
||||||
|
|
||||||
|
lock (_runLock)
|
||||||
|
{
|
||||||
|
listToCancel = _runCtsList.ToList();
|
||||||
|
counter = listToCancel.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var cts in listToCancel)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counter > 0)
|
||||||
{
|
{
|
||||||
_ = UpdateFunc("", ResUI.SpeedtestingStop);
|
_ = UpdateFunc("", ResUI.SpeedtestingStop);
|
||||||
|
|
||||||
_lstExitLoop.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldStopTest(string exitLoopKey)
|
private async Task RunLoopAsync(ESpeedActionType actionType, List<ProfileItem> selecteds, CancellationTokenSource runCts)
|
||||||
{
|
{
|
||||||
return _lstExitLoop.All(p => p != exitLoopKey);
|
try
|
||||||
|
{
|
||||||
|
await RunAsync(actionType, selecteds, runCts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (runCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProfileExManager.Instance.SaveTo();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await UpdateFunc("", ResUI.SpeedtestingCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 lstSelected = await GetClearItem(actionType, selecteds);
|
||||||
|
var completedIds = new ConcurrentDictionary<string, byte>();
|
||||||
|
|
||||||
switch (actionType)
|
try
|
||||||
{
|
{
|
||||||
case ESpeedActionType.Tcping:
|
switch (actionType)
|
||||||
await RunTcpingAsync(lstSelected, exitLoopKey);
|
{
|
||||||
break;
|
case ESpeedActionType.Tcping:
|
||||||
|
await RunTcpingAsync(lstSelected, completedIds, ct);
|
||||||
|
break;
|
||||||
|
|
||||||
case ESpeedActionType.Realping:
|
case ESpeedActionType.Realping:
|
||||||
await RunRealPingBatchAsync(lstSelected, exitLoopKey);
|
await RunRealPingBatchAsync(lstSelected, completedIds, 0, ct);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ESpeedActionType.UdpTest:
|
case ESpeedActionType.UdpTest:
|
||||||
await RunUdpTestBatchAsync(lstSelected, exitLoopKey);
|
await RunUdpTestBatchAsync(lstSelected, completedIds, 0, ct);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ESpeedActionType.Speedtest:
|
case ESpeedActionType.Speedtest:
|
||||||
await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey);
|
await RunMixedTestAsync(lstSelected, completedIds, 1, true, ct);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ESpeedActionType.Mixedtest:
|
case ESpeedActionType.Mixedtest:
|
||||||
await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, true, exitLoopKey);
|
await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, true,
|
||||||
break;
|
ct);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_ = UpdateFunc("", ResUI.SpeedtestingStop);
|
||||||
|
await SetTestResultAsync(lstSelected.Where(it => !completedIds.ContainsKey(it.IndexId)).ToList(),
|
||||||
|
actionType, ResUI.SpeedtestingSkip).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog(_tag, ex);
|
||||||
|
_ = UpdateFunc("", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,29 +165,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
}
|
}
|
||||||
|
|
||||||
//clear test result
|
//clear test result
|
||||||
foreach (var it in lstSelected)
|
await SetTestResultAsync(lstSelected, actionType, ResUI.Speedtesting).ConfigureAwait(false);
|
||||||
{
|
|
||||||
switch (actionType)
|
|
||||||
{
|
|
||||||
case ESpeedActionType.Tcping:
|
|
||||||
case ESpeedActionType.Realping:
|
|
||||||
case ESpeedActionType.UdpTest:
|
|
||||||
await UpdateFunc(it.IndexId, ResUI.Speedtesting, "");
|
|
||||||
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESpeedActionType.Speedtest:
|
|
||||||
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait);
|
|
||||||
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESpeedActionType.Mixedtest:
|
|
||||||
await UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait);
|
|
||||||
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
|
|
||||||
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lstSelected.Count > 1 && (actionType == ESpeedActionType.Speedtest || actionType == ESpeedActionType.Mixedtest))
|
if (lstSelected.Count > 1 && (actionType == ESpeedActionType.Speedtest || actionType == ESpeedActionType.Mixedtest))
|
||||||
{
|
{
|
||||||
@@ -135,56 +175,71 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
return lstSelected;
|
return lstSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunTcpingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
|
private async Task SetTestResultAsync(List<ServerTestItem> lstSelected, ESpeedActionType actionType, string message)
|
||||||
|
{
|
||||||
|
foreach (var it in lstSelected)
|
||||||
|
{
|
||||||
|
switch (actionType)
|
||||||
|
{
|
||||||
|
case ESpeedActionType.Tcping:
|
||||||
|
case ESpeedActionType.Realping:
|
||||||
|
case ESpeedActionType.UdpTest:
|
||||||
|
await UpdateFunc(it.IndexId, message, "");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ESpeedActionType.Speedtest:
|
||||||
|
await UpdateFunc(it.IndexId, "", message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ESpeedActionType.Mixedtest:
|
||||||
|
await UpdateFunc(it.IndexId, message, message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunTcpingAsync(List<ServerTestItem> selecteds,
|
||||||
|
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
|
var pageSize = Math.Min(selecteds.Count, _speedTestPageSize);
|
||||||
var lstBatch = GetTestBatchItem(selecteds, pageSize);
|
var lstBatch = GetTestBatchItem(selecteds, pageSize);
|
||||||
|
|
||||||
foreach (var lst in lstBatch)
|
foreach (var lst in lstBatch)
|
||||||
{
|
{
|
||||||
if (ShouldStopTest(exitLoopKey))
|
ct.ThrowIfCancellationRequested();
|
||||||
{
|
|
||||||
await UpdateFunc("", ResUI.SpeedtestingSkip);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Task> tasks = [];
|
var parallelOptions = new ParallelOptions
|
||||||
|
|
||||||
foreach (var it in lst)
|
|
||||||
{
|
{
|
||||||
if (ShouldStopTest(exitLoopKey))
|
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);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
tasks.Add(Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
try
|
throw;
|
||||||
{
|
}
|
||||||
var responseTime = await GetTcpingTime(it.Address, it.Port);
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog(_tag, ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
|
await Task.Delay(_delayInterval, ct);
|
||||||
await UpdateFunc(it.IndexId, responseTime.ToString());
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logging.SaveLog(_tag, ex);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
|
|
||||||
if (ShouldStopTest(exitLoopKey))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(_delayInterval);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
|
private async Task RunRealPingBatchAsync(List<ServerTestItem> lstSelected,
|
||||||
|
ConcurrentDictionary<string, byte> completedIds, int pageSize = 0, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (pageSize <= 0)
|
if (pageSize <= 0)
|
||||||
{
|
{
|
||||||
@@ -195,38 +250,35 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
List<ServerTestItem> lstFailed = [];
|
List<ServerTestItem> lstFailed = [];
|
||||||
foreach (var lst in lstTest)
|
foreach (var lst in lstTest)
|
||||||
{
|
{
|
||||||
var ret = await RunRealPingAsync(lst, exitLoopKey);
|
var ret = await RunRealPingAsync(lst, completedIds, ct);
|
||||||
if (ret == false)
|
if (ret == false)
|
||||||
{
|
{
|
||||||
lstFailed.AddRange(lst);
|
lstFailed.AddRange(lst);
|
||||||
}
|
}
|
||||||
await Task.Delay(_delayInterval);
|
await Task.Delay(_delayInterval, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Retest the failed part
|
//Retest the failed part
|
||||||
var pageSizeNext = pageSize / 2;
|
var pageSizeNext = pageSize / 2;
|
||||||
if (lstFailed.Count > 0 && pageSizeNext > 0)
|
if (lstFailed.Count > 0 && pageSizeNext > 0)
|
||||||
{
|
{
|
||||||
if (ShouldStopTest(exitLoopKey))
|
ct.ThrowIfCancellationRequested();
|
||||||
{
|
|
||||||
await UpdateFunc("", ResUI.SpeedtestingSkip);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
|
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
|
||||||
|
|
||||||
if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount)
|
if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount)
|
||||||
{
|
{
|
||||||
await RunRealPingBatchAsync(lstFailed, exitLoopKey, pageSizeNext);
|
await RunRealPingBatchAsync(lstFailed, completedIds, pageSizeNext, ct);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, false, exitLoopKey);
|
await RunMixedTestAsync(lstSelected, completedIds, _config.SpeedTestItem.MixedConcurrencyCount, false, ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
|
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds,
|
||||||
|
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ProcessService processService = null;
|
ProcessService processService = null;
|
||||||
try
|
try
|
||||||
@@ -236,28 +288,40 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000, ct);
|
||||||
|
|
||||||
List<Task> tasks = [];
|
var parallelOptions = new ParallelOptions
|
||||||
foreach (var it in selecteds)
|
{
|
||||||
|
MaxDegreeOfParallelism = selecteds.Count,
|
||||||
|
CancellationToken = ct,
|
||||||
|
};
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
|
||||||
{
|
{
|
||||||
if (!it.AllowTest)
|
if (!it.AllowTest)
|
||||||
{
|
{
|
||||||
await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
|
await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
|
||||||
continue;
|
completedIds.TryAdd(it.IndexId, 0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldStopTest(exitLoopKey))
|
try
|
||||||
{
|
{
|
||||||
return false;
|
await DoRealPing(it, completedIds, innerCt);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
tasks.Add(Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
await DoRealPing(it);
|
throw;
|
||||||
}));
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
await Task.WhenAll(tasks);
|
{
|
||||||
|
Logging.SaveLog(_tag, ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -273,7 +337,8 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunUdpTestBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
|
private async Task RunUdpTestBatchAsync(List<ServerTestItem> lstSelected,
|
||||||
|
ConcurrentDictionary<string, byte> completedIds, int pageSize = 0, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (pageSize <= 0)
|
if (pageSize <= 0)
|
||||||
{
|
{
|
||||||
@@ -284,30 +349,27 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
List<ServerTestItem> lstFailed = [];
|
List<ServerTestItem> lstFailed = [];
|
||||||
foreach (var lst in lstTest)
|
foreach (var lst in lstTest)
|
||||||
{
|
{
|
||||||
var ret = await RunUdpTestAsync(lst, exitLoopKey);
|
var ret = await RunUdpTestAsync(lst, completedIds, ct);
|
||||||
if (ret == false)
|
if (ret == false)
|
||||||
{
|
{
|
||||||
lstFailed.AddRange(lst);
|
lstFailed.AddRange(lst);
|
||||||
}
|
}
|
||||||
await Task.Delay(_delayInterval);
|
await Task.Delay(_delayInterval, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Retest the failed part
|
//Retest the failed part
|
||||||
if (lstFailed.Count > 0)
|
if (lstFailed.Count > 0)
|
||||||
{
|
{
|
||||||
if (ShouldStopTest(exitLoopKey))
|
ct.ThrowIfCancellationRequested();
|
||||||
{
|
|
||||||
await UpdateFunc("", ResUI.SpeedtestingSkip);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
|
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
|
||||||
|
|
||||||
await RunUdpTestAsync(lstFailed, exitLoopKey);
|
await RunUdpTestAsync(lstFailed, completedIds, ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> RunUdpTestAsync(List<ServerTestItem> selecteds, string exitLoopKey)
|
private async Task<bool> RunUdpTestAsync(List<ServerTestItem> selecteds,
|
||||||
|
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ProcessService processService = null;
|
ProcessService processService = null;
|
||||||
try
|
try
|
||||||
@@ -317,27 +379,40 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000, ct);
|
||||||
|
|
||||||
List<Task> tasks = [];
|
var parallelOptions = new ParallelOptions
|
||||||
foreach (var it in selecteds)
|
{
|
||||||
|
MaxDegreeOfParallelism = selecteds.Count,
|
||||||
|
CancellationToken = ct,
|
||||||
|
};
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
|
||||||
{
|
{
|
||||||
if (!it.AllowTest)
|
if (!it.AllowTest)
|
||||||
{
|
{
|
||||||
continue;
|
await UpdateFunc(it.IndexId, ResUI.SpeedtestingSkip);
|
||||||
|
completedIds.TryAdd(it.IndexId, 0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldStopTest(exitLoopKey))
|
try
|
||||||
{
|
{
|
||||||
return false;
|
await DoUdpTest(it, completedIds, innerCt);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
tasks.Add(Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
await DoUdpTest(it);
|
throw;
|
||||||
}));
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
await Task.WhenAll(tasks);
|
{
|
||||||
|
Logging.SaveLog(_tag, ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -353,81 +428,77 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunMixedTestAsync(List<ServerTestItem> selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey)
|
private async Task RunMixedTestAsync(List<ServerTestItem> selecteds,
|
||||||
|
ConcurrentDictionary<string, byte> completedIds, int concurrencyCount, bool blSpeedTest,
|
||||||
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
|
|
||||||
var downloadHandle = new DownloadService();
|
var downloadHandle = new DownloadService();
|
||||||
List<Task> tasks = [];
|
|
||||||
foreach (var it in selecteds)
|
var parallelOptions = new ParallelOptions
|
||||||
{
|
{
|
||||||
if (ShouldStopTest(exitLoopKey))
|
MaxDegreeOfParallelism = concurrencyCount,
|
||||||
|
CancellationToken = ct,
|
||||||
|
};
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(selecteds, parallelOptions, async (it, innerCt) =>
|
||||||
|
{
|
||||||
|
innerCt.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
ProcessService processService = null;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
|
processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
|
||||||
continue;
|
if (processService is null)
|
||||||
|
{
|
||||||
|
await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(1000, innerCt);
|
||||||
|
|
||||||
|
var delay = await DoRealPing(it, completedIds, innerCt);
|
||||||
|
if (blSpeedTest)
|
||||||
|
{
|
||||||
|
if (delay > 0)
|
||||||
|
{
|
||||||
|
await DoSpeedTest(downloadHandle, it, completedIds, innerCt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await concurrencySemaphore.WaitAsync();
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
|
|
||||||
tasks.Add(Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
ProcessService processService = null;
|
throw;
|
||||||
try
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog(_tag, ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (processService != null)
|
||||||
{
|
{
|
||||||
processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
|
await processService.StopAsync();
|
||||||
if (processService is null)
|
|
||||||
{
|
|
||||||
await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
var delay = await DoRealPing(it);
|
|
||||||
if (blSpeedTest)
|
|
||||||
{
|
|
||||||
if (ShouldStopTest(exitLoopKey))
|
|
||||||
{
|
|
||||||
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delay > 0)
|
|
||||||
{
|
|
||||||
await DoSpeedTest(downloadHandle, it);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
}
|
||||||
{
|
});
|
||||||
Logging.SaveLog(_tag, ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (processService != null)
|
|
||||||
{
|
|
||||||
await processService?.StopAsync();
|
|
||||||
}
|
|
||||||
concurrencySemaphore.Release();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> DoRealPing(ServerTestItem it)
|
private async Task<int> DoRealPing(ServerTestItem it,
|
||||||
|
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
|
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
|
||||||
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy);
|
var responseTime = await ConnectionHandler.GetRealPingTime(webProxy, ct);
|
||||||
|
|
||||||
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
|
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
|
||||||
await UpdateFunc(it.IndexId, responseTime.ToString());
|
await UpdateFunc(it.IndexId, responseTime.ToString());
|
||||||
|
|
||||||
if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
|
if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
|
||||||
{
|
{
|
||||||
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy);
|
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy, ct);
|
||||||
var ipStr = ipInfo?.ToString() ?? Global.None;
|
var ipStr = ipInfo?.ToString() ?? Global.None;
|
||||||
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
|
ProfileExManager.Instance.SetTestIpInfo(it.IndexId, ipStr);
|
||||||
await UpdateIpInfoFunc(it.IndexId, ipStr);
|
await UpdateIpInfoFunc(it.IndexId, ipStr);
|
||||||
@@ -437,17 +508,22 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
|
await UpdateIpInfoFunc(it.IndexId, ResUI.SpeedtestingSkip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
completedIds.TryAdd(it.IndexId, 0);
|
||||||
return responseTime;
|
return responseTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it)
|
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it,
|
||||||
|
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await UpdateFunc(it.IndexId, "", ResUI.Speedtesting);
|
await UpdateFunc(it.IndexId, "", ResUI.Speedtesting);
|
||||||
|
|
||||||
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
|
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
|
||||||
var url = _config.SpeedTestItem.SpeedTestUrl;
|
var url = _config.SpeedTestItem.SpeedTestUrl;
|
||||||
var timeout = _config.SpeedTestItem.SpeedTestTimeout;
|
var timeout = _config.SpeedTestItem.SpeedTestTimeout;
|
||||||
await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, msg) =>
|
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout));
|
||||||
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
|
||||||
|
var linkedCt = linkedCts.Token;
|
||||||
|
await downloadHandle.DownloadDataAsync(url, webProxy, async (success, msg) =>
|
||||||
{
|
{
|
||||||
decimal.TryParse(msg, out var dec);
|
decimal.TryParse(msg, out var dec);
|
||||||
if (dec > 0)
|
if (dec > 0)
|
||||||
@@ -455,34 +531,34 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec);
|
ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec);
|
||||||
}
|
}
|
||||||
await UpdateFunc(it.IndexId, "", msg);
|
await UpdateFunc(it.IndexId, "", msg);
|
||||||
});
|
}, linkedCt);
|
||||||
|
completedIds.TryAdd(it.IndexId, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> DoUdpTest(ServerTestItem it)
|
private async Task<int> DoUdpTest(ServerTestItem it,
|
||||||
|
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
|
var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
|
||||||
var responseTime = -1;
|
var responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, ct)).TotalMilliseconds;
|
||||||
try
|
|
||||||
{
|
|
||||||
responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, TimeSpan.FromSeconds(5))).TotalMilliseconds;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
|
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
|
||||||
await UpdateFunc(it.IndexId, responseTime.ToString());
|
await UpdateFunc(it.IndexId, responseTime.ToString());
|
||||||
|
completedIds.TryAdd(it.IndexId, 0);
|
||||||
return responseTime;
|
return responseTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> GetTcpingTime(string url, int port)
|
private async Task<int> GetTcpingTime(string? url, int port, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var responseTime = -1;
|
var responseTime = -1;
|
||||||
|
|
||||||
|
if (url.IsNullOrEmpty() || port <= 0)
|
||||||
|
{
|
||||||
|
return responseTime;
|
||||||
|
}
|
||||||
|
|
||||||
if (!IPAddress.TryParse(url, out var ipAddress))
|
if (!IPAddress.TryParse(url, out var ipAddress))
|
||||||
{
|
{
|
||||||
var ipHostInfo = await Dns.GetHostEntryAsync(url);
|
var ipHostInfo = await Dns.GetHostEntryAsync(url, ct);
|
||||||
ipAddress = ipHostInfo.AddressList.First();
|
ipAddress = ipHostInfo.AddressList.First();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,12 +568,14 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
var timer = Stopwatch.StartNew();
|
var timer = Stopwatch.StartNew();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
await clientSocket.ConnectAsync(endPoint, cts.Token).ConfigureAwait(false);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
|
||||||
|
await clientSocket.ConnectAsync(endPoint, linkedCts.Token).ConfigureAwait(false);
|
||||||
responseTime = (int)timer.ElapsedMilliseconds;
|
responseTime = (int)timer.ElapsedMilliseconds;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch
|
||||||
{
|
{
|
||||||
|
// Ignore
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -509,6 +587,12 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
|||||||
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
|
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
|
||||||
{
|
{
|
||||||
List<List<ServerTestItem>> lstTest = [];
|
List<List<ServerTestItem>> lstTest = [];
|
||||||
|
if (lstSelected.Count == 0)
|
||||||
|
{
|
||||||
|
return lstTest;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageSize = Math.Max(pageSize, 1);
|
||||||
var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList();
|
var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList();
|
||||||
var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();
|
var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ namespace ServiceLib.Services.Statistics;
|
|||||||
public class StatisticsSingboxService
|
public class StatisticsSingboxService
|
||||||
{
|
{
|
||||||
private readonly Config _config;
|
private readonly Config _config;
|
||||||
private bool _exitFlag;
|
private CancellationTokenSource? _cts;
|
||||||
private ClientWebSocket? webSocket;
|
|
||||||
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
|
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
|
||||||
private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic";
|
private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic";
|
||||||
private static readonly string _tag = "StatisticsSingboxService";
|
private static readonly string _tag = "StatisticsSingboxService";
|
||||||
@@ -15,40 +14,17 @@ public class StatisticsSingboxService
|
|||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_updateFunc = updateFunc;
|
_updateFunc = updateFunc;
|
||||||
_exitFlag = false;
|
|
||||||
|
|
||||||
_ = Task.Factory.StartNew(
|
Task.Run(Run);
|
||||||
Run,
|
|
||||||
CancellationToken.None,
|
|
||||||
TaskCreationOptions.LongRunning,
|
|
||||||
TaskScheduler.Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Init()
|
|
||||||
{
|
|
||||||
await Task.Delay(5000);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (webSocket == null)
|
|
||||||
{
|
|
||||||
webSocket = new ClientWebSocket();
|
|
||||||
await webSocket.ConnectAsync(new Uri(Url), CancellationToken.None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_exitFlag = true;
|
_cts?.Cancel();
|
||||||
if (webSocket != null)
|
_cts?.Dispose();
|
||||||
{
|
_cts = null;
|
||||||
webSocket.Abort();
|
|
||||||
webSocket = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -58,53 +34,63 @@ public class StatisticsSingboxService
|
|||||||
|
|
||||||
private async Task Run()
|
private async Task Run()
|
||||||
{
|
{
|
||||||
await Init();
|
Close();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
var token = _cts.Token;
|
||||||
|
|
||||||
while (!_exitFlag)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Delay(1000);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!AppManager.Instance.IsRunningCore(ECoreType.sing_box))
|
if (!AppManager.Instance.IsRunningCore(ECoreType.sing_box))
|
||||||
{
|
{
|
||||||
|
await Task.Delay(1000, token).ConfigureAwait(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (webSocket != null)
|
using var ws = new ClientWebSocket();
|
||||||
|
await ws.ConnectAsync(new Uri(Url), token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
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();
|
break;
|
||||||
webSocket = null;
|
}
|
||||||
await Init();
|
using var ms = new MemoryStream();
|
||||||
continue;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
ParseOutput(result, out var up, out var down);
|
||||||
|
|
||||||
var buffer = new byte[1024];
|
if (_updateFunc != null)
|
||||||
var res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
|
||||||
while (!res.CloseStatus.HasValue)
|
|
||||||
{
|
{
|
||||||
var result = Encoding.UTF8.GetString(buffer, 0, res.Count);
|
await _updateFunc.Invoke(new ServerSpeedItem
|
||||||
if (result.IsNotEmpty())
|
|
||||||
{
|
{
|
||||||
ParseOutput(result, out var up, out var down);
|
ProxyUp = (long)(up / 1000),
|
||||||
|
ProxyDown = (long)(down / 1000),
|
||||||
await _updateFunc?.Invoke(new ServerSpeedItem()
|
}).ConfigureAwait(false);
|
||||||
{
|
|
||||||
ProxyUp = (long)(up / 1000),
|
|
||||||
ProxyDown = (long)(down / 1000)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
await Task.Delay(3000, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,33 +5,42 @@ public class StatisticsXrayService
|
|||||||
private const long linkBase = 1024;
|
private const long linkBase = 1024;
|
||||||
private ServerSpeedItem _serverSpeedItem = new();
|
private ServerSpeedItem _serverSpeedItem = new();
|
||||||
private readonly Config _config;
|
private readonly Config _config;
|
||||||
private bool _exitFlag;
|
private CancellationTokenSource? _cts;
|
||||||
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
|
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
|
||||||
private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars";
|
private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars";
|
||||||
|
private static readonly string _tag = "StatisticsXrayService";
|
||||||
|
|
||||||
public StatisticsXrayService(Config config, Func<ServerSpeedItem, Task> updateFunc)
|
public StatisticsXrayService(Config config, Func<ServerSpeedItem, Task> updateFunc)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_updateFunc = updateFunc;
|
_updateFunc = updateFunc;
|
||||||
_exitFlag = false;
|
|
||||||
|
|
||||||
_ = Task.Factory.StartNew(
|
Task.Run(Run);
|
||||||
Run,
|
|
||||||
CancellationToken.None,
|
|
||||||
TaskCreationOptions.LongRunning,
|
|
||||||
TaskScheduler.Default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
{
|
{
|
||||||
_exitFlag = true;
|
try
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
_cts?.Dispose();
|
||||||
|
_cts = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog(_tag, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Run()
|
private async Task Run()
|
||||||
{
|
{
|
||||||
while (!_exitFlag)
|
Close();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
var token = _cts.Token;
|
||||||
|
|
||||||
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||||
|
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await Task.Delay(1000);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (AppManager.Instance.RunningCoreType != ECoreType.Xray)
|
if (AppManager.Instance.RunningCoreType != ECoreType.Xray)
|
||||||
@@ -39,16 +48,20 @@ public class StatisticsXrayService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await HttpClientHelper.Instance.TryGetAsync(Url);
|
var result = await HttpClientHelper.Instance.TryGetAsync(Url, token);
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
var server = ParseOutput(result) ?? new ServerSpeedItem();
|
var server = ParseOutput(result) ?? new ServerSpeedItem();
|
||||||
await _updateFunc?.Invoke(server);
|
await _updateFunc!.Invoke(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// ignored
|
await Task.Delay(3000, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
{
|
{
|
||||||
private readonly Config? _config = config;
|
private readonly Config? _config = config;
|
||||||
private readonly Func<bool, string, Task>? _updateFunc = updateFunc;
|
private readonly Func<bool, string, Task>? _updateFunc = updateFunc;
|
||||||
private readonly int _timeout = 30;
|
|
||||||
private static readonly string _tag = "UpdateService";
|
private static readonly string _tag = "UpdateService";
|
||||||
|
|
||||||
public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true)
|
public async Task CheckUpdateGuiN(bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var url = string.Empty;
|
var url = string.Empty;
|
||||||
var fileName = string.Empty;
|
var fileName = string.Empty;
|
||||||
@@ -39,7 +38,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
|
|
||||||
url = result.Url!;
|
url = result.Url!;
|
||||||
fileName = Utils.GetTempPath(Utils.GetGuid());
|
fileName = Utils.GetTempPath(Utils.GetGuid());
|
||||||
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, TimeSpan.FromSeconds(_timeout));
|
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -47,7 +46,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true)
|
public async Task CheckUpdateCore(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var url = string.Empty;
|
var url = string.Empty;
|
||||||
var fileName = string.Empty;
|
var fileName = string.Empty;
|
||||||
@@ -89,7 +88,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
url = result.Url!;
|
url = result.Url!;
|
||||||
var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url);
|
var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url);
|
||||||
fileName = Utils.GetTempPath(Utils.GetGuid() + ext);
|
fileName = Utils.GetTempPath(Utils.GetGuid() + ext);
|
||||||
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, TimeSpan.FromSeconds(_timeout));
|
await downloadHandle.DownloadFileAsync(new() { FileUrl = url, FilePath = fileName }, blProxy, cancellationToken);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -100,7 +99,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true)
|
public async Task<UpdateResult> CheckHasUpdateOnly(ECoreType type, bool preRelease, bool blProxy = true, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type))
|
if (!CoreInfoManager.Instance.IsCheckUpdateSupported(type))
|
||||||
{
|
{
|
||||||
@@ -109,10 +108,10 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
|
|
||||||
var downloadHandle = new DownloadService();
|
var downloadHandle = new DownloadService();
|
||||||
var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease);
|
var checkPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(type, preRelease);
|
||||||
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy);
|
return await CheckUpdateAsync(downloadHandle, type, checkPreRelease, blProxy, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<string>> CheckHasUpdateOnlyAll(bool preRelease, bool blProxy = true)
|
public async Task<List<string>> CheckHasUpdateOnlyAll(bool blProxy = true, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var msgs = new List<string>();
|
var msgs = new List<string>();
|
||||||
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
|
foreach (var type in CoreInfoManager.Instance.GetCheckUpdateCoreTypes())
|
||||||
@@ -122,7 +121,8 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
continue;
|
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)
|
if (result.Success && result.Version != null)
|
||||||
{
|
{
|
||||||
var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version);
|
var msg = string.Format(ResUI.MsgCheckUpdateHasNewVersion, type, result.Version);
|
||||||
@@ -137,7 +137,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
return msgs;
|
return msgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateGeoFileAll(bool blProxy = true)
|
public async Task UpdateGeoFileAll(bool blProxy = true, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var requests = new List<FileDownloadRequest>();
|
var requests = new List<FileDownloadRequest>();
|
||||||
requests.AddRange(GetGeoFilesRequest());
|
requests.AddRange(GetGeoFilesRequest());
|
||||||
@@ -145,22 +145,22 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
requests.AddRange(await GetSrsFileAllRequest());
|
requests.AddRange(await GetSrsFileAllRequest());
|
||||||
// NOTE: srs files are more small, so we reverse the order to ensure a good download experience for the user.
|
// NOTE: srs files are more small, so we reverse the order to ensure a good download experience for the user.
|
||||||
requests.Reverse();
|
requests.Reverse();
|
||||||
await DownloadGeoFiles(requests, blProxy);
|
await DownloadGeoFiles(requests, blProxy, cancellationToken);
|
||||||
await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
|
await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#region CheckUpdate private
|
#region CheckUpdate private
|
||||||
|
|
||||||
private async Task<UpdateResult> CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy)
|
private async Task<UpdateResult> CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease, bool blProxy, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy);
|
var result = await GetRemoteVersion(downloadHandle, type, preRelease, blProxy, cancellationToken);
|
||||||
if (!result.Success || result.Version is null)
|
if (!result.Success || result.Version is null)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
return await ParseDownloadUrl(type, result);
|
return await ParseDownloadUrl(type, result, cancellationToken);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -170,14 +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 coreInfo = CoreInfoManager.Instance.GetCoreInfo(type);
|
||||||
var tagName = string.Empty;
|
var tagName = string.Empty;
|
||||||
if (preRelease || coreInfo?.LockedMaxVersion != null)
|
if (preRelease || coreInfo?.LockedMaxVersion != null)
|
||||||
{
|
{
|
||||||
var url = coreInfo?.ReleaseApiUrl;
|
var url = coreInfo?.ReleaseApiUrl;
|
||||||
var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName);
|
var result = await downloadHandle.TryDownloadString(url, blProxy, Global.AppName, cancellationToken);
|
||||||
if (result.IsNullOrEmpty())
|
if (result.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
return new UpdateResult(false, "");
|
return new UpdateResult(false, "");
|
||||||
@@ -209,7 +209,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var url = Path.Combine(coreInfo.Url, "latest");
|
var url = Path.Combine(coreInfo.Url, "latest");
|
||||||
var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy);
|
var lastUrl = await downloadHandle.UrlRedirectAsync(url, blProxy, cancellationToken);
|
||||||
if (lastUrl == null)
|
if (lastUrl == null)
|
||||||
{
|
{
|
||||||
return new UpdateResult(false, "");
|
return new UpdateResult(false, "");
|
||||||
@@ -223,7 +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)]
|
[GeneratedRegex(@"v?(?<version>\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?)", RegexOptions.IgnoreCase)]
|
||||||
private static partial Regex SemVerRegex();
|
private static partial Regex SemVerRegex();
|
||||||
|
|
||||||
private async Task<SemanticVersion> GetCoreVersion(ECoreType type)
|
private async Task<SemanticVersion> GetCoreVersion(ECoreType type, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -246,7 +246,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
return new SemanticVersion("");
|
return new SemanticVersion("");
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg);
|
var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg, cancellationToken);
|
||||||
var echo = result ?? "";
|
var echo = result ?? "";
|
||||||
var version = SemVerRegex().Match(echo).Groups["version"].Value;
|
var version = SemVerRegex().Match(echo).Groups["version"].Value;
|
||||||
return new SemanticVersion(version);
|
return new SemanticVersion(version);
|
||||||
@@ -259,7 +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
|
try
|
||||||
{
|
{
|
||||||
@@ -276,7 +276,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
case ECoreType.v2fly_v5:
|
case ECoreType.v2fly_v5:
|
||||||
case ECoreType.mihomo:
|
case ECoreType.mihomo:
|
||||||
{
|
{
|
||||||
curVersion = await GetCoreVersion(type);
|
curVersion = await GetCoreVersion(type, cancellationToken);
|
||||||
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
|
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
|
||||||
url = string.Format(coreUrl, version);
|
url = string.Format(coreUrl, version);
|
||||||
break;
|
break;
|
||||||
@@ -284,7 +284,7 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
|||||||
|
|
||||||
case ECoreType.sing_box:
|
case ECoreType.sing_box:
|
||||||
{
|
{
|
||||||
curVersion = await GetCoreVersion(type);
|
curVersion = await GetCoreVersion(type, cancellationToken);
|
||||||
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
|
message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v"));
|
||||||
url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v"));
|
url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v"));
|
||||||
break;
|
break;
|
||||||
@@ -531,7 +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 tmpFilePathDict = new Dictionary<string, string>();
|
||||||
var tmpFileRequests = new List<FileDownloadRequest>();
|
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);
|
_ = UpdateFunc(false, args.GetException().Message);
|
||||||
};
|
};
|
||||||
|
|
||||||
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, TimeSpan.FromSeconds(_timeout));
|
await downloadHandle.DownloadSmallFilesAsync(tmpFileRequests, blProxy, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Geo private
|
#endregion Geo private
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
|||||||
[Reactive]
|
[Reactive]
|
||||||
public partial int GeckoMaxPacketSize { get; set; }
|
public partial int GeckoMaxPacketSize { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public partial string MasquePath { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public partial string MasqueHeaders { get; set; }
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public partial string RawHeaderType { get; set; }
|
public partial string RawHeaderType { get; set; }
|
||||||
|
|
||||||
@@ -322,6 +328,8 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
|||||||
Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty;
|
Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty;
|
||||||
GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
|
GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
|
||||||
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
|
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
|
||||||
|
MasquePath = protocolExtra.MasquePath ?? string.Empty;
|
||||||
|
MasqueHeaders = protocolExtra.MasqueHeaders ?? string.Empty;
|
||||||
|
|
||||||
RawHeaderType = transport.RawHeaderType ?? Global.None;
|
RawHeaderType = transport.RawHeaderType ?? Global.None;
|
||||||
Host = transport.Host ?? string.Empty;
|
Host = transport.Host ?? string.Empty;
|
||||||
@@ -443,6 +451,8 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
|||||||
Hy2RealmUrl = realm?.ToUri().NullIfEmpty(),
|
Hy2RealmUrl = realm?.ToUri().NullIfEmpty(),
|
||||||
GeckoMinPacketSize = GeckoMinPacketSize > 0 ? GeckoMinPacketSize.ToString() : null,
|
GeckoMinPacketSize = GeckoMinPacketSize > 0 ? GeckoMinPacketSize.ToString() : null,
|
||||||
GeckoMaxPacketSize = GeckoMaxPacketSize > 0 ? GeckoMaxPacketSize.ToString() : null,
|
GeckoMaxPacketSize = GeckoMaxPacketSize > 0 ? GeckoMaxPacketSize.ToString() : null,
|
||||||
|
MasquePath = MasquePath.NullIfEmpty(),
|
||||||
|
MasqueHeaders = MasqueHeaders.NullIfEmpty(),
|
||||||
});
|
});
|
||||||
SelectedSource.SetTransportExtra(transport);
|
SelectedSource.SetTransportExtra(transport);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
|||||||
public BulkObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = [];
|
public BulkObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = [];
|
||||||
public ReactiveCommand<RxVoid, RxVoid> CheckUpdateCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> CheckUpdateCmd { get; }
|
||||||
public ReactiveCommand<RxVoid, RxVoid> CheckOnlyCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> CheckOnlyCmd { get; }
|
||||||
[Reactive] public partial bool EnableCheckPreReleaseUpdate { get; set; }
|
|
||||||
[Reactive] public partial bool EnableUpdateViaProxy { get; set; }
|
[Reactive] public partial bool EnableUpdateViaProxy { get; set; }
|
||||||
|
|
||||||
public CheckUpdateViewModel()
|
public CheckUpdateViewModel()
|
||||||
@@ -33,20 +33,26 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
|||||||
_ = UpdateView(_v2rayN, ex.Message);
|
_ = UpdateView(_v2rayN, ex.Message);
|
||||||
});
|
});
|
||||||
|
|
||||||
EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate;
|
|
||||||
EnableUpdateViaProxy = _config.CheckUpdateItem.UpdateViaProxy;
|
EnableUpdateViaProxy = _config.CheckUpdateItem.UpdateViaProxy;
|
||||||
|
|
||||||
this.WhenAnyValue(x => x.EnableCheckPreReleaseUpdate)
|
|
||||||
.SubscribeAsync(async _ => await OnCheckPreReleaseUpdateChanged());
|
|
||||||
|
|
||||||
this.WhenAnyValue(x => x.EnableUpdateViaProxy)
|
this.WhenAnyValue(x => x.EnableUpdateViaProxy)
|
||||||
.Subscribe(c => _ = OnUpdateViaProxyChanged());
|
.Subscribe(c => _ = OnUpdateViaProxyChanged());
|
||||||
|
|
||||||
RefreshCheckUpdateItems();
|
AppEvents.HasUpdateNotified
|
||||||
|
.AsObservable()
|
||||||
|
.ObserveOn(RxSchedulers.MainThreadScheduler)
|
||||||
|
.Subscribe(bl => RefreshCheckUpdateItems(bl));
|
||||||
|
|
||||||
|
RefreshCheckUpdateItems(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshCheckUpdateItems()
|
private void RefreshCheckUpdateItems(bool hasUpdate)
|
||||||
{
|
{
|
||||||
|
if (!hasUpdate)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var models = CoreInfoManager.Instance.GetCheckUpdateCoreTypes()
|
var models = CoreInfoManager.Instance.GetCheckUpdateCoreTypes()
|
||||||
.Select(t => GetCheckUpdateModel(t))
|
.Select(t => GetCheckUpdateModel(t))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -63,6 +69,8 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
|||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
IsSelected = false,
|
IsSelected = false,
|
||||||
|
IsCheckPreRelease = false,
|
||||||
|
ShowCheckPreRelease = false,
|
||||||
CoreType = coreType,
|
CoreType = coreType,
|
||||||
IsGeoFile = false,
|
IsGeoFile = false,
|
||||||
Remarks = ResUI.menuCheckUpdate + $" ({ResUI.MsgNotSupport})",
|
Remarks = ResUI.menuCheckUpdate + $" ({ResUI.MsgNotSupport})",
|
||||||
@@ -73,6 +81,8 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
|||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType.ToString()) ?? true,
|
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType.ToString()) ?? true,
|
||||||
|
IsCheckPreRelease = _config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(coreType.ToString()) ?? false,
|
||||||
|
ShowCheckPreRelease = CoreInfoManager.Instance.GetCheckPreRelease(coreType, true),
|
||||||
CoreType = coreType,
|
CoreType = coreType,
|
||||||
IsGeoFile = false,
|
IsGeoFile = false,
|
||||||
Remarks = lastResult ?? ResUI.menuCheckUpdate,
|
Remarks = lastResult ?? ResUI.menuCheckUpdate,
|
||||||
@@ -84,22 +94,14 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
|||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(_geo) ?? true,
|
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(_geo) ?? true,
|
||||||
|
IsCheckPreRelease = false,
|
||||||
|
ShowCheckPreRelease = false,
|
||||||
CoreType = null,
|
CoreType = null,
|
||||||
IsGeoFile = true,
|
IsGeoFile = true,
|
||||||
Remarks = ResUI.menuCheckUpdate,
|
Remarks = ResUI.menuCheckUpdate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnCheckPreReleaseUpdateChanged()
|
|
||||||
{
|
|
||||||
if (_config.CheckUpdateItem.CheckPreReleaseUpdate == EnableCheckPreReleaseUpdate)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate;
|
|
||||||
await SaveSelectedCoreTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnUpdateViaProxyChanged()
|
private async Task OnUpdateViaProxyChanged()
|
||||||
{
|
{
|
||||||
if (_config.CheckUpdateItem.UpdateViaProxy == EnableUpdateViaProxy)
|
if (_config.CheckUpdateItem.UpdateViaProxy == EnableUpdateViaProxy)
|
||||||
@@ -117,6 +119,11 @@ public partial class CheckUpdateViewModel : MyReactiveObject
|
|||||||
.Select(t => t.CoreTypeForStorage)
|
.Select(t => t.CoreTypeForStorage)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
_config.CheckUpdateItem.CheckPreReleaseCoreTypes =
|
||||||
|
CheckUpdateModels.Where(t => t.IsCheckPreRelease == true)
|
||||||
|
.Select(t => t.CoreTypeForStorage)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
await ConfigHandler.SaveConfig(_config);
|
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 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)
|
if (result.Success && result.Version != null)
|
||||||
{
|
{
|
||||||
await UpdateView(item.CoreType, string.Format(ResUI.MsgCheckUpdateHasNewVersion, item.CoreType, result.Version));
|
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);
|
await UpdateView(_v2rayN, ResUI.MsgNotSupport);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await CheckUpdateN(EnableCheckPreReleaseUpdate);
|
await CheckUpdateN(item.IsCheckPreRelease);
|
||||||
}
|
|
||||||
else if (item.CoreType is ECoreType.Xray or ECoreType.sing_box)
|
|
||||||
{
|
|
||||||
await CheckUpdateCore(item, EnableCheckPreReleaseUpdate);
|
|
||||||
}
|
}
|
||||||
else if (item.CoreType.HasValue)
|
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);
|
await ClashConnectionClose(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
_ = Task.Factory.StartNew(
|
this.WhenActivated(disposables =>
|
||||||
async () => await GetClashConnectionsTask(),
|
{
|
||||||
CancellationToken.None,
|
var cancelDisposable = new CancellationDisposable();
|
||||||
TaskCreationOptions.LongRunning,
|
cancelDisposable.DisposeWith(disposables);
|
||||||
TaskScheduler.Default
|
var token = cancelDisposable.Token;
|
||||||
);
|
|
||||||
|
Task.Run(() => GetClashConnectionsTask(token));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
|
public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
|
||||||
@@ -117,29 +119,40 @@ public partial class ClashConnectionsViewModel : MyReactiveObject
|
|||||||
await GetClashConnections();
|
await GetClashConnections();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task GetClashConnectionsTask()
|
public async Task GetClashConnectionsTask(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var numOfExecuted = 1;
|
try
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
await Task.Delay(1000 * 5);
|
var numOfExecuted = 1;
|
||||||
numOfExecuted++;
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||||
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
|
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
||||||
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
|
|
||||||
{
|
{
|
||||||
continue;
|
numOfExecuted++;
|
||||||
}
|
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
|
||||||
|
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
|
if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
|
if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
await GetClashConnections();
|
||||||
}
|
}
|
||||||
await GetClashConnections();
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logging.SaveLog("GetClashConnectionsTask", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace ServiceLib.ViewModels;
|
|||||||
|
|
||||||
public partial class ClashProxiesViewModel : MyReactiveObject
|
public partial class ClashProxiesViewModel : MyReactiveObject
|
||||||
{
|
{
|
||||||
private readonly int _delayTimeout = 99999999;
|
private const int _delayTimeout = 99999999;
|
||||||
private ClashItem _clashItem = new();
|
private ClashItem _clashItem = new();
|
||||||
|
|
||||||
public ClashProxiesViewModel()
|
public ClashProxiesViewModel()
|
||||||
@@ -55,12 +55,14 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
|||||||
|
|
||||||
#endregion WhenAnyValue && ReactiveCommand
|
#endregion WhenAnyValue && ReactiveCommand
|
||||||
|
|
||||||
_ = Task.Factory.StartNew(
|
this.WhenActivated(disposables =>
|
||||||
async () => await GetClashProxiesTask(),
|
{
|
||||||
CancellationToken.None,
|
var cancelDisposable = new CancellationDisposable();
|
||||||
TaskCreationOptions.LongRunning,
|
cancelDisposable.DisposeWith(disposables);
|
||||||
TaskScheduler.Default
|
var token = cancelDisposable.Token;
|
||||||
);
|
|
||||||
|
Task.Run(() => GetClashProxiesTask(token));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
|
public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
|
||||||
@@ -101,27 +103,40 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
|||||||
|
|
||||||
#region task
|
#region task
|
||||||
|
|
||||||
public async Task GetClashProxiesTask()
|
public async Task GetClashProxiesTask(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var numOfExecuted = 1;
|
try
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
await Task.Delay(1000 * 60);
|
var numOfExecuted = 1;
|
||||||
numOfExecuted++;
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||||
if (!(AutoRefresh && AppManager.Instance.ShowInTaskbar &&
|
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
||||||
AppManager.Instance.IsRunningCore(ECoreType.sing_box)))
|
|
||||||
{
|
{
|
||||||
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)
|
}
|
||||||
{
|
catch (OperationCanceledException)
|
||||||
continue;
|
{
|
||||||
}
|
// Ignored
|
||||||
if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
continue;
|
{
|
||||||
}
|
Logging.SaveLog("GetClashProxiesTask", ex);
|
||||||
await GetClashProxies();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +235,11 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var dicCurrentDetails = new Dictionary<string, ClashProxyModel>();
|
||||||
|
foreach (var item in ProxyDetails)
|
||||||
|
{
|
||||||
|
dicCurrentDetails.TryAdd(item.Name, item);
|
||||||
|
}
|
||||||
var lstDetails = new List<ClashProxyModel>();
|
var lstDetails = new List<ClashProxyModel>();
|
||||||
foreach (var item in proxy.all)
|
foreach (var item in proxy.all)
|
||||||
{
|
{
|
||||||
@@ -228,15 +248,29 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var delay = proxy2.history?.Count > 0 ? proxy2.history.Last().delay : -1;
|
var delay = -1;
|
||||||
|
var hasData = false;
|
||||||
|
|
||||||
|
if (proxy2.history is { Count: > 0 })
|
||||||
|
{
|
||||||
|
delay = proxy2.history.Last().delay;
|
||||||
|
hasData = true;
|
||||||
|
}
|
||||||
|
else if (dicCurrentDetails.TryGetValue(item, out var detail))
|
||||||
|
{
|
||||||
|
delay = detail.Delay;
|
||||||
|
hasData = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isValidDelay = delay is > 0 and not _delayTimeout;
|
||||||
|
|
||||||
lstDetails.Add(new ClashProxyModel
|
lstDetails.Add(new ClashProxyModel
|
||||||
{
|
{
|
||||||
IsActive = item == proxy.now,
|
IsActive = item == proxy.now,
|
||||||
Name = item,
|
Name = item,
|
||||||
Type = proxy2.type,
|
Type = proxy2.type,
|
||||||
Delay = delay <= 0 ? _delayTimeout : delay,
|
Delay = isValidDelay ? delay : _delayTimeout,
|
||||||
DelayName = delay <= 0 ? string.Empty : $"{delay}ms",
|
DelayName = !hasData ? string.Empty : (isValidDelay ? $"{delay}ms" : "-1"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// sort
|
// sort
|
||||||
@@ -347,8 +381,9 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
detail.Delay = Convert.ToInt32(result.Delay);
|
var isValid = int.TryParse(result.Delay, out var delay) && delay > 0 && delay != _delayTimeout;
|
||||||
detail.DelayName = $"{detail.Delay}ms";
|
detail.Delay = isValid ? delay : _delayTimeout;
|
||||||
|
detail.DelayName = isValid ? $"{delay}ms" : "-1";
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
public ReactiveCommand<RxVoid, RxVoid> AddWireguardServerCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> AddWireguardServerCmd { get; }
|
||||||
public ReactiveCommand<RxVoid, RxVoid> AddAnytlsServerCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> AddAnytlsServerCmd { get; }
|
||||||
public ReactiveCommand<RxVoid, RxVoid> AddNaiveServerCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> AddNaiveServerCmd { get; }
|
||||||
|
public ReactiveCommand<RxVoid, RxVoid> AddMasqueServerCmd { get; }
|
||||||
public ReactiveCommand<RxVoid, RxVoid> AddCustomServerCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> AddCustomServerCmd { get; }
|
||||||
public ReactiveCommand<RxVoid, RxVoid> AddCustomOutboundServerCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> AddCustomOutboundServerCmd { get; }
|
||||||
public ReactiveCommand<RxVoid, RxVoid> AddPolicyGroupServerCmd { get; }
|
public ReactiveCommand<RxVoid, RxVoid> AddPolicyGroupServerCmd { get; }
|
||||||
@@ -140,6 +141,10 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
await AddServerAsync(EConfigType.Naive);
|
await AddServerAsync(EConfigType.Naive);
|
||||||
});
|
});
|
||||||
|
AddMasqueServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
await AddServerAsync(EConfigType.MASQUE);
|
||||||
|
});
|
||||||
AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||||
{
|
{
|
||||||
await AddServerAsync(EConfigType.Custom);
|
await AddServerAsync(EConfigType.Custom);
|
||||||
@@ -522,7 +527,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
public async Task AddServerViaImageAsync()
|
public async Task AddServerViaImageAsync()
|
||||||
{
|
{
|
||||||
var imageFileName = await BrowseImageFileInteraction.HandleSafe(RxVoid.Default);
|
var imageFileName = await BrowseImageFileInteraction.HandleSafe(RxVoid.Default);
|
||||||
await AddScanResultAsync(imageFileName);
|
await ScanImageResult(imageFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ScanImageResult(string fileName)
|
public async Task ScanImageResult(string fileName)
|
||||||
@@ -680,6 +685,13 @@ public partial class MainWindowViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
SetReloadEnabled(false);
|
SetReloadEnabled(false);
|
||||||
|
|
||||||
|
RxSchedulers.MainThreadScheduler.Schedule(() =>
|
||||||
|
{
|
||||||
|
if (TabMainSelectedIndex < 0)
|
||||||
|
{
|
||||||
|
TabMainSelectedIndex = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
var profileItem = await ConfigHandler.GetDefaultServer(_config);
|
var profileItem = await ConfigHandler.GetDefaultServer(_config);
|
||||||
if (profileItem == null)
|
if (profileItem == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ namespace ServiceLib.ViewModels;
|
|||||||
|
|
||||||
public partial class MsgViewModel : MyReactiveObject
|
public partial class MsgViewModel : MyReactiveObject
|
||||||
{
|
{
|
||||||
public Interaction<string, RxVoid> DispatcherShowMsgInteraction { get; } = new();
|
public Interaction<string, RxVoid> ShowMsgInteraction { get; } = new();
|
||||||
|
|
||||||
private readonly ConcurrentQueue<string> _queueMsg = new();
|
private readonly ConcurrentQueue<string> _queueMsg = new();
|
||||||
private volatile bool _lastMsgFilterNotAvailable;
|
private volatile bool _lastMsgFilterNotAvailable;
|
||||||
private int _showLock = 0; // 0 = unlocked, 1 = locked
|
public int NumMaxMsg => 500;
|
||||||
public int NumMaxMsg { get; } = 500;
|
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public partial string MsgFilter { get; set; }
|
public partial string MsgFilter { get; set; }
|
||||||
@@ -30,59 +29,39 @@ public partial class MsgViewModel : MyReactiveObject
|
|||||||
|
|
||||||
AppEvents.SendMsgViewRequested
|
AppEvents.SendMsgViewRequested
|
||||||
.AsObservable()
|
.AsObservable()
|
||||||
//.ObserveOn(RxSchedulers.MainThreadScheduler)
|
.Subscribe(EnqueueQueueMsg);
|
||||||
.Subscribe(content => _ = AppendQueueMsg(content));
|
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
|
{
|
||||||
|
Signal.Every(TimeSpan.FromSeconds(1))
|
||||||
|
.Where(_ => AutoRefresh && AppManager.Instance.ShowInTaskbar)
|
||||||
|
.ObserveOn(RxSchedulers.MainThreadScheduler)
|
||||||
|
.Subscribe(_ => FlushQueueToView())
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FlushQueueMsg()
|
private void FlushQueueToView()
|
||||||
{
|
{
|
||||||
_ = AppendQueueMsg(string.Empty);
|
if (!AutoRefresh || _queueMsg.IsEmpty)
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AppendQueueMsg(string msg)
|
|
||||||
{
|
|
||||||
if (AutoRefresh == false)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
EnqueueQueueMsg(msg);
|
|
||||||
|
|
||||||
if (!AppManager.Instance.ShowInTaskbar)
|
if (!AppManager.Instance.ShowInTaskbar)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0)
|
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);
|
ShowMsgInteraction.HandleSafe(sb.ToString()).Subscribe();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +77,7 @@ public partial class MsgViewModel : MyReactiveObject
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!Regex.IsMatch(msg, MsgFilter))
|
if (!Utils.IsRegexMatch(msg, MsgFilter))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,11 +89,11 @@ public partial class MsgViewModel : MyReactiveObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EnqueueWithLimit(msg);
|
var formattedMsg = msg.EndsWith(Environment.NewLine)
|
||||||
if (!msg.EndsWith(Environment.NewLine))
|
? msg
|
||||||
{
|
: msg + Environment.NewLine;
|
||||||
EnqueueWithLimit(Environment.NewLine);
|
|
||||||
}
|
EnqueueWithLimit(formattedMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnqueueWithLimit(string item)
|
private void EnqueueWithLimit(string item)
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
|
|||||||
[Reactive] public partial string CoreType6 { get; set; }
|
[Reactive] public partial string CoreType6 { get; set; }
|
||||||
[Reactive] public partial string CoreType7 { get; set; }
|
[Reactive] public partial string CoreType7 { get; set; }
|
||||||
[Reactive] public partial string CoreType9 { get; set; }
|
[Reactive] public partial string CoreType9 { get; set; }
|
||||||
|
[Reactive] public partial string CoreType14 { get; set; }
|
||||||
|
|
||||||
#endregion CoreType
|
#endregion CoreType
|
||||||
|
|
||||||
@@ -283,6 +284,10 @@ public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
|
|||||||
case 9:
|
case 9:
|
||||||
CoreType9 = type;
|
CoreType9 = type;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 14:
|
||||||
|
CoreType14 = type;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
@@ -451,6 +456,10 @@ public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
|
|||||||
type = CoreType9;
|
type = CoreType9;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 14:
|
||||||
|
type = CoreType14;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,7 +219,8 @@ public partial class StatusBarViewModel : MyReactiveObject
|
|||||||
await ConfigHandler.InitBuiltinRouting(_config);
|
await ConfigHandler.InitBuiltinRouting(_config);
|
||||||
await RefreshRoutingsMenu();
|
await RefreshRoutingsMenu();
|
||||||
await InboundDisplayStatus();
|
await InboundDisplayStatus();
|
||||||
await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true);
|
// The view refreshes its icon on initialization; interaction handlers are not registered yet.
|
||||||
|
await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, false);
|
||||||
|
|
||||||
BlRouting = true;
|
BlRouting = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
|
|||||||
@@ -23,12 +23,12 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
@@ -783,6 +783,70 @@
|
|||||||
HorizontalAlignment="Left" />
|
HorizontalAlignment="Left" />
|
||||||
</Grid>
|
</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
|
<Separator
|
||||||
x:Name="sepa2"
|
x:Name="sepa2"
|
||||||
Grid.Row="3"
|
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.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(currentTypeDisposables);
|
||||||
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(currentTypeDisposables);
|
this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(currentTypeDisposables);
|
||||||
break;
|
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);
|
.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.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeData(ProfileItem profileItem)
|
private void InitializeData(ProfileItem profileItem)
|
||||||
@@ -284,6 +290,14 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
|
|||||||
|
|
||||||
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
|
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EConfigType.MASQUE:
|
||||||
|
gridMasque.IsVisible = true;
|
||||||
|
sepa2.IsVisible = false;
|
||||||
|
gridTransport.IsVisible = false;
|
||||||
|
cmbFingerprint.IsEnabled = false;
|
||||||
|
cmbAlpn.IsEnabled = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
|
cmbStreamSecurity.ItemsSource = lstStreamSecurity;
|
||||||
cmbStreamSecurity.SelectedItem = profileItem.StreamSecurity;
|
cmbStreamSecurity.SelectedItem = profileItem.StreamSecurity;
|
||||||
|
|||||||
@@ -24,16 +24,23 @@
|
|||||||
HorizontalAlignment="Left" />
|
HorizontalAlignment="Left" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel>
|
<StackPanel Grid.IsSharedSizeScope="True">
|
||||||
<Border
|
<Border
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Theme="{DynamicResource CardBorder}">
|
Theme="{DynamicResource CardBorder}">
|
||||||
|
|
||||||
<Grid
|
<Grid Margin="{StaticResource Margin4}" RowDefinitions="Auto,Auto,Auto,Auto">
|
||||||
Margin="{StaticResource Margin4}"
|
<Grid.ColumnDefinitions>
|
||||||
ColumnDefinitions="300,200"
|
<ColumnDefinition
|
||||||
RowDefinitions="Auto,Auto,Auto,Auto">
|
Width="Auto"
|
||||||
|
MinWidth="300"
|
||||||
|
SharedSizeGroup="BackupLabel" />
|
||||||
|
<ColumnDefinition
|
||||||
|
Width="Auto"
|
||||||
|
MinWidth="200"
|
||||||
|
SharedSizeGroup="BackupButton" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
@@ -79,10 +86,17 @@
|
|||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Theme="{DynamicResource CardBorder}">
|
Theme="{DynamicResource CardBorder}">
|
||||||
<Grid
|
<Grid Margin="{StaticResource Margin4}" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
|
||||||
Margin="{StaticResource Margin4}"
|
<Grid.ColumnDefinitions>
|
||||||
ColumnDefinitions="300,200"
|
<ColumnDefinition
|
||||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
|
Width="Auto"
|
||||||
|
MinWidth="300"
|
||||||
|
SharedSizeGroup="BackupLabel" />
|
||||||
|
<ColumnDefinition
|
||||||
|
Width="Auto"
|
||||||
|
MinWidth="200"
|
||||||
|
SharedSizeGroup="BackupButton" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
|
|||||||
@@ -35,27 +35,17 @@
|
|||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Left" />
|
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
|
<Button
|
||||||
x:Name="btnCheckOnly"
|
x:Name="btnCheckOnly"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
Content="{x:Static resx:ResUI.menuCheckOnly}" />
|
Content="{x:Static resx:ResUI.menuCheckOnly}" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCheckUpdate"
|
x:Name="btnCheckUpdate"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
Content="{x:Static resx:ResUI.menuCheckUpdate}" />
|
Content="{x:Static resx:ResUI.menuCheckAndUpdate}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -75,8 +65,8 @@
|
|||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Panel Width="500" VerticalAlignment="Center">
|
<Panel Width="680" VerticalAlignment="Center">
|
||||||
<Grid ColumnDefinitions="1*,1*,3*" RowDefinitions="Auto">
|
<Grid ColumnDefinitions="1*,3*,1*,3*" RowDefinitions="Auto">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
x:Name="togAutoRefresh"
|
x:Name="togAutoRefresh"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
@@ -84,13 +74,32 @@
|
|||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsChecked="{Binding IsSelected}" />
|
IsChecked="{Binding IsSelected}" />
|
||||||
<TextBlock
|
|
||||||
|
<DockPanel
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
|
Margin="{StaticResource MarginLr4}"
|
||||||
|
IsVisible="{Binding ShowCheckPreRelease}">
|
||||||
|
<ToggleSwitch
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsChecked="{Binding IsCheckPreRelease}"
|
||||||
|
OffContent=""
|
||||||
|
OnContent="" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="{StaticResource Margin4}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Static resx:ResUI.TbSettingsEnableCheckPreReleaseUpdate}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="2"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{Binding CoreTypeForStorage}" />
|
Text="{Binding CoreTypeForStorage}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="2"
|
Grid.Column="3"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{Binding Remarks}"
|
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.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.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.CheckOnlyCmd, v => v.btnCheckOnly).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.CheckUpdateCmd, v => v.btnCheckUpdate).DisposeWith(disposables);
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
|
|||||||
@@ -23,17 +23,17 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnReset"
|
x:Name="btnReset"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbReset}" />
|
Content="{x:Static resx:ResUI.TbReset}" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
|
|||||||
@@ -18,6 +18,15 @@
|
|||||||
ShowInTaskbar="True"
|
ShowInTaskbar="True"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
mc:Ignorable="d">
|
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">
|
<dialogHost:DialogHost CloseOnClickAway="True" DisableOpeningAnimation="True">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<DockPanel Margin="{StaticResource Margin8}" DockPanel.Dock="Top">
|
<DockPanel Margin="{StaticResource Margin8}" DockPanel.Dock="Top">
|
||||||
@@ -45,6 +54,7 @@
|
|||||||
<MenuItem x:Name="menuAddTrojanServer" Header="{x:Static resx:ResUI.menuAddTrojanServer}" />
|
<MenuItem x:Name="menuAddTrojanServer" Header="{x:Static resx:ResUI.menuAddTrojanServer}" />
|
||||||
<MenuItem x:Name="menuAddHysteria2Server" Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
|
<MenuItem x:Name="menuAddHysteria2Server" Header="{x:Static resx:ResUI.menuAddHysteria2Server}" />
|
||||||
<MenuItem x:Name="menuAddWireguardServer" Header="{x:Static resx:ResUI.menuAddWireguardServer}" />
|
<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="menuAddSocksServer" Header="{x:Static resx:ResUI.menuAddSocksServer}" />
|
||||||
<MenuItem x:Name="menuAddHttpServer" Header="{x:Static resx:ResUI.menuAddHttpServer}" />
|
<MenuItem x:Name="menuAddHttpServer" Header="{x:Static resx:ResUI.menuAddHttpServer}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -123,6 +133,7 @@
|
|||||||
<TabControl
|
<TabControl
|
||||||
x:Name="tabMain"
|
x:Name="tabMain"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
|
Classes="logPane"
|
||||||
HorizontalContentAlignment="Left">
|
HorizontalContentAlignment="Left">
|
||||||
<TabItem x:Name="tabMsgView" Header="{x:Static resx:ResUI.MsgInformationTitle}" />
|
<TabItem x:Name="tabMsgView" Header="{x:Static resx:ResUI.MsgInformationTitle}" />
|
||||||
<TabItem x:Name="tabClashProxies" Header="{x:Static resx:ResUI.TbProxies}" />
|
<TabItem x:Name="tabClashProxies" Header="{x:Static resx:ResUI.TbProxies}" />
|
||||||
@@ -138,6 +149,7 @@
|
|||||||
<TabControl
|
<TabControl
|
||||||
x:Name="tabMain1"
|
x:Name="tabMain1"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
|
Classes="logPane"
|
||||||
TabStripPlacement="Left">
|
TabStripPlacement="Left">
|
||||||
<TabItem x:Name="tabMsgView1" Header="{x:Static resx:ResUI.MsgInformationTitle}" />
|
<TabItem x:Name="tabMsgView1" Header="{x:Static resx:ResUI.MsgInformationTitle}" />
|
||||||
<TabItem x:Name="tabClashProxies1" Header="{x:Static resx:ResUI.TbProxies}" />
|
<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.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).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.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.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddCustomOutboundServerCmd, v => v.menuAddCustomOutboundServer).DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.AddCustomOutboundServerCmd, v => v.menuAddCustomOutboundServer).DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).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.MsgViewModel, v => v.tabMsgView.Content).DisposeWith(currentLayoutDisposables);
|
||||||
this.OneWayBind(ViewModel, vm => vm.ClashProxiesViewModel, v => v.tabClashProxies.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.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.tabClashProxies.IsVisible).DisposeWith(currentLayoutDisposables);
|
||||||
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections.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);
|
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.MsgViewModel, v => v.tabMsgView1.Content).DisposeWith(currentLayoutDisposables);
|
||||||
this.OneWayBind(ViewModel, vm => vm.ClashProxiesViewModel, v => v.tabClashProxies1.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.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.tabClashProxies1.IsVisible).DisposeWith(currentLayoutDisposables);
|
||||||
this.OneWayBind(ViewModel, vm => vm.ShowClashUI, v => v.tabClashConnections1.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);
|
this.Bind(ViewModel, vm => vm.TabMainSelectedIndex, v => v.tabMain1.SelectedIndex).DisposeWith(currentLayoutDisposables);
|
||||||
|
|||||||
@@ -23,7 +23,8 @@
|
|||||||
Width="200"
|
Width="200"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
VerticalContentAlignment="Center"
|
VerticalContentAlignment="Center"
|
||||||
PlaceholderText="{x:Static resx:ResUI.MsgFilterTitle}" />
|
PlaceholderText="{x:Static resx:ResUI.MsgFilterTitle}"
|
||||||
|
ToolTip.Tip="{x:Static resx:ResUI.MsgFilterTitle}" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCopy"
|
x:Name="btnCopy"
|
||||||
|
|||||||
@@ -16,15 +16,12 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
|
|||||||
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
|
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
|
||||||
|
|
||||||
ViewModel.DispatcherShowMsgInteraction.RegisterHandler(interaction =>
|
ViewModel.ShowMsgInteraction.RegisterHandler(interaction =>
|
||||||
{
|
{
|
||||||
var msg = interaction.Input;
|
var msg = interaction.Input;
|
||||||
Dispatcher.UIThread.Post(() => ShowMsg(msg),
|
ShowMsg(msg);
|
||||||
DispatcherPriority.ApplicationIdle);
|
|
||||||
interaction.SetOutput(RxVoid.Default);
|
interaction.SetOutput(RxVoid.Default);
|
||||||
}).DisposeWith(disposables);
|
}).DisposeWith(disposables);
|
||||||
|
|
||||||
ViewModel?.FlushQueueMsg();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
|
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
|
||||||
@@ -50,9 +47,14 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
|
|||||||
if (togScrollToEnd.IsChecked ?? true)
|
if (togScrollToEnd.IsChecked ?? true)
|
||||||
{
|
{
|
||||||
txtMsg.ScrollToEnd();
|
txtMsg.ScrollToEnd();
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
// Let layout finish without a nested dispatcher loop during a native window drag.
|
||||||
txtMsg.ScrollTo(txtMsg.LineCount, 0, AvaloniaEdit.Rendering.VisualYPosition.TextBottom, txtMsg.Bounds.Height, 0),
|
Dispatcher.UIThread.Post(() =>
|
||||||
DispatcherPriority.Background);
|
{
|
||||||
|
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">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
@@ -1192,7 +1192,7 @@
|
|||||||
<Grid
|
<Grid
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
ColumnDefinitions="Auto,Auto"
|
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
|
<TextBlock
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
@@ -1296,6 +1296,19 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="200"
|
Width="200"
|
||||||
Margin="{StaticResource Margin4}" />
|
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>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
|
|||||||
cmbCoreType6.ItemsSource = Global.CoreTypes;
|
cmbCoreType6.ItemsSource = Global.CoreTypes;
|
||||||
cmbCoreType7.ItemsSource = Global.CoreTypes;
|
cmbCoreType7.ItemsSource = Global.CoreTypes;
|
||||||
cmbCoreType9.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();
|
cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
|
||||||
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
|
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
|
||||||
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
|
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
|
||||||
@@ -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.CoreType6, v => v.cmbCoreType6.SelectedValue).DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.CoreType7, v => v.cmbCoreType7.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.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);
|
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,12 +28,12 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
@@ -72,7 +72,8 @@
|
|||||||
Margin="{StaticResource MarginLr4}"
|
Margin="{StaticResource MarginLr4}"
|
||||||
VerticalContentAlignment="Center"
|
VerticalContentAlignment="Center"
|
||||||
Text="{Binding ServerFilter, Mode=TwoWay}"
|
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>
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- Profiles grid -->
|
<!-- Profiles grid -->
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
Width="200"
|
Width="200"
|
||||||
Margin="{StaticResource MarginLr4}"
|
Margin="{StaticResource MarginLr4}"
|
||||||
VerticalContentAlignment="Center"
|
VerticalContentAlignment="Center"
|
||||||
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}" />
|
PlaceholderText="{x:Static resx:ResUI.MsgServerTitle}"
|
||||||
|
ToolTip.Tip="{x:Static resx:ResUI.MsgServerTitle}" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnAutofitColumnWidth"
|
x:Name="btnAutofitColumnWidth"
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
|||||||
|
|
||||||
private static readonly DataFormat<ProfileItemModel> LstProfilesRowFormat =
|
private static readonly DataFormat<ProfileItemModel> LstProfilesRowFormat =
|
||||||
DataFormat.CreateInProcessFormat<ProfileItemModel>("LstProfilesRow");
|
DataFormat.CreateInProcessFormat<ProfileItemModel>("LstProfilesRow");
|
||||||
|
|
||||||
private (Point, PointerPressedEventArgs)? _dragStartPoint;
|
private (Point, PointerPressedEventArgs)? _dragStartPoint;
|
||||||
|
|
||||||
private void LstProfiles_PointerPressed(object? sender, PointerPressedEventArgs e)
|
private void LstProfiles_PointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
|||||||
@@ -208,12 +208,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
|
|||||||
@@ -22,12 +22,12 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
@@ -136,7 +136,8 @@
|
|||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{x:Static resx:ResUI.LvAutoUpdateInterval}" />
|
Text="{x:Static resx:ResUI.LvAutoUpdateInterval}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -230,7 +231,8 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}"
|
||||||
|
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSelectPrevProfile"
|
x:Name="btnSelectPrevProfile"
|
||||||
Grid.Row="10"
|
Grid.Row="10"
|
||||||
@@ -250,7 +252,8 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
PlaceholderText="{x:Static resx:ResUI.LvPrevProfileTip}"
|
||||||
|
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSelectNextProfile"
|
x:Name="btnSelectNextProfile"
|
||||||
Grid.Row="11"
|
Grid.Row="11"
|
||||||
|
|||||||
@@ -20,12 +20,12 @@
|
|||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnSave"
|
x:Name="btnSave"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Content="{x:Static resx:ResUI.TbConfirm}"
|
Content="{x:Static resx:ResUI.TbConfirm}"
|
||||||
IsDefault="True" />
|
IsDefault="True" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
Width="100"
|
MinWidth="100"
|
||||||
Margin="{StaticResource MarginLr8}"
|
Margin="{StaticResource MarginLr8}"
|
||||||
Content="{x:Static resx:ResUI.TbCancel}"
|
Content="{x:Static resx:ResUI.TbCancel}"
|
||||||
IsCancel="True" />
|
IsCancel="True" />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user