mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-29 01:52:08 +08:00
Compare commits
218
Commits
v2.3.2
...
pre-autobuild
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45fce7db73 | ||
|
|
1cf8e2384e | ||
|
|
d9a5c11d6a | ||
|
|
7811714f89 | ||
|
|
e869da8d4c | ||
|
|
e414b49879 | ||
|
|
627119bb22 | ||
|
|
74ade3ee41 | ||
|
|
324628dd3d | ||
|
|
9d96ac0f6a | ||
|
|
409571f54b | ||
|
|
a995a13163 | ||
|
|
7848d6b1de | ||
|
|
5d2e114b4d | ||
|
|
c207516b47 | ||
|
|
6724f1ae35 | ||
|
|
1787d5372e | ||
|
|
4c41144dd0 | ||
|
|
27636c848f | ||
|
|
8060d699f0 | ||
|
|
f36f31a636 | ||
|
|
d300fac3d9 | ||
|
|
8c2262dd95 | ||
|
|
c438e916ca | ||
|
|
0855bd4896 | ||
|
|
f2073a2f83 | ||
|
|
a1f468202f | ||
|
|
a24bf4042c | ||
|
|
15d22b4bf6 | ||
|
|
231d264652 | ||
|
|
67ac353fd5 | ||
|
|
1c5534ad36 | ||
|
|
251678493c | ||
|
|
ccbffa14f0 | ||
|
|
dfc1f736af | ||
|
|
c54d89a465 | ||
|
|
55b95a1985 | ||
|
|
31e3104c7f | ||
|
|
58a0089b19 | ||
|
|
043ed4cb31 | ||
|
|
f64c01044c | ||
|
|
5dca724017 | ||
|
|
579f9bd1f8 | ||
|
|
7c9104a5b9 | ||
|
|
14d1531469 | ||
|
|
74e1e92607 | ||
|
|
f7a56c0eb3 | ||
|
|
e2fa76332a | ||
|
|
0daa8720cd | ||
|
|
f70b8b1213 | ||
|
|
feb3dfbe86 | ||
|
|
f38e4a6cac | ||
|
|
893188d693 | ||
|
|
b989aeb7b0 | ||
|
|
40f87c834d | ||
|
|
0bb9cb5097 | ||
|
|
b51797e238 | ||
|
|
926c095409 | ||
|
|
0c65f8ebad | ||
|
|
63f4295063 | ||
|
|
d2b38a8a3c | ||
|
|
45ddb15d56 | ||
|
|
7aef9d2a5a | ||
|
|
45fdebeaca | ||
|
|
0ea875f7f7 | ||
|
|
1b54c9fc1b | ||
|
|
89f3adcbef | ||
|
|
b13fef5ad9 | ||
|
|
9110955b63 | ||
|
|
4508d062f1 | ||
|
|
bea0dde074 | ||
|
|
3e674b186f | ||
|
|
92d9c94e87 | ||
|
|
c09066c0a3 | ||
|
|
3a7be3dfb7 | ||
|
|
09f14c23e4 | ||
|
|
eaaae3b393 | ||
|
|
3939741a06 | ||
|
|
f86a1816e0 | ||
|
|
9cbd8b4529 | ||
|
|
5dea73fc2a | ||
|
|
01af1bea23 | ||
|
|
1227e86134 | ||
|
|
c6a6ea48dd | ||
|
|
2080dbdc0f | ||
|
|
6eecd70bd5 | ||
|
|
a9951e4eca | ||
|
|
53688f332f | ||
|
|
d23d1d9a1d | ||
|
|
51ff9e1851 | ||
|
|
824814da56 | ||
|
|
040fcd059f | ||
|
|
f2339620a5 | ||
|
|
d58c0a7df5 | ||
|
|
355a18e5eb | ||
|
|
4598c805eb | ||
|
|
aa204649fa | ||
|
|
fbaff3e90c | ||
|
|
0d070fb934 | ||
|
|
c416bd5755 | ||
|
|
90406ae883 | ||
|
|
600b3dfbac | ||
|
|
72e4491dc4 | ||
|
|
76c3695567 | ||
|
|
475a09bb54 | ||
|
|
6a1fce69e0 | ||
|
|
485fd0169b | ||
|
|
a9464ff776 | ||
|
|
335ca817d2 | ||
|
|
6d112c387d | ||
|
|
e4c243de2d | ||
|
|
02f67961a9 | ||
|
|
7d5fd295ed | ||
|
|
daa0b1592d | ||
|
|
b411783bbe | ||
|
|
40a59bbc1a | ||
|
|
93fc4932ee | ||
|
|
2277d7232e | ||
|
|
435318cf1d | ||
|
|
a9a9d8a78f | ||
|
|
a2544d237e | ||
|
|
9397ac0174 | ||
|
|
4c719da096 | ||
|
|
7613417c33 | ||
|
|
a2a65cade7 | ||
|
|
3a07402aa2 | ||
|
|
59b67f5d3f | ||
|
|
e30cfc3a2f | ||
|
|
52655d9702 | ||
|
|
e93846ddc1 | ||
|
|
4cf2f6b1e6 | ||
|
|
43a3cb74ac | ||
|
|
3360339c08 | ||
|
|
f0dbe9fa60 | ||
|
|
756751b765 | ||
|
|
85a9f6c8d4 | ||
|
|
7fe0381850 | ||
|
|
537d27d10b | ||
|
|
a5fdd3f1a2 | ||
|
|
be8a632a09 | ||
|
|
95c34f1df5 | ||
|
|
8b53a7bd99 | ||
|
|
b169ee8149 | ||
|
|
9cc6dde999 | ||
|
|
cf1fbb63c4 | ||
|
|
2aa629ff5d | ||
|
|
1e2b453c24 | ||
|
|
331e4a4970 | ||
|
|
ee3ffaef1d | ||
|
|
7cc3bc83a0 | ||
|
|
558e28ddaf | ||
|
|
45e69543b3 | ||
|
|
67ee41c5ea | ||
|
|
160ed05178 | ||
|
|
5ecfe121b3 | ||
|
|
a654137af9 | ||
|
|
cb591f19fb | ||
|
|
4823a348be | ||
|
|
32da6ae808 | ||
|
|
7eb70b0f0d | ||
|
|
52e8e45daf | ||
|
|
319c5b84fa | ||
|
|
6a93ff1fc1 | ||
|
|
6069b654d1 | ||
|
|
2af8c32497 | ||
|
|
a3957289c8 | ||
|
|
3f5cd6c26a | ||
|
|
8046dad56d | ||
|
|
cdc1fd2d87 | ||
|
|
499626b946 | ||
|
|
a9cfb2cfaa | ||
|
|
7b976c16eb | ||
|
|
44e8a035aa | ||
|
|
6b57607926 | ||
|
|
c3675e48fd | ||
|
|
a66393c609 | ||
|
|
776abaf56d | ||
|
|
18808004f4 | ||
|
|
db8761946d | ||
|
|
2194a96145 | ||
|
|
ecd396d70f | ||
|
|
5ab0438397 | ||
|
|
bcde047695 | ||
|
|
109f5f9648 | ||
|
|
d16c691c0f | ||
|
|
3eb2a5b3ef | ||
|
|
569e2d5192 | ||
|
|
0077157d28 | ||
|
|
9e19bab5a7 | ||
|
|
b1e2940db6 | ||
|
|
4113cd619c | ||
|
|
1f78d576a3 | ||
|
|
e2a548f6a5 | ||
|
|
2956725e66 | ||
|
|
5131d37d58 | ||
|
|
1dfba159e0 | ||
|
|
631718a138 | ||
|
|
3172ab2d1c | ||
|
|
6a6bde3764 | ||
|
|
f756b37f97 | ||
|
|
a26c28517a | ||
|
|
894428642b | ||
|
|
36d58d05b3 | ||
|
|
80de055fc2 | ||
|
|
4905b44c8a | ||
|
|
02e19bb132 | ||
|
|
84989e0ea3 | ||
|
|
9661c5fd82 | ||
|
|
c8dfdb7a5a | ||
|
|
df5897c908 | ||
|
|
8d0af75145 | ||
|
|
36c6f0ca8d | ||
|
|
3048a2ae08 | ||
|
|
27535c7bb7 | ||
|
|
15a1770ee9 | ||
|
|
f580409ade | ||
|
|
f209d17e3c | ||
|
|
5047b0f614 |
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "Clash Verge Rev Development Environment",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04",
|
||||||
|
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"version": "20"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/rust:1": {
|
||||||
|
"version": "latest",
|
||||||
|
"profile": "default"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"rust-lang.rust-analyzer",
|
||||||
|
"tauri-apps.tauri-vscode",
|
||||||
|
"ms-vscode.vscode-typescript-next",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"ms-vscode.vscode-json",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"formulahendry.auto-rename-tag",
|
||||||
|
"ms-vscode.hexeditor",
|
||||||
|
"christian-kohler.path-intellisense",
|
||||||
|
"yzhang.markdown-all-in-one",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"ms-vscode.vscode-eslint"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"rust-analyzer.cargo.features": ["verge-dev"],
|
||||||
|
"rust-analyzer.check.command": "clippy",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[rust]": {
|
||||||
|
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[yaml]": {
|
||||||
|
"editor.defaultFormatter": "redhat.vscode-yaml"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"forwardPorts": [1420, 3000, 8080, 9090, 7890, 7891],
|
||||||
|
|
||||||
|
"portsAttributes": {
|
||||||
|
"1420": {
|
||||||
|
"label": "Tauri Dev Server",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
},
|
||||||
|
"3000": {
|
||||||
|
"label": "Vite Dev Server",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
},
|
||||||
|
"7890": {
|
||||||
|
"label": "Clash HTTP Proxy",
|
||||||
|
"onAutoForward": "silent"
|
||||||
|
},
|
||||||
|
"7891": {
|
||||||
|
"label": "Clash SOCKS Proxy",
|
||||||
|
"onAutoForward": "silent"
|
||||||
|
},
|
||||||
|
"9090": {
|
||||||
|
"label": "Clash API",
|
||||||
|
"onAutoForward": "silent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"postCreateCommand": "bash .devcontainer/post-create.sh",
|
||||||
|
|
||||||
|
"mounts": [
|
||||||
|
"source=clash-verge-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
|
||||||
|
"source=clash-verge-cargo-registry,target=/usr/local/cargo/registry,type=volume",
|
||||||
|
"source=clash-verge-cargo-git,target=/usr/local/cargo/git,type=volume"
|
||||||
|
],
|
||||||
|
|
||||||
|
"containerEnv": {
|
||||||
|
"RUST_BACKTRACE": "1",
|
||||||
|
"NODE_OPTIONS": "--max-old-space-size=4096",
|
||||||
|
"TAURI_DEV_WATCHER_IGNORE_FILE": ".taurignore"
|
||||||
|
},
|
||||||
|
|
||||||
|
"remoteUser": "vscode",
|
||||||
|
"workspaceFolder": "/workspaces/clash-verge-rev",
|
||||||
|
"shutdownAction": "stopContainer"
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 讨论交流 / Communication
|
- name: 讨论交流 / Communication
|
||||||
url: https://t.me/clash_verge_rev
|
url: https://t.me/clash_verge_rev
|
||||||
|
|||||||
@@ -294,6 +294,15 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install x86 OpenSSL (macOS only)
|
||||||
|
if: matrix.target == 'x86_64-apple-darwin'
|
||||||
|
run: |
|
||||||
|
arch -x86_64 brew install openssl@3
|
||||||
|
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
|
||||||
|
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
|
||||||
|
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
|
||||||
|
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
name: Autobuild Check Logic Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_autobuild_logic:
|
||||||
|
name: Check Autobuild Should Run Logic
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Check if version or source changed, or assets already exist
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
# # 仅用于测试逻辑,手动触发自动跳过
|
||||||
|
# if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
# echo "should_run=skip" >> $GITHUB_OUTPUT
|
||||||
|
# echo "🟡 手动触发,跳过 should_run 检查"
|
||||||
|
# exit 0
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# 确保有 HEAD~1
|
||||||
|
if ! git rev-parse HEAD~1 > /dev/null 2>&1; then
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🟢 没有前一个提交,默认需要构建"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 版本号变更判断
|
||||||
|
CURRENT_VERSION=$(jq -r '.version' package.json)
|
||||||
|
PREVIOUS_VERSION=$(git show HEAD~1:package.json | jq -r '.version' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🟢 版本号变更: $PREVIOUS_VERSION → $CURRENT_VERSION"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查 src 变更(排除常见产物与缓存)
|
||||||
|
SRC_DIFF=$(git diff --name-only HEAD~1 HEAD -- src/ | grep -Ev '^src/(dist|build|node_modules|\.next|\.cache)' || true)
|
||||||
|
TAURI_DIFF=$(git diff --name-only HEAD~1 HEAD -- src-tauri/ | grep -Ev '^src-tauri/(target|node_modules|dist|\.cache)' || true)
|
||||||
|
|
||||||
|
if [ -n "$SRC_DIFF" ] || [ -n "$TAURI_DIFF" ]; then
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🟢 源码变更 detected"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 找到最后一个修改 Tauri 相关文件的 commit
|
||||||
|
echo "🔍 查找最后一个 Tauri 相关变更的 commit..."
|
||||||
|
|
||||||
|
LAST_TAURI_COMMIT=""
|
||||||
|
for commit in $(git rev-list HEAD --max-count=50); do
|
||||||
|
# 检查此 commit 是否修改了 Tauri 相关文件
|
||||||
|
CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ')
|
||||||
|
HAS_TAURI_CHANGES=false
|
||||||
|
|
||||||
|
# 检查各个模式
|
||||||
|
if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then
|
||||||
|
HAS_TAURI_CHANGES=true
|
||||||
|
elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then
|
||||||
|
HAS_TAURI_CHANGES=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$HAS_TAURI_CHANGES" = true ]; then
|
||||||
|
LAST_TAURI_COMMIT=$(git rev-parse --short $commit)
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$LAST_TAURI_COMMIT" ]; then
|
||||||
|
echo "⚠️ 最近的 commits 中未找到 Tauri 相关变更,使用当前 commit"
|
||||||
|
LAST_TAURI_COMMIT=$(git rev-parse --short HEAD)
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short HEAD)
|
||||||
|
echo "📝 最后 Tauri 相关 commit: $LAST_TAURI_COMMIT"
|
||||||
|
echo "📝 当前 commit: $CURRENT_COMMIT"
|
||||||
|
|
||||||
|
# 检查 autobuild release 是否存在
|
||||||
|
AUTOBUILD_RELEASE_EXISTS=$(gh release view "autobuild" --json id -q '.id' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🟢 没有 autobuild release,需构建"
|
||||||
|
else
|
||||||
|
# 检查 latest.json 是否存在
|
||||||
|
LATEST_JSON_EXISTS=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -z "$LATEST_JSON_EXISTS" ]; then
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🟢 没有 latest.json,需构建"
|
||||||
|
else
|
||||||
|
# 下载并解析 latest.json 检查版本和 commit hash
|
||||||
|
echo "📥 下载 latest.json 检查版本..."
|
||||||
|
LATEST_JSON_URL=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .browser_download_url' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$LATEST_JSON_URL" ]; then
|
||||||
|
LATEST_JSON_CONTENT=$(curl -s "$LATEST_JSON_URL" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$LATEST_JSON_CONTENT" ]; then
|
||||||
|
LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "")
|
||||||
|
echo "📦 最新 autobuild 版本: $LATEST_VERSION"
|
||||||
|
|
||||||
|
# 从版本字符串中提取 commit hash (格式: X.Y.Z+autobuild.MMDD.commit)
|
||||||
|
LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "")
|
||||||
|
echo "📝 最新 autobuild commit: $LATEST_COMMIT"
|
||||||
|
|
||||||
|
if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🟢 Tauri commit hash 不匹配 ($LAST_TAURI_COMMIT != $LATEST_COMMIT),需构建"
|
||||||
|
else
|
||||||
|
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "🔴 相同 Tauri commit hash ($LAST_TAURI_COMMIT),不需构建"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "⚠️ 无法下载或解析 latest.json,需构建"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "⚠️ 无法获取 latest.json 下载 URL,需构建"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Output should_run result
|
||||||
|
run: |
|
||||||
|
echo "Result: ${{ steps.check.outputs.should_run }}"
|
||||||
+234
-109
@@ -3,12 +3,14 @@ name: Auto Build
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
# UTC+8 0,6,12,18
|
# UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
|
||||||
- cron: "0 16,22,4,10 * * *"
|
- cron: "0 4,10 * * *"
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
TAG_NAME: autobuild
|
TAG_NAME: autobuild
|
||||||
TAG_CHANNEL: AutoBuild
|
TAG_CHANNEL: AutoBuild
|
||||||
|
PRE_TAG_NAME: pre-autobuild
|
||||||
|
PRE_TAG_CHANNEL: PreAutoBuild
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: short
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -18,55 +20,10 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
check_commit:
|
check_commit:
|
||||||
name: Check Commit Needs Build
|
name: Check Commit Needs Build
|
||||||
runs-on: ubuntu-latest
|
uses: clash-verge-rev/clash-verge-rev/.github/workflows/check-commit-needs-build.yml@dev
|
||||||
outputs:
|
with:
|
||||||
should_run: ${{ steps.check.outputs.should_run }}
|
tag_name: autobuild
|
||||||
steps:
|
force_build: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Check if version changed or src changed
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
|
||||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
|
||||||
echo "Current version: $CURRENT_VERSION"
|
|
||||||
|
|
||||||
git checkout HEAD~1 package.json
|
|
||||||
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
|
||||||
echo "Previous version: $PREVIOUS_VERSION"
|
|
||||||
|
|
||||||
git checkout HEAD package.json
|
|
||||||
|
|
||||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
|
||||||
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
|
||||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
CURRENT_SRC_HASH=$(git rev-parse HEAD:src)
|
|
||||||
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
|
|
||||||
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
|
|
||||||
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
echo "Current src hash: $CURRENT_SRC_HASH"
|
|
||||||
echo "Previous src hash: $PREVIOUS_SRC_HASH"
|
|
||||||
echo "Current tauri hash: $CURRENT_TAURI_HASH"
|
|
||||||
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
|
|
||||||
|
|
||||||
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
|
|
||||||
echo "Source directories changed"
|
|
||||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "Version and source directories unchanged"
|
|
||||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
update_tag:
|
update_tag:
|
||||||
name: Update tag
|
name: Update tag
|
||||||
@@ -95,9 +52,28 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Release AutoBuild Version
|
||||||
|
run: pnpm release-version autobuild-latest
|
||||||
|
|
||||||
- name: Set Env
|
- name: Set Env
|
||||||
run: |
|
run: |
|
||||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
VERSION=$(jq -r .version package.json)
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/${{ env.PRE_TAG_NAME }}" >> $GITHUB_ENV
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
@@ -111,25 +87,24 @@ jobs:
|
|||||||
cat > release.txt << EOF
|
cat > release.txt << EOF
|
||||||
$UPDATE_LOGS
|
$UPDATE_LOGS
|
||||||
|
|
||||||
## 我应该下载哪个版本?
|
## 下载地址
|
||||||
|
|
||||||
### MacOS
|
|
||||||
- MacOS intel芯片: x64.dmg
|
|
||||||
- MacOS apple M芯片: aarch64.dmg
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
- Linux 64位: amd64.deb/amd64.rpm
|
|
||||||
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
|
||||||
- Linux armv7架构: armhf.deb/armhfp.rpm
|
|
||||||
|
|
||||||
### Windows (不再支持Win7)
|
### Windows (不再支持Win7)
|
||||||
#### 正常版本(推荐)
|
#### 正常版本(推荐)
|
||||||
- 64位: x64-setup.exe
|
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||||
- arm64架构: arm64-setup.exe
|
|
||||||
#### 便携版问题很多不再提供
|
|
||||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||||
- 64位: x64_fixed_webview2-setup.exe
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||||
- arm64架构: arm64_fixed_webview2-setup.exe
|
|
||||||
|
### macOS
|
||||||
|
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||||
|
|
||||||
|
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
@@ -140,11 +115,11 @@ jobs:
|
|||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Pre-Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.PRE_TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
|
||||||
body_path: release.txt
|
body_path: release.txt
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -152,29 +127,18 @@ jobs:
|
|||||||
|
|
||||||
clean_old_assets:
|
clean_old_assets:
|
||||||
name: Clean Old Release Assets
|
name: Clean Old Release Assets
|
||||||
runs-on: ubuntu-latest
|
needs: [check_commit, update_tag]
|
||||||
needs: update_tag
|
if: ${{ needs.check_commit.outputs.should_run == 'true' && needs.update_tag.result == 'success' }}
|
||||||
if: ${{ needs.update_tag.result == 'success' }}
|
|
||||||
steps:
|
uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev
|
||||||
- name: Checkout repository
|
with:
|
||||||
uses: actions/checkout@v4
|
tag_name: pre-autobuild
|
||||||
- name: Remove old assets from release
|
dry_run: false
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAG_NAME: ${{ env.TAG_NAME }}
|
|
||||||
run: |
|
|
||||||
VERSION=$(cat package.json | jq -r '.version')
|
|
||||||
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
|
|
||||||
for asset in $assets; do
|
|
||||||
if [[ "$asset" != *"$VERSION"* ]]; then
|
|
||||||
echo "Deleting old asset: $asset"
|
|
||||||
gh release delete-asset "$TAG_NAME" "$asset" -y
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
autobuild-x86-windows-macos-linux:
|
autobuild-x86-windows-macos-linux:
|
||||||
name: Autobuild x86 Windows, MacOS and Linux
|
name: Autobuild x86 Windows, MacOS and Linux
|
||||||
needs: update_tag
|
needs: [check_commit, update_tag]
|
||||||
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -206,6 +170,7 @@ jobs:
|
|||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
|
shared-key: autobuild-shared
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
- name: Install dependencies (ubuntu only)
|
||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
@@ -213,6 +178,15 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install x86 OpenSSL (macOS only)
|
||||||
|
if: matrix.target == 'x86_64-apple-darwin'
|
||||||
|
run: |
|
||||||
|
arch -x86_64 brew install openssl@3
|
||||||
|
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
|
||||||
|
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
|
||||||
|
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
|
||||||
|
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
with:
|
with:
|
||||||
@@ -229,10 +203,10 @@ jobs:
|
|||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
|
||||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
run: pnpm release-version autobuild-latest
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build for Windows-macOS-Linux
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
@@ -246,8 +220,8 @@ jobs:
|
|||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: ${{ env.TAG_NAME }}
|
tagName: ${{ env.PRE_TAG_NAME }}
|
||||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
releaseName: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
|
||||||
releaseBody: "More new features are now supported."
|
releaseBody: "More new features are now supported."
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
@@ -256,7 +230,8 @@ jobs:
|
|||||||
|
|
||||||
autobuild-arm-linux:
|
autobuild-arm-linux:
|
||||||
name: Autobuild ARM Linux
|
name: Autobuild ARM Linux
|
||||||
needs: update_tag
|
needs: [check_commit, update_tag]
|
||||||
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -284,6 +259,7 @@ jobs:
|
|||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
|
shared-key: autobuild-shared
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -301,8 +277,8 @@ jobs:
|
|||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
|
||||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
run: pnpm release-version autobuild-latest
|
||||||
|
|
||||||
- name: Setup for linux
|
- name: Setup for linux
|
||||||
run: |
|
run: |
|
||||||
@@ -353,7 +329,7 @@ jobs:
|
|||||||
gcc-arm-linux-gnueabihf \
|
gcc-arm-linux-gnueabihf \
|
||||||
g++-arm-linux-gnueabihf
|
g++-arm-linux-gnueabihf
|
||||||
|
|
||||||
- name: Build for Linux
|
- name: Tauri Build for Linux
|
||||||
run: |
|
run: |
|
||||||
export PKG_CONFIG_ALLOW_CROSS=1
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
|
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
|
||||||
@@ -376,11 +352,11 @@ jobs:
|
|||||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Pre-Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.PRE_TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
@@ -389,7 +365,8 @@ jobs:
|
|||||||
|
|
||||||
autobuild-x86-arm-windows_webview2:
|
autobuild-x86-arm-windows_webview2:
|
||||||
name: Autobuild x86 and ARM Windows with WebView2
|
name: Autobuild x86 and ARM Windows with WebView2
|
||||||
needs: update_tag
|
needs: [check_commit, update_tag]
|
||||||
|
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -414,6 +391,7 @@ jobs:
|
|||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
cache-all-crates: true
|
cache-all-crates: true
|
||||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||||
|
shared-key: autobuild-shared
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -431,8 +409,8 @@ jobs:
|
|||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
- name: Release ${{ env.PRE_TAG_CHANNEL }} Version
|
||||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
run: pnpm release-version autobuild-latest
|
||||||
|
|
||||||
- name: Download WebView2 Runtime
|
- name: Download WebView2 Runtime
|
||||||
run: |
|
run: |
|
||||||
@@ -441,7 +419,7 @@ jobs:
|
|||||||
Remove-Item .\src-tauri\tauri.windows.conf.json
|
Remove-Item .\src-tauri\tauri.windows.conf.json
|
||||||
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build for Windows
|
||||||
id: build
|
id: build
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
@@ -473,16 +451,163 @@ jobs:
|
|||||||
Rename-Item $file.FullName $newName
|
Rename-Item $file.FullName $newName
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Pre-Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.PRE_TAG_NAME }}
|
||||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
|
|
||||||
- name: Portable Bundle
|
- name: Portable Bundle
|
||||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
|
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.PRE_TAG_NAME }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
promote-autobuild:
|
||||||
|
name: Promote PreAutoBuild to AutoBuild
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
[
|
||||||
|
autobuild-x86-windows-macos-linux,
|
||||||
|
autobuild-arm-linux,
|
||||||
|
autobuild-x86-arm-windows_webview2,
|
||||||
|
]
|
||||||
|
if: >
|
||||||
|
${{
|
||||||
|
needs.autobuild-x86-windows-macos-linux.result == 'success' &&
|
||||||
|
needs.autobuild-arm-linux.result == 'success' &&
|
||||||
|
needs.autobuild-x86-arm-windows_webview2.result == 'success'
|
||||||
|
}}
|
||||||
|
steps:
|
||||||
|
- name: Install gh CLI
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y gh jq
|
||||||
|
|
||||||
|
- name: Download PreAutoBuild Release Assets
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
mkdir pre-assets
|
||||||
|
release_id=$(gh api repos/${{ github.repository }}/releases/tags/${{ env.PRE_TAG_NAME }} | jq .id)
|
||||||
|
gh release download ${{ env.PRE_TAG_NAME }} --dir pre-assets
|
||||||
|
|
||||||
|
body=$(gh api repos/${{ github.repository }}/releases/$release_id | jq -r .body)
|
||||||
|
echo "$body" > release.txt
|
||||||
|
|
||||||
|
- name: Publish to autobuild tag
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||||
|
body_path: release.txt
|
||||||
|
prerelease: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: pre-assets/*
|
||||||
|
|
||||||
|
notify-telegram:
|
||||||
|
name: Notify Telegram
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
[
|
||||||
|
update_tag,
|
||||||
|
autobuild-x86-windows-macos-linux,
|
||||||
|
autobuild-arm-linux,
|
||||||
|
autobuild-x86-arm-windows_webview2,
|
||||||
|
promote-autobuild,
|
||||||
|
]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Fetch UPDATE logs
|
||||||
|
id: fetch_update_logs
|
||||||
|
run: |
|
||||||
|
if [ -f "UPDATELOG.md" ]; then
|
||||||
|
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||||
|
if [ -n "$UPDATE_LOGS" ]; then
|
||||||
|
echo "Found update logs"
|
||||||
|
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "No update sections found in UPDATELOG.md"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "UPDATELOG.md file not found"
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Release AutoBuild Version
|
||||||
|
run: pnpm release-version autobuild-latest
|
||||||
|
|
||||||
|
- name: Get Version and Release Info
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install jq
|
||||||
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
|
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
|
||||||
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Generate release.txt
|
||||||
|
run: |
|
||||||
|
if [ -z "$UPDATE_LOGS" ]; then
|
||||||
|
echo "No update logs found, using default message"
|
||||||
|
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||||
|
else
|
||||||
|
echo "Using found update logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > release.txt << EOF
|
||||||
|
$UPDATE_LOGS
|
||||||
|
|
||||||
|
## 下载地址
|
||||||
|
|
||||||
|
### Windows (不再支持Win7)
|
||||||
|
#### 正常版本(推荐)
|
||||||
|
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||||
|
|
||||||
|
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||||
|
|
||||||
|
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
|
### 稳定机场VPN推荐
|
||||||
|
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
|
Created at ${{ env.BUILDTIME }}.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Send Telegram Notification
|
||||||
|
run: node scripts/telegram.mjs
|
||||||
|
env:
|
||||||
|
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
BUILD_TYPE: autobuild
|
||||||
|
VERSION: ${{ env.VERSION }}
|
||||||
|
DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }}
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
name: Check Commit Needs Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: "Release tag name to check against (default: autobuild)"
|
||||||
|
required: false
|
||||||
|
default: "autobuild"
|
||||||
|
type: string
|
||||||
|
force_build:
|
||||||
|
description: "Force build regardless of checks"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: "Release tag name to check against (default: autobuild)"
|
||||||
|
required: false
|
||||||
|
default: "autobuild"
|
||||||
|
type: string
|
||||||
|
force_build:
|
||||||
|
description: "Force build regardless of checks"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
outputs:
|
||||||
|
should_run:
|
||||||
|
description: "Whether the build should run"
|
||||||
|
value: ${{ jobs.check_commit.outputs.should_run }}
|
||||||
|
last_tauri_commit:
|
||||||
|
description: "The last commit hash with Tauri-related changes"
|
||||||
|
value: ${{ jobs.check_commit.outputs.last_tauri_commit }}
|
||||||
|
autobuild_version:
|
||||||
|
description: "The generated autobuild version string"
|
||||||
|
value: ${{ jobs.check_commit.outputs.autobuild_version }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
TAG_NAME: ${{ inputs.tag_name || 'autobuild' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_commit:
|
||||||
|
name: Check Commit Needs Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
|
last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }}
|
||||||
|
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
|
||||||
|
- name: Check if version changed or src changed
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
# Force build if requested
|
||||||
|
if [ "${{ inputs.force_build }}" == "true" ]; then
|
||||||
|
echo "🚀 Force build requested"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "📦 Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
|
git checkout HEAD~1 package.json
|
||||||
|
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "📦 Previous version: $PREVIOUS_VERSION"
|
||||||
|
|
||||||
|
git checkout HEAD package.json
|
||||||
|
|
||||||
|
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||||
|
echo "✅ Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use get_latest_tauri_commit.bash to find the latest Tauri-related commit
|
||||||
|
echo "🔍 Finding last commit with Tauri-related changes using script..."
|
||||||
|
|
||||||
|
# Make script executable
|
||||||
|
chmod +x scripts-workflow/get_latest_tauri_commit.bash
|
||||||
|
|
||||||
|
# Get the latest Tauri-related commit hash (full hash)
|
||||||
|
LAST_TAURI_COMMIT_FULL=$(./scripts-workflow/get_latest_tauri_commit.bash)
|
||||||
|
if [[ $? -ne 0 ]] || [[ -z "$LAST_TAURI_COMMIT_FULL" ]]; then
|
||||||
|
echo "❌ Failed to get Tauri-related commit, using current commit"
|
||||||
|
LAST_TAURI_COMMIT_FULL=$(git rev-parse HEAD)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get short hash for display and version tagging
|
||||||
|
LAST_TAURI_COMMIT=$(git rev-parse --short "$LAST_TAURI_COMMIT_FULL")
|
||||||
|
|
||||||
|
echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT"
|
||||||
|
|
||||||
|
# Generate autobuild version using autobuild-latest format
|
||||||
|
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
|
||||||
|
MONTH=$(TZ=Asia/Shanghai date +%m)
|
||||||
|
DAY=$(TZ=Asia/Shanghai date +%d)
|
||||||
|
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
|
||||||
|
|
||||||
|
echo "🏷️ Autobuild version: $AUTOBUILD_VERSION"
|
||||||
|
echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT"
|
||||||
|
|
||||||
|
# Set outputs for other jobs to use
|
||||||
|
echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT
|
||||||
|
echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Check if autobuild release exists
|
||||||
|
echo "🔍 Checking autobuild release and latest.json..."
|
||||||
|
AUTOBUILD_RELEASE_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json id -q '.id' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then
|
||||||
|
echo "✅ No autobuild release exists, build needed"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
# Check if latest.json exists in the release
|
||||||
|
LATEST_JSON_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -z "$LATEST_JSON_EXISTS" ]; then
|
||||||
|
echo "✅ No latest.json found in autobuild release, build needed"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
# Download and parse latest.json to check version and commit hash
|
||||||
|
echo "📥 Downloading latest.json to check version..."
|
||||||
|
LATEST_JSON_URL="https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild/latest.json"
|
||||||
|
|
||||||
|
LATEST_JSON_CONTENT=$(curl -sL "$LATEST_JSON_URL" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$LATEST_JSON_CONTENT" ]; then
|
||||||
|
LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "")
|
||||||
|
echo "📦 Latest autobuild version: $LATEST_VERSION"
|
||||||
|
|
||||||
|
# Extract commit hash from version string (format: X.Y.Z+autobuild.MMDD.commit)
|
||||||
|
LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "")
|
||||||
|
echo "📝 Latest autobuild commit: $LATEST_COMMIT"
|
||||||
|
|
||||||
|
if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then
|
||||||
|
echo "✅ Tauri commit hash mismatch ($LAST_TAURI_COMMIT != $LATEST_COMMIT), build needed"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "❌ Same Tauri commit hash ($LAST_TAURI_COMMIT), no build needed"
|
||||||
|
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Failed to download or parse latest.json, build needed"
|
||||||
|
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
name: Clean Old Assets
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: "Release tag name to clean (default: autobuild)"
|
||||||
|
required: false
|
||||||
|
default: "autobuild"
|
||||||
|
type: string
|
||||||
|
dry_run:
|
||||||
|
description: "Dry run mode (only show what would be deleted)"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: "Release tag name to clean (default: autobuild)"
|
||||||
|
required: false
|
||||||
|
default: "autobuild"
|
||||||
|
type: string
|
||||||
|
dry_run:
|
||||||
|
description: "Dry run mode (only show what would be deleted)"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
permissions: write-all
|
||||||
|
|
||||||
|
env:
|
||||||
|
TAG_NAME: ${{ inputs.tag_name || 'autobuild' }}
|
||||||
|
TAG_CHANNEL: AutoBuild
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_current_version:
|
||||||
|
name: Check Current Version and Commit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
current_version: ${{ steps.check.outputs.current_version }}
|
||||||
|
last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }}
|
||||||
|
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
|
||||||
|
- name: Get current version and find last Tauri commit
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "📦 Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
|
# Find the last commit that changed Tauri-related files
|
||||||
|
echo "🔍 Finding last commit with Tauri-related changes..."
|
||||||
|
|
||||||
|
# Define patterns for Tauri-related files
|
||||||
|
TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities"
|
||||||
|
|
||||||
|
# Get the last commit that changed any of these patterns (excluding build artifacts)
|
||||||
|
LAST_TAURI_COMMIT=""
|
||||||
|
for commit in $(git rev-list HEAD --max-count=50); do
|
||||||
|
# Check if this commit changed any Tauri-related files
|
||||||
|
CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ')
|
||||||
|
HAS_TAURI_CHANGES=false
|
||||||
|
|
||||||
|
# Check each pattern
|
||||||
|
if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then
|
||||||
|
HAS_TAURI_CHANGES=true
|
||||||
|
elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then
|
||||||
|
HAS_TAURI_CHANGES=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$HAS_TAURI_CHANGES" = true ]; then
|
||||||
|
LAST_TAURI_COMMIT=$(git rev-parse --short $commit)
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$LAST_TAURI_COMMIT" ]; then
|
||||||
|
echo "⚠️ No Tauri-related changes found in recent commits, using current commit"
|
||||||
|
LAST_TAURI_COMMIT=$(git rev-parse --short HEAD)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT"
|
||||||
|
echo "📝 Current commit: $(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
|
# Generate autobuild version for consistency
|
||||||
|
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
|
||||||
|
MONTH=$(TZ=Asia/Shanghai date +%m)
|
||||||
|
DAY=$(TZ=Asia/Shanghai date +%d)
|
||||||
|
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
|
||||||
|
|
||||||
|
echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION"
|
||||||
|
|
||||||
|
# Set outputs for other jobs to use
|
||||||
|
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT
|
||||||
|
echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
clean_old_assets:
|
||||||
|
name: Clean Old Release Assets
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: check_current_version
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Clean old assets from release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAG_NAME: ${{ env.TAG_NAME }}
|
||||||
|
DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
run: |
|
||||||
|
# Use values from check_current_version job
|
||||||
|
CURRENT_AUTOBUILD_VERSION="${{ needs.check_current_version.outputs.autobuild_version }}"
|
||||||
|
LAST_TAURI_COMMIT="${{ needs.check_current_version.outputs.last_tauri_commit }}"
|
||||||
|
CURRENT_VERSION="${{ needs.check_current_version.outputs.current_version }}"
|
||||||
|
|
||||||
|
echo "📦 Current version: $CURRENT_VERSION"
|
||||||
|
echo "📦 Current autobuild version: $CURRENT_AUTOBUILD_VERSION"
|
||||||
|
echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT"
|
||||||
|
echo "🏷️ Target tag: $TAG_NAME"
|
||||||
|
echo "🔍 Dry run mode: $DRY_RUN"
|
||||||
|
|
||||||
|
# Check if release exists
|
||||||
|
RELEASE_EXISTS=$(gh release view "$TAG_NAME" --json id -q '.id' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_EXISTS" ]; then
|
||||||
|
echo "❌ Release '$TAG_NAME' not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Found release '$TAG_NAME'"
|
||||||
|
|
||||||
|
# Get all assets
|
||||||
|
echo "📋 Getting list of all assets..."
|
||||||
|
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
|
||||||
|
|
||||||
|
if [ -z "$assets" ]; then
|
||||||
|
echo "ℹ️ No assets found in release '$TAG_NAME'"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📋 Found assets:"
|
||||||
|
echo "$assets" | sed 's/^/ - /'
|
||||||
|
|
||||||
|
# Count assets to keep and delete
|
||||||
|
ASSETS_TO_KEEP=""
|
||||||
|
ASSETS_TO_DELETE=""
|
||||||
|
|
||||||
|
for asset in $assets; do
|
||||||
|
# Keep assets that match current autobuild version or are non-versioned files (like latest.json)
|
||||||
|
if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then
|
||||||
|
ASSETS_TO_KEEP="$ASSETS_TO_KEEP$asset\n"
|
||||||
|
else
|
||||||
|
ASSETS_TO_DELETE="$ASSETS_TO_DELETE$asset\n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔒 Assets to keep (current version: $CURRENT_AUTOBUILD_VERSION):"
|
||||||
|
if [ -n "$ASSETS_TO_KEEP" ]; then
|
||||||
|
echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | sed 's/^/ - /'
|
||||||
|
else
|
||||||
|
echo " - None"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🗑️ Assets to delete:"
|
||||||
|
if [ -n "$ASSETS_TO_DELETE" ]; then
|
||||||
|
echo -e "$ASSETS_TO_DELETE" | grep -v '^$' | sed 's/^/ - /'
|
||||||
|
else
|
||||||
|
echo " - None"
|
||||||
|
echo "ℹ️ No old assets to clean"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "🔍 DRY RUN MODE: No assets will actually be deleted"
|
||||||
|
echo " To actually delete these assets, run this workflow again with dry_run=false"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "🗑️ Deleting old assets..."
|
||||||
|
|
||||||
|
DELETED_COUNT=0
|
||||||
|
FAILED_COUNT=0
|
||||||
|
|
||||||
|
for asset in $assets; do
|
||||||
|
# Skip assets that should be kept
|
||||||
|
if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Deleting: $asset"
|
||||||
|
if gh release delete-asset "$TAG_NAME" "$asset" -y 2>/dev/null; then
|
||||||
|
DELETED_COUNT=$((DELETED_COUNT + 1))
|
||||||
|
else
|
||||||
|
echo " ⚠️ Failed to delete $asset"
|
||||||
|
FAILED_COUNT=$((FAILED_COUNT + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📊 Cleanup summary:"
|
||||||
|
echo " - Deleted: $DELETED_COUNT assets"
|
||||||
|
if [ $FAILED_COUNT -gt 0 ]; then
|
||||||
|
echo " - Failed: $FAILED_COUNT assets"
|
||||||
|
fi
|
||||||
|
echo " - Kept: $(echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | wc -l) assets"
|
||||||
|
|
||||||
|
if [ $FAILED_COUNT -gt 0 ]; then
|
||||||
|
echo "⚠️ Some assets failed to delete. Please check the logs above."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ Cleanup completed successfully!"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
+26
-13
@@ -13,14 +13,16 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
run_macos_x86_64:
|
run_windows_arm64:
|
||||||
description: "运行 macOS x86_64"
|
description: "运行 Windows ARM64"
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
|
TAG_NAME: deploytest
|
||||||
|
TAG_CHANNEL: DeployTest
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
RUST_BACKTRACE: short
|
RUST_BACKTRACE: short
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -43,14 +45,18 @@ jobs:
|
|||||||
bundle: dmg
|
bundle: dmg
|
||||||
id: macos-aarch64
|
id: macos-aarch64
|
||||||
input: run_macos_aarch64
|
input: run_macos_aarch64
|
||||||
- os: macos-latest
|
- os: windows-latest
|
||||||
target: x86_64-apple-darwin
|
target: aarch64-pc-windows-msvc
|
||||||
bundle: dmg
|
bundle: nsis
|
||||||
id: macos-x86_64
|
id: windows-arm64
|
||||||
input: run_macos_x86_64
|
input: run_windows_arm64
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Skip job if not selected
|
||||||
|
if: github.event.inputs[matrix.input] != 'true'
|
||||||
|
run: echo "Job ${{ matrix.id }} skipped as requested"
|
||||||
|
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -69,12 +75,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
save-if: false
|
save-if: false
|
||||||
|
cache-all-crates: true
|
||||||
- name: Install Node
|
shared-key: autobuild-shared
|
||||||
if: github.event.inputs[matrix.input] == 'true'
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -82,12 +84,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||||
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
|
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||||
|
|
||||||
- name: Tauri build
|
- name: Tauri build
|
||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name: Clippy Lint
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
clippy:
|
clippy:
|
||||||
@@ -32,6 +33,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workspaces: src-tauri
|
workspaces: src-tauri
|
||||||
save-if: false
|
save-if: false
|
||||||
|
cache-all-crates: false
|
||||||
|
shared-key: autobuild-shared
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
- name: Install dependencies (ubuntu only)
|
||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
@@ -39,21 +42,25 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
run: |
|
run: |
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run prebuild ${{ matrix.target }}
|
pnpm run prebuild ${{ matrix.target }}
|
||||||
|
|
||||||
|
# This workflow runs linting using cargo clippy.
|
||||||
|
# Note: If the web build step is skipped,
|
||||||
|
# cargo clippy will fail to run due to missing web dist in the Tauri environment.
|
||||||
- name: Build Web Assets
|
- name: Build Web Assets
|
||||||
run: pnpm run web:build
|
run: pnpm run web:build
|
||||||
env:
|
env:
|
||||||
+237
-17
@@ -5,10 +5,6 @@ on:
|
|||||||
# ! 不再使用 workflow_dispatch 触发。
|
# ! 不再使用 workflow_dispatch 触发。
|
||||||
# workflow_dispatch:
|
# workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
# 应当限制在 main 分支上触发发布。
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
# 应当限制 v*.*.* 的 tag 触发发布。
|
|
||||||
tags:
|
tags:
|
||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
@@ -27,22 +23,132 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check if tag is from main branch
|
||||||
|
run: |
|
||||||
|
TAG_REF="${GITHUB_REF##*/}"
|
||||||
|
echo "Checking if tag $TAG_REF is from main branch..."
|
||||||
|
|
||||||
|
TAG_COMMIT=$(git rev-list -n 1 $TAG_REF)
|
||||||
|
MAIN_COMMITS=$(git rev-list origin/main)
|
||||||
|
|
||||||
|
if echo "$MAIN_COMMITS" | grep -q "$TAG_COMMIT"; then
|
||||||
|
echo "✅ Tag $TAG_REF is from main branch"
|
||||||
|
else
|
||||||
|
echo "❌ Tag $TAG_REF is not from main branch"
|
||||||
|
echo "This release workflow only accepts tags from main branch."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Check tag and package.json version
|
- name: Check tag and package.json version
|
||||||
run: |
|
run: |
|
||||||
TAG_REF="${GITHUB_REF##*/}"
|
TAG_REF="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
|
||||||
echo "Current tag: $TAG_REF"
|
echo "Current tag: $TAG_REF"
|
||||||
|
|
||||||
PKG_VERSION=$(jq -r .version package.json)
|
PKG_VERSION=$(jq -r .version package.json)
|
||||||
echo "package.json version: $PKG_VERSION"
|
echo "package.json version: $PKG_VERSION"
|
||||||
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
|
|
||||||
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
|
EXPECTED_TAG="v$PKG_VERSION"
|
||||||
|
|
||||||
|
if [[ "$TAG_REF" != "$EXPECTED_TAG" ]]; then
|
||||||
|
echo "❌ Version mismatch:"
|
||||||
|
echo " Git tag : $TAG_REF"
|
||||||
|
echo " package.json : $EXPECTED_TAG"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Tag and package.json version are consistent."
|
|
||||||
|
echo "✅ Tag and package.json version are consistent."
|
||||||
|
|
||||||
|
update_tag:
|
||||||
|
name: Update tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release, release-for-linux-arm, release-for-fixed-webview2]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Fetch UPDATE logs
|
||||||
|
id: fetch_update_logs
|
||||||
|
run: |
|
||||||
|
if [ -f "UPDATELOG.md" ]; then
|
||||||
|
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||||
|
if [ -n "$UPDATE_LOGS" ]; then
|
||||||
|
echo "Found update logs"
|
||||||
|
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "No update sections found in UPDATELOG.md"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "UPDATELOG.md file not found"
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Set Env
|
||||||
|
run: |
|
||||||
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
TAG_REF="${GITHUB_REF##*/}"
|
||||||
|
echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV
|
||||||
|
VERSION=$(echo "$TAG_REF" | sed 's/^v//')
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/$TAG_REF" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
if [ -z "$UPDATE_LOGS" ]; then
|
||||||
|
echo "No update logs found, using default message"
|
||||||
|
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||||
|
else
|
||||||
|
echo "Using found update logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > release.txt << EOF
|
||||||
|
$UPDATE_LOGS
|
||||||
|
|
||||||
|
## 下载地址
|
||||||
|
|
||||||
|
### Windows (不再支持Win7)
|
||||||
|
#### 正常版本(推荐)
|
||||||
|
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||||
|
|
||||||
|
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||||
|
|
||||||
|
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
|
### 稳定机场VPN推荐
|
||||||
|
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
Created at ${{ env.BUILDTIME }}.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
name: "Clash Verge Rev ${{ env.TAG_NAME }}"
|
||||||
|
body_path: release.txt
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# generate_release_notes: true
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Release Build
|
name: Release Build
|
||||||
needs: check_tag_version
|
needs: [check_tag_version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -81,6 +187,15 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install x86 OpenSSL (macOS only)
|
||||||
|
if: matrix.target == 'x86_64-apple-darwin'
|
||||||
|
run: |
|
||||||
|
arch -x86_64 brew install openssl@3
|
||||||
|
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
|
||||||
|
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
|
||||||
|
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
|
||||||
|
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@@ -110,14 +225,18 @@ jobs:
|
|||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: v__VERSION__
|
tagName: ${{ github.ref_name }}
|
||||||
releaseName: "Clash Verge Rev v__VERSION__"
|
releaseName: "Clash Verge Rev ${{ github.ref_name }}"
|
||||||
releaseBody: "More new features are now supported."
|
releaseBody: "Draft release, will be updated later."
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
tauriScript: pnpm
|
tauriScript: pnpm
|
||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
|
includeUpdaterJson: true
|
||||||
|
|
||||||
release-for-linux-arm:
|
release-for-linux-arm:
|
||||||
name: Release Build for Linux ARM
|
name: Release Build for Linux ARM
|
||||||
|
needs: [check_tag_version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -232,7 +351,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: v${{env.VERSION}}
|
tag_name: v${{env.VERSION}}
|
||||||
name: "Clash Verge Rev v${{env.VERSION}}"
|
name: "Clash Verge Rev v${{env.VERSION}}"
|
||||||
body: "More new features are now supported."
|
body: "See release notes for detailed changelog."
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: |
|
files: |
|
||||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
@@ -240,6 +359,7 @@ jobs:
|
|||||||
|
|
||||||
release-for-fixed-webview2:
|
release-for-fixed-webview2:
|
||||||
name: Release Build for Fixed WebView2
|
name: Release Build for Fixed WebView2
|
||||||
|
needs: [check_tag_version]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -323,7 +443,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: v${{steps.build.outputs.appVersion}}
|
tag_name: v${{steps.build.outputs.appVersion}}
|
||||||
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
|
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
|
||||||
body: "More new features are now supported."
|
body: "See release notes for detailed changelog."
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||||
|
|
||||||
@@ -335,7 +455,7 @@ jobs:
|
|||||||
release-update:
|
release-update:
|
||||||
name: Release Update
|
name: Release Update
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [release, release-for-linux-arm]
|
needs: [update_tag]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -360,7 +480,7 @@ jobs:
|
|||||||
|
|
||||||
release-update-for-fixed-webview2:
|
release-update-for-fixed-webview2:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [release-for-fixed-webview2]
|
needs: [update_tag]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -386,7 +506,7 @@ jobs:
|
|||||||
submit-to-winget:
|
submit-to-winget:
|
||||||
name: Submit to Winget
|
name: Submit to Winget
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [release-update]
|
needs: [update_tag, release-update]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -405,3 +525,103 @@ jobs:
|
|||||||
release-tag: v${{env.VERSION}}
|
release-tag: v${{env.VERSION}}
|
||||||
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
|
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|
||||||
|
notify-telegram:
|
||||||
|
name: Notify Telegram
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
[
|
||||||
|
update_tag,
|
||||||
|
release-update,
|
||||||
|
release-update-for-fixed-webview2,
|
||||||
|
submit-to-winget,
|
||||||
|
]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Fetch UPDATE logs
|
||||||
|
id: fetch_update_logs
|
||||||
|
run: |
|
||||||
|
if [ -f "UPDATELOG.md" ]; then
|
||||||
|
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||||
|
if [ -n "$UPDATE_LOGS" ]; then
|
||||||
|
echo "Found update logs"
|
||||||
|
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "No update sections found in UPDATELOG.md"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "UPDATELOG.md file not found"
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Get Version and Release Info
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install jq
|
||||||
|
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
|
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||||
|
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Generate release.txt
|
||||||
|
run: |
|
||||||
|
if [ -z "$UPDATE_LOGS" ]; then
|
||||||
|
echo "No update logs found, using default message"
|
||||||
|
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||||
|
else
|
||||||
|
echo "Using found update logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > release.txt << EOF
|
||||||
|
$UPDATE_LOGS
|
||||||
|
|
||||||
|
## 下载地址
|
||||||
|
|
||||||
|
### Windows (不再支持Win7)
|
||||||
|
#### 正常版本(推荐)
|
||||||
|
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||||
|
|
||||||
|
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||||
|
|
||||||
|
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||||
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
|
### 稳定机场VPN推荐
|
||||||
|
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
Created at ${{ env.BUILDTIME }}.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Send Telegram Notification
|
||||||
|
run: node scripts/telegram.mjs
|
||||||
|
env:
|
||||||
|
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
BUILD_TYPE: release
|
||||||
|
VERSION: ${{ env.VERSION }}
|
||||||
|
DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }}
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ scripts/_env.sh
|
|||||||
.tool-versions
|
.tool-versions
|
||||||
.idea
|
.idea
|
||||||
.old
|
.old
|
||||||
|
.eslintcache
|
||||||
|
|||||||
+12
-2
@@ -12,6 +12,11 @@ Before you start contributing to the project, you need to set up your developmen
|
|||||||
|
|
||||||
### Setup for Windows Users
|
### Setup for Windows Users
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **If you are using a Windows ARM device, you additionally need to install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the environment variable.**
|
||||||
|
>
|
||||||
|
> Because the `ring` crate is compiled based on `clang` under Windows ARM.
|
||||||
|
|
||||||
If you're a Windows user, you may need to perform some additional steps:
|
If you're a Windows user, you may need to perform some additional steps:
|
||||||
|
|
||||||
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
|
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
|
||||||
@@ -51,11 +56,14 @@ apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev
|
|||||||
You have two options for downloading the clash binary:
|
You have two options for downloading the clash binary:
|
||||||
|
|
||||||
- Automatically download it via the provided script:
|
- Automatically download it via the provided script:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm run prebuild
|
pnpm run prebuild
|
||||||
# Use '--force' to force update to the latest version
|
# Use '--force' or '-f' to update both the Mihomo core version
|
||||||
# pnpm run prebuild --force
|
# and the Clash Verge Rev service version to the latest available.
|
||||||
|
pnpm run prebuild --force
|
||||||
```
|
```
|
||||||
|
|
||||||
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||||
|
|
||||||
### Run the Development Server
|
### Run the Development Server
|
||||||
@@ -66,6 +74,8 @@ To run the development server, use the following command:
|
|||||||
pnpm dev
|
pnpm dev
|
||||||
# If an app instance already exists, use a different command
|
# If an app instance already exists, use a different command
|
||||||
pnpm dev:diff
|
pnpm dev:diff
|
||||||
|
# To using tauri built-in dev tool
|
||||||
|
pnpm dev:tauri
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build the Project
|
### Build the Project
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
|||||||
|
|
||||||
#### 我应当怎样选择发行版
|
#### 我应当怎样选择发行版
|
||||||
|
|
||||||
| 版本 | 特征 | 链接 |
|
| 版本 | 特征 | 链接 |
|
||||||
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
|
| :---------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||||
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||||
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
| Alpha(废弃) | 测试发布流程。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
||||||
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
| AutoBuild | 滚动更新版,适合测试反馈,可能存在缺陷。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
||||||
|
|
||||||
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
|
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ To run the development server, execute the following commands after all prerequi
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm run check
|
pnpm run prebuild
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+151
-7
@@ -1,3 +1,147 @@
|
|||||||
|
## v2.4.3
|
||||||
|
|
||||||
|
### 🚀 优化改进
|
||||||
|
|
||||||
|
- 重构并简化服务模式启动检测流程,消除重复检测
|
||||||
|
- 重构并简化窗口创建流程
|
||||||
|
- 重构日志系统,单个日志默认最大 10 MB
|
||||||
|
- 优化前端资源占用
|
||||||
|
|
||||||
|
### 🐞 修复问题
|
||||||
|
|
||||||
|
- 优化服务模式重装逻辑,避免不必要的重复检查
|
||||||
|
- 修复轻量模式退出无响应的问题
|
||||||
|
- 修复托盘轻量模式支持退出/进入
|
||||||
|
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
|
||||||
|
- macOS Tun/系统代理 模式下图标大小不统一
|
||||||
|
- 托盘节点切换不再显示隐藏组
|
||||||
|
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
|
||||||
|
|
||||||
|
## v2.4.2
|
||||||
|
|
||||||
|
### ✨ 新增功能
|
||||||
|
|
||||||
|
- 增加托盘节点选择
|
||||||
|
|
||||||
|
### 🚀 性能优化
|
||||||
|
|
||||||
|
- 优化前端首页加载速度
|
||||||
|
- 优化前端未使用 i18n 文件缓存
|
||||||
|
- 优化后端内存占用
|
||||||
|
- 优化后端启动速度
|
||||||
|
|
||||||
|
### 🐞 修复问题
|
||||||
|
|
||||||
|
- 修复首页节点切换失效的问题
|
||||||
|
- 修复和优化服务检查流程
|
||||||
|
- 修复2.4.1引入的订阅地址重定向报错问题
|
||||||
|
- 修复 rpm/deb 包名称问题
|
||||||
|
- 修复托盘轻量模式状态检测异常
|
||||||
|
- 修复通过 scheme 导入订阅崩溃
|
||||||
|
- 修复单例检测实效
|
||||||
|
- 修复启动阶段可能导致的无法连接内核
|
||||||
|
- 修复导入订阅无法 Auth Basic
|
||||||
|
|
||||||
|
### 👙 界面样式
|
||||||
|
|
||||||
|
- 简化和改进代理设置样式
|
||||||
|
|
||||||
|
## v2.4.1
|
||||||
|
|
||||||
|
### 🏆 重大改进
|
||||||
|
|
||||||
|
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
|
||||||
|
|
||||||
|
### ✨ 新增功能
|
||||||
|
|
||||||
|
- **Mihomo(Meta) 内核升级至 v1.19.13**
|
||||||
|
|
||||||
|
### 🚀 性能优化
|
||||||
|
|
||||||
|
- 优化热键响应速度,提升快捷键操作体验
|
||||||
|
- 改进服务管理响应性,减少系统服务操作等待时间
|
||||||
|
- 提升文件和配置处理性能
|
||||||
|
- 优化任务管理和日志记录效率
|
||||||
|
- 优化异步内存管理,减少内存占用并提升多任务处理效率
|
||||||
|
- 优化启动阶段初始化性能
|
||||||
|
|
||||||
|
### 🐞 修复问题
|
||||||
|
|
||||||
|
- 修复应用在某些操作中可能出现的响应延迟问题
|
||||||
|
- 修复任务管理中的潜在并发问题
|
||||||
|
- 修复通过托盘重启应用无法恢复
|
||||||
|
- 修复订阅在某些情况下无法导入
|
||||||
|
- 修复无法新建订阅时使用远程链接
|
||||||
|
- 修复卸载服务后的 tun 开关状态问题
|
||||||
|
- 修复页面快速切换订阅时导致崩溃
|
||||||
|
- 修复丢失工作目录时无法恢复环境
|
||||||
|
- 修复从轻量模式恢复导致崩溃
|
||||||
|
|
||||||
|
### 👙 界面样式
|
||||||
|
|
||||||
|
- 统一代理设置样式
|
||||||
|
|
||||||
|
### 🗑️ 移除内容
|
||||||
|
|
||||||
|
- 移除启动阶段自动清理过期订阅
|
||||||
|
|
||||||
|
## v2.4.0
|
||||||
|
|
||||||
|
**发行代号:融**
|
||||||
|
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
|
||||||
|
|
||||||
|
### 🏆 重大改进
|
||||||
|
|
||||||
|
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
|
||||||
|
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
|
||||||
|
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
|
||||||
|
|
||||||
|
### ✨ 新增功能
|
||||||
|
|
||||||
|
- **Mihomo(Meta) 内核升级至 v1.19.12**
|
||||||
|
- 新增版本信息复制按钮
|
||||||
|
- 增强型流量监控,支持更详细的数据分析
|
||||||
|
- 新增流量图表多种显示模式
|
||||||
|
- 新增强制刷新配置和节点缓存功能
|
||||||
|
- 首页流量统计支持查看刻度线详情
|
||||||
|
|
||||||
|
### 🚀 性能优化
|
||||||
|
|
||||||
|
- 全面提升数据传输和处理效率
|
||||||
|
- 优化内存使用,减少系统资源消耗
|
||||||
|
- 改进流量图表渲染性能
|
||||||
|
- 优化配置和节点刷新策略,从5秒延长到60秒
|
||||||
|
- 改进数据缓存机制,减少重复请求
|
||||||
|
- 优化异步程序性能
|
||||||
|
|
||||||
|
### 🐞 修复问题
|
||||||
|
|
||||||
|
- 修复系统代理状态检测和显示不一致问题
|
||||||
|
- 修复系统主题窗口颜色不一致问题
|
||||||
|
- 修复特殊字符 URL 处理问题
|
||||||
|
- 修复配置修改后缓存不同步问题
|
||||||
|
- 修复 Windows 安装器自启设置问题
|
||||||
|
- 修复 macOS 下 Dock 图标恢复窗口问题
|
||||||
|
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
|
||||||
|
- 修复架构升级后节点测速功能异常
|
||||||
|
- 修复架构升级后流量统计功能异常
|
||||||
|
- 修复架构升级后日志功能异常
|
||||||
|
- 修复外部控制器跨域配置保存问题
|
||||||
|
- 修复首页端口显示不一致问题
|
||||||
|
- 修复首页流量统计刻度线显示问题
|
||||||
|
- 修复日志页面按钮功能混淆问题
|
||||||
|
- 修复日志等级设置保存问题
|
||||||
|
- 修复日志等级异常过滤
|
||||||
|
- 修复清理日志天数功能异常
|
||||||
|
- 修复偶发性启动卡死问题
|
||||||
|
- 修复首页虚拟网卡开关在管理模式下的状态问题
|
||||||
|
|
||||||
|
### 🔧 技术改进
|
||||||
|
|
||||||
|
- 统一使用新的内核通信方式
|
||||||
|
- 新增外部控制器配置界面
|
||||||
|
- 改进跨平台兼容性支持
|
||||||
|
|
||||||
## v2.3.2
|
## v2.3.2
|
||||||
|
|
||||||
### 🐞 修复问题
|
### 🐞 修复问题
|
||||||
@@ -390,7 +534,7 @@
|
|||||||
|
|
||||||
- 新增窗口状态实时监控与自动保存功能
|
- 新增窗口状态实时监控与自动保存功能
|
||||||
- 增强核心配置变更时的验证与错误处理机制
|
- 增强核心配置变更时的验证与错误处理机制
|
||||||
- 支持通过环境变量`CLASH_VERGE_REV_IP`自定义复制IP地址
|
- 支持通过环境变量 `CLASH_VERGE_REV_IP`自定义复制IP地址
|
||||||
- 添加连接表列宽持久化设置与进程过滤功能
|
- 添加连接表列宽持久化设置与进程过滤功能
|
||||||
- 新增代理组首字母导航与动态滚动定位功能
|
- 新增代理组首字母导航与动态滚动定位功能
|
||||||
- 实现连接追踪暂停/恢复功能
|
- 实现连接追踪暂停/恢复功能
|
||||||
@@ -711,7 +855,7 @@
|
|||||||
- 禁用部分 Webview2 快捷键
|
- 禁用部分 Webview2 快捷键
|
||||||
- 热键配置新增连接符 + 号
|
- 热键配置新增连接符 + 号
|
||||||
- 新增部分悬浮提示按钮,用于解释说明
|
- 新增部分悬浮提示按钮,用于解释说明
|
||||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||||
- 设置页面右上角新增 TG 频道链接
|
- 设置页面右上角新增 TG 频道链接
|
||||||
- 各种细节优化和界面性能优化
|
- 各种细节优化和界面性能优化
|
||||||
|
|
||||||
@@ -751,7 +895,7 @@
|
|||||||
- 禁用部分 Webview2 快捷键
|
- 禁用部分 Webview2 快捷键
|
||||||
- 热键配置新增连接符 + 号
|
- 热键配置新增连接符 + 号
|
||||||
- 新增部分悬浮提示按钮,用于解释说明
|
- 新增部分悬浮提示按钮,用于解释说明
|
||||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||||
- 设置页面右上角新增 TG 频道链接
|
- 设置页面右上角新增 TG 频道链接
|
||||||
- 各种细节优化和界面性能优化
|
- 各种细节优化和界面性能优化
|
||||||
|
|
||||||
@@ -787,7 +931,7 @@
|
|||||||
- 禁用部分 Webview2 快捷键
|
- 禁用部分 Webview2 快捷键
|
||||||
- 热键配置新增连接符 + 号
|
- 热键配置新增连接符 + 号
|
||||||
- 新增部分悬浮提示按钮,用于解释说明
|
- 新增部分悬浮提示按钮,用于解释说明
|
||||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||||
- 设置页面右上角新增 TG 频道链接
|
- 设置页面右上角新增 TG 频道链接
|
||||||
|
|
||||||
### Bugs Fixes
|
### Bugs Fixes
|
||||||
@@ -961,7 +1105,7 @@
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
- 缓存代理组图标
|
- 缓存代理组图标
|
||||||
- 使用`boa_engine` 代替 `rquickjs`
|
- 使用 `boa_engine` 代替 `rquickjs`
|
||||||
- 支持 Linux armv7
|
- 支持 Linux armv7
|
||||||
|
|
||||||
### Bugs Fixes
|
### Bugs Fixes
|
||||||
@@ -1026,7 +1170,7 @@
|
|||||||
- 支持自定义托盘图标
|
- 支持自定义托盘图标
|
||||||
- 支持禁用代理组图标
|
- 支持禁用代理组图标
|
||||||
- 代理组显示当前代理
|
- 代理组显示当前代理
|
||||||
- 修改 `打开面板` 快捷键为`打开/关闭面板`
|
- 修改 `打开面板` 快捷键为 `打开/关闭面板`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1190,7 +1334,7 @@
|
|||||||
|
|
||||||
### Bugs Fixes
|
### Bugs Fixes
|
||||||
|
|
||||||
- Windows 下更新时无法覆盖`clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
|
- Windows 下更新时无法覆盖 `clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
|
||||||
- 窗口最大化按钮变化问题
|
- 窗口最大化按钮变化问题
|
||||||
- 窗口尺寸保存错误问题
|
- 窗口尺寸保存错误问题
|
||||||
- 复制环境变量类型无法切换问题
|
- 复制环境变量类型无法切换问题
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import configPrettier from "eslint-config-prettier";
|
||||||
|
import pluginImport from "eslint-plugin-import";
|
||||||
|
import pluginPrettier from "eslint-plugin-prettier";
|
||||||
|
import pluginReact from "eslint-plugin-react";
|
||||||
|
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import pluginReactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import pluginUnusedImports from "eslint-plugin-unused-imports";
|
||||||
|
import { defineConfig } from "eslint/config";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
js,
|
||||||
|
react: pluginReact,
|
||||||
|
"react-hooks": pluginReactHooks,
|
||||||
|
import: pluginImport,
|
||||||
|
"react-refresh": pluginReactRefresh,
|
||||||
|
"unused-imports": pluginUnusedImports,
|
||||||
|
prettier: pluginPrettier,
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: [
|
||||||
|
"js/recommended",
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
pluginReact.configs.flat.recommended,
|
||||||
|
pluginReact.configs.flat["jsx-runtime"],
|
||||||
|
configPrettier,
|
||||||
|
],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
"import/resolver": {
|
||||||
|
typescript: {
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
// React
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "error",
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
|
||||||
|
// TypeScript
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
|
||||||
|
// unused-imports 代替 no-unused-vars
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"unused-imports/no-unused-imports": "error",
|
||||||
|
"unused-imports/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
vars: "all",
|
||||||
|
varsIgnorePattern: "^_+$",
|
||||||
|
args: "after-used",
|
||||||
|
argsIgnorePattern: "^_+$",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Import
|
||||||
|
"import/no-unresolved": "error",
|
||||||
|
"import/order": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal",
|
||||||
|
"parent",
|
||||||
|
"sibling",
|
||||||
|
"index",
|
||||||
|
],
|
||||||
|
"newlines-between": "always",
|
||||||
|
alphabetize: {
|
||||||
|
order: "asc",
|
||||||
|
caseInsensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// 其他常见
|
||||||
|
"prefer-const": "warn",
|
||||||
|
"no-case-declarations": "error",
|
||||||
|
"no-fallthrough": "error",
|
||||||
|
"no-empty": ["warn", { allowEmptyCatch: true }],
|
||||||
|
|
||||||
|
// Prettier 格式化问题
|
||||||
|
"prettier/prettier": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
+60
-48
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "2.3.2",
|
"version": "2.4.3",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
"dev": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
|
||||||
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
"dev:diff": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
|
||||||
|
"dev:trace": "cross-env RUST_BACKTRACE=full RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
|
||||||
|
"dev:tauri": "cross-env RUST_BACKTRACE=full tauri dev -f tauri-dev",
|
||||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
|
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
|
||||||
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
@@ -18,9 +20,12 @@
|
|||||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||||
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||||
"release-version": "node scripts/release-version.mjs",
|
"release-version": "node scripts/release-version.mjs",
|
||||||
|
"release:autobuild": "pnpm release-version autobuild",
|
||||||
|
"release:deploytest": "pnpm release-version deploytest",
|
||||||
"publish-version": "node scripts/publish-version.mjs",
|
"publish-version": "node scripts/publish-version.mjs",
|
||||||
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
||||||
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
|
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
|
||||||
|
"lint": "eslint -c eslint.config.ts --cache src",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check ."
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
@@ -31,74 +36,81 @@
|
|||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"@mui/icons-material": "^7.2.0",
|
"@mui/icons-material": "^7.3.2",
|
||||||
"@mui/lab": "7.0.0-beta.14",
|
"@mui/lab": "7.0.0-beta.17",
|
||||||
"@mui/material": "^7.2.0",
|
"@mui/material": "^7.3.2",
|
||||||
"@mui/x-data-grid": "^8.8.0",
|
"@mui/x-data-grid": "^8.11.2",
|
||||||
"@tauri-apps/api": "2.6.0",
|
"@tauri-apps/api": "2.8.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.3.0",
|
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.0",
|
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
|
"@tauri-apps/plugin-http": "~2",
|
||||||
"@tauri-apps/plugin-notification": "^2.3.0",
|
|
||||||
"@tauri-apps/plugin-process": "^2.3.0",
|
"@tauri-apps/plugin-process": "^2.3.0",
|
||||||
"@tauri-apps/plugin-shell": "2.3.0",
|
"@tauri-apps/plugin-shell": "2.3.1",
|
||||||
"@tauri-apps/plugin-updater": "2.9.0",
|
"@tauri-apps/plugin-updater": "2.9.0",
|
||||||
"@tauri-apps/plugin-window-state": "^2.3.0",
|
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"ahooks": "^3.9.0",
|
"ahooks": "^3.9.5",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.12.2",
|
||||||
"chart.js": "^4.5.0",
|
"dayjs": "1.11.18",
|
||||||
"cli-color": "^2.0.4",
|
|
||||||
"dayjs": "1.11.13",
|
|
||||||
"foxact": "^0.2.49",
|
"foxact": "^0.2.49",
|
||||||
"glob": "^11.0.3",
|
"i18next": "^25.5.2",
|
||||||
"i18next": "^25.3.2",
|
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json-schema": "^0.4.0",
|
"json-schema": "^0.4.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.53.0",
|
||||||
"monaco-yaml": "^5.4.0",
|
"monaco-yaml": "^5.4.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"react": "19.1.0",
|
"react": "19.1.1",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-dom": "19.1.1",
|
||||||
"react-dom": "19.1.0",
|
|
||||||
"react-error-boundary": "6.0.0",
|
"react-error-boundary": "6.0.0",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-i18next": "15.6.0",
|
"react-i18next": "15.7.3",
|
||||||
"react-markdown": "10.1.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-monaco-editor": "0.58.0",
|
"react-monaco-editor": "0.59.0",
|
||||||
"react-router-dom": "7.6.3",
|
"react-router-dom": "7.9.1",
|
||||||
"react-virtuoso": "^4.13.0",
|
"react-virtuoso": "^4.14.0",
|
||||||
"sockette": "^2.0.6",
|
"swr": "^2.3.6",
|
||||||
"swr": "^2.3.4",
|
|
||||||
"tar": "^7.4.3",
|
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
"zustand": "^5.0.6"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.1",
|
||||||
"@tauri-apps/cli": "2.6.2",
|
"@eslint/js": "^9.35.0",
|
||||||
|
"@tauri-apps/cli": "2.8.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.13",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.9",
|
||||||
"@vitejs/plugin-legacy": "^7.0.0",
|
"@vitejs/plugin-legacy": "^7.2.1",
|
||||||
"@vitejs/plugin-react": "4.6.0",
|
"@vitejs/plugin-react": "5.0.2",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"commander": "^14.0.0",
|
"cli-color": "^2.0.4",
|
||||||
"cross-env": "^7.0.3",
|
"commander": "^14.0.1",
|
||||||
|
"cross-env": "^10.0.0",
|
||||||
|
"eslint": "^9.35.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"eslint-plugin-unused-imports": "^4.2.0",
|
||||||
|
"glob": "^11.0.3",
|
||||||
|
"globals": "^16.4.0",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
"meta-json-schema": "^1.19.11",
|
"jiti": "^2.5.1",
|
||||||
|
"meta-json-schema": "^1.19.13",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"sass": "^1.92.1",
|
||||||
"sass": "^1.89.2",
|
"tar": "^7.4.3",
|
||||||
"terser": "^5.43.1",
|
"terser": "^5.44.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.0.4",
|
"typescript-eslint": "^8.44.0",
|
||||||
|
"vite": "^7.1.5",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
"vite-plugin-svgr": "^4.5.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@9.13.2"
|
"packageManager": "pnpm@9.13.2"
|
||||||
|
|||||||
Generated
+3438
-1082
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -38,5 +38,5 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"postUpdateOptions": ["pnpmDedupe"],
|
"postUpdateOptions": ["pnpmDedupe"],
|
||||||
"ignoreDeps": ["serde_yaml"]
|
"ignoreDeps": ["criterion"]
|
||||||
}
|
}
|
||||||
|
|||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 获取最近一个和 Tauri 相关的改动的 commit hash
|
||||||
|
# This script finds the latest commit that modified Tauri-related files
|
||||||
|
|
||||||
|
# Tauri 相关文件的模式
|
||||||
|
TAURI_PATTERNS=(
|
||||||
|
"src-tauri/"
|
||||||
|
"Cargo.toml"
|
||||||
|
"Cargo.lock"
|
||||||
|
"tauri.*.conf.json"
|
||||||
|
"package.json"
|
||||||
|
"pnpm-lock.yaml"
|
||||||
|
"src/"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 排除的文件模式(build artifacts 等)
|
||||||
|
EXCLUDE_PATTERNS=(
|
||||||
|
"src-tauri/target/"
|
||||||
|
"src-tauri/gen/"
|
||||||
|
"*.log"
|
||||||
|
"*.tmp"
|
||||||
|
"node_modules/"
|
||||||
|
".git/"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构建 git log 的路径过滤参数
|
||||||
|
PATHS=""
|
||||||
|
for pattern in "${TAURI_PATTERNS[@]}"; do
|
||||||
|
if [[ -e "$pattern" ]]; then
|
||||||
|
PATHS="$PATHS $pattern"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 如果没有找到相关路径,返回错误
|
||||||
|
if [[ -z "$PATHS" ]]; then
|
||||||
|
echo "Error: No Tauri-related paths found in current directory" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 获取最新的 commit hash
|
||||||
|
# 使用 git log 查找最近修改了 Tauri 相关文件的提交
|
||||||
|
LATEST_COMMIT=$(git log --format="%H" -n 1 -- $PATHS)
|
||||||
|
|
||||||
|
# 验证是否找到了 commit
|
||||||
|
if [[ -z "$LATEST_COMMIT" ]]; then
|
||||||
|
echo "Error: No commits found for Tauri-related files" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 输出结果
|
||||||
|
echo "$LATEST_COMMIT"
|
||||||
|
|
||||||
|
# 如果需要更多信息,可以取消注释以下行
|
||||||
|
# echo "Latest Tauri-related commit: $LATEST_COMMIT"
|
||||||
|
# git show --stat --oneline "$LATEST_COMMIT"
|
||||||
@@ -12,7 +12,7 @@ import { glob } from "glob";
|
|||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
|
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
|
||||||
const FORCE = process.argv.includes("--force");
|
const FORCE = process.argv.includes("--force") || process.argv.includes("-f");
|
||||||
|
|
||||||
const PLATFORM_MAP = {
|
const PLATFORM_MAP = {
|
||||||
"x86_64-pc-windows-msvc": "win32",
|
"x86_64-pc-windows-msvc": "win32",
|
||||||
@@ -43,7 +43,8 @@ const ARCH_MAP = {
|
|||||||
|
|
||||||
const arg1 = process.argv.slice(2)[0];
|
const arg1 = process.argv.slice(2)[0];
|
||||||
const arg2 = process.argv.slice(2)[1];
|
const arg2 = process.argv.slice(2)[1];
|
||||||
const target = arg1 === "--force" ? arg2 : arg1;
|
let target;
|
||||||
|
target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
|
||||||
const { platform, arch } = target
|
const { platform, arch } = target
|
||||||
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
||||||
: process;
|
: process;
|
||||||
@@ -61,12 +62,12 @@ const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/down
|
|||||||
let META_ALPHA_VERSION;
|
let META_ALPHA_VERSION;
|
||||||
|
|
||||||
const META_ALPHA_MAP = {
|
const META_ALPHA_MAP = {
|
||||||
"win32-x64": "mihomo-windows-amd64-compatible",
|
"win32-x64": "mihomo-windows-amd64-v2",
|
||||||
"win32-ia32": "mihomo-windows-386",
|
"win32-ia32": "mihomo-windows-386",
|
||||||
"win32-arm64": "mihomo-windows-arm64",
|
"win32-arm64": "mihomo-windows-arm64",
|
||||||
"darwin-x64": "mihomo-darwin-amd64-compatible",
|
"darwin-x64": "mihomo-darwin-amd64-v1",
|
||||||
"darwin-arm64": "mihomo-darwin-arm64",
|
"darwin-arm64": "mihomo-darwin-arm64",
|
||||||
"linux-x64": "mihomo-linux-amd64-compatible",
|
"linux-x64": "mihomo-linux-amd64-v2",
|
||||||
"linux-ia32": "mihomo-linux-386",
|
"linux-ia32": "mihomo-linux-386",
|
||||||
"linux-arm64": "mihomo-linux-arm64",
|
"linux-arm64": "mihomo-linux-arm64",
|
||||||
"linux-arm": "mihomo-linux-armv7",
|
"linux-arm": "mihomo-linux-armv7",
|
||||||
@@ -108,12 +109,12 @@ const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
|||||||
let META_VERSION;
|
let META_VERSION;
|
||||||
|
|
||||||
const META_MAP = {
|
const META_MAP = {
|
||||||
"win32-x64": "mihomo-windows-amd64-compatible",
|
"win32-x64": "mihomo-windows-amd64-v2",
|
||||||
"win32-ia32": "mihomo-windows-386",
|
"win32-ia32": "mihomo-windows-386",
|
||||||
"win32-arm64": "mihomo-windows-arm64",
|
"win32-arm64": "mihomo-windows-arm64",
|
||||||
"darwin-x64": "mihomo-darwin-amd64-compatible",
|
"darwin-x64": "mihomo-darwin-amd64-v2",
|
||||||
"darwin-arm64": "mihomo-darwin-arm64",
|
"darwin-arm64": "mihomo-darwin-arm64",
|
||||||
"linux-x64": "mihomo-linux-amd64-compatible",
|
"linux-x64": "mihomo-linux-amd64-v2",
|
||||||
"linux-ia32": "mihomo-linux-386",
|
"linux-ia32": "mihomo-linux-386",
|
||||||
"linux-arm64": "mihomo-linux-arm64",
|
"linux-arm64": "mihomo-linux-arm64",
|
||||||
"linux-arm": "mihomo-linux-armv7",
|
"linux-arm": "mihomo-linux-armv7",
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ async function run() {
|
|||||||
execSync(`git tag ${tag}`, { stdio: "inherit" });
|
execSync(`git tag ${tag}`, { stdio: "inherit" });
|
||||||
execSync(`git push origin ${tag}`, { stdio: "inherit" });
|
execSync(`git push origin ${tag}`, { stdio: "inherit" });
|
||||||
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
|
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
|
||||||
} catch (e) {
|
} catch {
|
||||||
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
|
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-14
@@ -6,15 +6,19 @@
|
|||||||
*
|
*
|
||||||
* <version> can be:
|
* <version> can be:
|
||||||
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
|
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
|
||||||
* - A tag: "alpha", "beta", "rc", or "autobuild"
|
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
|
||||||
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
||||||
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
|
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
|
||||||
|
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d)
|
||||||
|
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530)
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
* pnpm release-version 1.2.3
|
* pnpm release-version 1.2.3
|
||||||
* pnpm release-version v1.2.3-beta
|
* pnpm release-version v1.2.3-beta
|
||||||
* pnpm release-version beta
|
* pnpm release-version beta
|
||||||
* pnpm release-version autobuild
|
* pnpm release-version autobuild
|
||||||
|
* pnpm release-version autobuild-latest
|
||||||
|
* pnpm release-version deploytest
|
||||||
*
|
*
|
||||||
* The script will:
|
* The script will:
|
||||||
* - Validate and normalize the version argument
|
* - Validate and normalize the version argument
|
||||||
@@ -25,10 +29,10 @@
|
|||||||
* Errors are logged and the process exits with code 1 on failure.
|
* Errors are logged and the process exits with code 1 on failure.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { program } from "commander";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { program } from "commander";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前 git 短 commit hash
|
* 获取当前 git 短 commit hash
|
||||||
@@ -37,21 +41,51 @@ import { execSync } from "child_process";
|
|||||||
function getGitShortCommit() {
|
function getGitShortCommit() {
|
||||||
try {
|
try {
|
||||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
} catch (e) {
|
} catch {
|
||||||
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
|
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
|
||||||
return "nogit";
|
return "nogit";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
|
* 获取最新 Tauri 相关提交的短 hash
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getLatestTauriCommit() {
|
||||||
|
try {
|
||||||
|
const fullHash = execSync(
|
||||||
|
"bash ./scripts-workflow/get_latest_tauri_commit.bash",
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
.trim();
|
||||||
|
return execSync(`git rev-parse --short ${fullHash}`).toString().trim();
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
|
||||||
|
);
|
||||||
|
return getGitShortCommit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
|
||||||
|
* 使用 Asia/Shanghai 时区
|
||||||
* @param {boolean} withCommit 是否带 commit
|
* @param {boolean} withCommit 是否带 commit
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function generateShortTimestamp(withCommit = false) {
|
function generateShortTimestamp(withCommit = false) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(now.getDate()).padStart(2, "0");
|
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||||
|
timeZone: "Asia/Shanghai",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
const parts = formatter.formatToParts(now);
|
||||||
|
const month = parts.find((part) => part.type === "month").value;
|
||||||
|
const day = parts.find((part) => part.type === "day").value;
|
||||||
|
|
||||||
if (withCommit) {
|
if (withCommit) {
|
||||||
const gitShort = getGitShortCommit();
|
const gitShort = getGitShortCommit();
|
||||||
return `${month}${day}.${gitShort}`;
|
return `${month}${day}.${gitShort}`;
|
||||||
@@ -135,20 +169,19 @@ async function updateCargoVersion(newVersion) {
|
|||||||
const versionWithoutV = newVersion.startsWith("v")
|
const versionWithoutV = newVersion.startsWith("v")
|
||||||
? newVersion.slice(1)
|
? newVersion.slice(1)
|
||||||
: newVersion;
|
: newVersion;
|
||||||
const baseVersion = getBaseVersion(versionWithoutV);
|
|
||||||
|
|
||||||
const updatedLines = lines.map((line) => {
|
const updatedLines = lines.map((line) => {
|
||||||
if (line.trim().startsWith("version =")) {
|
if (line.trim().startsWith("version =")) {
|
||||||
return line.replace(
|
return line.replace(
|
||||||
/version\s*=\s*"[^"]+"/,
|
/version\s*=\s*"[^"]+"/,
|
||||||
`version = "${baseVersion}"`,
|
`version = "${versionWithoutV}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
});
|
});
|
||||||
|
|
||||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||||
console.log(`[INFO]: Cargo.toml version updated to: ${baseVersion}`);
|
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating Cargo.toml version:", error);
|
console.error("Error updating Cargo.toml version:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -168,19 +201,23 @@ async function updateTauriConfigVersion(newVersion) {
|
|||||||
const versionWithoutV = newVersion.startsWith("v")
|
const versionWithoutV = newVersion.startsWith("v")
|
||||||
? newVersion.slice(1)
|
? newVersion.slice(1)
|
||||||
: newVersion;
|
: newVersion;
|
||||||
const baseVersion = getBaseVersion(versionWithoutV);
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[INFO]: Current tauri.conf.json version is: ",
|
"[INFO]: Current tauri.conf.json version is: ",
|
||||||
tauriConfig.version,
|
tauriConfig.version,
|
||||||
);
|
);
|
||||||
tauriConfig.version = baseVersion;
|
|
||||||
|
// 使用完整版本信息,包含build metadata
|
||||||
|
tauriConfig.version = versionWithoutV;
|
||||||
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
tauriConfigPath,
|
tauriConfigPath,
|
||||||
JSON.stringify(tauriConfig, null, 2),
|
JSON.stringify(tauriConfig, null, 2),
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
|
console.log(
|
||||||
|
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating tauri.conf.json version:", error);
|
console.error("Error updating tauri.conf.json version:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -214,7 +251,14 @@ async function main(versionArg) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let newVersion;
|
let newVersion;
|
||||||
const validTags = ["alpha", "beta", "rc", "autobuild"];
|
const validTags = [
|
||||||
|
"alpha",
|
||||||
|
"beta",
|
||||||
|
"rc",
|
||||||
|
"autobuild",
|
||||||
|
"autobuild-latest",
|
||||||
|
"deploytest",
|
||||||
|
];
|
||||||
|
|
||||||
if (validTags.includes(versionArg.toLowerCase())) {
|
if (validTags.includes(versionArg.toLowerCase())) {
|
||||||
const currentVersion = await getCurrentVersion();
|
const currentVersion = await getCurrentVersion();
|
||||||
@@ -223,6 +267,13 @@ async function main(versionArg) {
|
|||||||
if (versionArg.toLowerCase() === "autobuild") {
|
if (versionArg.toLowerCase() === "autobuild") {
|
||||||
// 格式: 2.3.0+autobuild.250613.cc39b27
|
// 格式: 2.3.0+autobuild.250613.cc39b27
|
||||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
|
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
|
||||||
|
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
||||||
|
// 格式: 2.3.0+autobuild.0614.a1b2c3d (使用最新 Tauri 提交)
|
||||||
|
const latestTauriCommit = getLatestTauriCommit();
|
||||||
|
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
|
||||||
|
} else if (versionArg.toLowerCase() === "deploytest") {
|
||||||
|
// 格式: 2.3.0+deploytest.250613.cc39b27
|
||||||
|
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true)}`;
|
||||||
} else {
|
} else {
|
||||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { log_success, log_error, log_info } from "./utils.mjs";
|
||||||
|
|
||||||
|
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
|
||||||
|
const CHAT_ID_TEST = "@vergetest"; // 测试频道
|
||||||
|
|
||||||
|
async function sendTelegramNotification() {
|
||||||
|
if (!process.env.TELEGRAM_BOT_TOKEN) {
|
||||||
|
throw new Error("TELEGRAM_BOT_TOKEN is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const version =
|
||||||
|
process.env.VERSION ||
|
||||||
|
(() => {
|
||||||
|
const pkg = readFileSync("package.json", "utf-8");
|
||||||
|
return JSON.parse(pkg).version;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const downloadUrl =
|
||||||
|
process.env.DOWNLOAD_URL ||
|
||||||
|
`https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}`;
|
||||||
|
|
||||||
|
const isAutobuild =
|
||||||
|
process.env.BUILD_TYPE === "autobuild" || version.includes("autobuild");
|
||||||
|
const chatId = isAutobuild ? CHAT_ID_TEST : CHAT_ID_RELEASE;
|
||||||
|
const buildType = isAutobuild ? "滚动更新版" : "正式版";
|
||||||
|
|
||||||
|
log_info(`Preparing Telegram notification for ${buildType} ${version}`);
|
||||||
|
log_info(`Target channel: ${chatId}`);
|
||||||
|
log_info(`Download URL: ${downloadUrl}`);
|
||||||
|
|
||||||
|
// 读取发布说明和下载地址
|
||||||
|
let releaseContent = "";
|
||||||
|
try {
|
||||||
|
releaseContent = readFileSync("release.txt", "utf-8");
|
||||||
|
log_info("成功读取 release.txt 文件");
|
||||||
|
} catch (error) {
|
||||||
|
log_error("无法读取 release.txt,使用默认发布说明", error);
|
||||||
|
releaseContent = "更多新功能现已支持,详细更新日志请查看发布页面。";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown 转换为 HTML
|
||||||
|
function convertMarkdownToTelegramHTML(content) {
|
||||||
|
return content
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => {
|
||||||
|
if (line.trim().length === 0) {
|
||||||
|
return "";
|
||||||
|
} else if (line.startsWith("## ")) {
|
||||||
|
return `<b>${line.replace("## ", "")}</b>`;
|
||||||
|
} else if (line.startsWith("### ")) {
|
||||||
|
return `<b>${line.replace("### ", "")}</b>`;
|
||||||
|
} else if (line.startsWith("#### ")) {
|
||||||
|
return `<b>${line.replace("#### ", "")}</b>`;
|
||||||
|
} else {
|
||||||
|
let processedLine = line.replace(
|
||||||
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||||
|
(match, text, url) => {
|
||||||
|
const encodedUrl = encodeURI(url);
|
||||||
|
return `<a href="${encodedUrl}">${text}</a>`;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
processedLine = processedLine.replace(
|
||||||
|
/\*\*([^*]+)\*\*/g,
|
||||||
|
"<b>$1</b>",
|
||||||
|
);
|
||||||
|
return processedLine;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
|
||||||
|
|
||||||
|
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
|
||||||
|
const encodedVersion = encodeURIComponent(version);
|
||||||
|
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
|
||||||
|
|
||||||
|
// 发送到 Telegram
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
text: content,
|
||||||
|
link_preview_options: {
|
||||||
|
is_disabled: false,
|
||||||
|
url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}`,
|
||||||
|
prefer_large_media: true,
|
||||||
|
},
|
||||||
|
parse_mode: "HTML",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
log_success(`✅ Telegram 通知发送成功到 ${chatId}`);
|
||||||
|
} catch (error) {
|
||||||
|
log_error(
|
||||||
|
`❌ Telegram 通知发送失败到 ${chatId}:`,
|
||||||
|
error.response?.data || error.message,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行函数
|
||||||
|
sendTelegramNotification().catch((error) => {
|
||||||
|
log_error("脚本执行失败:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@ const UPDATE_LOG = "UPDATELOG.md";
|
|||||||
export async function resolveUpdateLog(tag) {
|
export async function resolveUpdateLog(tag) {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
||||||
const reTitle = /^## v[\d\.]+/;
|
const reTitle = /^## v[\d.]+/;
|
||||||
const reEnd = /^---/;
|
const reEnd = /^---/;
|
||||||
|
|
||||||
const file = path.join(cwd, UPDATE_LOG);
|
const file = path.join(cwd, UPDATE_LOG);
|
||||||
@@ -54,7 +54,7 @@ export async function resolveUpdateLogDefault() {
|
|||||||
|
|
||||||
const data = await fsp.readFile(file, "utf-8");
|
const data = await fsp.readFile(file, "utf-8");
|
||||||
|
|
||||||
const reTitle = /^## v[\d\.]+/;
|
const reTitle = /^## v[\d.]+/;
|
||||||
const reEnd = /^---/;
|
const reEnd = /^---/;
|
||||||
|
|
||||||
let isCapturing = false;
|
let isCapturing = false;
|
||||||
|
|||||||
Generated
+1395
-1332
File diff suppressed because it is too large
Load Diff
+114
-55
@@ -1,84 +1,92 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.3.2"
|
version = "2.4.3"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||||
default-run = "clash-verge"
|
default-run = "clash-verge"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[package.metadata.bundle]
|
[package.metadata.bundle]
|
||||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.3.0", features = [] }
|
tauri-build = { version = "2.4.1", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
warp = "0.3.7"
|
warp = { version = "0.4.2", features = ["server"] }
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.99"
|
||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
open = "5.3.2"
|
open = "5.3.2"
|
||||||
log = "0.4.27"
|
log = "0.4.28"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
log4rs = "1.3.0"
|
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.42"
|
||||||
sysinfo = "=0.35.2"
|
sysinfo = { version = "0.37.0", features = ["network", "system"] }
|
||||||
boa_engine = "0.20.0"
|
boa_engine = "0.20.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.145"
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml_ng = "0.10.0"
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
lazy_static = "1.5.0"
|
|
||||||
port_scanner = "0.1.5"
|
port_scanner = "0.1.5"
|
||||||
delay_timer = "0.11.6"
|
delay_timer = "0.11.6"
|
||||||
parking_lot = "0.12.4"
|
parking_lot = "0.12.4"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.2"
|
||||||
tokio = { version = "1.46.1", features = [
|
tokio = { version = "1.47.1", features = [
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
"macros",
|
"macros",
|
||||||
"time",
|
"time",
|
||||||
"sync",
|
"sync",
|
||||||
] }
|
] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.225", features = ["derive"] }
|
||||||
reqwest = { version = "0.12.22", features = ["json", "rustls-tls", "cookies"] }
|
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.2"
|
||||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||||
image = "0.25.6"
|
tauri = { version = "2.8.5", features = [
|
||||||
tauri = { version = "2.6.2", features = [
|
|
||||||
"protocol-asset",
|
"protocol-asset",
|
||||||
"devtools",
|
"devtools",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"image-ico",
|
"image-ico",
|
||||||
"image-png",
|
"image-png",
|
||||||
] }
|
] }
|
||||||
network-interface = { version = "2.0.1", features = ["serde"] }
|
network-interface = { version = "2.0.3", features = ["serde"] }
|
||||||
tauri-plugin-shell = "2.3.0"
|
tauri-plugin-shell = "2.3.1"
|
||||||
tauri-plugin-dialog = "2.3.0"
|
tauri-plugin-dialog = "2.4.0"
|
||||||
tauri-plugin-fs = "2.4.0"
|
tauri-plugin-fs = "2.4.2"
|
||||||
tauri-plugin-process = "2.3.0"
|
tauri-plugin-process = "2.3.0"
|
||||||
tauri-plugin-clipboard-manager = "2.3.0"
|
tauri-plugin-clipboard-manager = "2.3.0"
|
||||||
tauri-plugin-deep-link = "2.4.0"
|
tauri-plugin-deep-link = "2.4.3"
|
||||||
tauri-plugin-devtools = "2.0.0"
|
tauri-plugin-window-state = "2.4.0"
|
||||||
tauri-plugin-window-state = "2.3.0"
|
zip = "5.1.1"
|
||||||
zip = "=4.2.0"
|
reqwest_dav = "0.2.2"
|
||||||
reqwest_dav = "0.2.1"
|
|
||||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
getrandom = "0.3.3"
|
getrandom = "0.3.3"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.2"
|
||||||
async-trait = "0.1.88"
|
libc = "0.2.175"
|
||||||
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
|
||||||
libc = "0.2.174"
|
|
||||||
gethostname = "1.0.2"
|
gethostname = "1.0.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
tauri-plugin-notification = "2.3.0"
|
kode-bridge = "0.2.1"
|
||||||
dashmap = "7.0.0-rc2"
|
dashmap = "6.1.0"
|
||||||
|
tauri-plugin-notification = "2.3.1"
|
||||||
|
tokio-stream = "0.1.17"
|
||||||
|
isahc = { version = "1.7.2", default-features = false, features = [
|
||||||
|
"text-decoding",
|
||||||
|
"parking_lot",
|
||||||
|
] }
|
||||||
|
backoff = { version = "0.4.0", features = ["tokio"] }
|
||||||
|
tauri-plugin-http = "2"
|
||||||
|
flexi_logger = "0.31.2"
|
||||||
|
cfg-if = "1.0.3"
|
||||||
|
nu-ansi-term = { version = "0.50.1", optional = true }
|
||||||
|
console-subscriber = { version = "0.4.1", optional = true }
|
||||||
|
tauri-plugin-devtools = { version = "2.0.1" }
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
@@ -109,42 +117,93 @@ tauri-plugin-updater = "2.9.0"
|
|||||||
[features]
|
[features]
|
||||||
default = ["custom-protocol"]
|
default = ["custom-protocol"]
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
verge-dev = []
|
verge-dev = ["nu-ansi-term"]
|
||||||
|
tauri-dev = []
|
||||||
|
tokio-trace = ["console-subscriber"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
codegen-units = 1
|
codegen-units = 16
|
||||||
lto = true
|
lto = true
|
||||||
opt-level = "s"
|
opt-level = 2
|
||||||
|
debug = false
|
||||||
strip = true
|
strip = true
|
||||||
|
overflow-checks = false
|
||||||
|
rpath = false
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
incremental = true
|
incremental = true
|
||||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
codegen-units = 64
|
||||||
opt-level = 0 # 禁用优化,进一步提升编译速度
|
opt-level = 0
|
||||||
debug = true # 保留调试信息
|
debug = true
|
||||||
strip = false # 不剥离符号,保留调试信息
|
strip = "none"
|
||||||
|
overflow-checks = true
|
||||||
|
lto = false
|
||||||
|
rpath = false
|
||||||
|
|
||||||
[profile.fast-release]
|
[profile.fast-release]
|
||||||
inherits = "release" # 继承 release 的配置
|
inherits = "release"
|
||||||
panic = "abort" # 与 release 相同
|
incremental = true
|
||||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
codegen-units = 64
|
||||||
lto = false # 禁用 LTO,提升编译速度
|
lto = false
|
||||||
opt-level = 0 # 禁用优化,大幅提升编译速度
|
opt-level = 0
|
||||||
debug = true # 保留调试信息
|
debug = true
|
||||||
strip = false # 不剥离符号,保留调试信息
|
strip = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "app_lib"
|
name = "app_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.6.0"
|
criterion = "0.7.0"
|
||||||
tempfile = "3.20.0"
|
|
||||||
|
|
||||||
[workspace]
|
[lints.clippy]
|
||||||
members = ["src_crates/crate_mihomo_api"]
|
# Core categories - most important for code safety and correctness
|
||||||
|
correctness = { level = "deny", priority = -1 }
|
||||||
|
suspicious = { level = "deny", priority = -1 }
|
||||||
|
|
||||||
[[bench]]
|
# Critical safety lints - warn for now due to extensive existing usage
|
||||||
name = "draft_benchmark"
|
unwrap_used = "warn"
|
||||||
harness = false
|
expect_used = "warn"
|
||||||
|
panic = "deny"
|
||||||
|
unimplemented = "deny"
|
||||||
|
|
||||||
|
# Development quality lints
|
||||||
|
todo = "warn"
|
||||||
|
dbg_macro = "warn"
|
||||||
|
#print_stdout = "warn"
|
||||||
|
#print_stderr = "warn"
|
||||||
|
|
||||||
|
# Performance lints for proxy application
|
||||||
|
clone_on_ref_ptr = "warn"
|
||||||
|
rc_clone_in_vec_init = "warn"
|
||||||
|
large_stack_arrays = "warn"
|
||||||
|
large_const_arrays = "warn"
|
||||||
|
|
||||||
|
# Security lints
|
||||||
|
#integer_division = "warn"
|
||||||
|
#lossy_float_literal = "warn"
|
||||||
|
#default_numeric_fallback = "warn"
|
||||||
|
|
||||||
|
# Mutex and async lints - strict control
|
||||||
|
async_yields_async = "deny" # Prevents missing await in async blocks
|
||||||
|
mutex_atomic = "deny" # Use atomics instead of Mutex<bool/int>
|
||||||
|
mutex_integer = "deny" # Use AtomicInt instead of Mutex<int>
|
||||||
|
rc_mutex = "deny" # Single-threaded Rc with Mutex is wrong
|
||||||
|
unused_async = "deny" # Too many false positives in Tauri/framework code
|
||||||
|
await_holding_lock = "deny"
|
||||||
|
large_futures = "deny"
|
||||||
|
future_not_send = "deny"
|
||||||
|
|
||||||
|
# Common style improvements
|
||||||
|
redundant_else = "deny" # Too many in existing code
|
||||||
|
needless_continue = "deny" # Too many in existing code
|
||||||
|
needless_raw_string_hashes = "deny" # Too many in existing code
|
||||||
|
|
||||||
|
# Disable noisy categories for existing codebase but keep them available
|
||||||
|
#style = { level = "allow", priority = -1 }
|
||||||
|
#complexity = { level = "allow", priority = -1 }
|
||||||
|
#perf = { level = "allow", priority = -1 }
|
||||||
|
#pedantic = { level = "allow", priority = -1 }
|
||||||
|
#nursery = { level = "allow", priority = -1 }
|
||||||
|
#restriction = { level = "allow", priority = -1 }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use criterion::{criterion_group, criterion_main, Criterion};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
use std::hint::black_box;
|
use std::hint::black_box;
|
||||||
|
|
||||||
// 业务模型 & Draft
|
// 业务模型 & Draft
|
||||||
|
|||||||
@@ -18,6 +18,12 @@
|
|||||||
"autostart:allow-disable",
|
"autostart:allow-disable",
|
||||||
"autostart:allow-is-enabled",
|
"autostart:allow-is-enabled",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
"notification:default"
|
"notification:default",
|
||||||
|
"http:default",
|
||||||
|
"http:allow-fetch",
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 41 KiB |
@@ -523,7 +523,7 @@ FunctionEnd
|
|||||||
${If} $0 == 0
|
${If} $0 == 0
|
||||||
Push $0
|
Push $0
|
||||||
${If} $1 == 0
|
${If} $1 == 0
|
||||||
DetailPrint "Restart Clash Verge Service..."
|
DetailPrint "Restart ${PRODUCTNAME} Service..."
|
||||||
SimpleSC::StartService "clash_verge_service" "" 30
|
SimpleSC::StartService "clash_verge_service" "" 30
|
||||||
${EndIf}
|
${EndIf}
|
||||||
${ElseIf} $0 != 0
|
${ElseIf} $0 != 0
|
||||||
@@ -549,20 +549,20 @@ FunctionEnd
|
|||||||
${If} $0 == 0
|
${If} $0 == 0
|
||||||
Push $0
|
Push $0
|
||||||
${If} $1 == 1
|
${If} $1 == 1
|
||||||
DetailPrint "Stop Clash Verge Service..."
|
DetailPrint "Stop ${PRODUCTNAME} Service..."
|
||||||
SimpleSC::StopService "clash_verge_service" 1 30
|
SimpleSC::StopService "clash_verge_service" 1 30
|
||||||
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||||
${If} $0 == 0
|
${If} $0 == 0
|
||||||
DetailPrint "Removing Clash Verge Service..."
|
DetailPrint "Removing ${PRODUCTNAME} Service..."
|
||||||
SimpleSC::RemoveService "clash_verge_service"
|
SimpleSC::RemoveService "clash_verge_service"
|
||||||
${ElseIf} $0 != 0
|
${ElseIf} $0 != 0
|
||||||
Push $0
|
Push $0
|
||||||
SimpleSC::GetErrorMessage
|
SimpleSC::GetErrorMessage
|
||||||
Pop $0
|
Pop $0
|
||||||
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
|
MessageBox MB_OK|MB_ICONSTOP "${PRODUCTNAME} Service Stop Error ($0)"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
${ElseIf} $1 == 0
|
${ElseIf} $1 == 0
|
||||||
DetailPrint "Removing Clash Verge Service..."
|
DetailPrint "Removing ${PRODUCTNAME} Service..."
|
||||||
SimpleSC::RemoveService "clash_verge_service"
|
SimpleSC::RemoveService "clash_verge_service"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
${ElseIf} $0 != 0
|
${ElseIf} $0 != 0
|
||||||
@@ -661,7 +661,7 @@ SectionEnd
|
|||||||
Pop $R0
|
Pop $R0
|
||||||
${If} $R0 = 0
|
${If} $R0 = 0
|
||||||
IfSilent kill 0
|
IfSilent kill 0
|
||||||
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|}
|
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "${PRODUCTNAME} 正在运行 $\n点击确定以终止运行" IDOK kill IDCANCEL cancel ${|}
|
||||||
kill:
|
kill:
|
||||||
!if "${INSTALLMODE}" == "currentUser"
|
!if "${INSTALLMODE}" == "currentUser"
|
||||||
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
|
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
|
||||||
@@ -762,8 +762,18 @@ Section Install
|
|||||||
!insertmacro CheckIfAppIsRunning
|
!insertmacro CheckIfAppIsRunning
|
||||||
!insertmacro CheckAllVergeProcesses
|
!insertmacro CheckAllVergeProcesses
|
||||||
|
|
||||||
|
; 修复系统级启动文件夹
|
||||||
|
CreateDirectory "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"
|
||||||
|
DetailPrint "系统级启动文件夹已确保存在"
|
||||||
|
|
||||||
|
; 修复用户级启动文件夹
|
||||||
|
SetShellVarContext current
|
||||||
|
StrCpy $0 "$SMPROGRAMS\Startup"
|
||||||
|
CreateDirectory "$0"
|
||||||
|
DetailPrint "已确保用户级启动文件夹存在: $0"
|
||||||
|
|
||||||
; 删除 window-state.json 文件 .window-state.json 文件
|
; 删除 window-state.json 文件 .window-state.json 文件
|
||||||
DetailPrint "开始删除删除 window-state.json or .window-state.json"
|
DetailPrint "开始删除 window-state.json or .window-state.json"
|
||||||
SetShellVarContext current
|
SetShellVarContext current
|
||||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||||
@@ -924,7 +934,7 @@ Section Uninstall
|
|||||||
!insertmacro RemoveVergeService
|
!insertmacro RemoveVergeService
|
||||||
|
|
||||||
; 删除 window-state.json 文件 .window-state.json 文件
|
; 删除 window-state.json 文件 .window-state.json 文件
|
||||||
DetailPrint "开始删除删除 window-state.json or .window-state.json"
|
DetailPrint "开始删除 window-state.json or .window-state.json"
|
||||||
SetShellVarContext current
|
SetShellVarContext current
|
||||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||||
@@ -991,7 +1001,7 @@ Section Uninstall
|
|||||||
!insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk"
|
!insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk"
|
||||||
|
|
||||||
; 删除所有用户的桌面快捷方式
|
; 删除所有用户的桌面快捷方式
|
||||||
DetailPrint "开始删除所有用户桌面的 Clash Verge 快捷方式..."
|
DetailPrint "开始删除所有用户桌面的 ${PRODUCTNAME} 快捷方式..."
|
||||||
|
|
||||||
; 删除公共桌面快捷方式
|
; 删除公共桌面快捷方式
|
||||||
Delete "C:\Users\Public\Desktop\Clash Verge.lnk"
|
Delete "C:\Users\Public\Desktop\Clash Verge.lnk"
|
||||||
@@ -1019,7 +1029,7 @@ Section Uninstall
|
|||||||
Delete "$R4\Clash Verge.lnk"
|
Delete "$R4\Clash Verge.lnk"
|
||||||
Delete "$R4\clash-verge.lnk"
|
Delete "$R4\clash-verge.lnk"
|
||||||
|
|
||||||
DetailPrint "尝试删除用户 '$R3' 桌面的 Clash Verge 快捷方式"
|
DetailPrint "尝试删除用户 '$R3' 桌面的 ${PRODUCTNAME} 快捷方式"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
; 递增循环计数器
|
; 递增循环计数器
|
||||||
@@ -1029,18 +1039,20 @@ Section Uninstall
|
|||||||
|
|
||||||
DetailPrint "所有用户桌面快捷方式删除完成"
|
DetailPrint "所有用户桌面快捷方式删除完成"
|
||||||
|
|
||||||
; 删除用户开始菜单文件夹
|
; 删除用户级开始菜单中的应用程序文件夹和快捷方式
|
||||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
DetailPrint "删除用户级开始菜单中的应用程序文件夹和快捷方式..."
|
||||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
RMDir /r /REBOOTOK "$SMPROGRAMS\Clash Verge"
|
||||||
RMDir /r /REBOOTOK "$SMPROGRAMS\$AppStartMenuFolder"
|
RMDir /r /REBOOTOK "$SMPROGRAMS\clash-verge"
|
||||||
|
DetailPrint "删除用户级开始菜单中的应用程序文件夹和快捷方式完成"
|
||||||
|
|
||||||
; 删除系统级开始菜单中的 Clash Verge
|
; 删除系统级开始菜单中的应用程序文件夹和快捷方式
|
||||||
Delete "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge\Clash Verge.lnk"
|
DetailPrint "删除系统级开始菜单中的应用程序文件夹和快捷方式..."
|
||||||
Delete "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge\clash-verge.lnk"
|
|
||||||
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge"
|
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge"
|
||||||
|
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\clash-verge"
|
||||||
|
DetailPrint "删除系统级开始菜单中的应用程序文件夹和快捷方式完成"
|
||||||
|
|
||||||
; 删除所有带 Clash Verge 或 clash-verge 的注册表项
|
; 删除所有带 Clash Verge 或 clash-verge 的注册表项
|
||||||
DetailPrint "开始清理所有 Clash Verge 相关的注册表项..."
|
DetailPrint "开始清理所有 ${PRODUCTNAME} 相关的注册表项..."
|
||||||
|
|
||||||
; 设置注册表查看模式 (64位)
|
; 设置注册表查看模式 (64位)
|
||||||
SetRegView 64
|
SetRegView 64
|
||||||
|
|||||||
Vendored
+111
@@ -0,0 +1,111 @@
|
|||||||
|
use crate::singleton;
|
||||||
|
use anyhow::Result;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
pub const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250);
|
||||||
|
|
||||||
|
pub struct CacheEntry<T> {
|
||||||
|
pub value: Arc<T>,
|
||||||
|
pub expires_at: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Cache<T> {
|
||||||
|
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry<T>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Cache<T> {
|
||||||
|
fn new() -> Self {
|
||||||
|
Cache {
|
||||||
|
map: DashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_key(prefix: &str, id: &str) -> String {
|
||||||
|
format!("{prefix}:{id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<T>
|
||||||
|
where
|
||||||
|
F: Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
Fut: std::future::Future<Output = T> + Send + 'static,
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
let now = Instant::now();
|
||||||
|
let key_cloned = key.clone();
|
||||||
|
|
||||||
|
// Get or create the cell
|
||||||
|
let cell = self
|
||||||
|
.map
|
||||||
|
.entry(key_cloned.clone())
|
||||||
|
.or_insert_with(|| Arc::new(OnceCell::new()))
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// Check if we have a valid cached entry
|
||||||
|
if let Some(entry) = cell.get() {
|
||||||
|
if entry.expires_at > now {
|
||||||
|
return Arc::clone(&entry.value);
|
||||||
|
}
|
||||||
|
// Entry is expired, remove it
|
||||||
|
self.map
|
||||||
|
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
|
||||||
|
continue; // Retry with fresh cell
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to set a new value
|
||||||
|
let value = fetch_fn().await;
|
||||||
|
let entry = Box::new(CacheEntry {
|
||||||
|
value: Arc::new(value),
|
||||||
|
expires_at: Instant::now() + ttl,
|
||||||
|
});
|
||||||
|
|
||||||
|
match cell.set(entry) {
|
||||||
|
Ok(_) => {
|
||||||
|
// Successfully set the value, it must exist now
|
||||||
|
if let Some(set_entry) = cell.get() {
|
||||||
|
return Arc::clone(&set_entry.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
if let Some(existing_entry) = cell.get() {
|
||||||
|
if existing_entry.expires_at > Instant::now() {
|
||||||
|
return Arc::clone(&existing_entry.value);
|
||||||
|
}
|
||||||
|
self.map
|
||||||
|
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn clean_key(&self, key: &str) {
|
||||||
|
// self.map.remove(key);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
pub fn clean_default_keys(&self) {
|
||||||
|
// logging!(info, Type::Cache, "Cleaning proxies keys");
|
||||||
|
// let proxies_key = Self::make_key("proxies", "default");
|
||||||
|
// self.map.remove(&proxies_key);
|
||||||
|
|
||||||
|
// logging!(info, Type::Cache, "Cleaning providers keys");
|
||||||
|
// let providers_key = Self::make_key("providers", "default");
|
||||||
|
// self.map.remove(&providers_key);
|
||||||
|
|
||||||
|
// !The frontend goes crash if we clean the clash_config cache
|
||||||
|
// logging!(info, Type::Cache, "Cleaning clash config keys");
|
||||||
|
// let clash_config_key = Self::make_key("clash_config", "default");
|
||||||
|
// self.map.remove(&clash_config_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type CacheService = Cache<Result<String>>;
|
||||||
|
pub type CacheProxy = Cache<Value>;
|
||||||
|
|
||||||
|
singleton!(Cache<Value>, PROXY_INSTANCE);
|
||||||
|
singleton!(Cache<Result<String>>, SERVICE_INSTANCE);
|
||||||
+12
-21
@@ -4,18 +4,18 @@ use crate::{
|
|||||||
utils::{dirs, logging::Type},
|
utils::{dirs, logging::Type},
|
||||||
wrap_err,
|
wrap_err,
|
||||||
};
|
};
|
||||||
use tauri::Manager;
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
/// 打开应用程序所在目录
|
/// 打开应用程序所在目录
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_app_dir() -> CmdResult<()> {
|
pub async fn open_app_dir() -> CmdResult<()> {
|
||||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||||
wrap_err!(open::that(app_dir))
|
wrap_err!(open::that(app_dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 打开核心所在目录
|
/// 打开核心所在目录
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_core_dir() -> CmdResult<()> {
|
pub async fn open_core_dir() -> CmdResult<()> {
|
||||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||||
wrap_err!(open::that(core_dir))
|
wrap_err!(open::that(core_dir))
|
||||||
@@ -23,7 +23,7 @@ pub fn open_core_dir() -> CmdResult<()> {
|
|||||||
|
|
||||||
/// 打开日志目录
|
/// 打开日志目录
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_logs_dir() -> CmdResult<()> {
|
pub async fn open_logs_dir() -> CmdResult<()> {
|
||||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||||
wrap_err!(open::that(log_dir))
|
wrap_err!(open::that(log_dir))
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
|
|||||||
|
|
||||||
/// 打开/关闭开发者工具
|
/// 打开/关闭开发者工具
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_devtools(app_handle: tauri::AppHandle) {
|
pub fn open_devtools(app_handle: AppHandle) {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
if !window.is_devtools_open() {
|
if !window.is_devtools_open() {
|
||||||
window.open_devtools();
|
window.open_devtools();
|
||||||
@@ -48,14 +48,14 @@ pub fn open_devtools(app_handle: tauri::AppHandle) {
|
|||||||
|
|
||||||
/// 退出应用
|
/// 退出应用
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn exit_app() {
|
pub async fn exit_app() {
|
||||||
feat::quit();
|
feat::quit().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重启应用
|
/// 重启应用
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn restart_app() -> CmdResult<()> {
|
pub async fn restart_app() -> CmdResult<()> {
|
||||||
feat::restart_app();
|
feat::restart_app().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +121,8 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
|||||||
Err(_) => {
|
Err(_) => {
|
||||||
if icon_path.exists() {
|
if icon_path.exists() {
|
||||||
return Ok(icon_path.to_string_lossy().to_string());
|
return Ok(icon_path.to_string_lossy().to_string());
|
||||||
} else {
|
|
||||||
return Err("Failed to create temporary file".into());
|
|
||||||
}
|
}
|
||||||
|
return Err("Failed to create temporary file".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,7 +209,7 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn notify_ui_ready() -> CmdResult<()> {
|
pub fn notify_ui_ready() -> CmdResult<()> {
|
||||||
log::info!(target: "app", "前端UI已准备就绪");
|
log::info!(target: "app", "前端UI已准备就绪");
|
||||||
crate::utils::resolve::mark_ui_ready();
|
crate::utils::resolve::ui::mark_ui_ready();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +218,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
|
|||||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||||
log::info!(target: "app", "UI加载阶段更新: {stage}");
|
log::info!(target: "app", "UI加载阶段更新: {stage}");
|
||||||
|
|
||||||
use crate::utils::resolve::UiReadyStage;
|
use crate::utils::resolve::ui::UiReadyStage;
|
||||||
|
|
||||||
let stage_enum = match stage.as_str() {
|
let stage_enum = match stage.as_str() {
|
||||||
"NotStarted" => UiReadyStage::NotStarted,
|
"NotStarted" => UiReadyStage::NotStarted,
|
||||||
@@ -233,14 +232,6 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
crate::utils::resolve::update_ui_ready_stage(stage_enum);
|
crate::utils::resolve::ui::update_ui_ready_stage(stage_enum);
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 重置UI就绪状态
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn reset_ui_ready_state() -> CmdResult<()> {
|
|
||||||
log::info!(target: "app", "重置UI就绪状态");
|
|
||||||
crate::utils::resolve::reset_ui_ready();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+462
-108
@@ -1,20 +1,33 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::*, core::*, feat, module::mihomo::MihomoManager, process::AsyncHandler, wrap_err,
|
cache::CacheProxy,
|
||||||
|
config::Config,
|
||||||
|
core::{CoreManager, handle},
|
||||||
};
|
};
|
||||||
use serde_yaml::Mapping;
|
use crate::{
|
||||||
|
config::*,
|
||||||
|
feat,
|
||||||
|
ipc::{self, IpcManager},
|
||||||
|
logging,
|
||||||
|
utils::logging::Type,
|
||||||
|
wrap_err,
|
||||||
|
};
|
||||||
|
use serde_yaml_ng::Mapping;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
/// 复制Clash环境变量
|
/// 复制Clash环境变量
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn copy_clash_env() -> CmdResult {
|
pub async fn copy_clash_env() -> CmdResult {
|
||||||
feat::copy_clash_env();
|
feat::copy_clash_env().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取Clash信息
|
/// 获取Clash信息
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||||
Ok(Config::clash().latest_ref().get_client_info())
|
Ok(Config::clash().await.latest_ref().get_client_info())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改Clash配置
|
/// 修改Clash配置
|
||||||
@@ -26,14 +39,14 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
|||||||
/// 修改Clash模式
|
/// 修改Clash模式
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||||
feat::change_clash_mode(payload);
|
feat::change_clash_mode(payload).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 切换Clash核心
|
/// 切换Clash核心
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||||
log::info!(target: "app", "changing core to {clash_core}");
|
logging!(info, Type::Config, "changing core to {clash_core}");
|
||||||
|
|
||||||
match CoreManager::global()
|
match CoreManager::global()
|
||||||
.change_core(Some(clash_core.clone()))
|
.change_core(Some(clash_core.clone()))
|
||||||
@@ -43,14 +56,18 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
|||||||
// 切换内核后重启内核
|
// 切换内核后重启内核
|
||||||
match CoreManager::global().restart_core().await {
|
match CoreManager::global().restart_core().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
log::info!(target: "app", "core changed and restarted to {clash_core}");
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Core,
|
||||||
|
"core changed and restarted to {clash_core}"
|
||||||
|
);
|
||||||
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error_msg = format!("Core changed but failed to restart: {err}");
|
let error_msg = format!("Core changed but failed to restart: {err}");
|
||||||
log::error!(target: "app", "{error_msg}");
|
logging!(error, Type::Core, "{error_msg}");
|
||||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||||
Ok(Some(error_msg))
|
Ok(Some(error_msg))
|
||||||
}
|
}
|
||||||
@@ -58,7 +75,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error_msg = err.to_string();
|
let error_msg = err.to_string();
|
||||||
log::error!(target: "app", "failed to change core: {error_msg}");
|
logging!(error, Type::Core, "failed to change core: {error_msg}");
|
||||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||||
Ok(Some(error_msg))
|
Ok(Some(error_msg))
|
||||||
}
|
}
|
||||||
@@ -68,19 +85,31 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
|||||||
/// 启动核心
|
/// 启动核心
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn start_core() -> CmdResult {
|
pub async fn start_core() -> CmdResult {
|
||||||
wrap_err!(CoreManager::global().start_core().await)
|
let result = wrap_err!(CoreManager::global().start_core().await);
|
||||||
|
if result.is_ok() {
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 关闭核心
|
/// 关闭核心
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn stop_core() -> CmdResult {
|
pub async fn stop_core() -> CmdResult {
|
||||||
wrap_err!(CoreManager::global().stop_core().await)
|
let result = wrap_err!(CoreManager::global().stop_core().await);
|
||||||
|
if result.is_ok() {
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重启核心
|
/// 重启核心
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn restart_core() -> CmdResult {
|
pub async fn restart_core() -> CmdResult {
|
||||||
wrap_err!(CoreManager::global().restart_core().await)
|
let result = wrap_err!(CoreManager::global().restart_core().await);
|
||||||
|
if result.is_ok() {
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取代理延迟
|
/// 获取代理延迟
|
||||||
@@ -90,23 +119,32 @@ pub async fn clash_api_get_proxy_delay(
|
|||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
timeout: i32,
|
timeout: i32,
|
||||||
) -> CmdResult<serde_json::Value> {
|
) -> CmdResult<serde_json::Value> {
|
||||||
MihomoManager::global()
|
wrap_err!(
|
||||||
.test_proxy_delay(&name, url, timeout)
|
IpcManager::global()
|
||||||
.await
|
.test_proxy_delay(&name, url, timeout)
|
||||||
|
.await
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 测试URL延迟
|
/// 测试URL延迟
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||||
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
let result = match feat::test_delay(url).await {
|
||||||
|
Ok(delay) => delay,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "{}", e);
|
||||||
|
10000u32
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 保存DNS配置到单独文件
|
/// 保存DNS配置到单独文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
use serde_yaml;
|
use serde_yaml_ng;
|
||||||
use std::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
// 获取DNS配置文件路径
|
// 获取DNS配置文件路径
|
||||||
let dns_path = dirs::app_home_dir()
|
let dns_path = dirs::app_home_dir()
|
||||||
@@ -114,104 +152,103 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
|||||||
.join("dns_config.yaml");
|
.join("dns_config.yaml");
|
||||||
|
|
||||||
// 保存DNS配置到文件
|
// 保存DNS配置到文件
|
||||||
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
let yaml_str = serde_yaml_ng::to_string(&dns_config).map_err(|e| e.to_string())?;
|
||||||
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
|
fs::write(&dns_path, yaml_str)
|
||||||
log::info!(target: "app", "DNS config saved to {dns_path:?}");
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 应用或撤销DNS配置
|
/// 应用或撤销DNS配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
core::{handle, CoreManager},
|
core::{CoreManager, handle},
|
||||||
utils::dirs,
|
utils::dirs,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用spawn来处理异步操作
|
if apply {
|
||||||
AsyncHandler::spawn(move || async move {
|
// 读取DNS配置文件
|
||||||
if apply {
|
let dns_path = dirs::app_home_dir()
|
||||||
// 读取DNS配置文件
|
.map_err(|e| e.to_string())?
|
||||||
let dns_path = match dirs::app_home_dir() {
|
.join("dns_config.yaml");
|
||||||
Ok(path) => path.join("dns_config.yaml"),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "Failed to get home dir: {e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !dns_path.exists() {
|
if !dns_path.exists() {
|
||||||
log::warn!(target: "app", "DNS config file not found");
|
logging!(warn, Type::Config, "DNS config file not found");
|
||||||
return;
|
return Err("DNS config file not found".into());
|
||||||
}
|
|
||||||
|
|
||||||
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
|
||||||
Ok(content) => content,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "Failed to read DNS config: {e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解析DNS配置并创建patch
|
|
||||||
let patch_config = match serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
|
||||||
Ok(config) => {
|
|
||||||
let mut patch = serde_yaml::Mapping::new();
|
|
||||||
patch.insert("dns".into(), config.into());
|
|
||||||
patch
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "Failed to parse DNS config: {e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!(target: "app", "Applying DNS config from file");
|
|
||||||
|
|
||||||
// 重新生成配置,确保DNS配置被正确应用
|
|
||||||
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
|
|
||||||
Config::runtime()
|
|
||||||
.draft_mut()
|
|
||||||
.patch_config(patch_config.clone());
|
|
||||||
|
|
||||||
// 首先重新生成配置
|
|
||||||
if let Err(err) = Config::generate().await {
|
|
||||||
log::error!(target: "app", "Failed to regenerate config with DNS: {err}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 然后应用新配置
|
|
||||||
if let Err(err) = CoreManager::global().update_config().await {
|
|
||||||
log::error!(target: "app", "Failed to apply config with DNS: {err}");
|
|
||||||
} else {
|
|
||||||
log::info!(target: "app", "DNS config successfully applied");
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 当关闭DNS设置时,不需要对配置进行任何修改
|
|
||||||
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
|
|
||||||
log::info!(target: "app", "DNS settings disabled, regenerating config");
|
|
||||||
|
|
||||||
// 重新生成配置
|
|
||||||
if let Err(err) = Config::generate().await {
|
|
||||||
log::error!(target: "app", "Failed to regenerate config: {err}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用新配置
|
|
||||||
match CoreManager::global().update_config().await {
|
|
||||||
Ok(_) => {
|
|
||||||
log::info!(target: "app", "Config regenerated successfully");
|
|
||||||
handle::Handle::refresh_clash();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "Failed to apply regenerated config: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
let dns_yaml = tokio::fs::read_to_string(&dns_path).await.map_err(|e| {
|
||||||
|
logging!(error, Type::Config, "Failed to read DNS config: {e}");
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 解析DNS配置
|
||||||
|
let patch_config =
|
||||||
|
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).map_err(|e| {
|
||||||
|
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
logging!(info, Type::Config, "Applying DNS config from file");
|
||||||
|
|
||||||
|
// 创建包含DNS配置的patch
|
||||||
|
let mut patch = serde_yaml_ng::Mapping::new();
|
||||||
|
patch.insert("dns".into(), patch_config.into());
|
||||||
|
|
||||||
|
// 应用DNS配置到运行时配置
|
||||||
|
Config::runtime().await.draft_mut().patch_config(patch);
|
||||||
|
|
||||||
|
// 重新生成配置
|
||||||
|
Config::generate().await.map_err(|err| {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Config,
|
||||||
|
"Failed to regenerate config with DNS: {err}"
|
||||||
|
);
|
||||||
|
"Failed to regenerate config with DNS".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 应用新配置
|
||||||
|
CoreManager::global().update_config().await.map_err(|err| {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Config,
|
||||||
|
"Failed to apply config with DNS: {err}"
|
||||||
|
);
|
||||||
|
"Failed to apply config with DNS".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
logging!(info, Type::Config, "DNS config successfully applied");
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
} else {
|
||||||
|
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
"DNS settings disabled, regenerating config"
|
||||||
|
);
|
||||||
|
|
||||||
|
Config::generate().await.map_err(|err| {
|
||||||
|
logging!(error, Type::Config, "Failed to regenerate config: {err}");
|
||||||
|
"Failed to regenerate config".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
CoreManager::global().update_config().await.map_err(|err| {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Config,
|
||||||
|
"Failed to apply regenerated config: {err}"
|
||||||
|
);
|
||||||
|
"Failed to apply regenerated config".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
logging!(info, Type::Config, "Config regenerated successfully");
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -232,17 +269,19 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_dns_config_content() -> CmdResult<String> {
|
pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
use std::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
let dns_path = dirs::app_home_dir()
|
let dns_path = dirs::app_home_dir()
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.join("dns_config.yaml");
|
.join("dns_config.yaml");
|
||||||
|
|
||||||
if !dns_path.exists() {
|
if !fs::try_exists(&dns_path).await.map_err(|e| e.to_string())? {
|
||||||
return Err("DNS config file not found".into());
|
return Err("DNS config file not found".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
|
let content = fs::read_to_string(&dns_path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,3 +306,318 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
|||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取Clash版本信息
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
|
||||||
|
wrap_err!(IpcManager::global().get_version().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取Clash配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
|
||||||
|
let manager = IpcManager::global();
|
||||||
|
let cache = CacheProxy::global();
|
||||||
|
let key = CacheProxy::make_key("clash_config", "default");
|
||||||
|
let value = cache
|
||||||
|
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
|
||||||
|
manager.get_config().await.unwrap_or_else(|e| {
|
||||||
|
logging!(error, Type::Cmd, "Failed to fetch clash config: {e}");
|
||||||
|
serde_json::Value::Object(serde_json::Map::new())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
Ok((*value).clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 强制刷新Clash配置缓存
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
|
||||||
|
let cache = CacheProxy::global();
|
||||||
|
let key = CacheProxy::make_key("clash_config", "default");
|
||||||
|
cache.map.remove(&key);
|
||||||
|
get_clash_config().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新地理数据
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_geo_data() -> CmdResult {
|
||||||
|
wrap_err!(IpcManager::global().update_geo_data().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 升级Clash核心
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn upgrade_clash_core() -> CmdResult {
|
||||||
|
wrap_err!(IpcManager::global().upgrade_core().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取规则
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_clash_rules() -> CmdResult<serde_json::Value> {
|
||||||
|
wrap_err!(IpcManager::global().get_rules().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新代理选择
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_proxy_choice(group: String, proxy: String) -> CmdResult {
|
||||||
|
wrap_err!(IpcManager::global().update_proxy(&group, &proxy).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取代理提供者
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_proxy_providers() -> CmdResult<serde_json::Value> {
|
||||||
|
wrap_err!(IpcManager::global().get_providers_proxies().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取规则提供者
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_rule_providers() -> CmdResult<serde_json::Value> {
|
||||||
|
wrap_err!(IpcManager::global().get_rule_providers().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 代理提供者健康检查
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_provider_health_check(name: String) -> CmdResult {
|
||||||
|
wrap_err!(
|
||||||
|
IpcManager::global()
|
||||||
|
.proxy_provider_health_check(&name)
|
||||||
|
.await
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新代理提供者
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_proxy_provider(name: String) -> CmdResult {
|
||||||
|
wrap_err!(IpcManager::global().update_proxy_provider(&name).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新规则提供者
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_rule_provider(name: String) -> CmdResult {
|
||||||
|
wrap_err!(IpcManager::global().update_rule_provider(&name).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取连接
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_clash_connections() -> CmdResult<serde_json::Value> {
|
||||||
|
wrap_err!(IpcManager::global().get_connections().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除连接
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_clash_connection(id: String) -> CmdResult {
|
||||||
|
wrap_err!(IpcManager::global().delete_connection(&id).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 关闭所有连接
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn close_all_clash_connections() -> CmdResult {
|
||||||
|
wrap_err!(IpcManager::global().close_all_connections().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取流量数据 (使用新的IPC流式监控)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_traffic_data() -> CmdResult<serde_json::Value> {
|
||||||
|
let traffic = crate::ipc::get_current_traffic().await;
|
||||||
|
let result = serde_json::json!({
|
||||||
|
"up": traffic.total_up,
|
||||||
|
"down": traffic.total_down,
|
||||||
|
"up_rate": traffic.up_rate,
|
||||||
|
"down_rate": traffic.down_rate,
|
||||||
|
"last_updated": traffic.last_updated.elapsed().as_secs()
|
||||||
|
});
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取内存数据 (使用新的IPC流式监控)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_memory_data() -> CmdResult<serde_json::Value> {
|
||||||
|
let memory = crate::ipc::get_current_memory().await;
|
||||||
|
let usage_percent = if memory.oslimit > 0 {
|
||||||
|
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let result = serde_json::json!({
|
||||||
|
"inuse": memory.inuse,
|
||||||
|
"oslimit": memory.oslimit,
|
||||||
|
"usage_percent": usage_percent,
|
||||||
|
"last_updated": memory.last_updated.elapsed().as_secs()
|
||||||
|
});
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn start_traffic_service() -> CmdResult {
|
||||||
|
logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)");
|
||||||
|
// 新的IPC监控在首次访问时自动启动
|
||||||
|
// 触发一次访问以确保监控器已初始化
|
||||||
|
let _ = crate::ipc::get_current_traffic().await;
|
||||||
|
let _ = crate::ipc::get_current_memory().await;
|
||||||
|
logging!(info, Type::Ipc, "IPC流式监控已激活");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn stop_traffic_service() -> CmdResult {
|
||||||
|
logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)");
|
||||||
|
// 新的IPC监控是持久的,无需显式停止
|
||||||
|
logging!(info, Type::Ipc, "IPC流式监控继续运行");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取格式化的流量数据 (包含单位,便于前端显示)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_formatted_traffic_data() -> CmdResult<serde_json::Value> {
|
||||||
|
logging!(trace, Type::Ipc, "获取格式化流量数据");
|
||||||
|
let (up_rate, down_rate, total_up, total_down, is_fresh) =
|
||||||
|
crate::ipc::get_formatted_traffic().await;
|
||||||
|
let result = serde_json::json!({
|
||||||
|
"up_rate_formatted": up_rate,
|
||||||
|
"down_rate_formatted": down_rate,
|
||||||
|
"total_up_formatted": total_up,
|
||||||
|
"total_down_formatted": total_down,
|
||||||
|
"is_fresh": is_fresh
|
||||||
|
});
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Ipc,
|
||||||
|
"格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"
|
||||||
|
);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取格式化的内存数据 (包含单位,便于前端显示)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_formatted_memory_data() -> CmdResult<serde_json::Value> {
|
||||||
|
logging!(info, Type::Ipc, "获取格式化内存数据");
|
||||||
|
let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await;
|
||||||
|
let result = serde_json::json!({
|
||||||
|
"inuse_formatted": inuse,
|
||||||
|
"oslimit_formatted": oslimit,
|
||||||
|
"usage_percent": usage_percent,
|
||||||
|
"is_fresh": is_fresh
|
||||||
|
});
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Ipc,
|
||||||
|
"格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"
|
||||||
|
);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_system_monitor_overview() -> CmdResult<serde_json::Value> {
|
||||||
|
logging!(debug, Type::Ipc, "获取系统监控概览");
|
||||||
|
|
||||||
|
// 并发获取流量和内存数据
|
||||||
|
let (traffic, memory) = tokio::join!(
|
||||||
|
crate::ipc::get_current_traffic(),
|
||||||
|
crate::ipc::get_current_memory()
|
||||||
|
);
|
||||||
|
|
||||||
|
let (traffic_formatted, memory_formatted) = tokio::join!(
|
||||||
|
crate::ipc::get_formatted_traffic(),
|
||||||
|
crate::ipc::get_formatted_memory()
|
||||||
|
);
|
||||||
|
|
||||||
|
let traffic_is_fresh = traffic.last_updated.elapsed().as_secs() < 5;
|
||||||
|
let memory_is_fresh = memory.last_updated.elapsed().as_secs() < 10;
|
||||||
|
|
||||||
|
let result = serde_json::json!({
|
||||||
|
"traffic": {
|
||||||
|
"raw": {
|
||||||
|
"up": traffic.total_up,
|
||||||
|
"down": traffic.total_down,
|
||||||
|
"up_rate": traffic.up_rate,
|
||||||
|
"down_rate": traffic.down_rate
|
||||||
|
},
|
||||||
|
"formatted": {
|
||||||
|
"up_rate": traffic_formatted.0,
|
||||||
|
"down_rate": traffic_formatted.1,
|
||||||
|
"total_up": traffic_formatted.2,
|
||||||
|
"total_down": traffic_formatted.3
|
||||||
|
},
|
||||||
|
"is_fresh": traffic_is_fresh
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"raw": {
|
||||||
|
"inuse": memory.inuse,
|
||||||
|
"oslimit": memory.oslimit,
|
||||||
|
"usage_percent": if memory.oslimit > 0 {
|
||||||
|
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatted": {
|
||||||
|
"inuse": memory_formatted.0,
|
||||||
|
"oslimit": memory_formatted.1,
|
||||||
|
"usage_percent": memory_formatted.2
|
||||||
|
},
|
||||||
|
"is_fresh": memory_is_fresh
|
||||||
|
},
|
||||||
|
"overall_status": if traffic_is_fresh && memory_is_fresh { "healthy" } else { "stale" }
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取代理组延迟
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_group_proxy_delays(
|
||||||
|
group_name: String,
|
||||||
|
url: Option<String>,
|
||||||
|
timeout: Option<i32>,
|
||||||
|
) -> CmdResult<serde_json::Value> {
|
||||||
|
wrap_err!(
|
||||||
|
IpcManager::global()
|
||||||
|
.get_group_proxy_delays(&group_name, url, timeout.unwrap_or(10000))
|
||||||
|
.await
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查调试是否启用
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn is_clash_debug_enabled() -> CmdResult<bool> {
|
||||||
|
match IpcManager::global().is_debug_enabled().await {
|
||||||
|
Ok(enabled) => Ok(enabled),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 垃圾回收
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn clash_gc() -> CmdResult {
|
||||||
|
wrap_err!(IpcManager::global().gc().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取日志 (使用新的流式实现)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_clash_logs() -> CmdResult<serde_json::Value> {
|
||||||
|
Ok(ipc::get_logs_json().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动日志监控
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn start_logs_monitoring(level: Option<String>) -> CmdResult {
|
||||||
|
ipc::start_logs_monitoring(level).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止日志监控
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn stop_logs_monitoring() -> CmdResult {
|
||||||
|
ipc::stop_logs_monitoring().await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清除日志
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn clear_logs() -> CmdResult {
|
||||||
|
ipc::clear_logs().await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ use super::CmdResult;
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn entry_lightweight_mode() -> CmdResult {
|
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||||
lightweight::entry_lightweight_mode();
|
lightweight::entry_lightweight_mode().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn exit_lightweight_mode() -> CmdResult {
|
pub async fn exit_lightweight_mode() -> CmdResult {
|
||||||
lightweight::exit_lightweight_mode();
|
lightweight::exit_lightweight_mode().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
@@ -250,7 +251,23 @@ async fn check_gemini(client: &Client) -> UnlockItem {
|
|||||||
let status = if is_ok { "Yes" } else { "No" };
|
let status = if is_ok { "Yes" } else { "No" };
|
||||||
|
|
||||||
// 尝试提取国家代码
|
// 尝试提取国家代码
|
||||||
let re = Regex::new(r#",2,1,200,"([A-Z]{3})""#).unwrap();
|
let re = match Regex::new(r#",2,1,200,"([A-Z]{3})""#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Gemini regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Gemini".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
let region = re.captures(&body).and_then(|caps| {
|
let region = re.captures(&body).and_then(|caps| {
|
||||||
caps.get(1).map(|m| {
|
caps.get(1).map(|m| {
|
||||||
let country_code = m.as_str();
|
let country_code = m.as_str();
|
||||||
@@ -303,7 +320,23 @@ async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
|||||||
}
|
}
|
||||||
} else if body_lower.contains("ad-free") {
|
} else if body_lower.contains("ad-free") {
|
||||||
// 尝试解析国家代码
|
// 尝试解析国家代码
|
||||||
let re = Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#).unwrap();
|
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile YouTube Premium regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Youtube Premium".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
let region = re.captures(&body).and_then(|caps| {
|
let region = re.captures(&body).and_then(|caps| {
|
||||||
caps.get(1).map(|m| {
|
caps.get(1).map(|m| {
|
||||||
let country_code = m.as_str().trim();
|
let country_code = m.as_str().trim();
|
||||||
@@ -350,11 +383,16 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
|||||||
let cookie_store = Arc::new(reqwest::cookie::Jar::default());
|
let cookie_store = Arc::new(reqwest::cookie::Jar::default());
|
||||||
|
|
||||||
// 使用带Cookie的客户端
|
// 使用带Cookie的客户端
|
||||||
let client_with_cookies = reqwest::Client::builder()
|
let client_with_cookies = match reqwest::Client::builder()
|
||||||
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||||
.cookie_provider(Arc::clone(&cookie_store))
|
.cookie_provider(Arc::clone(&cookie_store))
|
||||||
.build()
|
.build() {
|
||||||
.unwrap_or_else(|_| client.clone());
|
Ok(client) => client,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Network, "Failed to create client with cookies for Bahamut Anime: {}", e);
|
||||||
|
client.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 第一步:获取设备ID (会自动保存Cookie)
|
// 第一步:获取设备ID (会自动保存Cookie)
|
||||||
let device_url = "https://ani.gamer.com.tw/ajax/getdeviceid.php";
|
let device_url = "https://ani.gamer.com.tw/ajax/getdeviceid.php";
|
||||||
@@ -363,10 +401,21 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
|||||||
match response.text().await {
|
match response.text().await {
|
||||||
Ok(text) => {
|
Ok(text) => {
|
||||||
// 使用正则提取deviceid
|
// 使用正则提取deviceid
|
||||||
let re = Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#).unwrap();
|
match Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#) {
|
||||||
re.captures(&text)
|
Ok(re) => re
|
||||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
.captures(&text)
|
||||||
.unwrap_or_default()
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile deviceid regex for Bahamut Anime: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(_) => String::new(),
|
Err(_) => String::new(),
|
||||||
}
|
}
|
||||||
@@ -421,17 +470,25 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(response) => match response.text().await {
|
Ok(response) => match response.text().await {
|
||||||
Ok(body) => {
|
Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) {
|
||||||
let region_re = Regex::new(r#"data-geo="([^"]+)"#).unwrap();
|
Ok(region_re) => region_re
|
||||||
region_re
|
|
||||||
.captures(&body)
|
.captures(&body)
|
||||||
.and_then(|caps| caps.get(1))
|
.and_then(|caps| caps.get(1))
|
||||||
.map(|m| {
|
.map(|m| {
|
||||||
let country_code = m.as_str();
|
let country_code = m.as_str();
|
||||||
let emoji = country_code_to_emoji(country_code);
|
let emoji = country_code_to_emoji(country_code);
|
||||||
format!("{emoji}{country_code}")
|
format!("{emoji}{country_code}")
|
||||||
})
|
}),
|
||||||
}
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile region regex for Bahamut Anime: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -495,8 +552,40 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态码
|
// 获取状态码
|
||||||
let status1 = result1.unwrap().status().as_u16();
|
let status1 = match result1 {
|
||||||
let status2 = result2.unwrap().status().as_u16();
|
Ok(response) => response.status().as_u16(),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Netflix response 1: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let status2 = match result2 {
|
||||||
|
Ok(response) => response.status().as_u16(),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Netflix response 2: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 根据状态码判断解锁状况
|
// 根据状态码判断解锁状况
|
||||||
if status1 == 404 && status2 == 404 {
|
if status1 == 404 && status2 == 404 {
|
||||||
@@ -529,20 +618,20 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
|||||||
{
|
{
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
// 检查重定向位置
|
// 检查重定向位置
|
||||||
if let Some(location) = response.headers().get("location") {
|
if let Some(location) = response.headers().get("location")
|
||||||
if let Ok(location_str) = location.to_str() {
|
&& let Ok(location_str) = location.to_str()
|
||||||
// 解析位置获取区域
|
{
|
||||||
let parts: Vec<&str> = location_str.split('/').collect();
|
// 解析位置获取区域
|
||||||
if parts.len() >= 4 {
|
let parts: Vec<&str> = location_str.split('/').collect();
|
||||||
let region_code = parts[3].split('-').next().unwrap_or("unknown");
|
if parts.len() >= 4 {
|
||||||
let emoji = country_code_to_emoji(region_code);
|
let region_code = parts[3].split('-').next().unwrap_or("unknown");
|
||||||
return UnlockItem {
|
let emoji = country_code_to_emoji(region_code);
|
||||||
name: "Netflix".to_string(),
|
return UnlockItem {
|
||||||
status: "Yes".to_string(),
|
name: "Netflix".to_string(),
|
||||||
region: Some(format!("{emoji}{region_code}")),
|
status: "Yes".to_string(),
|
||||||
check_time: Some(get_local_date_string()),
|
region: Some(format!("{emoji}{region_code}")),
|
||||||
};
|
check_time: Some(get_local_date_string()),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果没有重定向,假设是美国
|
// 如果没有重定向,假设是美国
|
||||||
@@ -602,22 +691,18 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
|||||||
match response.json::<serde_json::Value>().await {
|
match response.json::<serde_json::Value>().await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
// 尝试从数据中提取区域信息
|
// 尝试从数据中提取区域信息
|
||||||
if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) {
|
if let Some(targets) = data.get("targets").and_then(|t| t.as_array())
|
||||||
if !targets.is_empty() {
|
&& !targets.is_empty()
|
||||||
if let Some(location) = targets[0].get("location") {
|
&& let Some(location) = targets[0].get("location")
|
||||||
if let Some(country) =
|
&& let Some(country) = location.get("country").and_then(|c| c.as_str())
|
||||||
location.get("country").and_then(|c| c.as_str())
|
{
|
||||||
{
|
let emoji = country_code_to_emoji(country);
|
||||||
let emoji = country_code_to_emoji(country);
|
return UnlockItem {
|
||||||
return UnlockItem {
|
name: "Netflix".to_string(),
|
||||||
name: "Netflix".to_string(),
|
status: "Yes".to_string(),
|
||||||
status: "Yes".to_string(),
|
region: Some(format!("{emoji}{country}")),
|
||||||
region: Some(format!("{emoji}{country}")),
|
check_time: Some(get_local_date_string()),
|
||||||
check_time: Some(get_local_date_string()),
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果无法解析区域信息
|
// 如果无法解析区域信息
|
||||||
@@ -685,7 +770,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let device_response = device_result.unwrap();
|
let device_response = match device_result {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Disney+ device response: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 检查是否 403 错误
|
// 检查是否 403 错误
|
||||||
if device_response.status().as_u16() == 403 {
|
if device_response.status().as_u16() == 403 {
|
||||||
@@ -710,7 +811,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 提取 assertion
|
// 提取 assertion
|
||||||
let re = Regex::new(r#""assertion"\s*:\s*"([^"]+)"#).unwrap();
|
let re = match Regex::new(r#""assertion"\s*:\s*"([^"]+)"#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile assertion regex for Disney+: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Regex Error)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
let assertion = match re.captures(&device_body) {
|
let assertion = match re.captures(&device_body) {
|
||||||
Some(caps) => caps.get(1).map(|m| m.as_str().to_string()),
|
Some(caps) => caps.get(1).map(|m| m.as_str().to_string()),
|
||||||
None => None,
|
None => None,
|
||||||
@@ -729,7 +846,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
let token_url = "https://disney.api.edge.bamgrid.com/token";
|
let token_url = "https://disney.api.edge.bamgrid.com/token";
|
||||||
|
|
||||||
// 构建请求体 - 使用表单数据格式而非 JSON
|
// 构建请求体 - 使用表单数据格式而非 JSON
|
||||||
let assertion_str = assertion.unwrap();
|
let assertion_str = match assertion {
|
||||||
|
Some(assertion) => assertion,
|
||||||
|
None => {
|
||||||
|
logging!(error, Type::Network, "No assertion found for Disney+");
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (No Assertion)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
let token_body = [
|
let token_body = [
|
||||||
(
|
(
|
||||||
"grant_type",
|
"grant_type",
|
||||||
@@ -762,7 +890,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let token_response = token_result.unwrap();
|
let token_response = match token_result {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Disney+ token response: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
let token_status = token_response.status();
|
let token_status = token_response.status();
|
||||||
|
|
||||||
// 保存原始响应用于调试
|
// 保存原始响应用于调试
|
||||||
@@ -798,10 +942,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
.map(|s| s.to_string()),
|
.map(|s| s.to_string()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// 如果 JSON 解析失败,尝试使用正则表达式
|
// 如果 JSON 解析失败,尝试使用正则表达式
|
||||||
let refresh_token_re = Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#).unwrap();
|
match Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#) {
|
||||||
refresh_token_re
|
Ok(refresh_token_re) => refresh_token_re
|
||||||
.captures(&token_body_text)
|
.captures(&token_body_text)
|
||||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile refresh_token regex for Disney+: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -825,7 +979,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
// GraphQL API 通常接受 JSON 格式
|
// GraphQL API 通常接受 JSON 格式
|
||||||
let graphql_payload = format!(
|
let graphql_payload = format!(
|
||||||
r#"{{"query":"mutation refreshToken($input: RefreshTokenInput!) {{ refreshToken(refreshToken: $input) {{ activeSession {{ sessionId }} }} }}","variables":{{"input":{{"refreshToken":"{}"}}}}}}"#,
|
r#"{{"query":"mutation refreshToken($input: RefreshTokenInput!) {{ refreshToken(refreshToken: $input) {{ activeSession {{ sessionId }} }} }}","variables":{{"input":{{"refreshToken":"{}"}}}}}}"#,
|
||||||
refresh_token.unwrap()
|
refresh_token.unwrap_or_default()
|
||||||
);
|
);
|
||||||
|
|
||||||
let graphql_result = client
|
let graphql_result = client
|
||||||
@@ -857,21 +1011,56 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 解析 GraphQL 响应获取区域信息
|
// 解析 GraphQL 响应获取区域信息
|
||||||
let graphql_response = graphql_result.unwrap();
|
let graphql_response = match graphql_result {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Disney+ GraphQL response: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
let graphql_status = graphql_response.status();
|
let graphql_status = graphql_response.status();
|
||||||
let graphql_body_text = (graphql_response.text().await).unwrap_or_default();
|
let graphql_body_text = match graphql_response.text().await {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to read Disney+ GraphQL response text: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 如果 GraphQL 响应为空或明显错误,尝试直接获取区域信息
|
// 如果 GraphQL 响应为空或明显错误,尝试直接获取区域信息
|
||||||
if graphql_body_text.is_empty() || graphql_status.as_u16() >= 400 {
|
if graphql_body_text.is_empty() || graphql_status.as_u16() >= 400 {
|
||||||
// 尝试直接从主页获取区域信息
|
// 尝试直接从主页获取区域信息
|
||||||
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||||
Ok(response) => match response.text().await {
|
Ok(response) => match response.text().await {
|
||||||
Ok(body) => {
|
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||||
let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap();
|
Ok(region_re) => region_re
|
||||||
region_re
|
|
||||||
.captures(&body)
|
.captures(&body)
|
||||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||||
}
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Disney+ main page region regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -898,28 +1087,59 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
region: None,
|
region: None,
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
};
|
||||||
} else {
|
}
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: format!(
|
||||||
|
"Failed (GraphQL error: {}, status: {})",
|
||||||
|
graphql_body_text.chars().take(50).collect::<String>() + "...",
|
||||||
|
graphql_status.as_u16()
|
||||||
|
),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取国家代码
|
||||||
|
let region_re = match Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Disney+ countryCode regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
return UnlockItem {
|
return UnlockItem {
|
||||||
name: "Disney+".to_string(),
|
name: "Disney+".to_string(),
|
||||||
status: format!(
|
status: "Failed (Regex Error)".to_string(),
|
||||||
"Failed (GraphQL error: {}, status: {})",
|
|
||||||
graphql_body_text.chars().take(50).collect::<String>() + "...",
|
|
||||||
graphql_status.as_u16()
|
|
||||||
),
|
|
||||||
region: None,
|
region: None,
|
||||||
check_time: Some(get_local_date_string()),
|
check_time: Some(get_local_date_string()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// 提取国家代码
|
|
||||||
let region_re = Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#).unwrap();
|
|
||||||
let region_code = region_re
|
let region_code = region_re
|
||||||
.captures(&graphql_body_text)
|
.captures(&graphql_body_text)
|
||||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
||||||
|
|
||||||
// 提取支持状态
|
// 提取支持状态
|
||||||
let supported_re = Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#).unwrap();
|
let supported_re = match Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Disney+ supported location regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Regex Error)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
let in_supported_location = supported_re
|
let in_supported_location = supported_re
|
||||||
.captures(&graphql_body_text)
|
.captures(&graphql_body_text)
|
||||||
.and_then(|caps| caps.get(1).map(|m| m.as_str() == "true"));
|
.and_then(|caps| caps.get(1).map(|m| m.as_str() == "true"));
|
||||||
@@ -929,12 +1149,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
// 尝试直接从主页获取区域信息
|
// 尝试直接从主页获取区域信息
|
||||||
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||||
Ok(response) => match response.text().await {
|
Ok(response) => match response.text().await {
|
||||||
Ok(body) => {
|
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||||
let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap();
|
Ok(region_re) => region_re
|
||||||
region_re
|
|
||||||
.captures(&body)
|
.captures(&body)
|
||||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||||
}
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Disney+ main page region regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -958,7 +1186,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let region = region_code.unwrap();
|
let region = match region_code {
|
||||||
|
Some(code) => code,
|
||||||
|
None => {
|
||||||
|
logging!(error, Type::Network, "No region code found for Disney+");
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "No".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 判断日本地区
|
// 判断日本地区
|
||||||
if region == "JP" {
|
if region == "JP" {
|
||||||
@@ -1028,13 +1267,47 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析响应内容
|
// 解析响应内容
|
||||||
match result.unwrap().text().await {
|
let response = match result {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Prime Video response: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match response.text().await {
|
||||||
Ok(body) => {
|
Ok(body) => {
|
||||||
// 检查是否被地区限制
|
// 检查是否被地区限制
|
||||||
let is_blocked = body.contains("isServiceRestricted");
|
let is_blocked = body.contains("isServiceRestricted");
|
||||||
|
|
||||||
// 提取地区信息
|
// 提取地区信息
|
||||||
let region_re = Regex::new(r#""currentTerritory":"([^"]+)"#).unwrap();
|
let region_re = match Regex::new(r#""currentTerritory":"([^"]+)"#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Prime Video region regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "Failed (Regex Error)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
let region_code = region_re
|
let region_code = region_re
|
||||||
.captures(&body)
|
.captures(&body)
|
||||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
||||||
@@ -1182,8 +1455,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
|
|
||||||
// 添加哔哩哔哩大陆检测任务
|
// 添加哔哩哔哩大陆检测任务
|
||||||
{
|
{
|
||||||
let client = client_arc.clone();
|
let client = Arc::clone(&client_arc);
|
||||||
let results = results.clone();
|
let results = Arc::clone(&results);
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let result = check_bilibili_china_mainland(&client).await;
|
let result = check_bilibili_china_mainland(&client).await;
|
||||||
let mut results = results.lock().await;
|
let mut results = results.lock().await;
|
||||||
@@ -1193,8 +1466,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
|
|
||||||
// 添加哔哩哔哩港澳台检测任务
|
// 添加哔哩哔哩港澳台检测任务
|
||||||
{
|
{
|
||||||
let client = client_arc.clone();
|
let client = Arc::clone(&client_arc);
|
||||||
let results = results.clone();
|
let results = Arc::clone(&results);
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let result = check_bilibili_hk_mc_tw(&client).await;
|
let result = check_bilibili_hk_mc_tw(&client).await;
|
||||||
let mut results = results.lock().await;
|
let mut results = results.lock().await;
|
||||||
@@ -1204,8 +1477,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
|
|
||||||
// 添加合并的ChatGPT检测任务
|
// 添加合并的ChatGPT检测任务
|
||||||
{
|
{
|
||||||
let client = client_arc.clone();
|
let client = Arc::clone(&client_arc);
|
||||||
let results = results.clone();
|
let results = Arc::clone(&results);
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let chatgpt_results = check_chatgpt_combined(&client).await;
|
let chatgpt_results = check_chatgpt_combined(&client).await;
|
||||||
let mut results = results.lock().await;
|
let mut results = results.lock().await;
|
||||||
@@ -1215,8 +1488,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
|
|
||||||
// 添加Gemini检测任务
|
// 添加Gemini检测任务
|
||||||
{
|
{
|
||||||
let client = client_arc.clone();
|
let client = Arc::clone(&client_arc);
|
||||||
let results = results.clone();
|
let results = Arc::clone(&results);
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let result = check_gemini(&client).await;
|
let result = check_gemini(&client).await;
|
||||||
let mut results = results.lock().await;
|
let mut results = results.lock().await;
|
||||||
@@ -1226,8 +1499,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
|
|
||||||
// 添加YouTube Premium检测任务
|
// 添加YouTube Premium检测任务
|
||||||
{
|
{
|
||||||
let client = client_arc.clone();
|
let client = Arc::clone(&client_arc);
|
||||||
let results = results.clone();
|
let results = Arc::clone(&results);
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let result = check_youtube_premium(&client).await;
|
let result = check_youtube_premium(&client).await;
|
||||||
let mut results = results.lock().await;
|
let mut results = results.lock().await;
|
||||||
@@ -1237,8 +1510,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
|
|
||||||
// 添加动画疯检测任务
|
// 添加动画疯检测任务
|
||||||
{
|
{
|
||||||
let client = client_arc.clone();
|
let client = Arc::clone(&client_arc);
|
||||||
let results = results.clone();
|
let results = Arc::clone(&results);
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let result = check_bahamut_anime(&client).await;
|
let result = check_bahamut_anime(&client).await;
|
||||||
let mut results = results.lock().await;
|
let mut results = results.lock().await;
|
||||||
@@ -1248,8 +1521,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
|
|
||||||
// 添加 Netflix 检测任务
|
// 添加 Netflix 检测任务
|
||||||
{
|
{
|
||||||
let client = client_arc.clone();
|
let client = Arc::clone(&client_arc);
|
||||||
let results = results.clone();
|
let results = Arc::clone(&results);
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let result = check_netflix(&client).await;
|
let result = check_netflix(&client).await;
|
||||||
let mut results = results.lock().await;
|
let mut results = results.lock().await;
|
||||||
@@ -1259,8 +1532,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
|
|
||||||
// 添加 Disney+ 检测任务
|
// 添加 Disney+ 检测任务
|
||||||
{
|
{
|
||||||
let client = client_arc.clone();
|
let client = Arc::clone(&client_arc);
|
||||||
let results = results.clone();
|
let results = Arc::clone(&results);
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let result = check_disney_plus(&client).await;
|
let result = check_disney_plus(&client).await;
|
||||||
let mut results = results.lock().await;
|
let mut results = results.lock().await;
|
||||||
@@ -1270,8 +1543,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
|
|
||||||
// 添加 Prime Video 检测任务
|
// 添加 Prime Video 检测任务
|
||||||
{
|
{
|
||||||
let client = client_arc.clone();
|
let client = Arc::clone(&client_arc);
|
||||||
let results = results.clone();
|
let results = Arc::clone(&results);
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let result = check_prime_video(&client).await;
|
let result = check_prime_video(&client).await;
|
||||||
let mut results = results.lock().await;
|
let mut results = results.lock().await;
|
||||||
@@ -1287,9 +1560,17 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有结果
|
// 获取所有结果
|
||||||
let results = Arc::try_unwrap(results)
|
let results = match Arc::try_unwrap(results) {
|
||||||
.expect("无法获取结果,可能仍有引用存在")
|
Ok(mutex) => mutex.into_inner(),
|
||||||
.into_inner();
|
Err(_) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to unwrap results Arc, references still exist"
|
||||||
|
);
|
||||||
|
return Err("Failed to collect results".to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
// Common result type used by command functions
|
|
||||||
pub type CmdResult<T = ()> = Result<T, String>;
|
pub type CmdResult<T = ()> = Result<T, String>;
|
||||||
|
|
||||||
// Command modules
|
// Command modules
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
|
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
|
||||||
|
use crate::process::AsyncHandler;
|
||||||
use crate::wrap_err;
|
use crate::wrap_err;
|
||||||
use network_interface::NetworkInterface;
|
use network_interface::NetworkInterface;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
|
|
||||||
/// get the system proxy
|
/// get the system proxy
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -30,9 +31,9 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
|||||||
|
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
let proxy_manager = EventDrivenProxyManager::global();
|
||||||
|
|
||||||
let current = proxy_manager.get_auto_proxy_cached();
|
let current = proxy_manager.get_auto_proxy_cached().await;
|
||||||
// 异步请求更新,立即返回缓存数据
|
// 异步请求更新,立即返回缓存数据
|
||||||
tokio::spawn(async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
let _ = proxy_manager.get_auto_proxy_async().await;
|
let _ = proxy_manager.get_auto_proxy_async().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+314
-277
@@ -1,97 +1,76 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IProfiles, PrfItem, PrfOption},
|
config::{
|
||||||
core::{handle, timer::Timer, tray::Tray, CoreManager},
|
Config, IProfiles, PrfItem, PrfOption,
|
||||||
feat, logging, ret_err,
|
profiles::{
|
||||||
|
profiles_append_item_with_filedata_safe, profiles_delete_item_safe,
|
||||||
|
profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_safe,
|
||||||
|
},
|
||||||
|
profiles_append_item_safe,
|
||||||
|
},
|
||||||
|
core::{CoreManager, handle, timer::Timer, tray::Tray},
|
||||||
|
feat, logging,
|
||||||
|
process::AsyncHandler,
|
||||||
|
ret_err,
|
||||||
utils::{dirs, help, logging::Type},
|
utils::{dirs, help, logging::Type},
|
||||||
wrap_err,
|
wrap_err,
|
||||||
};
|
};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::{Mutex, RwLock};
|
|
||||||
|
|
||||||
// 全局互斥锁防止并发配置更新
|
|
||||||
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
|
||||||
|
|
||||||
// 全局请求序列号跟踪,用于避免队列化执行
|
// 全局请求序列号跟踪,用于避免队列化执行
|
||||||
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
static CURRENT_PROCESSING_PROFILE: RwLock<Option<String>> = RwLock::const_new(None);
|
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
/// 清理配置处理状态
|
|
||||||
async fn cleanup_processing_state(sequence: u64, reason: &str) {
|
|
||||||
*CURRENT_PROCESSING_PROFILE.write().await = None;
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"{},清理状态,序列号: {}",
|
|
||||||
reason,
|
|
||||||
sequence
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取配置文件避免锁竞争
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
// 策略1: 尝试快速获取latest数据
|
// 策略1: 尝试快速获取latest数据
|
||||||
let latest_result = tokio::time::timeout(
|
let latest_result = tokio::time::timeout(Duration::from_millis(500), async {
|
||||||
Duration::from_millis(500),
|
let profiles = Config::profiles().await;
|
||||||
tokio::task::spawn_blocking(move || {
|
let latest = profiles.latest_ref();
|
||||||
let profiles = Config::profiles();
|
IProfiles {
|
||||||
let latest = profiles.latest_ref();
|
current: latest.current.clone(),
|
||||||
IProfiles {
|
items: latest.items.clone(),
|
||||||
current: latest.current.clone(),
|
}
|
||||||
items: latest.items.clone(),
|
})
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match latest_result {
|
match latest_result {
|
||||||
Ok(Ok(profiles)) => {
|
Ok(profiles) => {
|
||||||
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
|
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
|
||||||
return Ok(profiles);
|
return Ok(profiles);
|
||||||
}
|
}
|
||||||
Ok(Err(join_err)) => {
|
|
||||||
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
|
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略2: 如果快速获取失败,尝试获取data()
|
// 策略2: 如果快速获取失败,尝试获取data()
|
||||||
let data_result = tokio::time::timeout(
|
let data_result = tokio::time::timeout(Duration::from_secs(2), async {
|
||||||
Duration::from_secs(2),
|
let profiles = Config::profiles().await;
|
||||||
tokio::task::spawn_blocking(move || {
|
let data = profiles.latest_ref();
|
||||||
let profiles = Config::profiles();
|
IProfiles {
|
||||||
let data = profiles.latest_ref();
|
current: data.current.clone(),
|
||||||
IProfiles {
|
items: data.items.clone(),
|
||||||
current: data.current.clone(),
|
}
|
||||||
items: data.items.clone(),
|
})
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match data_result {
|
match data_result {
|
||||||
Ok(Ok(profiles)) => {
|
Ok(profiles) => {
|
||||||
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
|
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
|
||||||
return Ok(profiles);
|
return Ok(profiles);
|
||||||
}
|
}
|
||||||
Ok(Err(join_err)) => {
|
Err(join_err) => {
|
||||||
logging!(
|
logging!(
|
||||||
error,
|
error,
|
||||||
Type::Cmd,
|
Type::Cmd,
|
||||||
true,
|
true,
|
||||||
"获取draft配置任务失败: {}",
|
"获取draft配置任务失败或超时: {}",
|
||||||
join_err
|
join_err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
|
||||||
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略3: fallback,尝试重新创建配置
|
// 策略3: fallback,尝试重新创建配置
|
||||||
@@ -102,26 +81,19 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
|
|||||||
"所有获取配置策略都失败,尝试fallback"
|
"所有获取配置策略都失败,尝试fallback"
|
||||||
);
|
);
|
||||||
|
|
||||||
match tokio::task::spawn_blocking(IProfiles::new).await {
|
Ok(IProfiles::new().await)
|
||||||
Ok(profiles) => {
|
|
||||||
logging!(info, Type::Cmd, true, "使用fallback配置成功");
|
|
||||||
Ok(profiles)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
|
|
||||||
// 返回空配置避免崩溃
|
|
||||||
Ok(IProfiles {
|
|
||||||
current: None,
|
|
||||||
items: Some(vec![]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 增强配置文件
|
/// 增强配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn enhance_profiles() -> CmdResult {
|
pub async fn enhance_profiles() -> CmdResult {
|
||||||
wrap_err!(feat::enhance_profiles().await)?;
|
match feat::enhance_profiles().await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "{}", e);
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -129,40 +101,152 @@ pub async fn enhance_profiles() -> CmdResult {
|
|||||||
/// 导入配置文件
|
/// 导入配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url);
|
||||||
wrap_err!(Config::profiles().data_mut().append_item(item))
|
|
||||||
|
let import_result = tokio::time::timeout(Duration::from_secs(60), async {
|
||||||
|
let item = PrfItem::from_url(&url, None, None, option).await?;
|
||||||
|
logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置");
|
||||||
|
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let pre_count = profiles
|
||||||
|
.latest_ref()
|
||||||
|
.items
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |items| items.len());
|
||||||
|
|
||||||
|
let result = profiles_append_item_safe(item.clone()).await;
|
||||||
|
result?;
|
||||||
|
|
||||||
|
let post_count = profiles
|
||||||
|
.latest_ref()
|
||||||
|
.items
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |items| items.len());
|
||||||
|
if post_count <= pre_count {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"[导入订阅] 配置未增加,导入可能失败"
|
||||||
|
);
|
||||||
|
return Err(anyhow::anyhow!("配置导入后数量未增加"));
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"[导入订阅] 配置保存成功,数量: {} -> {}",
|
||||||
|
pre_count,
|
||||||
|
post_count
|
||||||
|
);
|
||||||
|
|
||||||
|
// 立即发送配置变更通知
|
||||||
|
if let Some(uid) = &item.uid {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"[导入订阅] 发送配置变更通知: {}",
|
||||||
|
uid
|
||||||
|
);
|
||||||
|
handle::Handle::notify_profile_changed(uid.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步保存配置文件并发送全局通知
|
||||||
|
let uid_clone = item.uid.clone();
|
||||||
|
crate::process::AsyncHandler::spawn(move || async move {
|
||||||
|
// 使用Send-safe helper函数
|
||||||
|
if let Err(e) = profiles_save_file_safe().await {
|
||||||
|
logging!(error, Type::Cmd, true, "[导入订阅] 保存配置文件失败: {}", e);
|
||||||
|
} else {
|
||||||
|
logging!(info, Type::Cmd, true, "[导入订阅] 配置文件保存成功");
|
||||||
|
|
||||||
|
// 发送全局配置更新通知
|
||||||
|
if let Some(uid) = uid_clone {
|
||||||
|
// 延迟发送,确保文件已完全写入
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
handle::Handle::notify_profile_changed(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match import_result {
|
||||||
|
Ok(Ok(())) => {
|
||||||
|
logging!(info, Type::Cmd, true, "[导入订阅] 导入完成: {}", url);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
logging!(error, Type::Cmd, true, "[导入订阅] 导入失败: {}", e);
|
||||||
|
Err(format!("导入订阅失败: {e}"))
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
logging!(error, Type::Cmd, true, "[导入订阅] 导入超时(60秒): {}", url);
|
||||||
|
Err("导入订阅超时,请检查网络连接".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重新排序配置文件
|
/// 调整profile的顺序
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||||
wrap_err!(Config::profiles().data_mut().reorder(active_id, over_id))
|
match profiles_reorder_safe(active_id, over_id).await {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!(target: "app", "重新排序配置文件");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "重新排序配置文件失败: {}", err);
|
||||||
|
Err(format!("重新排序配置文件失败: {}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 创建配置文件
|
/// 创建新的profile
|
||||||
|
/// 创建一个新的配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
match profiles_append_item_with_filedata_safe(item, file_data).await {
|
||||||
wrap_err!(Config::profiles().data_mut().append_item(item))
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => match err.to_string().as_str() {
|
||||||
|
"the file already exists" => Err("the file already exists".into()),
|
||||||
|
_ => Err(format!("add profile error: {err}")),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新配置文件
|
/// 更新配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||||
wrap_err!(feat::update_profile(index, option, Some(true)).await)
|
match feat::update_profile(index, option, Some(true)).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "{}", e);
|
||||||
|
Err(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除配置文件
|
/// 删除配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_profile(index: String) -> CmdResult {
|
pub async fn delete_profile(index: String) -> CmdResult {
|
||||||
let should_update = wrap_err!({ Config::profiles().data_mut().delete_item(index) })?;
|
// 使用Send-safe helper函数
|
||||||
|
let should_update = wrap_err!(profiles_delete_item_safe(index).await)?;
|
||||||
// 删除后自动清理冗余文件
|
|
||||||
let _ = Config::profiles().latest_ref().auto_cleanup();
|
|
||||||
|
|
||||||
if should_update {
|
if should_update {
|
||||||
wrap_err!(CoreManager::global().update_config().await)?;
|
match CoreManager::global().update_config().await {
|
||||||
handle::Handle::refresh_clash();
|
Ok(_) => {
|
||||||
|
handle::Handle::refresh_clash();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(target: "app", "{}", e);
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -170,6 +254,12 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
|||||||
/// 修改profiles的配置
|
/// 修改profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||||
|
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
|
||||||
|
logging!(info, Type::Cmd, true, "当前正在切换配置,放弃请求");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
// 为当前请求分配序列号
|
// 为当前请求分配序列号
|
||||||
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
let target_profile = profiles.current.clone();
|
let target_profile = profiles.current.clone();
|
||||||
@@ -183,35 +273,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
target_profile
|
target_profile
|
||||||
);
|
);
|
||||||
|
|
||||||
let mutex_result =
|
|
||||||
tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await;
|
|
||||||
|
|
||||||
let _guard = match mutex_result {
|
|
||||||
Ok(guard) => guard,
|
|
||||||
Err(_) => {
|
|
||||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
|
||||||
if current_sequence < latest_sequence {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"检测到更新的请求 (序列号: {} < {}),放弃当前请求",
|
|
||||||
current_sequence,
|
|
||||||
latest_sequence
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"强制获取锁以处理最新请求: {}",
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
PROFILE_UPDATE_MUTEX.lock().await
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
if current_sequence < latest_sequence {
|
if current_sequence < latest_sequence {
|
||||||
logging!(
|
logging!(
|
||||||
@@ -226,113 +287,110 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 保存当前配置,以便在验证失败时恢复
|
// 保存当前配置,以便在验证失败时恢复
|
||||||
let current_profile = Config::profiles().latest_ref().current.clone();
|
let current_profile = Config::profiles().await.latest_ref().current.clone();
|
||||||
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
|
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
|
||||||
|
|
||||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||||
if let Some(new_profile) = profiles.current.as_ref() {
|
if let Some(new_profile) = profiles.current.as_ref()
|
||||||
if current_profile.as_ref() != Some(new_profile) {
|
&& current_profile.as_ref() != Some(new_profile)
|
||||||
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
|
{
|
||||||
|
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
|
||||||
|
|
||||||
// 获取目标配置文件路径
|
// 获取目标配置文件路径
|
||||||
let config_file_result = {
|
let config_file_result = {
|
||||||
let profiles_config = Config::profiles();
|
let profiles_config = Config::profiles().await;
|
||||||
let profiles_data = profiles_config.latest_ref();
|
let profiles_data = profiles_config.latest_ref();
|
||||||
match profiles_data.get_item(new_profile) {
|
match profiles_data.get_item(new_profile) {
|
||||||
Ok(item) => {
|
Ok(item) => {
|
||||||
if let Some(file) = &item.file {
|
if let Some(file) = &item.file {
|
||||||
let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
|
let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
|
||||||
path.ok()
|
path.ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
Err(e) => {
|
||||||
|
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
|
||||||
// 如果获取到文件路径,检查YAML语法
|
None
|
||||||
if let Some(file_path) = config_file_result {
|
|
||||||
if !file_path.exists() {
|
|
||||||
logging!(
|
|
||||||
error,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"目标配置文件不存在: {}",
|
|
||||||
file_path.display()
|
|
||||||
);
|
|
||||||
handle::Handle::notice_message(
|
|
||||||
"config_validate::file_not_found",
|
|
||||||
format!("{}", file_path.display()),
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 超时保护
|
// 如果获取到文件路径,检查YAML语法
|
||||||
let file_read_result = tokio::time::timeout(
|
if let Some(file_path) = config_file_result {
|
||||||
Duration::from_secs(5),
|
if !file_path.exists() {
|
||||||
tokio::fs::read_to_string(&file_path),
|
logging!(
|
||||||
)
|
error,
|
||||||
.await;
|
Type::Cmd,
|
||||||
|
true,
|
||||||
|
"目标配置文件不存在: {}",
|
||||||
|
file_path.display()
|
||||||
|
);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::file_not_found",
|
||||||
|
format!("{}", file_path.display()),
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
match file_read_result {
|
// 超时保护
|
||||||
Ok(Ok(content)) => {
|
let file_read_result = tokio::time::timeout(
|
||||||
let yaml_parse_result = tokio::task::spawn_blocking(move || {
|
Duration::from_secs(5),
|
||||||
serde_yaml::from_str::<serde_yaml::Value>(&content)
|
tokio::fs::read_to_string(&file_path),
|
||||||
})
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match yaml_parse_result {
|
match file_read_result {
|
||||||
Ok(Ok(_)) => {
|
Ok(Ok(content)) => {
|
||||||
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
|
||||||
}
|
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
|
||||||
Ok(Err(err)) => {
|
})
|
||||||
let error_msg = format!(" {err}");
|
.await;
|
||||||
logging!(
|
|
||||||
error,
|
match yaml_parse_result {
|
||||||
Type::Cmd,
|
Ok(Ok(_)) => {
|
||||||
true,
|
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
||||||
"目标配置文件存在YAML语法错误:{}",
|
}
|
||||||
error_msg
|
Ok(Err(err)) => {
|
||||||
);
|
let error_msg = format!(" {err}");
|
||||||
handle::Handle::notice_message(
|
logging!(
|
||||||
"config_validate::yaml_syntax_error",
|
error,
|
||||||
&error_msg,
|
Type::Cmd,
|
||||||
);
|
true,
|
||||||
return Ok(false);
|
"目标配置文件存在YAML语法错误:{}",
|
||||||
}
|
error_msg
|
||||||
Err(join_err) => {
|
);
|
||||||
let error_msg = format!("YAML解析任务失败: {join_err}");
|
handle::Handle::notice_message(
|
||||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
"config_validate::yaml_syntax_error",
|
||||||
handle::Handle::notice_message(
|
&error_msg,
|
||||||
"config_validate::yaml_parse_error",
|
);
|
||||||
&error_msg,
|
return Ok(false);
|
||||||
);
|
}
|
||||||
return Ok(false);
|
Err(join_err) => {
|
||||||
}
|
let error_msg = format!("YAML解析任务失败: {join_err}");
|
||||||
|
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||||
|
handle::Handle::notice_message(
|
||||||
|
"config_validate::yaml_parse_error",
|
||||||
|
&error_msg,
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Err(err)) => {
|
}
|
||||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
Ok(Err(err)) => {
|
||||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||||
handle::Handle::notice_message(
|
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||||
"config_validate::file_read_error",
|
handle::Handle::notice_message("config_validate::file_read_error", &error_msg);
|
||||||
&error_msg,
|
return Ok(false);
|
||||||
);
|
}
|
||||||
return Ok(false);
|
Err(_) => {
|
||||||
}
|
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||||
Err(_) => {
|
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
handle::Handle::notice_message(
|
||||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
"config_validate::file_read_timeout",
|
||||||
handle::Handle::notice_message(
|
&error_msg,
|
||||||
"config_validate::file_read_timeout",
|
);
|
||||||
&error_msg,
|
return Ok(false);
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,18 +410,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref profile) = target_profile {
|
|
||||||
*CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone());
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Cmd,
|
|
||||||
true,
|
|
||||||
"设置当前处理profile: {}, 序列号: {}",
|
|
||||||
profile,
|
|
||||||
current_sequence
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新profiles配置
|
// 更新profiles配置
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
@@ -375,7 +421,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
|
|
||||||
let current_value = profiles.current.clone();
|
let current_value = profiles.current.clone();
|
||||||
|
|
||||||
let _ = Config::profiles().draft_mut().patch_config(profiles);
|
let _ = Config::profiles().await.draft_mut().patch_config(profiles);
|
||||||
|
|
||||||
// 在调用内核前再次验证请求有效性
|
// 在调用内核前再次验证请求有效性
|
||||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||||
@@ -388,7 +434,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
current_sequence,
|
current_sequence,
|
||||||
latest_sequence
|
latest_sequence
|
||||||
);
|
);
|
||||||
Config::profiles().discard();
|
Config::profiles().await.discard();
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +466,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
current_sequence,
|
current_sequence,
|
||||||
latest_sequence
|
latest_sequence
|
||||||
);
|
);
|
||||||
Config::profiles().discard();
|
Config::profiles().await.discard();
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +477,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
"配置更新成功,序列号: {}",
|
"配置更新成功,序列号: {}",
|
||||||
current_sequence
|
current_sequence
|
||||||
);
|
);
|
||||||
Config::profiles().apply();
|
Config::profiles().await.apply();
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
|
|
||||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||||
@@ -441,20 +487,18 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
if let Err(e) = Tray::global().update_tooltip().await {
|
||||||
if let Err(e) = Tray::global().update_tooltip() {
|
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = Tray::global().update_menu() {
|
if let Err(e) = Tray::global().update_menu().await {
|
||||||
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存配置文件
|
// 保存配置文件
|
||||||
if let Err(e) = Config::profiles().data_mut().save_file() {
|
if let Err(e) = profiles_save_file_safe().await {
|
||||||
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 立即通知前端配置变更
|
// 立即通知前端配置变更
|
||||||
if let Some(current) = ¤t_value {
|
if let Some(current) = ¤t_value {
|
||||||
@@ -469,13 +513,12 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
handle::Handle::notify_profile_changed(current.clone());
|
handle::Handle::notify_profile_changed(current.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup_processing_state(current_sequence, "配置切换完成").await;
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Ok(Ok((false, error_msg))) => {
|
Ok(Ok((false, error_msg))) => {
|
||||||
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
|
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
|
||||||
Config::profiles().discard();
|
Config::profiles().await.discard();
|
||||||
// 如果验证失败,恢复到之前的配置
|
// 如果验证失败,恢复到之前的配置
|
||||||
if let Some(prev_profile) = current_profile {
|
if let Some(prev_profile) = current_profile {
|
||||||
logging!(
|
logging!(
|
||||||
@@ -492,13 +535,14 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
// 静默恢复,不触发验证
|
// 静默恢复,不触发验证
|
||||||
wrap_err!({
|
wrap_err!({
|
||||||
Config::profiles()
|
Config::profiles()
|
||||||
|
.await
|
||||||
.draft_mut()
|
.draft_mut()
|
||||||
.patch_config(restore_profiles)
|
.patch_config(restore_profiles)
|
||||||
})?;
|
})?;
|
||||||
Config::profiles().apply();
|
Config::profiles().await.apply();
|
||||||
|
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
if let Err(e) = Config::profiles().data_mut().save_file() {
|
if let Err(e) = profiles_save_file_safe().await {
|
||||||
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
|
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -508,9 +552,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
|
|
||||||
// 发送验证错误通知
|
// 发送验证错误通知
|
||||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||||
cleanup_processing_state(current_sequence, "配置验证失败").await;
|
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
@@ -522,11 +564,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
e,
|
e,
|
||||||
current_sequence
|
current_sequence
|
||||||
);
|
);
|
||||||
Config::profiles().discard();
|
Config::profiles().await.discard();
|
||||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||||
|
|
||||||
cleanup_processing_state(current_sequence, "更新过程错误").await;
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -540,7 +581,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
timeout_msg,
|
timeout_msg,
|
||||||
current_sequence
|
current_sequence
|
||||||
);
|
);
|
||||||
Config::profiles().discard();
|
Config::profiles().await.discard();
|
||||||
|
|
||||||
if let Some(prev_profile) = current_profile {
|
if let Some(prev_profile) = current_profile {
|
||||||
logging!(
|
logging!(
|
||||||
@@ -557,16 +598,15 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
};
|
};
|
||||||
wrap_err!({
|
wrap_err!({
|
||||||
Config::profiles()
|
Config::profiles()
|
||||||
|
.await
|
||||||
.draft_mut()
|
.draft_mut()
|
||||||
.patch_config(restore_profiles)
|
.patch_config(restore_profiles)
|
||||||
})?;
|
})?;
|
||||||
Config::profiles().apply();
|
Config::profiles().await.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||||
|
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||||
cleanup_processing_state(current_sequence, "配置更新超时").await;
|
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -574,10 +614,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
|
|
||||||
/// 根据profile name修改profiles
|
/// 根据profile name修改profiles
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profiles_config_by_profile_index(
|
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<bool> {
|
||||||
_app_handle: tauri::AppHandle,
|
|
||||||
profile_index: String,
|
|
||||||
) -> CmdResult<bool> {
|
|
||||||
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
|
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
|
||||||
|
|
||||||
let profiles = IProfiles {
|
let profiles = IProfiles {
|
||||||
@@ -589,28 +626,26 @@ pub async fn patch_profiles_config_by_profile_index(
|
|||||||
|
|
||||||
/// 修改某个profile item的
|
/// 修改某个profile item的
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||||
// 保存修改前检查是否有更新 update_interval
|
// 保存修改前检查是否有更新 update_interval
|
||||||
let update_interval_changed =
|
let profiles = Config::profiles().await;
|
||||||
if let Ok(old_profile) = Config::profiles().latest_ref().get_item(&index) {
|
let update_interval_changed = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) {
|
||||||
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
|
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
|
||||||
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
|
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
|
||||||
old_interval != new_interval
|
old_interval != new_interval
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存修改
|
// 保存修改
|
||||||
wrap_err!(Config::profiles()
|
wrap_err!(profiles_patch_item_safe(index.clone(), profile).await)?;
|
||||||
.data_mut()
|
|
||||||
.patch_item(index.clone(), profile))?;
|
|
||||||
|
|
||||||
// 如果更新间隔变更,异步刷新定时器
|
// 如果更新间隔变更,异步刷新定时器
|
||||||
if update_interval_changed {
|
if update_interval_changed {
|
||||||
let index_clone = index.clone();
|
let index_clone = index.clone();
|
||||||
crate::process::AsyncHandler::spawn(move || async move {
|
crate::process::AsyncHandler::spawn(move || async move {
|
||||||
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
|
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
|
||||||
if let Err(e) = crate::core::Timer::global().refresh() {
|
if let Err(e) = crate::core::Timer::global().refresh().await {
|
||||||
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
|
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
|
||||||
} else {
|
} else {
|
||||||
// 刷新成功后发送自定义事件,不触发配置重载
|
// 刷新成功后发送自定义事件,不触发配置重载
|
||||||
@@ -624,9 +659,11 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
|||||||
|
|
||||||
/// 查看配置文件
|
/// 查看配置文件
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
pub async fn view_profile(index: String) -> CmdResult {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles_ref = profiles.latest_ref();
|
||||||
let file = {
|
let file = {
|
||||||
wrap_err!(Config::profiles().latest_ref().get_item(&index))?
|
wrap_err!(profiles_ref.get_item(&index))?
|
||||||
.file
|
.file
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or("the file field is null")
|
.ok_or("the file field is null")
|
||||||
@@ -637,23 +674,23 @@ pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
|||||||
ret_err!("the file not found");
|
ret_err!("the file not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
wrap_err!(help::open_file(app_handle, path))
|
wrap_err!(help::open_file(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读取配置文件内容
|
/// 读取配置文件内容
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles().await;
|
||||||
let profiles = profiles.latest_ref();
|
let profiles_ref = profiles.latest_ref();
|
||||||
let item = wrap_err!(profiles.get_item(&index))?;
|
let item = wrap_err!(profiles_ref.get_item(&index))?;
|
||||||
let data = wrap_err!(item.read_file())?;
|
let data = wrap_err!(item.read_file())?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取下一次更新时间
|
/// 获取下一次更新时间
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
||||||
let timer = Timer::global();
|
let timer = Timer::global();
|
||||||
let next_time = timer.get_next_update_time(&uid);
|
let next_time = timer.get_next_update_time(&uid).await;
|
||||||
Ok(next_time)
|
Ok(next_time)
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-14
@@ -1,20 +1,29 @@
|
|||||||
use super::CmdResult;
|
use tauri::Emitter;
|
||||||
use crate::module::mihomo::MihomoManager;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::state::proxy::ProxyRequestCache;
|
use super::CmdResult;
|
||||||
|
use crate::{
|
||||||
|
cache::CacheProxy,
|
||||||
|
core::{handle::Handle, tray::Tray},
|
||||||
|
ipc::IpcManager,
|
||||||
|
logging,
|
||||||
|
utils::logging::Type,
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let manager = MihomoManager::global();
|
let cache = CacheProxy::global();
|
||||||
let cache = ProxyRequestCache::global();
|
let key = CacheProxy::make_key("proxies", "default");
|
||||||
let key = ProxyRequestCache::make_key("proxies", "default");
|
|
||||||
let value = cache
|
let value = cache
|
||||||
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
|
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
|
||||||
manager.get_refresh_proxies().await.expect("fetch failed")
|
let manager = IpcManager::global();
|
||||||
|
manager.get_proxies().await.unwrap_or_else(|e| {
|
||||||
|
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
|
||||||
|
serde_json::Value::Object(serde_json::Map::new())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
Ok((*value).clone())
|
Ok((*value).clone())
|
||||||
@@ -23,21 +32,92 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
|||||||
/// 强制刷新代理缓存用于profile切换
|
/// 强制刷新代理缓存用于profile切换
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let cache = ProxyRequestCache::global();
|
let cache = CacheProxy::global();
|
||||||
let key = ProxyRequestCache::make_key("proxies", "default");
|
let key = CacheProxy::make_key("proxies", "default");
|
||||||
cache.map.remove(&key);
|
cache.map.remove(&key);
|
||||||
get_proxies().await
|
get_proxies().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let manager = MihomoManager::global();
|
let cache = CacheProxy::global();
|
||||||
let cache = ProxyRequestCache::global();
|
let key = CacheProxy::make_key("providers", "default");
|
||||||
let key = ProxyRequestCache::make_key("providers", "default");
|
|
||||||
let value = cache
|
let value = cache
|
||||||
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
|
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
|
||||||
manager.get_providers_proxies().await.expect("fetch failed")
|
let manager = IpcManager::global();
|
||||||
|
manager.get_providers_proxies().await.unwrap_or_else(|e| {
|
||||||
|
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
|
||||||
|
serde_json::Value::Object(serde_json::Map::new())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
Ok((*value).clone())
|
Ok((*value).clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 同步托盘和GUI的代理选择状态
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
||||||
|
use crate::core::tray::Tray;
|
||||||
|
|
||||||
|
match Tray::global().update_menu().await {
|
||||||
|
Ok(_) => {
|
||||||
|
logging!(info, Type::Cmd, "Tray proxy selection synced successfully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}");
|
||||||
|
Err(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新代理选择并同步托盘和GUI状态
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> {
|
||||||
|
match IpcManager::global().update_proxy(&group, &proxy).await {
|
||||||
|
Ok(_) => {
|
||||||
|
// println!("Proxy updated successfully: {} -> {}", group,proxy);
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"Proxy updated successfully: {} -> {}",
|
||||||
|
group,
|
||||||
|
proxy
|
||||||
|
);
|
||||||
|
|
||||||
|
let cache = CacheProxy::global();
|
||||||
|
let key = CacheProxy::make_key("proxies", "default");
|
||||||
|
cache.map.remove(&key);
|
||||||
|
|
||||||
|
if let Err(e) = Tray::global().update_menu().await {
|
||||||
|
logging!(error, Type::Cmd, "Failed to sync tray menu: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(app_handle) = Handle::global().app_handle() {
|
||||||
|
let _ = app_handle.emit("verge://force-refresh-proxies", ());
|
||||||
|
let _ = app_handle.emit("verge://refresh-proxy-config", ());
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Cmd,
|
||||||
|
"Proxy and sync completed successfully: {} -> {}",
|
||||||
|
group,
|
||||||
|
proxy
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("1111111111111111");
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Cmd,
|
||||||
|
"Failed to update proxy: {} -> {}, error: {}",
|
||||||
|
group,
|
||||||
|
proxy,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
Err(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+104
-15
@@ -1,36 +1,125 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{config::*, wrap_err};
|
use crate::{config::*, core::CoreManager, log_err, wrap_err};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// 获取运行时配置
|
/// 获取运行时配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||||
Ok(Config::runtime().latest_ref().config.clone())
|
Ok(Config::runtime().await.latest_ref().config.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取运行时YAML配置
|
/// 获取运行时YAML配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
pub async fn get_runtime_yaml() -> CmdResult<String> {
|
||||||
let runtime = Config::runtime();
|
let runtime = Config::runtime().await;
|
||||||
let runtime = runtime.latest_ref();
|
let runtime = runtime.latest_ref();
|
||||||
|
|
||||||
let config = runtime.config.as_ref();
|
let config = runtime.config.as_ref();
|
||||||
wrap_err!(config
|
wrap_err!(
|
||||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
config
|
||||||
.and_then(
|
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
.and_then(|config| serde_yaml_ng::to_string(config)
|
||||||
))
|
.context("failed to convert config to yaml"))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取运行时存在的键
|
/// 获取运行时存在的键
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||||
Ok(Config::runtime().latest_ref().exists_keys.clone())
|
Ok(Config::runtime().await.latest_ref().exists_keys.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取运行时日志
|
/// 获取运行时日志
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||||
Ok(Config::runtime().latest_ref().chain_logs.clone())
|
Ok(Config::runtime().await.latest_ref().chain_logs.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取运行时链式代理配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_runtime_proxy_chain_config() -> CmdResult<String> {
|
||||||
|
let runtime = Config::runtime().await;
|
||||||
|
let runtime = runtime.latest_ref();
|
||||||
|
|
||||||
|
let config = wrap_err!(
|
||||||
|
runtime
|
||||||
|
.config
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if let (
|
||||||
|
Some(serde_yaml_ng::Value::Sequence(proxies)),
|
||||||
|
Some(serde_yaml_ng::Value::Sequence(proxy_groups)),
|
||||||
|
) = (config.get("proxies"), config.get("proxy-groups"))
|
||||||
|
{
|
||||||
|
let mut proxy_name = None;
|
||||||
|
let mut proxies_chain = Vec::new();
|
||||||
|
|
||||||
|
let proxy_chain_groups = proxy_groups
|
||||||
|
.iter()
|
||||||
|
.filter_map(
|
||||||
|
|proxy_group| match proxy_group.get("name").and_then(|n| n.as_str()) {
|
||||||
|
Some("proxy_chain") => {
|
||||||
|
if let Some(serde_yaml_ng::Value::Sequence(ps)) = proxy_group.get("proxies")
|
||||||
|
&& let Some(x) = ps.first()
|
||||||
|
{
|
||||||
|
proxy_name = Some(x); //插入出口节点名字
|
||||||
|
}
|
||||||
|
Some(proxy_group.to_owned())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect::<Vec<serde_yaml_ng::Value>>();
|
||||||
|
|
||||||
|
while let Some(proxy) = proxies.iter().find(|proxy| {
|
||||||
|
if let serde_yaml_ng::Value::Mapping(proxy_map) = proxy {
|
||||||
|
proxy_map.get("name") == proxy_name && proxy_map.get("dialer-proxy").is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
proxies_chain.push(proxy.to_owned());
|
||||||
|
proxy_name = proxy.get("dialer-proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(entry_proxy) = proxies.iter().find(|proxy| proxy.get("name") == proxy_name) {
|
||||||
|
proxies_chain.push(entry_proxy.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies_chain.reverse();
|
||||||
|
|
||||||
|
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
|
||||||
|
config.insert("proxies".to_string(), proxies_chain);
|
||||||
|
config.insert("proxy-groups".to_string(), proxy_chain_groups);
|
||||||
|
|
||||||
|
wrap_err!(serde_yaml_ng::to_string(&config).context("YAML generation failed"))
|
||||||
|
} else {
|
||||||
|
wrap_err!(Err(anyhow::anyhow!(
|
||||||
|
"failed to get proxies or proxy-groups".to_string()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新运行时链式代理配置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_proxy_chain_config_in_runtime(
|
||||||
|
proxy_chain_config: Option<serde_yaml_ng::Value>,
|
||||||
|
) -> CmdResult<()> {
|
||||||
|
{
|
||||||
|
let runtime = Config::runtime().await;
|
||||||
|
let mut draft = runtime.draft_mut();
|
||||||
|
draft.update_proxy_chain_config(proxy_chain_config);
|
||||||
|
drop(draft);
|
||||||
|
runtime.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成新的运行配置文件并通知 Clash 核心重新加载
|
||||||
|
let run_path = wrap_err!(Config::generate_file(ConfigType::Run).await)?;
|
||||||
|
log_err!(CoreManager::global().put_configs_force(run_path).await);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
utils::{dirs, logging::Type},
|
utils::{dirs, logging::Type},
|
||||||
wrap_err,
|
wrap_err,
|
||||||
};
|
};
|
||||||
use std::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
/// 保存profiles的配置
|
/// 保存profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -17,7 +17,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
|
|
||||||
// 在异步操作前完成所有文件操作
|
// 在异步操作前完成所有文件操作
|
||||||
let (file_path, original_content, is_merge_file) = {
|
let (file_path, original_content, is_merge_file) = {
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles().await;
|
||||||
let profiles_guard = profiles.latest_ref();
|
let profiles_guard = profiles.latest_ref();
|
||||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||||
// 确定是否为merge类型文件
|
// 确定是否为merge类型文件
|
||||||
@@ -29,7 +29,8 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 保存新的配置文件
|
// 保存新的配置文件
|
||||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
let file_data = file_data.ok_or("file_data is None")?;
|
||||||
|
wrap_err!(fs::write(&file_path, &file_data).await)?;
|
||||||
|
|
||||||
let file_path_str = file_path.to_string_lossy().to_string();
|
let file_path_str = file_path.to_string_lossy().to_string();
|
||||||
logging!(
|
logging!(
|
||||||
@@ -61,14 +62,20 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
"[cmd配置save] merge文件语法验证通过"
|
"[cmd配置save] merge文件语法验证通过"
|
||||||
);
|
);
|
||||||
// 成功后尝试更新整体配置
|
// 成功后尝试更新整体配置
|
||||||
if let Err(e) = CoreManager::global().update_config().await {
|
match CoreManager::global().update_config().await {
|
||||||
logging!(
|
Ok(_) => {
|
||||||
warn,
|
// 配置更新成功,刷新前端
|
||||||
Type::Config,
|
handle::Handle::refresh_clash();
|
||||||
true,
|
}
|
||||||
"[cmd配置save] 更新整体配置时发生错误: {}",
|
Err(e) => {
|
||||||
e
|
logging!(
|
||||||
);
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[cmd配置save] 更新整体配置时发生错误: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -81,7 +88,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
error_msg
|
error_msg
|
||||||
);
|
);
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||||
// 发送合并文件专用错误通知
|
// 发送合并文件专用错误通知
|
||||||
let result = (false, error_msg.clone());
|
let result = (false, error_msg.clone());
|
||||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||||
@@ -96,7 +103,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
e
|
e
|
||||||
);
|
);
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||||
return Err(e.to_string());
|
return Err(e.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +127,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
error_msg
|
error_msg
|
||||||
);
|
);
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||||
|
|
||||||
// 智能判断错误类型
|
// 智能判断错误类型
|
||||||
let is_script_error = file_path_str.ends_with(".js")
|
let is_script_error = file_path_str.ends_with(".js")
|
||||||
@@ -133,17 +140,29 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||||
{
|
{
|
||||||
// 普通YAML错误使用YAML通知处理
|
// 普通YAML错误使用YAML通知处理
|
||||||
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
"[cmd配置save] YAML配置文件验证失败,发送通知"
|
||||||
|
);
|
||||||
let result = (false, error_msg.clone());
|
let result = (false, error_msg.clone());
|
||||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||||
} else if is_script_error {
|
} else if is_script_error {
|
||||||
// 脚本错误使用专门的通知处理
|
// 脚本错误使用专门的通知处理
|
||||||
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
"[cmd配置save] 脚本文件验证失败,发送通知"
|
||||||
|
);
|
||||||
let result = (false, error_msg.clone());
|
let result = (false, error_msg.clone());
|
||||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||||
} else {
|
} else {
|
||||||
// 普通配置错误使用一般通知
|
// 普通配置错误使用一般通知
|
||||||
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Config,
|
||||||
|
"[cmd配置save] 其他类型验证失败,发送一般通知"
|
||||||
|
);
|
||||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +177,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
|||||||
e
|
e
|
||||||
);
|
);
|
||||||
// 恢复原始配置文件
|
// 恢复原始配置文件
|
||||||
wrap_err!(fs::write(&file_path, original_content))?;
|
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||||
Err(e.to_string())
|
Err(e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,47 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{service, CoreManager},
|
core::{
|
||||||
|
CoreManager,
|
||||||
|
service::{self, SERVICE_MANAGER, ServiceStatus},
|
||||||
|
},
|
||||||
utils::i18n::t,
|
utils::i18n::t,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn execute_service_operation(
|
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
|
||||||
service_op: impl std::future::Future<Output = Result<(), impl ToString + std::fmt::Debug>>,
|
if let Err(e) = SERVICE_MANAGER
|
||||||
op_type: &str,
|
.lock()
|
||||||
) -> CmdResult {
|
.await
|
||||||
if service_op.await.is_err() {
|
.handle_service_status(&status)
|
||||||
let emsg = format!("{} {} failed", op_type, "Service");
|
.await
|
||||||
return Err(t(emsg.as_str()));
|
{
|
||||||
|
let emsg = format!("{} Service failed: {}", op_type, e);
|
||||||
|
return Err(t(emsg.as_str()).await);
|
||||||
}
|
}
|
||||||
if CoreManager::global().restart_core().await.is_err() {
|
if CoreManager::global().restart_core().await.is_err() {
|
||||||
let emsg = format!("{} {} failed", "Restart", "Core");
|
let emsg = "Restart Core failed";
|
||||||
return Err(t(emsg.as_str()));
|
return Err(t(emsg).await);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn install_service() -> CmdResult {
|
pub async fn install_service() -> CmdResult {
|
||||||
execute_service_operation(service::install_service(), "Install").await
|
execute_service_operation_sync(ServiceStatus::InstallRequired, "Install").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn uninstall_service() -> CmdResult {
|
pub async fn uninstall_service() -> CmdResult {
|
||||||
execute_service_operation(service::uninstall_service(), "Uninstall").await
|
execute_service_operation_sync(ServiceStatus::UninstallRequired, "Uninstall").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn reinstall_service() -> CmdResult {
|
pub async fn reinstall_service() -> CmdResult {
|
||||||
execute_service_operation(service::reinstall_service(), "Reinstall").await
|
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "Reinstall").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn repair_service() -> CmdResult {
|
pub async fn repair_service() -> CmdResult {
|
||||||
execute_service_operation(service::force_reinstall_service(), "Repair").await
|
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "Repair").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{handle, CoreManager},
|
core::{CoreManager, handle},
|
||||||
|
logging,
|
||||||
module::sysinfo::PlatformSpecification,
|
module::sysinfo::PlatformSpecification,
|
||||||
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -23,20 +25,22 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||||
let sysinfo = PlatformSpecification::new_async().await;
|
let sysinfo = PlatformSpecification::new_sync();
|
||||||
let info = format!("{sysinfo:?}");
|
let info = format!("{sysinfo:?}");
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global()
|
||||||
|
.app_handle()
|
||||||
|
.ok_or("Failed to get app handle")?;
|
||||||
let cliboard = app_handle.clipboard();
|
let cliboard = app_handle.clipboard();
|
||||||
if cliboard.write_text(info).is_err() {
|
if cliboard.write_text(info).is_err() {
|
||||||
log::error!(target: "app", "Failed to write to clipboard");
|
logging!(error, Type::System, "Failed to write to clipboard");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_system_info() -> CmdResult<String> {
|
pub async fn get_system_info() -> CmdResult<String> {
|
||||||
let sysinfo = PlatformSpecification::new_async().await;
|
let sysinfo = PlatformSpecification::new_sync();
|
||||||
let info = format!("{sysinfo:?}");
|
let info = format!("{sysinfo:?}");
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
@@ -44,7 +48,7 @@ pub async fn get_system_info() -> CmdResult<String> {
|
|||||||
/// 获取当前内核运行模式
|
/// 获取当前内核运行模式
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_running_mode() -> Result<String, String> {
|
pub async fn get_running_mode() -> Result<String, String> {
|
||||||
Ok(CoreManager::global().get_running_mode().await.to_string())
|
Ok(CoreManager::global().get_running_mode().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取应用的运行时间(毫秒)
|
/// 获取应用的运行时间(毫秒)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ mod platform {
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{core::win_uwp, wrap_err};
|
use crate::{core::win_uwp, wrap_err};
|
||||||
|
|
||||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
pub fn invoke_uwp_tool() -> CmdResult {
|
||||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
wrap_err!(win_uwp::invoke_uwptools())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ mod platform {
|
|||||||
mod platform {
|
mod platform {
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
|
|
||||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
pub fn invoke_uwp_tool() -> CmdResult {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,5 +24,5 @@ mod platform {
|
|||||||
/// Command exposed to Tauri
|
/// Command exposed to Tauri
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||||
platform::invoke_uwp_tool().await
|
platform::invoke_uwp_tool()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ use crate::{config::*, feat, wrap_err};
|
|||||||
|
|
||||||
/// 获取Verge配置
|
/// 获取Verge配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge().await;
|
||||||
let verge_data = verge.latest_ref().clone();
|
let verge_data = {
|
||||||
Ok(IVergeResponse::from(*verge_data))
|
let ref_data = verge.latest_ref();
|
||||||
|
ref_data.clone()
|
||||||
|
};
|
||||||
|
let verge_response = IVergeResponse::from(*verge_data);
|
||||||
|
Ok(verge_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改Verge配置
|
/// 修改Verge配置
|
||||||
|
|||||||
@@ -11,11 +11,17 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
|||||||
webdav_password: Some(password),
|
webdav_password: Some(password),
|
||||||
..IVerge::default()
|
..IVerge::default()
|
||||||
};
|
};
|
||||||
Config::verge().draft_mut().patch_config(patch.clone());
|
|
||||||
Config::verge().apply();
|
|
||||||
Config::verge()
|
Config::verge()
|
||||||
.latest_ref()
|
.await
|
||||||
|
.draft_mut()
|
||||||
|
.patch_config(patch.clone());
|
||||||
|
Config::verge().await.apply();
|
||||||
|
|
||||||
|
// 分离数据获取和异步调用
|
||||||
|
let verge_data = Config::verge().await.latest_ref().clone();
|
||||||
|
verge_data
|
||||||
.save_file()
|
.save_file()
|
||||||
|
.await
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
core::backup::WebDavClient::global().reset();
|
core::backup::WebDavClient::global().reset();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
use crate::config::Config;
|
||||||
|
use crate::utils::dirs::{ipc_path, path_to_str};
|
||||||
use crate::utils::{dirs, help};
|
use crate::utils::{dirs, help};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
@@ -11,20 +13,29 @@ use std::{
|
|||||||
pub struct IClashTemp(pub Mapping);
|
pub struct IClashTemp(pub Mapping);
|
||||||
|
|
||||||
impl IClashTemp {
|
impl IClashTemp {
|
||||||
pub fn new() -> Self {
|
pub async fn new() -> Self {
|
||||||
let template = Self::template();
|
let template = Self::template();
|
||||||
match dirs::clash_path().and_then(|path| help::read_mapping(&path)) {
|
let clash_path_result = dirs::clash_path();
|
||||||
|
let map_result = if let Ok(path) = clash_path_result {
|
||||||
|
help::read_mapping(&path).await
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Failed to get clash path"))
|
||||||
|
};
|
||||||
|
|
||||||
|
match map_result {
|
||||||
Ok(mut map) => {
|
Ok(mut map) => {
|
||||||
template.0.keys().for_each(|key| {
|
template.0.keys().for_each(|key| {
|
||||||
if !map.contains_key(key) {
|
if !map.contains_key(key)
|
||||||
map.insert(key.clone(), template.0.get(key).unwrap().clone());
|
&& let Some(value) = template.0.get(key)
|
||||||
|
{
|
||||||
|
map.insert(key.clone(), value.clone());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 确保 secret 字段存在且不为空
|
// 确保 secret 字段存在且不为空
|
||||||
if let Some(Value::String(s)) = map.get_mut("secret") {
|
if let Some(Value::String(s)) = map.get_mut("secret")
|
||||||
if s.is_empty() {
|
&& s.is_empty()
|
||||||
*s = "set-your-secret".to_string();
|
{
|
||||||
}
|
*s = "set-your-secret".to_string();
|
||||||
}
|
}
|
||||||
Self(Self::guard(map))
|
Self(Self::guard(map))
|
||||||
}
|
}
|
||||||
@@ -57,6 +68,16 @@ impl IClashTemp {
|
|||||||
map.insert("ipv6".into(), true.into());
|
map.insert("ipv6".into(), true.into());
|
||||||
map.insert("mode".into(), "rule".into());
|
map.insert("mode".into(), "rule".into());
|
||||||
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
||||||
|
#[cfg(unix)]
|
||||||
|
map.insert(
|
||||||
|
"external-controller-unix".into(),
|
||||||
|
Self::guard_external_controller_ipc().into(),
|
||||||
|
);
|
||||||
|
#[cfg(windows)]
|
||||||
|
map.insert(
|
||||||
|
"external-controller-pipe".into(),
|
||||||
|
Self::guard_external_controller_ipc().into(),
|
||||||
|
);
|
||||||
cors_map.insert("allow-private-network".into(), true.into());
|
cors_map.insert("allow-private-network".into(), true.into());
|
||||||
cors_map.insert(
|
cors_map.insert(
|
||||||
"allow-origins".into(),
|
"allow-origins".into(),
|
||||||
@@ -87,7 +108,12 @@ impl IClashTemp {
|
|||||||
let mixed_port = Self::guard_mixed_port(&config);
|
let mixed_port = Self::guard_mixed_port(&config);
|
||||||
let socks_port = Self::guard_socks_port(&config);
|
let socks_port = Self::guard_socks_port(&config);
|
||||||
let port = Self::guard_port(&config);
|
let port = Self::guard_port(&config);
|
||||||
let ctrl = Self::guard_server_ctrl(&config);
|
let ctrl = Self::guard_external_controller(&config);
|
||||||
|
#[cfg(unix)]
|
||||||
|
let external_controller_unix = Self::guard_external_controller_ipc();
|
||||||
|
#[cfg(windows)]
|
||||||
|
let external_controller_pipe = Self::guard_external_controller_ipc();
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
config.insert("redir-port".into(), redir_port.into());
|
config.insert("redir-port".into(), redir_port.into());
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -97,6 +123,16 @@ impl IClashTemp {
|
|||||||
config.insert("port".into(), port.into());
|
config.insert("port".into(), port.into());
|
||||||
config.insert("external-controller".into(), ctrl.into());
|
config.insert("external-controller".into(), ctrl.into());
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
config.insert(
|
||||||
|
"external-controller-unix".into(),
|
||||||
|
external_controller_unix.into(),
|
||||||
|
);
|
||||||
|
#[cfg(windows)]
|
||||||
|
config.insert(
|
||||||
|
"external-controller-pipe".into(),
|
||||||
|
external_controller_pipe.into(),
|
||||||
|
);
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +142,13 @@ impl IClashTemp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_config(&self) -> Result<()> {
|
pub async fn save_config(&self) -> Result<()> {
|
||||||
help::save_yaml(
|
help::save_yaml(
|
||||||
&dirs::clash_path()?,
|
&dirs::clash_path()?,
|
||||||
&self.0,
|
&self.0,
|
||||||
Some("# Generated by Clash Verge"),
|
Some("# Generated by Clash Verge"),
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mixed_port(&self) -> u16 {
|
pub fn get_mixed_port(&self) -> u16 {
|
||||||
@@ -245,6 +282,27 @@ impl IClashTemp {
|
|||||||
.unwrap_or("127.0.0.1:9097".into())
|
.unwrap_or("127.0.0.1:9097".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn guard_external_controller(config: &Mapping) -> String {
|
||||||
|
// 在初始化阶段,直接返回配置中的值,不进行额外检查
|
||||||
|
// 这样可以避免在配置加载期间的循环依赖
|
||||||
|
Self::guard_server_ctrl(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn guard_external_controller_with_setting(config: &Mapping) -> String {
|
||||||
|
// 检查 enable_external_controller 设置,用于运行时配置生成
|
||||||
|
let enable_external_controller = Config::verge()
|
||||||
|
.await
|
||||||
|
.latest_ref()
|
||||||
|
.enable_external_controller
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if enable_external_controller {
|
||||||
|
Self::guard_server_ctrl(config)
|
||||||
|
} else {
|
||||||
|
"".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn guard_client_ctrl(config: &Mapping) -> String {
|
pub fn guard_client_ctrl(config: &Mapping) -> String {
|
||||||
let value = Self::guard_server_ctrl(config);
|
let value = Self::guard_server_ctrl(config);
|
||||||
match SocketAddr::from_str(value.as_str()) {
|
match SocketAddr::from_str(value.as_str()) {
|
||||||
@@ -257,6 +315,17 @@ impl IClashTemp {
|
|||||||
Err(_) => "127.0.0.1:9097".into(),
|
Err(_) => "127.0.0.1:9097".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn guard_external_controller_ipc() -> String {
|
||||||
|
// 总是使用当前的 IPC 路径,确保配置文件与运行时路径一致
|
||||||
|
ipc_path()
|
||||||
|
.ok()
|
||||||
|
.and_then(|path| path_to_str(&path).ok().map(|s| s.to_string()))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
log::error!(target: "app", "Failed to get IPC path, using default");
|
||||||
|
"127.0.0.1:9090".to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::PrfItem,
|
config::{PrfItem, profiles_append_item_safe},
|
||||||
core::{handle, CoreManager},
|
core::{CoreManager, handle},
|
||||||
enhance, logging,
|
enhance, logging,
|
||||||
process::AsyncHandler,
|
|
||||||
utils::{dirs, help, logging::Type},
|
utils::{dirs, help, logging::Type},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{Result, anyhow};
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tokio::time::{sleep, Duration};
|
use tokio::sync::OnceCell;
|
||||||
|
use tokio::time::{Duration, sleep};
|
||||||
|
|
||||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||||
@@ -22,54 +21,55 @@ pub struct Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn global() -> &'static Config {
|
pub async fn global() -> &'static Config {
|
||||||
static CONFIG: OnceCell<Config> = OnceCell::new();
|
static CONFIG: OnceCell<Config> = OnceCell::const_new();
|
||||||
|
CONFIG
|
||||||
CONFIG.get_or_init(|| Config {
|
.get_or_init(|| async {
|
||||||
clash_config: Draft::from(Box::new(IClashTemp::new())),
|
Config {
|
||||||
verge_config: Draft::from(Box::new(IVerge::new())),
|
clash_config: Draft::from(Box::new(IClashTemp::new().await)),
|
||||||
profiles_config: Draft::from(Box::new(IProfiles::new())),
|
verge_config: Draft::from(Box::new(IVerge::new().await)),
|
||||||
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
profiles_config: Draft::from(Box::new(IProfiles::new().await)),
|
||||||
})
|
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clash() -> Draft<Box<IClashTemp>> {
|
pub async fn clash() -> Draft<Box<IClashTemp>> {
|
||||||
Self::global().clash_config.clone()
|
Self::global().await.clash_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verge() -> Draft<Box<IVerge>> {
|
pub async fn verge() -> Draft<Box<IVerge>> {
|
||||||
Self::global().verge_config.clone()
|
Self::global().await.verge_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profiles() -> Draft<Box<IProfiles>> {
|
pub async fn profiles() -> Draft<Box<IProfiles>> {
|
||||||
Self::global().profiles_config.clone()
|
Self::global().await.profiles_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runtime() -> Draft<Box<IRuntime>> {
|
pub async fn runtime() -> Draft<Box<IRuntime>> {
|
||||||
Self::global().runtime_config.clone()
|
Self::global().await.runtime_config.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初始化订阅
|
/// 初始化订阅
|
||||||
pub async fn init_config() -> Result<()> {
|
pub async fn init_config() -> Result<()> {
|
||||||
if Self::profiles()
|
if Self::profiles()
|
||||||
|
.await
|
||||||
.latest_ref()
|
.latest_ref()
|
||||||
.get_item(&"Merge".to_string())
|
.get_item(&"Merge".to_string())
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
|
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
|
||||||
Self::profiles()
|
profiles_append_item_safe(merge_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(merge_item.clone())?;
|
|
||||||
}
|
}
|
||||||
if Self::profiles()
|
if Self::profiles()
|
||||||
|
.await
|
||||||
.latest_ref()
|
.latest_ref()
|
||||||
.get_item(&"Script".to_string())
|
.get_item(&"Script".to_string())
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||||
Self::profiles()
|
profiles_append_item_safe(script_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(script_item.clone())?;
|
|
||||||
}
|
}
|
||||||
// 生成运行时配置
|
// 生成运行时配置
|
||||||
if let Err(err) = Self::generate().await {
|
if let Err(err) = Self::generate().await {
|
||||||
@@ -79,7 +79,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成运行时配置文件并验证
|
// 生成运行时配置文件并验证
|
||||||
let config_result = Self::generate_file(ConfigType::Run);
|
let config_result = Self::generate_file(ConfigType::Run).await;
|
||||||
|
|
||||||
let validation_result = if config_result.is_ok() {
|
let validation_result = if config_result.is_ok() {
|
||||||
// 验证配置文件
|
// 验证配置文件
|
||||||
@@ -101,7 +101,9 @@ impl Config {
|
|||||||
Some(("config_validate::boot_error", error_msg))
|
Some(("config_validate::boot_error", error_msg))
|
||||||
} else {
|
} else {
|
||||||
logging!(info, Type::Config, true, "配置验证成功");
|
logging!(info, Type::Config, true, "配置验证成功");
|
||||||
Some(("config_validate::success", String::new()))
|
// 前端没有必要知道验证成功的消息,也没有事件驱动
|
||||||
|
// Some(("config_validate::success", String::new()))
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -122,30 +124,30 @@ impl Config {
|
|||||||
|
|
||||||
// 在单独的任务中发送通知
|
// 在单独的任务中发送通知
|
||||||
if let Some((msg_type, msg_content)) = validation_result {
|
if let Some((msg_type, msg_content)) = validation_result {
|
||||||
AsyncHandler::spawn(move || async move {
|
sleep(Duration::from_secs(2)).await;
|
||||||
sleep(Duration::from_secs(2)).await;
|
handle::Handle::notice_message(msg_type, &msg_content);
|
||||||
handle::Handle::notice_message(msg_type, &msg_content);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将订阅丢到对应的文件中
|
/// 将订阅丢到对应的文件中
|
||||||
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
pub async fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||||
let path = match typ {
|
let path = match typ {
|
||||||
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
||||||
ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG),
|
ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG),
|
||||||
};
|
};
|
||||||
|
|
||||||
let runtime = Config::runtime();
|
let runtime = Config::runtime().await;
|
||||||
let runtime = runtime.latest_ref();
|
|
||||||
let config = runtime
|
let config = runtime
|
||||||
|
.latest_ref()
|
||||||
.config
|
.config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(anyhow!("failed to get runtime config"))?;
|
.ok_or(anyhow!("failed to get runtime config"))?
|
||||||
|
.clone();
|
||||||
|
drop(runtime); // 显式释放锁
|
||||||
|
|
||||||
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
|
help::save_yaml(&path, &config, Some("# Generated by Clash Verge")).await?;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +155,7 @@ impl Config {
|
|||||||
pub async fn generate() -> Result<()> {
|
pub async fn generate() -> Result<()> {
|
||||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||||
|
|
||||||
*Config::runtime().draft_mut() = Box::new(IRuntime {
|
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
|
||||||
config: Some(config),
|
config: Some(config),
|
||||||
exists_keys,
|
exists_keys,
|
||||||
chain_logs: logs,
|
chain_logs: logs,
|
||||||
@@ -174,33 +176,33 @@ mod tests {
|
|||||||
use std::mem;
|
use std::mem;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
fn test_prfitem_from_merge_size() {
|
fn test_prfitem_from_merge_size() {
|
||||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap();
|
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))
|
||||||
dbg!(&merge_item);
|
.expect("Failed to create merge item in test");
|
||||||
let prfitem_size = mem::size_of_val(&merge_item);
|
let prfitem_size = mem::size_of_val(&merge_item);
|
||||||
dbg!(prfitem_size);
|
|
||||||
// Boxed version
|
// Boxed version
|
||||||
let boxed_merge_item = Box::new(merge_item);
|
let boxed_merge_item = Box::new(merge_item);
|
||||||
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
|
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
|
||||||
dbg!(box_prfitem_size);
|
|
||||||
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
|
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
|
||||||
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
|
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
|
||||||
assert!(box_prfitem_size < prfitem_size);
|
assert!(box_prfitem_size < prfitem_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(unused_variables)]
|
||||||
fn test_draft_size_non_boxed() {
|
fn test_draft_size_non_boxed() {
|
||||||
let draft = Draft::from(IRuntime::new());
|
let draft = Draft::from(IRuntime::new());
|
||||||
let iruntime_size = std::mem::size_of_val(&draft);
|
let iruntime_size = std::mem::size_of_val(&draft);
|
||||||
dbg!(iruntime_size);
|
|
||||||
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
|
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(unused_variables)]
|
||||||
fn test_draft_size_boxed() {
|
fn test_draft_size_boxed() {
|
||||||
let draft = Draft::from(Box::new(IRuntime::new()));
|
let draft = Draft::from(Box::new(IRuntime::new()));
|
||||||
let box_iruntime_size = std::mem::size_of_val(&draft);
|
let box_iruntime_size = std::mem::size_of_val(&draft);
|
||||||
dbg!(box_iruntime_size);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
box_iruntime_size,
|
box_iruntime_size,
|
||||||
std::mem::size_of::<Draft<Box<IRuntime>>>()
|
std::mem::size_of::<Draft<Box<IRuntime>>>()
|
||||||
|
|||||||
@@ -46,11 +46,18 @@ impl<T: Clone + ToOwned> Draft<Box<T>> {
|
|||||||
if guard.1.is_none() {
|
if guard.1.is_none() {
|
||||||
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
|
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
|
||||||
guard.1 = Some(guard.0.clone());
|
guard.1 = Some(guard.0.clone());
|
||||||
return RwLockWriteGuard::map(guard, |inner| inner.1.as_mut().unwrap());
|
return RwLockWriteGuard::map(guard, |inner| {
|
||||||
|
inner.1.as_mut().unwrap_or_else(|| {
|
||||||
|
unreachable!("Draft was just created above, this should never fail")
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// 已存在草稿,升级为写锁映射
|
// 已存在草稿,升级为写锁映射
|
||||||
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
|
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
|
||||||
inner.1.as_mut().unwrap()
|
inner
|
||||||
|
.1
|
||||||
|
.as_mut()
|
||||||
|
.unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::utils::dirs::get_encryption_key;
|
use crate::utils::dirs::get_encryption_key;
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{Aead, KeyInit},
|
|
||||||
Aes256Gcm, Key,
|
Aes256Gcm, Key,
|
||||||
|
aead::{Aead, KeyInit},
|
||||||
};
|
};
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
const NONCE_LENGTH: usize = 12;
|
const NONCE_LENGTH: usize = 12;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ mod config;
|
|||||||
mod draft;
|
mod draft;
|
||||||
mod encrypt;
|
mod encrypt;
|
||||||
mod prfitem;
|
mod prfitem;
|
||||||
mod profiles;
|
pub mod profiles;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
mod verge;
|
mod verge;
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,11 @@ use crate::utils::{
|
|||||||
network::{NetworkManager, ProxyType},
|
network::{NetworkManager, ProxyType},
|
||||||
tmpl,
|
tmpl,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use reqwest::StatusCode;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
use std::{fs, time::Duration};
|
use std::{fs, time::Duration};
|
||||||
|
|
||||||
use super::Config;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
pub struct PrfItem {
|
pub struct PrfItem {
|
||||||
pub uid: Option<String>,
|
pub uid: Option<String>,
|
||||||
@@ -147,12 +144,15 @@ impl PrfItem {
|
|||||||
bail!("type should not be null");
|
bail!("type should not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
match item.itype.unwrap().as_str() {
|
let itype = item
|
||||||
|
.itype
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("type should not be null"))?;
|
||||||
|
match itype.as_str() {
|
||||||
"remote" => {
|
"remote" => {
|
||||||
if item.url.is_none() {
|
let url = item
|
||||||
bail!("url should not be null");
|
.url
|
||||||
}
|
.as_ref()
|
||||||
let url = item.url.as_ref().unwrap().as_str();
|
.ok_or_else(|| anyhow::anyhow!("url should not be null"))?;
|
||||||
let name = item.name;
|
let name = item.name;
|
||||||
let desc = item.desc;
|
let desc = item.desc;
|
||||||
PrfItem::from_url(url, name, desc, item.option).await
|
PrfItem::from_url(url, name, desc, item.option).await
|
||||||
@@ -160,7 +160,7 @@ impl PrfItem {
|
|||||||
"local" => {
|
"local" => {
|
||||||
let name = item.name.unwrap_or("Local File".into());
|
let name = item.name.unwrap_or("Local File".into());
|
||||||
let desc = item.desc.unwrap_or("".into());
|
let desc = item.desc.unwrap_or("".into());
|
||||||
PrfItem::from_local(name, desc, file_data, item.option)
|
PrfItem::from_local(name, desc, file_data, item.option).await
|
||||||
}
|
}
|
||||||
typ => bail!("invalid profile item type \"{typ}\""),
|
typ => bail!("invalid profile item type \"{typ}\""),
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,7 @@ impl PrfItem {
|
|||||||
|
|
||||||
/// ## Local type
|
/// ## Local type
|
||||||
/// create a new item from name/desc
|
/// create a new item from name/desc
|
||||||
pub fn from_local(
|
pub async fn from_local(
|
||||||
name: String,
|
name: String,
|
||||||
desc: String,
|
desc: String,
|
||||||
file_data: Option<String>,
|
file_data: Option<String>,
|
||||||
@@ -186,37 +186,27 @@ impl PrfItem {
|
|||||||
|
|
||||||
if merge.is_none() {
|
if merge.is_none() {
|
||||||
let merge_item = PrfItem::from_merge(None)?;
|
let merge_item = PrfItem::from_merge(None)?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(merge_item.clone())?;
|
|
||||||
merge = merge_item.uid;
|
merge = merge_item.uid;
|
||||||
}
|
}
|
||||||
if script.is_none() {
|
if script.is_none() {
|
||||||
let script_item = PrfItem::from_script(None)?;
|
let script_item = PrfItem::from_script(None)?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(script_item.clone())?;
|
|
||||||
script = script_item.uid;
|
script = script_item.uid;
|
||||||
}
|
}
|
||||||
if rules.is_none() {
|
if rules.is_none() {
|
||||||
let rules_item = PrfItem::from_rules()?;
|
let rules_item = PrfItem::from_rules()?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(rules_item.clone())?;
|
|
||||||
rules = rules_item.uid;
|
rules = rules_item.uid;
|
||||||
}
|
}
|
||||||
if proxies.is_none() {
|
if proxies.is_none() {
|
||||||
let proxies_item = PrfItem::from_proxies()?;
|
let proxies_item = PrfItem::from_proxies()?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(proxies_item.clone())?;
|
|
||||||
proxies = proxies_item.uid;
|
proxies = proxies_item.uid;
|
||||||
}
|
}
|
||||||
if groups.is_none() {
|
if groups.is_none() {
|
||||||
let groups_item = PrfItem::from_groups()?;
|
let groups_item = PrfItem::from_groups()?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(groups_item.clone())?;
|
|
||||||
groups = groups_item.uid;
|
groups = groups_item.uid;
|
||||||
}
|
}
|
||||||
Ok(PrfItem {
|
Ok(PrfItem {
|
||||||
@@ -275,7 +265,7 @@ impl PrfItem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 使用网络管理器发送请求
|
// 使用网络管理器发送请求
|
||||||
let resp = match NetworkManager::global()
|
let resp = match NetworkManager::new()
|
||||||
.get_with_interrupt(
|
.get_with_interrupt(
|
||||||
url,
|
url,
|
||||||
proxy_type,
|
proxy_type,
|
||||||
@@ -293,7 +283,7 @@ impl PrfItem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let status_code = resp.status();
|
let status_code = resp.status();
|
||||||
if !StatusCode::is_success(&status_code) {
|
if !status_code.is_success() {
|
||||||
bail!("failed to fetch remote profile with status {status_code}")
|
bail!("failed to fetch remote profile with status {status_code}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,13 +349,13 @@ impl PrfItem {
|
|||||||
let uid = help::get_uid("R");
|
let uid = help::get_uid("R");
|
||||||
let file = format!("{uid}.yaml");
|
let file = format!("{uid}.yaml");
|
||||||
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||||
let data = resp.text_with_charset("utf-8").await?;
|
let data = resp.text_with_charset()?;
|
||||||
|
|
||||||
// process the charset "UTF-8 with BOM"
|
// process the charset "UTF-8 with BOM"
|
||||||
let data = data.trim_start_matches('\u{feff}');
|
let data = data.trim_start_matches('\u{feff}');
|
||||||
|
|
||||||
// check the data whether the valid yaml format
|
// check the data whether the valid yaml format
|
||||||
let yaml = serde_yaml::from_str::<Mapping>(data)
|
let yaml = serde_yaml_ng::from_str::<Mapping>(data)
|
||||||
.context("the remote profile data is invalid yaml")?;
|
.context("the remote profile data is invalid yaml")?;
|
||||||
|
|
||||||
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
|
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
|
||||||
@@ -374,37 +364,27 @@ impl PrfItem {
|
|||||||
|
|
||||||
if merge.is_none() {
|
if merge.is_none() {
|
||||||
let merge_item = PrfItem::from_merge(None)?;
|
let merge_item = PrfItem::from_merge(None)?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(merge_item.clone())?;
|
|
||||||
merge = merge_item.uid;
|
merge = merge_item.uid;
|
||||||
}
|
}
|
||||||
if script.is_none() {
|
if script.is_none() {
|
||||||
let script_item = PrfItem::from_script(None)?;
|
let script_item = PrfItem::from_script(None)?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(script_item.clone())?;
|
|
||||||
script = script_item.uid;
|
script = script_item.uid;
|
||||||
}
|
}
|
||||||
if rules.is_none() {
|
if rules.is_none() {
|
||||||
let rules_item = PrfItem::from_rules()?;
|
let rules_item = PrfItem::from_rules()?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(rules_item.clone())?;
|
|
||||||
rules = rules_item.uid;
|
rules = rules_item.uid;
|
||||||
}
|
}
|
||||||
if proxies.is_none() {
|
if proxies.is_none() {
|
||||||
let proxies_item = PrfItem::from_proxies()?;
|
let proxies_item = PrfItem::from_proxies()?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(proxies_item.clone())?;
|
|
||||||
proxies = proxies_item.uid;
|
proxies = proxies_item.uid;
|
||||||
}
|
}
|
||||||
if groups.is_none() {
|
if groups.is_none() {
|
||||||
let groups_item = PrfItem::from_groups()?;
|
let groups_item = PrfItem::from_groups()?;
|
||||||
Config::profiles()
|
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
|
||||||
.data_mut()
|
|
||||||
.append_item(groups_item.clone())?;
|
|
||||||
groups = groups_item.uid;
|
groups = groups_item.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,22 +529,20 @@ impl PrfItem {
|
|||||||
|
|
||||||
/// get the file data
|
/// get the file data
|
||||||
pub fn read_file(&self) -> Result<String> {
|
pub fn read_file(&self) -> Result<String> {
|
||||||
if self.file.is_none() {
|
let file = self
|
||||||
bail!("could not find the file");
|
.file
|
||||||
}
|
.clone()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
|
||||||
let file = self.file.clone().unwrap();
|
|
||||||
let path = dirs::app_profiles_dir()?.join(file);
|
let path = dirs::app_profiles_dir()?.join(file);
|
||||||
fs::read_to_string(path).context("failed to read the file")
|
fs::read_to_string(path).context("failed to read the file")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// save the file data
|
/// save the file data
|
||||||
pub fn save_file(&self, data: String) -> Result<()> {
|
pub fn save_file(&self, data: String) -> Result<()> {
|
||||||
if self.file.is_none() {
|
let file = self
|
||||||
bail!("could not find the file");
|
.file
|
||||||
}
|
.clone()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
|
||||||
let file = self.file.clone().unwrap();
|
|
||||||
let path = dirs::app_profiles_dir()?.join(file);
|
let path = dirs::app_profiles_dir()?.join(file);
|
||||||
fs::write(path, data.as_bytes()).context("failed to save the file")
|
fs::write(path, data.as_bytes()).context("failed to save the file")
|
||||||
}
|
}
|
||||||
|
|||||||
+303
-169
@@ -1,9 +1,14 @@
|
|||||||
use super::{prfitem::PrfItem, PrfOption};
|
use super::{PrfOption, prfitem::PrfItem};
|
||||||
use crate::utils::{dirs, help};
|
use crate::{
|
||||||
use anyhow::{bail, Context, Result};
|
logging_error,
|
||||||
|
process::AsyncHandler,
|
||||||
|
utils::{dirs, help, logging::Type},
|
||||||
|
};
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
use std::{collections::HashSet, fs, io::Write};
|
use std::collections::HashSet;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
/// Define the `profiles.yaml` schema
|
/// Define the `profiles.yaml` schema
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -32,22 +37,28 @@ macro_rules! patch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IProfiles {
|
impl IProfiles {
|
||||||
pub fn new() -> Self {
|
pub async fn new() -> Self {
|
||||||
match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) {
|
match dirs::profiles_path() {
|
||||||
Ok(mut profiles) => {
|
Ok(path) => match help::read_yaml::<Self>(&path).await {
|
||||||
if profiles.items.is_none() {
|
Ok(mut profiles) => {
|
||||||
profiles.items = Some(vec![]);
|
if profiles.items.is_none() {
|
||||||
}
|
profiles.items = Some(vec![]);
|
||||||
// compatible with the old old old version
|
}
|
||||||
if let Some(items) = profiles.items.as_mut() {
|
// compatible with the old old old version
|
||||||
for item in items.iter_mut() {
|
if let Some(items) = profiles.items.as_mut() {
|
||||||
if item.uid.is_none() {
|
for item in items.iter_mut() {
|
||||||
item.uid = Some(help::get_uid("d"));
|
if item.uid.is_none() {
|
||||||
|
item.uid = Some(help::get_uid("d"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
profiles
|
||||||
}
|
}
|
||||||
profiles
|
Err(err) => {
|
||||||
}
|
log::error!(target: "app", "{err}");
|
||||||
|
Self::template()
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "{err}");
|
log::error!(target: "app", "{err}");
|
||||||
Self::template()
|
Self::template()
|
||||||
@@ -62,12 +73,13 @@ impl IProfiles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_file(&self) -> Result<()> {
|
pub async fn save_file(&self) -> Result<()> {
|
||||||
help::save_yaml(
|
help::save_yaml(
|
||||||
&dirs::profiles_path()?,
|
&dirs::profiles_path()?,
|
||||||
self,
|
self,
|
||||||
Some("# Profiles Config for Clash Verge"),
|
Some("# Profiles Config for Clash Verge"),
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 只修改current,valid和chain
|
/// 只修改current,valid和chain
|
||||||
@@ -76,8 +88,9 @@ impl IProfiles {
|
|||||||
self.items = Some(vec![]);
|
self.items = Some(vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(current) = patch.current {
|
if let Some(current) = patch.current
|
||||||
let items = self.items.as_ref().unwrap();
|
&& let Some(items) = self.items.as_ref()
|
||||||
|
{
|
||||||
let some_uid = Some(current);
|
let some_uid = Some(current);
|
||||||
if items.iter().any(|e| e.uid == some_uid) {
|
if items.iter().any(|e| e.uid == some_uid) {
|
||||||
self.current = some_uid;
|
self.current = some_uid;
|
||||||
@@ -114,7 +127,7 @@ impl IProfiles {
|
|||||||
/// append new item
|
/// append new item
|
||||||
/// if the file_data is some
|
/// if the file_data is some
|
||||||
/// then should save the data to file
|
/// then should save the data to file
|
||||||
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
|
pub async fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
|
||||||
if item.uid.is_none() {
|
if item.uid.is_none() {
|
||||||
bail!("the uid should not be null");
|
bail!("the uid should not be null");
|
||||||
}
|
}
|
||||||
@@ -127,12 +140,13 @@ impl IProfiles {
|
|||||||
bail!("the file should not be null");
|
bail!("the file should not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
let file = item.file.clone().unwrap();
|
let file = item.file.clone().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("file field is required when file_data is provided")
|
||||||
|
})?;
|
||||||
let path = dirs::app_profiles_dir()?.join(&file);
|
let path = dirs::app_profiles_dir()?.join(&file);
|
||||||
|
|
||||||
fs::File::create(path)
|
fs::write(&path, file_data.as_bytes())
|
||||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
.await
|
||||||
.write(file_data.as_bytes())
|
|
||||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +164,11 @@ impl IProfiles {
|
|||||||
items.push(item)
|
items.push(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save_file()
|
self.save_file().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// reorder items
|
/// reorder items
|
||||||
pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
|
pub async fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
|
||||||
let mut items = self.items.take().unwrap_or_default();
|
let mut items = self.items.take().unwrap_or_default();
|
||||||
let mut old_index = None;
|
let mut old_index = None;
|
||||||
let mut new_index = None;
|
let mut new_index = None;
|
||||||
@@ -168,17 +182,18 @@ impl IProfiles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if old_index.is_none() || new_index.is_none() {
|
let (old_idx, new_idx) = match (old_index, new_index) {
|
||||||
return Ok(());
|
(Some(old), Some(new)) => (old, new),
|
||||||
}
|
_ => return Ok(()),
|
||||||
let item = items.remove(old_index.unwrap());
|
};
|
||||||
items.insert(new_index.unwrap(), item);
|
let item = items.remove(old_idx);
|
||||||
|
items.insert(new_idx, item);
|
||||||
self.items = Some(items);
|
self.items = Some(items);
|
||||||
self.save_file()
|
self.save_file().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// update the item value
|
/// update the item value
|
||||||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
pub async fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||||
let mut items = self.items.take().unwrap_or_default();
|
let mut items = self.items.take().unwrap_or_default();
|
||||||
|
|
||||||
for each in items.iter_mut() {
|
for each in items.iter_mut() {
|
||||||
@@ -194,7 +209,7 @@ impl IProfiles {
|
|||||||
patch!(each, item, option);
|
patch!(each, item, option);
|
||||||
|
|
||||||
self.items = Some(items);
|
self.items = Some(items);
|
||||||
return self.save_file();
|
return self.save_file().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +219,7 @@ impl IProfiles {
|
|||||||
|
|
||||||
/// be used to update the remote item
|
/// be used to update the remote item
|
||||||
/// only patch `updated` `extra` `file_data`
|
/// only patch `updated` `extra` `file_data`
|
||||||
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
|
pub async fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
|
||||||
if self.items.is_none() {
|
if self.items.is_none() {
|
||||||
self.items = Some(vec![]);
|
self.items = Some(vec![]);
|
||||||
}
|
}
|
||||||
@@ -233,9 +248,8 @@ impl IProfiles {
|
|||||||
|
|
||||||
let path = dirs::app_profiles_dir()?.join(&file);
|
let path = dirs::app_profiles_dir()?.join(&file);
|
||||||
|
|
||||||
fs::File::create(path)
|
fs::write(&path, file_data.as_bytes())
|
||||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
.await
|
||||||
.write(file_data.as_bytes())
|
|
||||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,12 +258,12 @@ impl IProfiles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save_file()
|
self.save_file().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// delete item
|
/// delete item
|
||||||
/// if delete the current then return true
|
/// if delete the current then return true
|
||||||
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
|
pub async fn delete_item(&mut self, uid: String) -> Result<bool> {
|
||||||
let current = self.current.as_ref().unwrap_or(&uid);
|
let current = self.current.as_ref().unwrap_or(&uid);
|
||||||
let current = current.clone();
|
let current = current.clone();
|
||||||
let item = self.get_item(&uid)?;
|
let item = self.get_item(&uid)?;
|
||||||
@@ -273,15 +287,24 @@ impl IProfiles {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(index) = index {
|
if let Some(index) = index
|
||||||
if let Some(file) = items.remove(index).file {
|
&& let Some(file) = items.remove(index).file
|
||||||
let _ = dirs::app_profiles_dir().map(|path| {
|
{
|
||||||
let path = path.join(file);
|
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||||
if path.exists() {
|
let path = path.join(file);
|
||||||
let _ = fs::remove_file(path);
|
if path.exists() {
|
||||||
|
let result = fs::remove_file(path.clone()).await;
|
||||||
|
if let Err(err) = result {
|
||||||
|
logging_error!(
|
||||||
|
Type::Config,
|
||||||
|
false,
|
||||||
|
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
// get the merge index
|
// get the merge index
|
||||||
for (i, _) in items.iter().enumerate() {
|
for (i, _) in items.iter().enumerate() {
|
||||||
@@ -290,15 +313,24 @@ impl IProfiles {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(index) = merge_index {
|
if let Some(index) = merge_index
|
||||||
if let Some(file) = items.remove(index).file {
|
&& let Some(file) = items.remove(index).file
|
||||||
let _ = dirs::app_profiles_dir().map(|path| {
|
{
|
||||||
let path = path.join(file);
|
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||||
if path.exists() {
|
let path = path.join(file);
|
||||||
let _ = fs::remove_file(path);
|
if path.exists() {
|
||||||
|
let result = fs::remove_file(path.clone()).await;
|
||||||
|
if let Err(err) = result {
|
||||||
|
logging_error!(
|
||||||
|
Type::Config,
|
||||||
|
false,
|
||||||
|
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
// get the script index
|
// get the script index
|
||||||
for (i, _) in items.iter().enumerate() {
|
for (i, _) in items.iter().enumerate() {
|
||||||
@@ -307,15 +339,24 @@ impl IProfiles {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(index) = script_index {
|
if let Some(index) = script_index
|
||||||
if let Some(file) = items.remove(index).file {
|
&& let Some(file) = items.remove(index).file
|
||||||
let _ = dirs::app_profiles_dir().map(|path| {
|
{
|
||||||
let path = path.join(file);
|
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||||
if path.exists() {
|
let path = path.join(file);
|
||||||
let _ = fs::remove_file(path);
|
if path.exists() {
|
||||||
|
let result = fs::remove_file(path.clone()).await;
|
||||||
|
if let Err(err) = result {
|
||||||
|
logging_error!(
|
||||||
|
Type::Config,
|
||||||
|
false,
|
||||||
|
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
// get the rules index
|
// get the rules index
|
||||||
for (i, _) in items.iter().enumerate() {
|
for (i, _) in items.iter().enumerate() {
|
||||||
@@ -324,15 +365,24 @@ impl IProfiles {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(index) = rules_index {
|
if let Some(index) = rules_index
|
||||||
if let Some(file) = items.remove(index).file {
|
&& let Some(file) = items.remove(index).file
|
||||||
let _ = dirs::app_profiles_dir().map(|path| {
|
{
|
||||||
let path = path.join(file);
|
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||||
if path.exists() {
|
let path = path.join(file);
|
||||||
let _ = fs::remove_file(path);
|
if path.exists() {
|
||||||
|
let result = fs::remove_file(path.clone()).await;
|
||||||
|
if let Err(err) = result {
|
||||||
|
logging_error!(
|
||||||
|
Type::Config,
|
||||||
|
false,
|
||||||
|
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
// get the proxies index
|
// get the proxies index
|
||||||
for (i, _) in items.iter().enumerate() {
|
for (i, _) in items.iter().enumerate() {
|
||||||
@@ -341,15 +391,24 @@ impl IProfiles {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(index) = proxies_index {
|
if let Some(index) = proxies_index
|
||||||
if let Some(file) = items.remove(index).file {
|
&& let Some(file) = items.remove(index).file
|
||||||
let _ = dirs::app_profiles_dir().map(|path| {
|
{
|
||||||
let path = path.join(file);
|
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||||
if path.exists() {
|
let path = path.join(file);
|
||||||
let _ = fs::remove_file(path);
|
if path.exists() {
|
||||||
|
let result = fs::remove_file(path.clone()).await;
|
||||||
|
if let Err(err) = result {
|
||||||
|
logging_error!(
|
||||||
|
Type::Config,
|
||||||
|
false,
|
||||||
|
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
// get the groups index
|
// get the groups index
|
||||||
for (i, _) in items.iter().enumerate() {
|
for (i, _) in items.iter().enumerate() {
|
||||||
@@ -358,15 +417,24 @@ impl IProfiles {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(index) = groups_index {
|
if let Some(index) = groups_index
|
||||||
if let Some(file) = items.remove(index).file {
|
&& let Some(file) = items.remove(index).file
|
||||||
let _ = dirs::app_profiles_dir().map(|path| {
|
{
|
||||||
let path = path.join(file);
|
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||||
if path.exists() {
|
let path = path.join(file);
|
||||||
let _ = fs::remove_file(path);
|
if path.exists() {
|
||||||
|
let result = fs::remove_file(path.clone()).await;
|
||||||
|
if let Err(err) = result {
|
||||||
|
logging_error!(
|
||||||
|
Type::Config,
|
||||||
|
false,
|
||||||
|
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
// delete the original uid
|
// delete the original uid
|
||||||
if current == uid {
|
if current == uid {
|
||||||
@@ -382,12 +450,12 @@ impl IProfiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.items = Some(items);
|
self.items = Some(items);
|
||||||
self.save_file()?;
|
self.save_file().await?;
|
||||||
Ok(current == uid)
|
Ok(current == uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取current指向的订阅内容
|
/// 获取current指向的订阅内容
|
||||||
pub fn current_mapping(&self) -> Result<Mapping> {
|
pub async fn current_mapping(&self) -> Result<Mapping> {
|
||||||
match (self.current.as_ref(), self.items.as_ref()) {
|
match (self.current.as_ref(), self.items.as_ref()) {
|
||||||
(Some(current), Some(items)) => {
|
(Some(current), Some(items)) => {
|
||||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||||
@@ -395,7 +463,7 @@ impl IProfiles {
|
|||||||
Some(file) => dirs::app_profiles_dir()?.join(file),
|
Some(file) => dirs::app_profiles_dir()?.join(file),
|
||||||
None => bail!("failed to get the file field"),
|
None => bail!("failed to get the file field"),
|
||||||
};
|
};
|
||||||
return help::read_mapping(&file_path);
|
return help::read_mapping(&file_path).await;
|
||||||
}
|
}
|
||||||
bail!("failed to find the current profile \"uid:{current}\"");
|
bail!("failed to find the current profile \"uid:{current}\"");
|
||||||
}
|
}
|
||||||
@@ -527,25 +595,25 @@ impl IProfiles {
|
|||||||
|
|
||||||
total_files += 1;
|
total_files += 1;
|
||||||
|
|
||||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
|
||||||
if Self::is_profile_file(file_name) {
|
&& Self::is_profile_file(file_name)
|
||||||
// 检查是否为全局扩展文件
|
{
|
||||||
if protected_files.contains(file_name) {
|
// 检查是否为全局扩展文件
|
||||||
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
|
if protected_files.contains(file_name) {
|
||||||
continue;
|
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否为活跃文件
|
// 检查是否为活跃文件
|
||||||
if !active_files.contains(file_name) {
|
if !active_files.contains(file_name) {
|
||||||
match std::fs::remove_file(&path) {
|
match std::fs::remove_file(&path) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
deleted_files.push(file_name.to_string());
|
deleted_files.push(file_name.to_string());
|
||||||
log::info!(target: "app", "已清理冗余文件: {file_name}");
|
log::info!(target: "app", "已清理冗余文件: {file_name}");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
failed_deletions.push(format!("{file_name}: {e}"));
|
failed_deletions.push(format!("{file_name}: {e}"));
|
||||||
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
|
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -591,50 +659,44 @@ impl IProfiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
|
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
|
||||||
if let Some(itype) = &item.itype {
|
if let Some(itype) = &item.itype
|
||||||
if itype == "remote" || itype == "local" {
|
&& (itype == "remote" || itype == "local")
|
||||||
if let Some(option) = &item.option {
|
&& let Some(option) = &item.option
|
||||||
// 收集关联的扩展文件
|
{
|
||||||
if let Some(merge_uid) = &option.merge {
|
// 收集关联的扩展文件
|
||||||
if let Ok(merge_item) = self.get_item(merge_uid) {
|
if let Some(merge_uid) = &option.merge
|
||||||
if let Some(file) = &merge_item.file {
|
&& let Ok(merge_item) = self.get_item(merge_uid)
|
||||||
active_files.insert(file.clone());
|
&& let Some(file) = &merge_item.file
|
||||||
}
|
{
|
||||||
}
|
active_files.insert(file.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(script_uid) = &option.script {
|
if let Some(script_uid) = &option.script
|
||||||
if let Ok(script_item) = self.get_item(script_uid) {
|
&& let Ok(script_item) = self.get_item(script_uid)
|
||||||
if let Some(file) = &script_item.file {
|
&& let Some(file) = &script_item.file
|
||||||
active_files.insert(file.clone());
|
{
|
||||||
}
|
active_files.insert(file.clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(rules_uid) = &option.rules {
|
if let Some(rules_uid) = &option.rules
|
||||||
if let Ok(rules_item) = self.get_item(rules_uid) {
|
&& let Ok(rules_item) = self.get_item(rules_uid)
|
||||||
if let Some(file) = &rules_item.file {
|
&& let Some(file) = &rules_item.file
|
||||||
active_files.insert(file.clone());
|
{
|
||||||
}
|
active_files.insert(file.clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(proxies_uid) = &option.proxies {
|
if let Some(proxies_uid) = &option.proxies
|
||||||
if let Ok(proxies_item) = self.get_item(proxies_uid) {
|
&& let Ok(proxies_item) = self.get_item(proxies_uid)
|
||||||
if let Some(file) = &proxies_item.file {
|
&& let Some(file) = &proxies_item.file
|
||||||
active_files.insert(file.clone());
|
{
|
||||||
}
|
active_files.insert(file.clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(groups_uid) = &option.groups {
|
if let Some(groups_uid) = &option.groups
|
||||||
if let Ok(groups_item) = self.get_item(groups_uid) {
|
&& let Ok(groups_item) = self.get_item(groups_uid)
|
||||||
if let Some(file) = &groups_item.file {
|
&& let Some(file) = &groups_item.file
|
||||||
active_files.insert(file.clone());
|
{
|
||||||
}
|
active_files.insert(file.clone());
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -667,23 +729,95 @@ impl IProfiles {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn auto_cleanup(&self) -> Result<()> {
|
// 特殊的Send-safe helper函数,完全避免跨await持有guard
|
||||||
match self.cleanup_orphaned_files() {
|
use crate::config::Config;
|
||||||
Ok(result) => {
|
|
||||||
if !result.deleted_files.is_empty() {
|
pub async fn profiles_append_item_with_filedata_safe(
|
||||||
log::info!(
|
item: PrfItem,
|
||||||
target: "app",
|
file_data: Option<String>,
|
||||||
"自动清理完成,删除了 {} 个冗余文件",
|
) -> Result<()> {
|
||||||
result.deleted_files.len()
|
AsyncHandler::spawn_blocking(move || {
|
||||||
);
|
AsyncHandler::handle().block_on(async {
|
||||||
}
|
let item = PrfItem::from(item, file_data).await?;
|
||||||
Ok(())
|
let profiles = Config::profiles().await;
|
||||||
}
|
let mut profiles_guard = profiles.data_mut();
|
||||||
Err(e) => {
|
profiles_guard.append_item(item).await
|
||||||
log::warn!(target: "app", "自动清理失败: {e}");
|
})
|
||||||
Ok(())
|
})
|
||||||
}
|
.await
|
||||||
}
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> {
|
||||||
|
AsyncHandler::spawn_blocking(move || {
|
||||||
|
AsyncHandler::handle().block_on(async {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let mut profiles_guard = profiles.data_mut();
|
||||||
|
profiles_guard.append_item(item).await
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()> {
|
||||||
|
AsyncHandler::spawn_blocking(move || {
|
||||||
|
AsyncHandler::handle().block_on(async {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let mut profiles_guard = profiles.data_mut();
|
||||||
|
profiles_guard.patch_item(index, item).await
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
|
||||||
|
AsyncHandler::spawn_blocking(move || {
|
||||||
|
AsyncHandler::handle().block_on(async {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let mut profiles_guard = profiles.data_mut();
|
||||||
|
profiles_guard.delete_item(index).await
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn profiles_reorder_safe(active_id: String, over_id: String) -> Result<()> {
|
||||||
|
AsyncHandler::spawn_blocking(move || {
|
||||||
|
AsyncHandler::handle().block_on(async {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let mut profiles_guard = profiles.data_mut();
|
||||||
|
profiles_guard.reorder(active_id, over_id).await
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn profiles_save_file_safe() -> Result<()> {
|
||||||
|
AsyncHandler::spawn_blocking(move || {
|
||||||
|
AsyncHandler::handle().block_on(async {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles_guard = profiles.data_mut();
|
||||||
|
profiles_guard.save_file().await
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn profiles_draft_update_item_safe(index: String, item: PrfItem) -> Result<()> {
|
||||||
|
AsyncHandler::spawn_blocking(move || {
|
||||||
|
AsyncHandler::handle().block_on(async {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let mut profiles_guard = profiles.draft_mut();
|
||||||
|
profiles_guard.update_item(index, item).await
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::enhance::field::use_keys;
|
use crate::enhance::field::use_keys;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct IRuntime {
|
pub struct IRuntime {
|
||||||
@@ -46,4 +46,139 @@ impl IRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//跟新链式代理配置文件
|
||||||
|
/// {
|
||||||
|
/// "proxies":[
|
||||||
|
/// {
|
||||||
|
/// name : 入口节点,
|
||||||
|
/// type: xxx
|
||||||
|
/// server: xxx
|
||||||
|
/// port: xxx
|
||||||
|
/// ports: xxx
|
||||||
|
/// password: xxx
|
||||||
|
/// skip-cert-verify: xxx,
|
||||||
|
/// },
|
||||||
|
/// {
|
||||||
|
/// name : hop_node_1_xxxx,
|
||||||
|
/// type: xxx
|
||||||
|
/// server: xxx
|
||||||
|
/// port: xxx
|
||||||
|
/// ports: xxx
|
||||||
|
/// password: xxx
|
||||||
|
/// skip-cert-verify: xxx,
|
||||||
|
/// dialer-proxy : "入口节点"
|
||||||
|
/// },
|
||||||
|
/// {
|
||||||
|
/// name : 出口节点,
|
||||||
|
/// type: xxx
|
||||||
|
/// server: xxx
|
||||||
|
/// port: xxx
|
||||||
|
/// ports: xxx
|
||||||
|
/// password: xxx
|
||||||
|
/// skip-cert-verify: xxx,
|
||||||
|
/// dialer-proxy : "hop_node_1_xxxx"
|
||||||
|
/// }
|
||||||
|
/// ],
|
||||||
|
/// "proxy-groups" : [
|
||||||
|
/// {
|
||||||
|
/// name : "proxy_chain",
|
||||||
|
/// type: "select",
|
||||||
|
/// proxies ["出口节点"]
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// 传入none 为删除
|
||||||
|
pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) {
|
||||||
|
if let Some(config) = self.config.as_mut() {
|
||||||
|
// 获取 默认第一的代理组的名字
|
||||||
|
let proxy_group_name =
|
||||||
|
if let Some(Value::Sequence(proxy_groups)) = config.get("proxy-groups") {
|
||||||
|
if let Some(Value::Mapping(proxy_group)) = proxy_groups.first() {
|
||||||
|
if let Some(Value::String(proxy_group_name)) = proxy_group.get("name") {
|
||||||
|
proxy_group_name.to_string()
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
|
||||||
|
proxies.iter_mut().for_each(|proxy| {
|
||||||
|
if let Some(proxy) = proxy.as_mapping_mut()
|
||||||
|
&& proxy.get("dialer-proxy").is_some()
|
||||||
|
{
|
||||||
|
proxy.remove("dialer-proxy");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除proxy_chain代理组
|
||||||
|
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") {
|
||||||
|
proxy_groups.retain(|proxy_group| {
|
||||||
|
!matches!(proxy_group.get("name").and_then(|n| n.as_str()), Some(name) if name== "proxy_chain")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除rules
|
||||||
|
if let Some(Value::Sequence(rules)) = config.get_mut("rules") {
|
||||||
|
rules.retain(|rule| rule.as_str() != Some("MATCH,proxy_chain"));
|
||||||
|
rules.push(Value::String(format!("MATCH,{}", proxy_group_name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// some 则写入新配置
|
||||||
|
// 给proxy添加dialer-proxy字段
|
||||||
|
// 第一个proxy不添加dialer-proxy
|
||||||
|
// 然后第二个开始,dialer-proxy为上一个元素的name
|
||||||
|
if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config {
|
||||||
|
let mut proxy_chain_group = Mapping::new();
|
||||||
|
|
||||||
|
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
|
||||||
|
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
|
||||||
|
if let Some(Value::Mapping(proxy)) = proxies
|
||||||
|
.iter_mut()
|
||||||
|
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
|
||||||
|
{
|
||||||
|
if i != 0
|
||||||
|
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
|
||||||
|
{
|
||||||
|
proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
|
||||||
|
}
|
||||||
|
if i == dialer_proxies.len() - 1 {
|
||||||
|
// 添加proxy-groups
|
||||||
|
proxy_chain_group
|
||||||
|
.insert("name".into(), Value::String("proxy_chain".into()));
|
||||||
|
proxy_chain_group
|
||||||
|
.insert("type".into(), Value::String("select".into()));
|
||||||
|
proxy_chain_group.insert(
|
||||||
|
"proxies".into(),
|
||||||
|
Value::Sequence(vec![dialer_proxy.to_owned()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") {
|
||||||
|
proxy_groups.push(Value::Mapping(proxy_chain_group));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加rules
|
||||||
|
if let Some(Value::Sequence(rules)) = config.get_mut("rules")
|
||||||
|
&& let Ok(rule) = serde_yaml_ng::to_value("MATCH,proxy_chain")
|
||||||
|
{
|
||||||
|
rules.retain(|rule| {
|
||||||
|
rule.as_str() != Some(&format!("MATCH,{}", proxy_group_name))
|
||||||
|
});
|
||||||
|
rules.push(rule);
|
||||||
|
// *rules = vec![rule];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
|
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
|
||||||
logging,
|
logging,
|
||||||
utils::{dirs, help, i18n, logging::Type},
|
utils::{dirs, help, i18n, logging::Type},
|
||||||
};
|
};
|
||||||
@@ -138,9 +138,6 @@ pub struct IVerge {
|
|||||||
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
|
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
|
||||||
pub auto_log_clean: Option<i32>,
|
pub auto_log_clean: Option<i32>,
|
||||||
|
|
||||||
/// 是否启用随机端口
|
|
||||||
pub enable_random_port: Option<bool>,
|
|
||||||
|
|
||||||
/// verge 的各种 port 用于覆盖 clash 的各种 port
|
/// verge 的各种 port 用于覆盖 clash 的各种 port
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub verge_redir_port: Option<u16>,
|
pub verge_redir_port: Option<u16>,
|
||||||
@@ -204,8 +201,8 @@ pub struct IVerge {
|
|||||||
/// 启用代理页面自动滚动
|
/// 启用代理页面自动滚动
|
||||||
pub enable_hover_jump_navigator: Option<bool>,
|
pub enable_hover_jump_navigator: Option<bool>,
|
||||||
|
|
||||||
/// 服务状态跟踪
|
/// 启用外部控制器
|
||||||
pub service_state: Option<crate::core::service::ServiceState>,
|
pub enable_external_controller: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||||
@@ -237,9 +234,9 @@ impl IVerge {
|
|||||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
||||||
|
|
||||||
/// 验证并修正配置文件中的clash_core值
|
/// 验证并修正配置文件中的clash_core值
|
||||||
pub fn validate_and_fix_config() -> Result<()> {
|
pub async fn validate_and_fix_config() -> Result<()> {
|
||||||
let config_path = dirs::verge_path()?;
|
let config_path = dirs::verge_path()?;
|
||||||
let mut config = match help::read_yaml::<IVerge>(&config_path) {
|
let mut config = match help::read_yaml::<IVerge>(&config_path).await {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(_) => Self::template(),
|
Err(_) => Self::template(),
|
||||||
};
|
};
|
||||||
@@ -273,7 +270,7 @@ impl IVerge {
|
|||||||
// 修正后保存配置
|
// 修正后保存配置
|
||||||
if needs_fix {
|
if needs_fix {
|
||||||
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
|
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
|
||||||
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
|
help::save_yaml(&config_path, &config, Some("# Clash Verge Config")).await?;
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::Config,
|
Type::Config,
|
||||||
@@ -281,7 +278,7 @@ impl IVerge {
|
|||||||
"配置文件修正完成,需要重新加载配置"
|
"配置文件修正完成,需要重新加载配置"
|
||||||
);
|
);
|
||||||
|
|
||||||
Self::reload_config_after_fix(config)?;
|
Self::reload_config_after_fix(config).await?;
|
||||||
} else {
|
} else {
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
@@ -296,10 +293,10 @@ impl IVerge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 配置修正后重新加载配置
|
/// 配置修正后重新加载配置
|
||||||
fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
|
async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
let config_draft = Config::verge();
|
let config_draft = Config::verge().await;
|
||||||
*config_draft.draft_mut() = Box::new(updated_config.clone());
|
*config_draft.draft_mut() = Box::new(updated_config.clone());
|
||||||
config_draft.apply();
|
config_draft.apply();
|
||||||
|
|
||||||
@@ -335,9 +332,15 @@ impl IVerge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub async fn new() -> Self {
|
||||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
match dirs::verge_path() {
|
||||||
Ok(config) => config,
|
Ok(path) => match help::read_yaml::<IVerge>(&path).await {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
Self::template()
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!(target: "app", "{err}");
|
log::error!(target: "app", "{err}");
|
||||||
Self::template()
|
Self::template()
|
||||||
@@ -371,7 +374,6 @@ impl IVerge {
|
|||||||
proxy_auto_config: Some(false),
|
proxy_auto_config: Some(false),
|
||||||
pac_file_content: Some(DEFAULT_PAC.into()),
|
pac_file_content: Some(DEFAULT_PAC.into()),
|
||||||
proxy_host: Some("127.0.0.1".into()),
|
proxy_host: Some("127.0.0.1".into()),
|
||||||
enable_random_port: Some(false),
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
verge_redir_port: Some(7895),
|
verge_redir_port: Some(7895),
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
@@ -391,7 +393,7 @@ impl IVerge {
|
|||||||
auto_close_connection: Some(true),
|
auto_close_connection: Some(true),
|
||||||
auto_check_update: Some(true),
|
auto_check_update: Some(true),
|
||||||
enable_builtin_enhanced: Some(true),
|
enable_builtin_enhanced: Some(true),
|
||||||
auto_log_clean: Some(2),
|
auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天
|
||||||
webdav_url: None,
|
webdav_url: None,
|
||||||
webdav_username: None,
|
webdav_username: None,
|
||||||
webdav_password: None,
|
webdav_password: None,
|
||||||
@@ -402,14 +404,14 @@ impl IVerge {
|
|||||||
auto_light_weight_minutes: Some(10),
|
auto_light_weight_minutes: Some(10),
|
||||||
enable_dns_settings: Some(false),
|
enable_dns_settings: Some(false),
|
||||||
home_cards: None,
|
home_cards: None,
|
||||||
service_state: None,
|
enable_external_controller: Some(false),
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save IVerge App Config
|
/// Save IVerge App Config
|
||||||
pub fn save_file(&self) -> Result<()> {
|
pub async fn save_file(&self) -> Result<()> {
|
||||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
|
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// patch verge config
|
/// patch verge config
|
||||||
@@ -444,7 +446,6 @@ impl IVerge {
|
|||||||
patch!(enable_auto_launch);
|
patch!(enable_auto_launch);
|
||||||
patch!(enable_silent_start);
|
patch!(enable_silent_start);
|
||||||
patch!(enable_hover_jump_navigator);
|
patch!(enable_hover_jump_navigator);
|
||||||
patch!(enable_random_port);
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
patch!(verge_redir_port);
|
patch!(verge_redir_port);
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
@@ -490,7 +491,7 @@ impl IVerge {
|
|||||||
patch!(auto_light_weight_minutes);
|
patch!(auto_light_weight_minutes);
|
||||||
patch!(enable_dns_settings);
|
patch!(enable_dns_settings);
|
||||||
patch!(home_cards);
|
patch!(home_cards);
|
||||||
patch!(service_state);
|
patch!(enable_external_controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在初始化前尝试拿到单例端口的值
|
/// 在初始化前尝试拿到单例端口的值
|
||||||
@@ -562,7 +563,6 @@ pub struct IVergeResponse {
|
|||||||
pub proxy_layout_column: Option<i32>,
|
pub proxy_layout_column: Option<i32>,
|
||||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||||
pub auto_log_clean: Option<i32>,
|
pub auto_log_clean: Option<i32>,
|
||||||
pub enable_random_port: Option<bool>,
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub verge_redir_port: Option<u16>,
|
pub verge_redir_port: Option<u16>,
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
@@ -586,7 +586,7 @@ pub struct IVergeResponse {
|
|||||||
pub enable_dns_settings: Option<bool>,
|
pub enable_dns_settings: Option<bool>,
|
||||||
pub home_cards: Option<serde_json::Value>,
|
pub home_cards: Option<serde_json::Value>,
|
||||||
pub enable_hover_jump_navigator: Option<bool>,
|
pub enable_hover_jump_navigator: Option<bool>,
|
||||||
pub service_state: Option<crate::core::service::ServiceState>,
|
pub enable_external_controller: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<IVerge> for IVergeResponse {
|
impl From<IVerge> for IVergeResponse {
|
||||||
@@ -634,7 +634,6 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
proxy_layout_column: verge.proxy_layout_column,
|
proxy_layout_column: verge.proxy_layout_column,
|
||||||
test_list: verge.test_list,
|
test_list: verge.test_list,
|
||||||
auto_log_clean: verge.auto_log_clean,
|
auto_log_clean: verge.auto_log_clean,
|
||||||
enable_random_port: verge.enable_random_port,
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
verge_redir_port: verge.verge_redir_port,
|
verge_redir_port: verge.verge_redir_port,
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
@@ -658,7 +657,7 @@ impl From<IVerge> for IVergeResponse {
|
|||||||
enable_dns_settings: verge.enable_dns_settings,
|
enable_dns_settings: verge.enable_dns_settings,
|
||||||
home_cards: verge.home_cards,
|
home_cards: verge.home_cards,
|
||||||
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
||||||
service_state: verge.service_state,
|
enable_external_controller: verge.enable_external_controller,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use crate::process::AsyncHandler;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::time::{timeout, Duration};
|
use tokio::time::{Duration, timeout};
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
@@ -26,7 +28,7 @@ impl Default for AsyncSysproxy {
|
|||||||
Self {
|
Self {
|
||||||
enable: false,
|
enable: false,
|
||||||
host: "127.0.0.1".to_string(),
|
host: "127.0.0.1".to_string(),
|
||||||
port: 7890,
|
port: 7897,
|
||||||
bypass: String::new(),
|
bypass: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +76,7 @@ impl AsyncProxyQuery {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||||
// Windows: 从注册表读取PAC配置
|
// Windows: 从注册表读取PAC配置
|
||||||
tokio::task::spawn_blocking(move || -> Result<AsyncAutoproxy> {
|
AsyncHandler::spawn_blocking(move || -> Result<AsyncAutoproxy> {
|
||||||
Self::get_pac_config_from_registry()
|
Self::get_pac_config_from_registry()
|
||||||
})
|
})
|
||||||
.await?
|
.await?
|
||||||
@@ -85,7 +87,7 @@ impl AsyncProxyQuery {
|
|||||||
use std::ptr;
|
use std::ptr;
|
||||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||||
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||||
@@ -207,13 +209,13 @@ impl AsyncProxyQuery {
|
|||||||
// Linux: 检查环境变量和GNOME设置
|
// Linux: 检查环境变量和GNOME设置
|
||||||
|
|
||||||
// 首先检查环境变量
|
// 首先检查环境变量
|
||||||
if let Ok(auto_proxy) = std::env::var("auto_proxy") {
|
if let Ok(auto_proxy) = std::env::var("auto_proxy")
|
||||||
if !auto_proxy.is_empty() {
|
&& !auto_proxy.is_empty()
|
||||||
return Ok(AsyncAutoproxy {
|
{
|
||||||
enable: true,
|
return Ok(AsyncAutoproxy {
|
||||||
url: auto_proxy,
|
enable: true,
|
||||||
});
|
url: auto_proxy,
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||||
@@ -222,31 +224,31 @@ impl AsyncProxyQuery {
|
|||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(output) = output {
|
if let Ok(output) = output
|
||||||
if output.status.success() {
|
&& output.status.success()
|
||||||
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
{
|
||||||
if mode.contains("auto") {
|
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
// 获取 PAC URL
|
if mode.contains("auto") {
|
||||||
let pac_output = Command::new("gsettings")
|
// 获取 PAC URL
|
||||||
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
|
let pac_output = Command::new("gsettings")
|
||||||
.output()
|
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
|
||||||
.await;
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
if let Ok(pac_output) = pac_output {
|
if let Ok(pac_output) = pac_output
|
||||||
if pac_output.status.success() {
|
&& pac_output.status.success()
|
||||||
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
|
{
|
||||||
.trim()
|
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
|
||||||
.trim_matches('\'')
|
.trim()
|
||||||
.trim_matches('"')
|
.trim_matches('\'')
|
||||||
.to_string();
|
.trim_matches('"')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
if !pac_url.is_empty() {
|
if !pac_url.is_empty() {
|
||||||
return Ok(AsyncAutoproxy {
|
return Ok(AsyncAutoproxy {
|
||||||
enable: true,
|
enable: true,
|
||||||
url: pac_url,
|
url: pac_url,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +260,7 @@ impl AsyncProxyQuery {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||||
// Windows: 使用注册表直接读取代理设置
|
// Windows: 使用注册表直接读取代理设置
|
||||||
tokio::task::spawn_blocking(move || -> Result<AsyncSysproxy> {
|
AsyncHandler::spawn_blocking(move || -> Result<AsyncSysproxy> {
|
||||||
Self::get_system_proxy_from_registry()
|
Self::get_system_proxy_from_registry()
|
||||||
})
|
})
|
||||||
.await?
|
.await?
|
||||||
@@ -269,7 +271,7 @@ impl AsyncProxyQuery {
|
|||||||
use std::ptr;
|
use std::ptr;
|
||||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||||
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
|
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||||
@@ -400,10 +402,10 @@ impl AsyncProxyQuery {
|
|||||||
http_host = host_part.trim().to_string();
|
http_host = host_part.trim().to_string();
|
||||||
}
|
}
|
||||||
} else if line.contains("HTTPPort") {
|
} else if line.contains("HTTPPort") {
|
||||||
if let Some(port_part) = line.split(':').nth(1) {
|
if let Some(port_part) = line.split(':').nth(1)
|
||||||
if let Ok(port) = port_part.trim().parse::<u16>() {
|
&& let Ok(port) = port_part.trim().parse::<u16>()
|
||||||
http_port = port;
|
{
|
||||||
}
|
http_port = port;
|
||||||
}
|
}
|
||||||
} else if line.contains("ExceptionsList") {
|
} else if line.contains("ExceptionsList") {
|
||||||
// 解析异常列表
|
// 解析异常列表
|
||||||
@@ -429,16 +431,16 @@ impl AsyncProxyQuery {
|
|||||||
// Linux: 检查环境变量和桌面环境设置
|
// Linux: 检查环境变量和桌面环境设置
|
||||||
|
|
||||||
// 首先检查环境变量
|
// 首先检查环境变量
|
||||||
if let Ok(http_proxy) = std::env::var("http_proxy") {
|
if let Ok(http_proxy) = std::env::var("http_proxy")
|
||||||
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) {
|
&& let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy)
|
||||||
return Ok(proxy_info);
|
{
|
||||||
}
|
return Ok(proxy_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(https_proxy) = std::env::var("https_proxy") {
|
if let Ok(https_proxy) = std::env::var("https_proxy")
|
||||||
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) {
|
&& let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy)
|
||||||
return Ok(proxy_info);
|
{
|
||||||
}
|
return Ok(proxy_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||||
@@ -447,45 +449,46 @@ impl AsyncProxyQuery {
|
|||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(mode_output) = mode_output {
|
if let Ok(mode_output) = mode_output
|
||||||
if mode_output.status.success() {
|
&& mode_output.status.success()
|
||||||
let mode = String::from_utf8_lossy(&mode_output.stdout)
|
{
|
||||||
.trim()
|
let mode = String::from_utf8_lossy(&mode_output.stdout)
|
||||||
.to_string();
|
.trim()
|
||||||
if mode.contains("manual") {
|
.to_string();
|
||||||
// 获取HTTP代理设置
|
if mode.contains("manual") {
|
||||||
let host_result = Command::new("gsettings")
|
// 获取HTTP代理设置
|
||||||
.args(["get", "org.gnome.system.proxy.http", "host"])
|
let host_result = Command::new("gsettings")
|
||||||
.output()
|
.args(["get", "org.gnome.system.proxy.http", "host"])
|
||||||
.await;
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
let port_result = Command::new("gsettings")
|
let port_result = Command::new("gsettings")
|
||||||
.args(["get", "org.gnome.system.proxy.http", "port"])
|
.args(["get", "org.gnome.system.proxy.http", "port"])
|
||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) {
|
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result)
|
||||||
if host_output.status.success() && port_output.status.success() {
|
&& host_output.status.success()
|
||||||
let host = String::from_utf8_lossy(&host_output.stdout)
|
&& port_output.status.success()
|
||||||
.trim()
|
{
|
||||||
.trim_matches('\'')
|
let host = String::from_utf8_lossy(&host_output.stdout)
|
||||||
.trim_matches('"')
|
.trim()
|
||||||
.to_string();
|
.trim_matches('\'')
|
||||||
|
.trim_matches('"')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let port = String::from_utf8_lossy(&port_output.stdout)
|
let port = String::from_utf8_lossy(&port_output.stdout)
|
||||||
.trim()
|
.trim()
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.unwrap_or(8080);
|
.unwrap_or(8080);
|
||||||
|
|
||||||
if !host.is_empty() {
|
if !host.is_empty() {
|
||||||
return Ok(AsyncSysproxy {
|
return Ok(AsyncSysproxy {
|
||||||
enable: true,
|
enable: true,
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
bypass: String::new(),
|
bypass: String::new(),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,11 +74,14 @@ impl WebDavClient {
|
|||||||
|
|
||||||
// 获取或创建配置
|
// 获取或创建配置
|
||||||
let config = {
|
let config = {
|
||||||
let mut lock = self.config.lock();
|
// 首先检查是否已有配置
|
||||||
if let Some(cfg) = lock.as_ref() {
|
let existing_config = self.config.lock().as_ref().cloned();
|
||||||
cfg.clone()
|
|
||||||
|
if let Some(cfg) = existing_config {
|
||||||
|
cfg
|
||||||
} else {
|
} else {
|
||||||
let verge = Config::verge().latest_ref().clone();
|
// 释放锁后获取异步配置
|
||||||
|
let verge = Config::verge().await.latest_ref().clone();
|
||||||
if verge.webdav_url.is_none()
|
if verge.webdav_url.is_none()
|
||||||
|| verge.webdav_username.is_none()
|
|| verge.webdav_username.is_none()
|
||||||
|| verge.webdav_password.is_none()
|
|| verge.webdav_password.is_none()
|
||||||
@@ -97,7 +100,8 @@ impl WebDavClient {
|
|||||||
password: verge.webdav_password.unwrap_or_default(),
|
password: verge.webdav_password.unwrap_or_default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
*lock = Some(config.clone());
|
// 重新获取锁并存储配置
|
||||||
|
*self.config.lock() = Some(config.clone());
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -117,8 +121,7 @@ impl WebDavClient {
|
|||||||
attempt.follow()
|
attempt.follow()
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.build()
|
.build()?,
|
||||||
.unwrap(),
|
|
||||||
)
|
)
|
||||||
.set_host(config.url)
|
.set_host(config.url)
|
||||||
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
|
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
|
||||||
@@ -243,12 +246,17 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
|||||||
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||||
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
|
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let entry = entry.unwrap();
|
let entry = entry?;
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap());
|
let file_name_os = entry.file_name();
|
||||||
|
let file_name = file_name_os
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?;
|
||||||
|
let backup_path = format!("profiles/{}", file_name);
|
||||||
zip.start_file(backup_path, options)?;
|
zip.start_file(backup_path, options)?;
|
||||||
zip.write_all(fs::read(path).unwrap().as_slice())?;
|
let file_content = fs::read(&path)?;
|
||||||
|
zip.write_all(&file_content)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,14 +264,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
|||||||
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
|
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
|
||||||
|
|
||||||
let mut verge_config: serde_json::Value =
|
let mut verge_config: serde_json::Value =
|
||||||
serde_yaml::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
|
serde_yaml_ng::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
|
||||||
if let Some(obj) = verge_config.as_object_mut() {
|
if let Some(obj) = verge_config.as_object_mut() {
|
||||||
obj.remove("webdav_username");
|
obj.remove("webdav_username");
|
||||||
obj.remove("webdav_password");
|
obj.remove("webdav_password");
|
||||||
obj.remove("webdav_url");
|
obj.remove("webdav_url");
|
||||||
}
|
}
|
||||||
zip.start_file(dirs::VERGE_CONFIG, options)?;
|
zip.start_file(dirs::VERGE_CONFIG, options)?;
|
||||||
zip.write_all(serde_yaml::to_string(&verge_config)?.as_bytes())?;
|
zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?;
|
||||||
|
|
||||||
zip.start_file(dirs::PROFILE_YAML, options)?;
|
zip.start_file(dirs::PROFILE_YAML, options)?;
|
||||||
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
|
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
|
||||||
|
|||||||
+134
-314
@@ -1,11 +1,13 @@
|
|||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use crate::AsyncHandler;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::*,
|
config::*,
|
||||||
core::{
|
core::{
|
||||||
handle,
|
handle,
|
||||||
service::{self},
|
service::{self, SERVICE_MANAGER, ServiceStatus},
|
||||||
},
|
},
|
||||||
logging, logging_error,
|
ipc::IpcManager,
|
||||||
module::mihomo::MihomoManager,
|
logging, logging_error, singleton_lazy,
|
||||||
utils::{
|
utils::{
|
||||||
dirs,
|
dirs,
|
||||||
help::{self},
|
help::{self},
|
||||||
@@ -14,16 +16,15 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use once_cell::sync::OnceCell;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt,
|
||||||
fs::{create_dir_all, File},
|
fs::{File, create_dir_all},
|
||||||
io::Write,
|
io::Write,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use tauri_plugin_shell::{process::CommandChild, ShellExt};
|
use tauri_plugin_shell::{ShellExt, process::CommandChild};
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CoreManager {
|
pub struct CoreManager {
|
||||||
@@ -138,23 +139,23 @@ impl CoreManager {
|
|||||||
/// 使用默认配置
|
/// 使用默认配置
|
||||||
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
||||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||||
*Config::runtime().draft_mut() = Box::new(IRuntime {
|
|
||||||
config: Some(Config::clash().latest_ref().0.clone()),
|
// Extract clash config before async operations
|
||||||
|
let clash_config = Config::clash().await.latest_ref().0.clone();
|
||||||
|
|
||||||
|
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
|
||||||
|
config: Some(clash_config.clone()),
|
||||||
exists_keys: vec![],
|
exists_keys: vec![],
|
||||||
chain_logs: Default::default(),
|
chain_logs: Default::default(),
|
||||||
});
|
});
|
||||||
help::save_yaml(
|
help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?;
|
||||||
&runtime_path,
|
|
||||||
&Config::clash().latest_ref().0,
|
|
||||||
Some("# Clash Verge Runtime"),
|
|
||||||
)?;
|
|
||||||
handle::Handle::notice_message(msg_type, msg_content);
|
handle::Handle::notice_message(msg_type, msg_content);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
/// 验证运行时配置
|
/// 验证运行时配置
|
||||||
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||||
logging!(info, Type::Config, true, "生成临时配置文件用于验证");
|
logging!(info, Type::Config, true, "生成临时配置文件用于验证");
|
||||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
let config_path = Config::generate_file(ConfigType::Check).await?;
|
||||||
let config_path = dirs::path_to_str(&config_path)?;
|
let config_path = dirs::path_to_str(&config_path)?;
|
||||||
self.validate_config_internal(config_path).await
|
self.validate_config_internal(config_path).await
|
||||||
}
|
}
|
||||||
@@ -186,7 +187,7 @@ impl CoreManager {
|
|||||||
"检测到Merge文件,仅进行语法检查: {}",
|
"检测到Merge文件,仅进行语法检查: {}",
|
||||||
config_path
|
config_path
|
||||||
);
|
);
|
||||||
return self.validate_file_syntax(config_path).await;
|
return self.validate_file_syntax(config_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否为脚本文件
|
// 检查是否为脚本文件
|
||||||
@@ -218,7 +219,7 @@ impl CoreManager {
|
|||||||
"检测到脚本文件,使用JavaScript验证: {}",
|
"检测到脚本文件,使用JavaScript验证: {}",
|
||||||
config_path
|
config_path
|
||||||
);
|
);
|
||||||
return self.validate_script_file(config_path).await;
|
return self.validate_script_file(config_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对YAML配置文件使用Clash内核验证
|
// 对YAML配置文件使用Clash内核验证
|
||||||
@@ -247,10 +248,14 @@ impl CoreManager {
|
|||||||
config_path
|
config_path
|
||||||
);
|
);
|
||||||
|
|
||||||
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
|
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
|
||||||
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
|
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
|
||||||
|
let msg = "Failed to get app handle";
|
||||||
|
logging!(error, Type::Core, true, "{}", msg);
|
||||||
|
anyhow::anyhow!(msg)
|
||||||
|
})?;
|
||||||
let app_dir = dirs::app_home_dir()?;
|
let app_dir = dirs::app_home_dir()?;
|
||||||
let app_dir_str = dirs::path_to_str(&app_dir)?;
|
let app_dir_str = dirs::path_to_str(&app_dir)?;
|
||||||
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
|
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
|
||||||
@@ -298,7 +303,7 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 只进行文件语法检查,不进行完整验证
|
/// 只进行文件语法检查,不进行完整验证
|
||||||
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
||||||
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
|
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
|
||||||
|
|
||||||
// 读取文件内容
|
// 读取文件内容
|
||||||
@@ -312,7 +317,7 @@ impl CoreManager {
|
|||||||
};
|
};
|
||||||
// 对YAML文件尝试解析,只检查语法正确性
|
// 对YAML文件尝试解析,只检查语法正确性
|
||||||
logging!(info, Type::Config, true, "进行YAML语法检查");
|
logging!(info, Type::Config, true, "进行YAML语法检查");
|
||||||
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
|
match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
logging!(info, Type::Config, true, "YAML语法检查通过");
|
logging!(info, Type::Config, true, "YAML语法检查通过");
|
||||||
Ok((true, String::new()))
|
Ok((true, String::new()))
|
||||||
@@ -326,7 +331,7 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// 验证脚本文件语法
|
/// 验证脚本文件语法
|
||||||
async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
|
fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
|
||||||
// 读取脚本内容
|
// 读取脚本内容
|
||||||
let content = match std::fs::read_to_string(path) {
|
let content = match std::fs::read_to_string(path) {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
@@ -379,8 +384,6 @@ impl CoreManager {
|
|||||||
return Ok((true, String::new()));
|
return Ok((true, String::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
logging!(info, Type::Config, true, "开始更新配置");
|
|
||||||
|
|
||||||
// 1. 先生成新的配置内容
|
// 1. 先生成新的配置内容
|
||||||
logging!(info, Type::Config, true, "生成新的配置内容");
|
logging!(info, Type::Config, true, "生成新的配置内容");
|
||||||
Config::generate().await?;
|
Config::generate().await?;
|
||||||
@@ -388,21 +391,20 @@ impl CoreManager {
|
|||||||
// 2. 验证配置
|
// 2. 验证配置
|
||||||
match self.validate_config().await {
|
match self.validate_config().await {
|
||||||
Ok((true, _)) => {
|
Ok((true, _)) => {
|
||||||
logging!(info, Type::Config, true, "配置验证通过");
|
|
||||||
// 4. 验证通过后,生成正式的运行时配置
|
// 4. 验证通过后,生成正式的运行时配置
|
||||||
logging!(info, Type::Config, true, "生成运行时配置");
|
logging!(info, Type::Config, true, "配置验证通过, 生成运行时配置");
|
||||||
let run_path = Config::generate_file(ConfigType::Run)?;
|
let run_path = Config::generate_file(ConfigType::Run).await?;
|
||||||
logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
|
logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
|
||||||
Ok((true, "something".into()))
|
Ok((true, "something".into()))
|
||||||
}
|
}
|
||||||
Ok((false, error_msg)) => {
|
Ok((false, error_msg)) => {
|
||||||
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
|
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
|
||||||
Config::runtime().discard();
|
Config::runtime().await.discard();
|
||||||
Ok((false, error_msg))
|
Ok((false, error_msg))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
|
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
|
||||||
Config::runtime().discard();
|
Config::runtime().await.discard();
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,18 +415,15 @@ impl CoreManager {
|
|||||||
logging_error!(Type::Core, true, "{}", msg);
|
logging_error!(Type::Core, true, "{}", msg);
|
||||||
msg
|
msg
|
||||||
});
|
});
|
||||||
match MihomoManager::global()
|
match IpcManager::global().put_configs_force(run_path_str?).await {
|
||||||
.put_configs_force(run_path_str?)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
Config::runtime().apply();
|
Config::runtime().await.apply();
|
||||||
logging!(info, Type::Core, true, "Configuration updated successfully");
|
logging!(info, Type::Core, true, "Configuration updated successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let msg = e.to_string();
|
let msg = e.to_string();
|
||||||
Config::runtime().discard();
|
Config::runtime().await.discard();
|
||||||
logging_error!(Type::Core, true, "Failed to update configuration: {}", msg);
|
logging_error!(Type::Core, true, "Failed to update configuration: {}", msg);
|
||||||
Err(msg)
|
Err(msg)
|
||||||
}
|
}
|
||||||
@@ -439,7 +438,7 @@ impl CoreManager {
|
|||||||
|
|
||||||
// 获取当前管理的进程 PID
|
// 获取当前管理的进程 PID
|
||||||
let current_pid = {
|
let current_pid = {
|
||||||
let child_guard = self.child_sidecar.lock().await;
|
let child_guard = self.child_sidecar.lock();
|
||||||
child_guard.as_ref().map(|child| child.pid())
|
child_guard.as_ref().map(|child| child.pid())
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -465,18 +464,18 @@ impl CoreManager {
|
|||||||
Ok((pids, process_name)) => {
|
Ok((pids, process_name)) => {
|
||||||
for pid in pids {
|
for pid in pids {
|
||||||
// 跳过当前管理的进程
|
// 跳过当前管理的进程
|
||||||
if let Some(current) = current_pid {
|
if let Some(current) = current_pid
|
||||||
if pid == current {
|
&& pid == current
|
||||||
logging!(
|
{
|
||||||
debug,
|
logging!(
|
||||||
Type::Core,
|
debug,
|
||||||
true,
|
Type::Core,
|
||||||
"跳过当前管理的进程: {} (PID: {})",
|
true,
|
||||||
process_name,
|
"跳过当前管理的进程: {} (PID: {})",
|
||||||
pid
|
process_name,
|
||||||
);
|
pid
|
||||||
continue;
|
);
|
||||||
}
|
continue;
|
||||||
}
|
}
|
||||||
pids_to_kill.push((pid, process_name.clone()));
|
pids_to_kill.push((pid, process_name.clone()));
|
||||||
}
|
}
|
||||||
@@ -525,13 +524,13 @@ impl CoreManager {
|
|||||||
use std::mem;
|
use std::mem;
|
||||||
use winapi::um::handleapi::CloseHandle;
|
use winapi::um::handleapi::CloseHandle;
|
||||||
use winapi::um::tlhelp32::{
|
use winapi::um::tlhelp32::{
|
||||||
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
|
||||||
TH32CS_SNAPPROCESS,
|
TH32CS_SNAPPROCESS,
|
||||||
};
|
};
|
||||||
use winapi::um::winnt::HANDLE;
|
use winapi::um::winnt::HANDLE;
|
||||||
|
|
||||||
let process_name_clone = process_name.clone();
|
let process_name_clone = process_name.clone();
|
||||||
let pids = tokio::task::spawn_blocking(move || -> Result<Vec<u32>> {
|
let pids = AsyncHandler::spawn_blocking(move || -> Result<Vec<u32>> {
|
||||||
let mut pids = Vec::new();
|
let mut pids = Vec::new();
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -626,7 +625,7 @@ impl CoreManager {
|
|||||||
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
|
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
|
||||||
use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE};
|
use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE};
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || -> bool {
|
AsyncHandler::spawn_blocking(move || -> bool {
|
||||||
unsafe {
|
unsafe {
|
||||||
let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
|
let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
|
||||||
if process_handle.is_null() {
|
if process_handle.is_null() {
|
||||||
@@ -701,7 +700,7 @@ impl CoreManager {
|
|||||||
use winapi::um::processthreadsapi::OpenProcess;
|
use winapi::um::processthreadsapi::OpenProcess;
|
||||||
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
|
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
|
||||||
|
|
||||||
let result = tokio::task::spawn_blocking(move || -> Result<bool> {
|
AsyncHandler::spawn_blocking(move || -> Result<bool> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
|
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
|
||||||
if process_handle.is_null() {
|
if process_handle.is_null() {
|
||||||
@@ -717,9 +716,7 @@ impl CoreManager {
|
|||||||
Ok(exit_code == 259)
|
Ok(exit_code == 259)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await?;
|
.await?
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
@@ -734,12 +731,13 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn start_core_by_sidecar(&self) -> Result<()> {
|
async fn start_core_by_sidecar(&self) -> Result<()> {
|
||||||
logging!(trace, Type::Core, true, "Running core by sidecar");
|
logging!(info, Type::Core, true, "Running core by sidecar");
|
||||||
let config_file = &Config::generate_file(ConfigType::Run)?;
|
|
||||||
|
let config_file = &Config::generate_file(ConfigType::Run).await?;
|
||||||
let app_handle = handle::Handle::global()
|
let app_handle = handle::Handle::global()
|
||||||
.app_handle()
|
.app_handle()
|
||||||
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
|
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
|
||||||
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
|
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
|
||||||
let config_dir = dirs::app_home_dir()?;
|
let config_dir = dirs::app_home_dir()?;
|
||||||
|
|
||||||
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
|
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
|
||||||
@@ -763,21 +761,19 @@ impl CoreManager {
|
|||||||
])
|
])
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
tokio::spawn(async move {
|
while let Some(event) = rx.recv().await {
|
||||||
while let Some(event) = rx.recv().await {
|
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event
|
||||||
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event {
|
&& let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line))
|
||||||
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
|
{
|
||||||
logging!(
|
logging!(
|
||||||
error,
|
error,
|
||||||
Type::Core,
|
Type::Core,
|
||||||
true,
|
true,
|
||||||
"[Sidecar] Failed to write stdout to file: {}",
|
"[Sidecar] Failed to write stdout to file: {}",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
let pid = child.pid();
|
let pid = child.pid();
|
||||||
logging!(
|
logging!(
|
||||||
@@ -787,14 +783,14 @@ impl CoreManager {
|
|||||||
"Started core by sidecar pid: {}",
|
"Started core by sidecar pid: {}",
|
||||||
pid
|
pid
|
||||||
);
|
);
|
||||||
*self.child_sidecar.lock().await = Some(child);
|
*self.child_sidecar.lock() = Some(child);
|
||||||
self.set_running_mode(RunningMode::Sidecar).await;
|
self.set_running_mode(RunningMode::Sidecar);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn stop_core_by_sidecar(&self) -> Result<()> {
|
fn stop_core_by_sidecar(&self) -> Result<()> {
|
||||||
logging!(trace, Type::Core, true, "Stopping core by sidecar");
|
logging!(info, Type::Core, true, "Stopping core by sidecar");
|
||||||
|
|
||||||
if let Some(child) = self.child_sidecar.lock().await.take() {
|
if let Some(child) = self.child_sidecar.lock().take() {
|
||||||
let pid = child.pid();
|
let pid = child.pid();
|
||||||
child.kill()?;
|
child.kill()?;
|
||||||
logging!(
|
logging!(
|
||||||
@@ -805,83 +801,39 @@ impl CoreManager {
|
|||||||
pid
|
pid
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.set_running_mode(RunningMode::NotRunning).await;
|
self.set_running_mode(RunningMode::NotRunning);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoreManager {
|
impl CoreManager {
|
||||||
async fn start_core_by_service(&self) -> Result<()> {
|
async fn start_core_by_service(&self) -> Result<()> {
|
||||||
logging!(trace, Type::Core, true, "Running core by service");
|
logging!(info, Type::Core, true, "Running core by service");
|
||||||
let config_file = &Config::generate_file(ConfigType::Run)?;
|
let config_file = &Config::generate_file(ConfigType::Run).await?;
|
||||||
service::run_core_by_service(config_file).await?;
|
service::run_core_by_service(config_file).await?;
|
||||||
self.set_running_mode(RunningMode::Service).await;
|
self.set_running_mode(RunningMode::Service);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn stop_core_by_service(&self) -> Result<()> {
|
async fn stop_core_by_service(&self) -> Result<()> {
|
||||||
logging!(trace, Type::Core, true, "Stopping core by service");
|
logging!(info, Type::Core, true, "Stopping core by service");
|
||||||
service::stop_core_by_service().await?;
|
service::stop_core_by_service().await?;
|
||||||
self.set_running_mode(RunningMode::NotRunning).await;
|
self.set_running_mode(RunningMode::NotRunning);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoreManager {
|
impl Default for CoreManager {
|
||||||
pub fn global() -> &'static CoreManager {
|
fn default() -> Self {
|
||||||
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
CoreManager {
|
||||||
CORE_MANAGER.get_or_init(|| CoreManager {
|
|
||||||
running: Arc::new(Mutex::new(RunningMode::NotRunning)),
|
running: Arc::new(Mutex::new(RunningMode::NotRunning)),
|
||||||
child_sidecar: Arc::new(Mutex::new(None)),
|
child_sidecar: Arc::new(Mutex::new(None)),
|
||||||
})
|
|
||||||
}
|
|
||||||
// 当服务安装失败时的回退逻辑
|
|
||||||
async fn attempt_service_init(&self) -> Result<()> {
|
|
||||||
if service::check_service_needs_reinstall().await {
|
|
||||||
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装");
|
|
||||||
if let Err(e) = service::reinstall_service().await {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"服务重装失败 during attempt_service_init: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
// 如果重装成功,还需要尝试启动服务
|
|
||||||
logging!(info, Type::Core, true, "服务重装成功,尝试启动服务");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = self.start_core_by_service().await {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"通过服务启动核心失败 during attempt_service_init: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
|
|
||||||
let mut state = service::ServiceState::get();
|
|
||||||
if !state.prefer_sidecar {
|
|
||||||
state.prefer_sidecar = true;
|
|
||||||
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
|
|
||||||
if let Err(save_err) = state.save() {
|
|
||||||
logging!(
|
|
||||||
error,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}",
|
|
||||||
save_err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoreManager {
|
||||||
pub async fn init(&self) -> Result<()> {
|
pub async fn init(&self) -> Result<()> {
|
||||||
logging!(trace, Type::Core, "Initializing core");
|
logging!(info, Type::Core, "Initializing core");
|
||||||
|
|
||||||
// 应用启动时先清理任何遗留的 mihomo 进程
|
// 应用启动时先清理任何遗留的 mihomo 进程
|
||||||
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
|
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
|
||||||
@@ -894,208 +846,66 @@ impl CoreManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut core_started_successfully = false;
|
// 使用简化的启动流程
|
||||||
|
logging!(info, Type::Core, true, "开始核心初始化");
|
||||||
|
self.start_core().await?;
|
||||||
|
|
||||||
if service::is_service_available().await.is_ok() {
|
logging!(info, Type::Core, true, "核心初始化完成");
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"服务当前可用或看似可用,尝试通过服务模式启动/重装"
|
|
||||||
);
|
|
||||||
match self.attempt_service_init().await {
|
|
||||||
Ok(_) => {
|
|
||||||
logging!(info, Type::Core, true, "服务模式成功启动核心");
|
|
||||||
core_started_successfully = true;
|
|
||||||
}
|
|
||||||
Err(_err) => {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"服务模式启动或重装失败。将尝试Sidecar模式回退。"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"服务初始不可用 (is_service_available 调用失败)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !core_started_successfully {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑"
|
|
||||||
);
|
|
||||||
|
|
||||||
let service_state = service::ServiceState::get();
|
|
||||||
|
|
||||||
if service_state.prefer_sidecar {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动"
|
|
||||||
);
|
|
||||||
self.start_core_by_sidecar().await?;
|
|
||||||
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
|
|
||||||
// 后续的 Tray::global().subscribe_traffic().await 仍然会执行
|
|
||||||
} else {
|
|
||||||
let has_service_install_record = service_state.last_install_time > 0;
|
|
||||||
if !has_service_install_record {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
|
|
||||||
);
|
|
||||||
match service::install_service().await {
|
|
||||||
Ok(_) => {
|
|
||||||
logging!(info, Type::Core, true, "服务安装成功(首次尝试)");
|
|
||||||
let mut new_state = service::ServiceState::default();
|
|
||||||
new_state.record_install();
|
|
||||||
new_state.prefer_sidecar = false;
|
|
||||||
new_state.save()?;
|
|
||||||
|
|
||||||
if service::is_service_available().await.is_ok() {
|
|
||||||
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动");
|
|
||||||
if self.start_core_by_service().await.is_ok() {
|
|
||||||
logging!(info, Type::Core, true, "新安装的服务启动成功");
|
|
||||||
} else {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"新安装的服务启动失败,回退到Sidecar模式"
|
|
||||||
);
|
|
||||||
let mut final_state = service::ServiceState::get();
|
|
||||||
final_state.prefer_sidecar = true;
|
|
||||||
final_state.last_error =
|
|
||||||
Some("Newly installed service failed to start".to_string());
|
|
||||||
final_state.save()?;
|
|
||||||
self.start_core_by_sidecar().await?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"服务安装成功但未能连接/立即可用,回退到Sidecar模式"
|
|
||||||
);
|
|
||||||
let mut final_state = service::ServiceState::get();
|
|
||||||
final_state.prefer_sidecar = true;
|
|
||||||
final_state.last_error = Some(
|
|
||||||
"Newly installed service not immediately available/connectable"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
final_state.save()?;
|
|
||||||
self.start_core_by_sidecar().await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
logging!(warn, Type::Core, true, "服务首次安装失败: {}", err);
|
|
||||||
let new_state = service::ServiceState {
|
|
||||||
last_error: Some(err.to_string()),
|
|
||||||
prefer_sidecar: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
new_state.save()?;
|
|
||||||
self.start_core_by_sidecar().await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 有安装记录,服务未成功启动,且初始不偏好sidecar
|
|
||||||
// 这意味着服务之前可能可用,但 attempt_service_init 失败了(并应已设置 prefer_sidecar),
|
|
||||||
// 或者服务初始不可用,无偏好,有记录。应强制使用 sidecar。
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式"
|
|
||||||
);
|
|
||||||
let mut final_state = service::ServiceState::get();
|
|
||||||
if !final_state.prefer_sidecar {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"prefer_sidecar 为 false,因服务启动失败或不可用而强制设置为 true"
|
|
||||||
);
|
|
||||||
final_state.prefer_sidecar = true;
|
|
||||||
final_state.last_error =
|
|
||||||
Some(final_state.last_error.unwrap_or_else(|| {
|
|
||||||
"Service startup failed or unavailable before sidecar fallback"
|
|
||||||
.to_string()
|
|
||||||
}));
|
|
||||||
final_state.save()?;
|
|
||||||
}
|
|
||||||
self.start_core_by_sidecar().await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(trace, Type::Core, "Initied core logic completed");
|
|
||||||
// #[cfg(target_os = "macos")]
|
|
||||||
// logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_running_mode(&self, mode: RunningMode) {
|
pub fn set_running_mode(&self, mode: RunningMode) {
|
||||||
let mut guard = self.running.lock().await;
|
let mut guard = self.running.lock();
|
||||||
*guard = mode;
|
*guard = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_running_mode(&self) -> RunningMode {
|
pub fn get_running_mode(&self) -> RunningMode {
|
||||||
let guard = self.running.lock().await;
|
let guard = self.running.lock();
|
||||||
(*guard).clone()
|
(*guard).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn prestart_core(&self) -> Result<()> {
|
||||||
|
SERVICE_MANAGER.lock().await.refresh().await?;
|
||||||
|
match SERVICE_MANAGER.lock().await.current() {
|
||||||
|
ServiceStatus::Ready => {
|
||||||
|
self.set_running_mode(RunningMode::Service);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.set_running_mode(RunningMode::Sidecar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 启动核心
|
/// 启动核心
|
||||||
pub async fn start_core(&self) -> Result<()> {
|
pub async fn start_core(&self) -> Result<()> {
|
||||||
if service::is_service_available().await.is_ok() {
|
self.prestart_core().await?;
|
||||||
if service::check_service_needs_reinstall().await {
|
|
||||||
service::reinstall_service().await?;
|
match self.get_running_mode() {
|
||||||
|
RunningMode::Service => {
|
||||||
|
logging_error!(Type::Core, true, self.start_core_by_service().await);
|
||||||
}
|
}
|
||||||
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
|
RunningMode::NotRunning | RunningMode::Sidecar => {
|
||||||
self.start_core_by_service().await?;
|
logging_error!(Type::Core, true, self.start_core_by_sidecar().await);
|
||||||
} else {
|
|
||||||
// 服务不可用,检查用户偏好
|
|
||||||
let service_state = service::ServiceState::get();
|
|
||||||
if service_state.prefer_sidecar {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Core,
|
|
||||||
true,
|
|
||||||
"服务不可用,根据用户偏好使用Sidecar模式"
|
|
||||||
);
|
|
||||||
self.start_core_by_sidecar().await?;
|
|
||||||
} else {
|
|
||||||
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
|
|
||||||
self.start_core_by_sidecar().await?;
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 停止核心运行
|
/// 停止核心运行
|
||||||
pub async fn stop_core(&self) -> Result<()> {
|
pub async fn stop_core(&self) -> Result<()> {
|
||||||
match self.get_running_mode().await {
|
match self.get_running_mode() {
|
||||||
RunningMode::Service => self.stop_core_by_service().await,
|
RunningMode::Service => self.stop_core_by_service().await,
|
||||||
RunningMode::Sidecar => self.stop_core_by_sidecar().await,
|
RunningMode::Sidecar => self.stop_core_by_sidecar(),
|
||||||
RunningMode::NotRunning => Ok(()),
|
RunningMode::NotRunning => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重启内核
|
/// 重启内核
|
||||||
pub async fn restart_core(&self) -> Result<()> {
|
pub async fn restart_core(&self) -> Result<()> {
|
||||||
|
logging!(info, Type::Core, true, "Restarting core");
|
||||||
self.stop_core().await?;
|
self.stop_core().await?;
|
||||||
|
|
||||||
self.start_core().await?;
|
self.start_core().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1107,18 +917,25 @@ impl CoreManager {
|
|||||||
logging!(error, Type::Core, true, "{}", error_message);
|
logging!(error, Type::Core, true, "{}", error_message);
|
||||||
return Err(error_message.to_string());
|
return Err(error_message.to_string());
|
||||||
}
|
}
|
||||||
let core: &str = &clash_core.clone().unwrap();
|
let core = clash_core.as_ref().ok_or_else(|| {
|
||||||
if !IVerge::VALID_CLASH_CORES.contains(&core) {
|
let msg = "Clash core should not be None";
|
||||||
|
logging!(error, Type::Core, true, "{}", msg);
|
||||||
|
msg.to_string()
|
||||||
|
})?;
|
||||||
|
if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) {
|
||||||
let error_message = format!("Clash core invalid name: {core}");
|
let error_message = format!("Clash core invalid name: {core}");
|
||||||
logging!(error, Type::Core, true, "{}", error_message);
|
logging!(error, Type::Core, true, "{}", error_message);
|
||||||
return Err(error_message);
|
return Err(error_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
Config::verge().draft_mut().clash_core = clash_core.clone();
|
Config::verge().await.draft_mut().clash_core = clash_core.clone();
|
||||||
Config::verge().apply();
|
Config::verge().await.apply();
|
||||||
logging_error!(Type::Core, true, Config::verge().latest_ref().save_file());
|
|
||||||
|
|
||||||
let run_path = Config::generate_file(ConfigType::Run).map_err(|e| {
|
// 分离数据获取和异步调用避免Send问题
|
||||||
|
let verge_data = Config::verge().await.latest_ref().clone();
|
||||||
|
logging_error!(Type::Core, true, verge_data.save_file().await);
|
||||||
|
|
||||||
|
let run_path = Config::generate_file(ConfigType::Run).await.map_err(|e| {
|
||||||
let msg = e.to_string();
|
let msg = e.to_string();
|
||||||
logging_error!(Type::Core, true, "{}", msg);
|
logging_error!(Type::Core, true, "{}", msg);
|
||||||
msg
|
msg
|
||||||
@@ -1129,3 +946,6 @@ impl CoreManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use simplified singleton_lazy macro
|
||||||
|
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use parking_lot::RwLock;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use tokio::time::{sleep, timeout, Duration};
|
use tokio::time::{Duration, sleep, timeout};
|
||||||
|
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
||||||
|
|
||||||
use crate::config::{Config, IVerge};
|
use crate::config::{Config, IVerge};
|
||||||
use crate::core::async_proxy_query::AsyncProxyQuery;
|
use crate::core::async_proxy_query::AsyncProxyQuery;
|
||||||
use crate::logging_error;
|
use crate::logging_error;
|
||||||
|
use crate::process::AsyncHandler;
|
||||||
use crate::utils::logging::Type;
|
use crate::utils::logging::Type;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use sysproxy::{Autoproxy, Sysproxy};
|
use sysproxy::{Autoproxy, Sysproxy};
|
||||||
@@ -58,7 +60,7 @@ impl Default for ProxyState {
|
|||||||
sys_proxy: Sysproxy {
|
sys_proxy: Sysproxy {
|
||||||
enable: false,
|
enable: false,
|
||||||
host: "127.0.0.1".to_string(),
|
host: "127.0.0.1".to_string(),
|
||||||
port: 7890,
|
port: 7897,
|
||||||
bypass: "".to_string(),
|
bypass: "".to_string(),
|
||||||
},
|
},
|
||||||
last_updated: std::time::Instant::now(),
|
last_updated: std::time::Instant::now(),
|
||||||
@@ -74,7 +76,7 @@ pub struct EventDrivenProxyManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct QueryRequest {
|
pub struct QueryRequest {
|
||||||
response_tx: oneshot::Sender<Autoproxy>,
|
response_tx: oneshot::Sender<Autoproxy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +99,8 @@ impl EventDrivenProxyManager {
|
|||||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||||
let (query_tx, query_rx) = mpsc::unbounded_channel();
|
let (query_tx, query_rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
Self::start_event_loop(state.clone(), event_rx, query_rx);
|
let state_clone = Arc::clone(&state);
|
||||||
|
AsyncHandler::spawn(move || Self::start_event_loop(state_clone, event_rx, query_rx));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state,
|
state,
|
||||||
@@ -107,8 +110,8 @@ impl EventDrivenProxyManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 获取自动代理配置(缓存)
|
/// 获取自动代理配置(缓存)
|
||||||
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
|
pub async fn get_auto_proxy_cached(&self) -> Autoproxy {
|
||||||
self.state.read().auto_proxy.clone()
|
self.state.read().await.auto_proxy.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 异步获取最新的自动代理配置
|
/// 异步获取最新的自动代理配置
|
||||||
@@ -118,14 +121,14 @@ impl EventDrivenProxyManager {
|
|||||||
|
|
||||||
if self.query_sender.send(query).is_err() {
|
if self.query_sender.send(query).is_err() {
|
||||||
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
|
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
|
||||||
return self.get_auto_proxy_cached();
|
return self.get_auto_proxy_cached().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
match timeout(Duration::from_secs(5), rx).await {
|
match timeout(Duration::from_secs(5), rx).await {
|
||||||
Ok(Ok(result)) => result,
|
Ok(Ok(result)) => result,
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!(target: "app", "查询超时,返回缓存数据");
|
log::warn!(target: "app", "查询超时,返回缓存数据");
|
||||||
self.get_auto_proxy_cached()
|
self.get_auto_proxy_cached().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,43 +173,34 @@ impl EventDrivenProxyManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_event_loop(
|
pub async fn start_event_loop(
|
||||||
state: Arc<RwLock<ProxyState>>,
|
state: Arc<RwLock<ProxyState>>,
|
||||||
mut event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
|
event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
|
||||||
mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
||||||
) {
|
) {
|
||||||
tokio::spawn(async move {
|
log::info!(target: "app", "事件驱动代理管理器启动");
|
||||||
log::info!(target: "app", "事件驱动代理管理器启动");
|
|
||||||
|
|
||||||
loop {
|
// 将 mpsc 接收器包装成 Stream,避免每次循环创建 future
|
||||||
tokio::select! {
|
let mut event_stream = UnboundedReceiverStream::new(event_rx);
|
||||||
event = event_rx.recv() => {
|
let mut query_stream = UnboundedReceiverStream::new(query_rx);
|
||||||
match event {
|
|
||||||
Some(event) => {
|
loop {
|
||||||
log::debug!(target: "app", "处理代理事件: {event:?}");
|
tokio::select! {
|
||||||
Self::handle_event(&state, event).await;
|
Some(event) = event_stream.next() => {
|
||||||
}
|
log::debug!(target: "app", "处理代理事件: {event:?}");
|
||||||
None => {
|
Self::handle_event(&state, event).await;
|
||||||
log::info!(target: "app", "事件通道关闭,代理管理器停止");
|
}
|
||||||
break;
|
Some(query) = query_stream.next() => {
|
||||||
}
|
let result = Self::handle_query(&state).await;
|
||||||
}
|
let _ = query.response_tx.send(result);
|
||||||
}
|
}
|
||||||
query = query_rx.recv() => {
|
else => {
|
||||||
match query {
|
// 两个通道都关闭时退出
|
||||||
Some(query) => {
|
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
|
||||||
let result = Self::handle_query(&state).await;
|
break;
|
||||||
let _ = query.response_tx.send(result);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
log::info!(target: "app", "查询通道关闭");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
|
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
|
||||||
@@ -218,7 +212,7 @@ impl EventDrivenProxyManager {
|
|||||||
Self::enable_system_proxy(state).await;
|
Self::enable_system_proxy(state).await;
|
||||||
}
|
}
|
||||||
ProxyEvent::DisableProxy => {
|
ProxyEvent::DisableProxy => {
|
||||||
Self::disable_system_proxy(state).await;
|
Self::disable_system_proxy(state);
|
||||||
}
|
}
|
||||||
ProxyEvent::SwitchToPac => {
|
ProxyEvent::SwitchToPac => {
|
||||||
Self::switch_proxy_mode(state, true).await;
|
Self::switch_proxy_mode(state, true).await;
|
||||||
@@ -240,7 +234,8 @@ impl EventDrivenProxyManager {
|
|||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
Self::update_state_timestamp(state, |s| {
|
||||||
s.auto_proxy = auto_proxy.clone();
|
s.auto_proxy = auto_proxy.clone();
|
||||||
});
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
auto_proxy
|
auto_proxy
|
||||||
}
|
}
|
||||||
@@ -248,7 +243,7 @@ impl EventDrivenProxyManager {
|
|||||||
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
||||||
log::info!(target: "app", "初始化代理状态");
|
log::info!(target: "app", "初始化代理状态");
|
||||||
|
|
||||||
let config = Self::get_proxy_config();
|
let config = Self::get_proxy_config().await;
|
||||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||||
let sys_proxy = Self::get_sys_proxy_with_timeout().await;
|
let sys_proxy = Self::get_sys_proxy_with_timeout().await;
|
||||||
|
|
||||||
@@ -258,7 +253,8 @@ impl EventDrivenProxyManager {
|
|||||||
s.auto_proxy = auto_proxy;
|
s.auto_proxy = auto_proxy;
|
||||||
s.sys_proxy = sys_proxy;
|
s.sys_proxy = sys_proxy;
|
||||||
s.is_healthy = true;
|
s.is_healthy = true;
|
||||||
});
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
||||||
}
|
}
|
||||||
@@ -266,12 +262,13 @@ impl EventDrivenProxyManager {
|
|||||||
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
||||||
log::debug!(target: "app", "更新代理配置");
|
log::debug!(target: "app", "更新代理配置");
|
||||||
|
|
||||||
let config = Self::get_proxy_config();
|
let config = Self::get_proxy_config().await;
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
Self::update_state_timestamp(state, |s| {
|
||||||
s.sys_enabled = config.sys_enabled;
|
s.sys_enabled = config.sys_enabled;
|
||||||
s.pac_enabled = config.pac_enabled;
|
s.pac_enabled = config.pac_enabled;
|
||||||
});
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
if config.guard_enabled && config.sys_enabled {
|
if config.guard_enabled && config.sys_enabled {
|
||||||
Self::check_and_restore_proxy(state).await;
|
Self::check_and_restore_proxy(state).await;
|
||||||
@@ -280,7 +277,7 @@ impl EventDrivenProxyManager {
|
|||||||
|
|
||||||
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
|
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||||
let (sys_enabled, pac_enabled) = {
|
let (sys_enabled, pac_enabled) = {
|
||||||
let s = state.read();
|
let s = state.read().await;
|
||||||
(s.sys_enabled, s.pac_enabled)
|
(s.sys_enabled, s.pac_enabled)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -299,15 +296,18 @@ impl EventDrivenProxyManager {
|
|||||||
|
|
||||||
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
|
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||||
let current = Self::get_auto_proxy_with_timeout().await;
|
let current = Self::get_auto_proxy_with_timeout().await;
|
||||||
let expected = Self::get_expected_pac_config();
|
let expected = Self::get_expected_pac_config().await;
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
Self::update_state_timestamp(state, |s| {
|
||||||
s.auto_proxy = current.clone();
|
s.auto_proxy = current.clone();
|
||||||
});
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
if !current.enable || current.url != expected.url {
|
if !current.enable || current.url != expected.url {
|
||||||
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
|
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
|
||||||
Self::restore_pac_proxy(&expected.url).await;
|
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
|
||||||
|
log::error!(target: "app", "恢复PAC代理失败: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
sleep(Duration::from_millis(500)).await;
|
sleep(Duration::from_millis(500)).await;
|
||||||
let restored = Self::get_auto_proxy_with_timeout().await;
|
let restored = Self::get_auto_proxy_with_timeout().await;
|
||||||
@@ -315,21 +315,25 @@ impl EventDrivenProxyManager {
|
|||||||
Self::update_state_timestamp(state, |s| {
|
Self::update_state_timestamp(state, |s| {
|
||||||
s.is_healthy = restored.enable && restored.url == expected.url;
|
s.is_healthy = restored.enable && restored.url == expected.url;
|
||||||
s.auto_proxy = restored;
|
s.auto_proxy = restored;
|
||||||
});
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
|
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||||
let current = Self::get_sys_proxy_with_timeout().await;
|
let current = Self::get_sys_proxy_with_timeout().await;
|
||||||
let expected = Self::get_expected_sys_proxy();
|
let expected = Self::get_expected_sys_proxy().await;
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| {
|
Self::update_state_timestamp(state, |s| {
|
||||||
s.sys_proxy = current.clone();
|
s.sys_proxy = current.clone();
|
||||||
});
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
if !current.enable || current.host != expected.host || current.port != expected.port {
|
if !current.enable || current.host != expected.host || current.port != expected.port {
|
||||||
log::info!(target: "app", "系统代理设置异常,正在恢复...");
|
log::info!(target: "app", "系统代理设置异常,正在恢复...");
|
||||||
Self::restore_sys_proxy(&expected).await;
|
if let Err(e) = Self::restore_sys_proxy(&expected).await {
|
||||||
|
log::error!(target: "app", "恢复系统代理失败: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
sleep(Duration::from_millis(500)).await;
|
sleep(Duration::from_millis(500)).await;
|
||||||
let restored = Self::get_sys_proxy_with_timeout().await;
|
let restored = Self::get_sys_proxy_with_timeout().await;
|
||||||
@@ -339,27 +343,32 @@ impl EventDrivenProxyManager {
|
|||||||
&& restored.host == expected.host
|
&& restored.host == expected.host
|
||||||
&& restored.port == expected.port;
|
&& restored.port == expected.port;
|
||||||
s.sys_proxy = restored;
|
s.sys_proxy = restored;
|
||||||
});
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
|
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||||
log::info!(target: "app", "启用系统代理");
|
log::info!(target: "app", "启用系统代理");
|
||||||
|
|
||||||
let pac_enabled = state.read().pac_enabled;
|
let pac_enabled = state.read().await.pac_enabled;
|
||||||
|
|
||||||
if pac_enabled {
|
if pac_enabled {
|
||||||
let expected = Self::get_expected_pac_config();
|
let expected = Self::get_expected_pac_config().await;
|
||||||
Self::restore_pac_proxy(&expected.url).await;
|
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
|
||||||
|
log::error!(target: "app", "启用PAC代理失败: {}", e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let expected = Self::get_expected_sys_proxy();
|
let expected = Self::get_expected_sys_proxy().await;
|
||||||
Self::restore_sys_proxy(&expected).await;
|
if let Err(e) = Self::restore_sys_proxy(&expected).await {
|
||||||
|
log::error!(target: "app", "启用系统代理失败: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::check_and_restore_proxy(state).await;
|
Self::check_and_restore_proxy(state).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
|
fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
|
||||||
log::info!(target: "app", "禁用系统代理");
|
log::info!(target: "app", "禁用系统代理");
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
@@ -379,17 +388,21 @@ impl EventDrivenProxyManager {
|
|||||||
let disabled_sys = Sysproxy::default();
|
let disabled_sys = Sysproxy::default();
|
||||||
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
||||||
|
|
||||||
let expected = Self::get_expected_pac_config();
|
let expected = Self::get_expected_pac_config().await;
|
||||||
Self::restore_pac_proxy(&expected.url).await;
|
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
|
||||||
|
log::error!(target: "app", "切换到PAC模式失败: {}", e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let disabled_auto = Autoproxy::default();
|
let disabled_auto = Autoproxy::default();
|
||||||
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
||||||
|
|
||||||
let expected = Self::get_expected_sys_proxy();
|
let expected = Self::get_expected_sys_proxy().await;
|
||||||
Self::restore_sys_proxy(&expected).await;
|
if let Err(e) = Self::restore_sys_proxy(&expected).await {
|
||||||
|
log::error!(target: "app", "切换到HTTP代理模式失败: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac);
|
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac).await;
|
||||||
Self::check_and_restore_proxy(state).await;
|
Self::check_and_restore_proxy(state).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,18 +429,18 @@ impl EventDrivenProxyManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 统一的状态更新方法
|
// 统一的状态更新方法
|
||||||
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
|
async fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut ProxyState),
|
F: FnOnce(&mut ProxyState),
|
||||||
{
|
{
|
||||||
let mut state_guard = state.write();
|
let mut state_guard = state.write().await;
|
||||||
update_fn(&mut state_guard);
|
update_fn(&mut state_guard);
|
||||||
state_guard.last_updated = std::time::Instant::now();
|
state_guard.last_updated = std::time::Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_proxy_config() -> ProxyConfig {
|
async fn get_proxy_config() -> ProxyConfig {
|
||||||
let (sys_enabled, pac_enabled, guard_enabled) = {
|
let (sys_enabled, pac_enabled, guard_enabled) = {
|
||||||
let verge_config = Config::verge();
|
let verge_config = Config::verge().await;
|
||||||
let verge = verge_config.latest_ref();
|
let verge = verge_config.latest_ref();
|
||||||
(
|
(
|
||||||
verge.enable_system_proxy.unwrap_or(false),
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
@@ -442,9 +455,9 @@ impl EventDrivenProxyManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_expected_pac_config() -> Autoproxy {
|
async fn get_expected_pac_config() -> Autoproxy {
|
||||||
let proxy_host = {
|
let proxy_host = {
|
||||||
let verge_config = Config::verge();
|
let verge_config = Config::verge().await;
|
||||||
let verge = verge_config.latest_ref();
|
let verge = verge_config.latest_ref();
|
||||||
verge
|
verge
|
||||||
.proxy_host
|
.proxy_host
|
||||||
@@ -458,28 +471,25 @@ impl EventDrivenProxyManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_expected_sys_proxy() -> Sysproxy {
|
async fn get_expected_sys_proxy() -> Sysproxy {
|
||||||
let verge_config = Config::verge();
|
let verge_config = Config::verge().await;
|
||||||
let verge = verge_config.latest_ref();
|
let verge_mixed_port = verge_config.latest_ref().verge_mixed_port;
|
||||||
let port = verge
|
let proxy_host = verge_config.latest_ref().proxy_host.clone();
|
||||||
.verge_mixed_port
|
|
||||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
let port = verge_mixed_port.unwrap_or(Config::clash().await.latest_ref().get_mixed_port());
|
||||||
let proxy_host = verge
|
let proxy_host = proxy_host.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||||
.proxy_host
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "127.0.0.1".to_string());
|
|
||||||
|
|
||||||
Sysproxy {
|
Sysproxy {
|
||||||
enable: true,
|
enable: true,
|
||||||
host: proxy_host,
|
host: proxy_host,
|
||||||
port,
|
port,
|
||||||
bypass: Self::get_bypass_config(),
|
bypass: Self::get_bypass_config().await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_bypass_config() -> String {
|
async fn get_bypass_config() -> String {
|
||||||
let (use_default, custom_bypass) = {
|
let (use_default, custom_bypass) = {
|
||||||
let verge_config = Config::verge();
|
let verge_config = Config::verge().await;
|
||||||
let verge = verge_config.latest_ref();
|
let verge = verge_config.latest_ref();
|
||||||
(
|
(
|
||||||
verge.use_default_bypass.unwrap_or(true),
|
verge.use_default_bypass.unwrap_or(true),
|
||||||
@@ -506,37 +516,45 @@ impl EventDrivenProxyManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restore_pac_proxy(expected_url: &str) {
|
#[cfg(target_os = "windows")]
|
||||||
#[cfg(not(target_os = "windows"))]
|
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
|
||||||
|
Self::execute_sysproxy_command(&["pac", expected_url]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
|
||||||
{
|
{
|
||||||
let new_autoproxy = Autoproxy {
|
let new_autoproxy = Autoproxy {
|
||||||
enable: true,
|
enable: true,
|
||||||
url: expected_url.to_string(),
|
url: expected_url.to_string(),
|
||||||
};
|
};
|
||||||
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
// logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
||||||
}
|
new_autoproxy
|
||||||
|
.set_auto_proxy()
|
||||||
#[cfg(target_os = "windows")]
|
.map_err(|e| anyhow::anyhow!("Failed to set auto proxy: {}", e))
|
||||||
{
|
|
||||||
Self::execute_sysproxy_command(&["pac", expected_url]).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restore_sys_proxy(expected: &Sysproxy) {
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
logging_error!(Type::System, true, expected.set_system_proxy());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let address = format!("{}:{}", expected.host, expected.port);
|
|
||||||
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
async fn execute_sysproxy_command(args: &[&str]) {
|
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
|
||||||
|
let address = format!("{}:{}", expected.host, expected.port);
|
||||||
|
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
|
||||||
|
{
|
||||||
|
// logging_error!(Type::System, true, expected.set_system_proxy());
|
||||||
|
expected
|
||||||
|
.set_system_proxy()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to set system proxy: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> {
|
||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
#[allow(unused_imports)] // creation_flags必须
|
#[allow(unused_imports)] // creation_flags必须
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
@@ -546,37 +564,22 @@ impl EventDrivenProxyManager {
|
|||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "app", "获取服务路径失败: {e}");
|
log::error!(target: "app", "获取服务路径失败: {e}");
|
||||||
return;
|
return Err(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||||
if !sysproxy_exe.exists() {
|
if !sysproxy_exe.exists() {
|
||||||
log::error!(target: "app", "sysproxy.exe 不存在");
|
log::error!(target: "app", "sysproxy.exe 不存在");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist");
|
||||||
|
|
||||||
let output = Command::new(sysproxy_exe)
|
let _output = Command::new(sysproxy_exe)
|
||||||
.args(args)
|
.args(args)
|
||||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
|
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
|
||||||
.output()
|
.output()
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
match output {
|
Ok(())
|
||||||
Ok(output) => {
|
|
||||||
if !output.status.success() {
|
|
||||||
log::error!(target: "app", "执行sysproxy命令失败: {args:?}");
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
log::error!(target: "app", "sysproxy错误输出: {stderr}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::debug!(target: "app", "成功执行sysproxy命令: {args:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "app", "执行sysproxy命令出错: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+171
-132
@@ -1,9 +1,10 @@
|
|||||||
use once_cell::sync::OnceCell;
|
use crate::singleton;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::{
|
use std::{
|
||||||
sync::{
|
sync::{
|
||||||
|
Arc,
|
||||||
atomic::{AtomicU64, Ordering},
|
atomic::{AtomicU64, Ordering},
|
||||||
mpsc, Arc,
|
mpsc,
|
||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
@@ -20,7 +21,6 @@ enum FrontendEvent {
|
|||||||
NoticeMessage { status: String, message: String },
|
NoticeMessage { status: String, message: String },
|
||||||
ProfileChanged { current_profile_id: String },
|
ProfileChanged { current_profile_id: String },
|
||||||
TimerUpdated { profile_index: String },
|
TimerUpdated { profile_index: String },
|
||||||
StartupCompleted,
|
|
||||||
ProfileUpdateStarted { uid: String },
|
ProfileUpdateStarted { uid: String },
|
||||||
ProfileUpdateCompleted { uid: String },
|
ProfileUpdateCompleted { uid: String },
|
||||||
}
|
}
|
||||||
@@ -82,132 +82,128 @@ impl NotificationSystem {
|
|||||||
|
|
||||||
*self.last_emit_time.write() = Instant::now();
|
*self.last_emit_time.write() = Instant::now();
|
||||||
|
|
||||||
self.worker_handle = Some(
|
match thread::Builder::new()
|
||||||
thread::Builder::new()
|
.name("frontend-notifier".into())
|
||||||
.name("frontend-notifier".into())
|
.spawn(move || {
|
||||||
.spawn(move || {
|
let handle = Handle::global();
|
||||||
let handle = Handle::global();
|
|
||||||
|
|
||||||
while !handle.is_exiting() {
|
while !handle.is_exiting() {
|
||||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
let system_guard = handle.notification_system.read();
|
let system_guard = handle.notification_system.read();
|
||||||
if system_guard.as_ref().is_none() {
|
let Some(system) = system_guard.as_ref() else {
|
||||||
log::warn!("NotificationSystem not found in handle while processing event.");
|
log::warn!("NotificationSystem not found in handle while processing event.");
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
let system = system_guard.as_ref().unwrap();
|
|
||||||
|
|
||||||
let is_emergency = *system.emergency_mode.read();
|
let is_emergency = *system.emergency_mode.read();
|
||||||
|
|
||||||
if is_emergency {
|
if is_emergency
|
||||||
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
&& let FrontendEvent::NoticeMessage { ref status, .. } = event
|
||||||
if status == "info" {
|
&& status == "info" {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Emergency mode active, skipping info message"
|
"Emergency mode active, skipping info message"
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = handle.get_window() {
|
||||||
|
*system.last_emit_time.write() = Instant::now();
|
||||||
|
|
||||||
|
let (event_name_str, payload_result) = match event {
|
||||||
|
FrontendEvent::RefreshClash => {
|
||||||
|
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
|
||||||
|
}
|
||||||
|
FrontendEvent::RefreshVerge => {
|
||||||
|
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
|
||||||
|
}
|
||||||
|
FrontendEvent::NoticeMessage { status, message } => {
|
||||||
|
match serde_json::to_value((status, message)) {
|
||||||
|
Ok(p) => ("verge://notice-message", Ok(p)),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to serialize NoticeMessage payload: {e}");
|
||||||
|
("verge://notice-message", Err(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
FrontendEvent::ProfileChanged { current_profile_id } => {
|
||||||
|
("profile-changed", Ok(serde_json::json!(current_profile_id)))
|
||||||
|
}
|
||||||
|
FrontendEvent::TimerUpdated { profile_index } => {
|
||||||
|
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
|
||||||
|
}
|
||||||
|
FrontendEvent::ProfileUpdateStarted { uid } => {
|
||||||
|
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
|
||||||
|
}
|
||||||
|
FrontendEvent::ProfileUpdateCompleted { uid } => {
|
||||||
|
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(window) = handle.get_window() {
|
if let Ok(payload) = payload_result {
|
||||||
*system.last_emit_time.write() = Instant::now();
|
match window.emit(event_name_str, payload) {
|
||||||
|
Ok(_) => {
|
||||||
let (event_name_str, payload_result) = match event {
|
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||||
FrontendEvent::RefreshClash => {
|
// 记录成功发送的事件
|
||||||
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
|
if log::log_enabled!(log::Level::Debug) {
|
||||||
}
|
log::debug!("Successfully emitted event: {event_name_str}");
|
||||||
FrontendEvent::RefreshVerge => {
|
|
||||||
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
|
|
||||||
}
|
|
||||||
FrontendEvent::NoticeMessage { status, message } => {
|
|
||||||
match serde_json::to_value((status, message)) {
|
|
||||||
Ok(p) => ("verge://notice-message", Ok(p)),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to serialize NoticeMessage payload: {e}");
|
|
||||||
("verge://notice-message", Err(e))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FrontendEvent::ProfileChanged { current_profile_id } => {
|
Err(e) => {
|
||||||
("profile-changed", Ok(serde_json::json!(current_profile_id)))
|
log::warn!("Failed to emit event {event_name_str}: {e}");
|
||||||
}
|
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
FrontendEvent::TimerUpdated { profile_index } => {
|
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||||
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
|
|
||||||
}
|
|
||||||
FrontendEvent::StartupCompleted => {
|
|
||||||
("verge://startup-completed", Ok(serde_json::json!(null)))
|
|
||||||
}
|
|
||||||
FrontendEvent::ProfileUpdateStarted { uid } => {
|
|
||||||
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
|
|
||||||
}
|
|
||||||
FrontendEvent::ProfileUpdateCompleted { uid } => {
|
|
||||||
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(payload) = payload_result {
|
let errors = system.stats.total_errors.load(Ordering::SeqCst);
|
||||||
match window.emit(event_name_str, payload) {
|
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
||||||
Ok(_) => {
|
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
||||||
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
log::warn!(
|
||||||
// 记录成功发送的事件
|
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
|
||||||
if log::log_enabled!(log::Level::Debug) {
|
);
|
||||||
log::debug!("Successfully emitted event: {event_name_str}");
|
*system.emergency_mode.write() = true;
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to emit event {event_name_str}: {e}");
|
|
||||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
|
||||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
|
||||||
|
|
||||||
let errors = system.stats.total_errors.load(Ordering::SeqCst);
|
|
||||||
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
|
||||||
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
|
||||||
log::warn!(
|
|
||||||
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
|
|
||||||
);
|
|
||||||
*system.emergency_mode.write() = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
|
||||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
|
||||||
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("No window found, skipping event emit.");
|
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||||
|
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||||
|
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(20));
|
} else {
|
||||||
}
|
log::warn!("No window found, skipping event emit.");
|
||||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
|
||||||
log::info!(
|
|
||||||
"Notification channel disconnected, exiting worker thread"
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||||
|
log::info!(
|
||||||
|
"Notification channel disconnected, exiting worker thread"
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log::info!("Notification worker thread exiting");
|
log::info!("Notification worker thread exiting");
|
||||||
})
|
}) {
|
||||||
.expect("Failed to start notification worker thread"),
|
Ok(handle) => {
|
||||||
);
|
self.worker_handle = Some(handle);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to start notification worker thread: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发送事件到队列
|
/// 发送事件到队列
|
||||||
fn send_event(&self, event: FrontendEvent) -> bool {
|
fn send_event(&self, event: FrontendEvent) -> bool {
|
||||||
if *self.emergency_mode.read() {
|
if *self.emergency_mode.read()
|
||||||
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
&& let FrontendEvent::NoticeMessage { ref status, .. } = event
|
||||||
if status == "info" {
|
&& status == "info"
|
||||||
log::info!("Skipping info message in emergency mode");
|
{
|
||||||
return false;
|
log::info!("Skipping info message in emergency mode");
|
||||||
}
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(sender) = &self.sender {
|
if let Some(sender) = &self.sender {
|
||||||
@@ -272,16 +268,18 @@ impl Default for Handle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use singleton macro
|
||||||
|
singleton!(Handle, HANDLE);
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
pub fn global() -> &'static Handle {
|
pub fn new() -> Self {
|
||||||
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
Self::default()
|
||||||
HANDLE.get_or_init(Handle::default)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self, app_handle: &AppHandle) {
|
pub fn init(&self, app_handle: AppHandle) {
|
||||||
{
|
{
|
||||||
let mut handle = self.app_handle.write();
|
let mut handle = self.app_handle.write();
|
||||||
*handle = Some(app_handle.clone());
|
*handle = Some(app_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut system_opt = self.notification_system.write();
|
let mut system_opt = self.notification_system.write();
|
||||||
@@ -290,6 +288,7 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取 AppHandle
|
||||||
pub fn app_handle(&self) -> Option<AppHandle> {
|
pub fn app_handle(&self) -> Option<AppHandle> {
|
||||||
self.app_handle.read().clone()
|
self.app_handle.read().clone()
|
||||||
}
|
}
|
||||||
@@ -361,22 +360,6 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notify_startup_completed() {
|
|
||||||
let handle = Self::global();
|
|
||||||
if handle.is_exiting() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let system_opt = handle.notification_system.read();
|
|
||||||
if let Some(system) = system_opt.as_ref() {
|
|
||||||
system.send_event(FrontendEvent::StartupCompleted);
|
|
||||||
} else {
|
|
||||||
log::warn!(
|
|
||||||
"Notification system not initialized when trying to send StartupCompleted event."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notify_profile_update_started(uid: String) {
|
pub fn notify_profile_update_started(uid: String) {
|
||||||
let handle = Self::global();
|
let handle = Self::global();
|
||||||
if handle.is_exiting() {
|
if handle.is_exiting() {
|
||||||
@@ -387,7 +370,9 @@ impl Handle {
|
|||||||
if let Some(system) = system_opt.as_ref() {
|
if let Some(system) = system_opt.as_ref() {
|
||||||
system.send_event(FrontendEvent::ProfileUpdateStarted { uid });
|
system.send_event(FrontendEvent::ProfileUpdateStarted { uid });
|
||||||
} else {
|
} else {
|
||||||
log::warn!("Notification system not initialized when trying to send ProfileUpdateStarted event.");
|
log::warn!(
|
||||||
|
"Notification system not initialized when trying to send ProfileUpdateStarted event."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +386,9 @@ impl Handle {
|
|||||||
if let Some(system) = system_opt.as_ref() {
|
if let Some(system) = system_opt.as_ref() {
|
||||||
system.send_event(FrontendEvent::ProfileUpdateCompleted { uid });
|
system.send_event(FrontendEvent::ProfileUpdateCompleted { uid });
|
||||||
} else {
|
} else {
|
||||||
log::warn!("Notification system not initialized when trying to send ProfileUpdateCompleted event.");
|
log::warn!(
|
||||||
|
"Notification system not initialized when trying to send ProfileUpdateCompleted event."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,8 +453,9 @@ impl Handle {
|
|||||||
info,
|
info,
|
||||||
Type::Frontend,
|
Type::Frontend,
|
||||||
true,
|
true,
|
||||||
"发送{}条启动时累积的错误消息",
|
"发送{}条启动时累积的错误消息: {:?}",
|
||||||
errors.len()
|
errors.len(),
|
||||||
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
// 启动单独线程处理启动错误,避免阻塞主线程
|
// 启动单独线程处理启动错误,避免阻塞主线程
|
||||||
@@ -517,3 +505,54 @@ impl Handle {
|
|||||||
*self.is_exiting.read()
|
*self.is_exiting.read()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
impl Handle {
|
||||||
|
pub fn set_activation_policy(&self, policy: tauri::ActivationPolicy) -> Result<(), String> {
|
||||||
|
let app_handle = self.app_handle();
|
||||||
|
if let Some(app_handle) = app_handle.as_ref() {
|
||||||
|
app_handle
|
||||||
|
.set_activation_policy(policy)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
Err("AppHandle not initialized".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_activation_policy_regular(&self) {
|
||||||
|
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Regular) {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"Failed to set regular activation policy: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_activation_policy_accessory(&self) {
|
||||||
|
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Accessory) {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"Failed to set accessory activation policy: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_activation_policy_prohibited(&self) {
|
||||||
|
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Prohibited) {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"Failed to set prohibited activation policy: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+315
-193
@@ -1,30 +1,296 @@
|
|||||||
use crate::utils::notification::{notify_event, NotificationEvent};
|
use crate::process::AsyncHandler;
|
||||||
|
use crate::utils::notification::{NotificationEvent, notify_event};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config, core::handle, feat, logging, logging_error,
|
config::Config, core::handle, feat, logging, logging_error,
|
||||||
module::lightweight::entry_lightweight_mode, utils::logging::Type,
|
module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result, bail};
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
|
||||||
use tauri::Manager;
|
use tauri::{AppHandle, Manager};
|
||||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||||
|
|
||||||
|
/// Enum representing all available hotkey functions
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum HotkeyFunction {
|
||||||
|
OpenOrCloseDashboard,
|
||||||
|
ClashModeRule,
|
||||||
|
ClashModeGlobal,
|
||||||
|
ClashModeDirect,
|
||||||
|
ToggleSystemProxy,
|
||||||
|
ToggleTunMode,
|
||||||
|
EntryLightweightMode,
|
||||||
|
Quit,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Hide,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for HotkeyFunction {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard",
|
||||||
|
HotkeyFunction::ClashModeRule => "clash_mode_rule",
|
||||||
|
HotkeyFunction::ClashModeGlobal => "clash_mode_global",
|
||||||
|
HotkeyFunction::ClashModeDirect => "clash_mode_direct",
|
||||||
|
HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy",
|
||||||
|
HotkeyFunction::ToggleTunMode => "toggle_tun_mode",
|
||||||
|
HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode",
|
||||||
|
HotkeyFunction::Quit => "quit",
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
HotkeyFunction::Hide => "hide",
|
||||||
|
};
|
||||||
|
write!(f, "{s}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for HotkeyFunction {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.trim() {
|
||||||
|
"open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard),
|
||||||
|
"clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule),
|
||||||
|
"clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal),
|
||||||
|
"clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect),
|
||||||
|
"toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy),
|
||||||
|
"toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode),
|
||||||
|
"entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode),
|
||||||
|
"quit" => Ok(HotkeyFunction::Quit),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"hide" => Ok(HotkeyFunction::Hide),
|
||||||
|
_ => bail!("invalid hotkey function: {}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
/// Enum representing predefined system hotkeys
|
||||||
|
pub enum SystemHotkey {
|
||||||
|
CmdQ,
|
||||||
|
CmdW,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
impl fmt::Display for SystemHotkey {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
SystemHotkey::CmdQ => "CMD+Q",
|
||||||
|
SystemHotkey::CmdW => "CMD+W",
|
||||||
|
};
|
||||||
|
write!(f, "{s}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
impl SystemHotkey {
|
||||||
|
pub fn function(self) -> HotkeyFunction {
|
||||||
|
match self {
|
||||||
|
SystemHotkey::CmdQ => HotkeyFunction::Quit,
|
||||||
|
SystemHotkey::CmdW => HotkeyFunction::Hide,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Hotkey {
|
pub struct Hotkey {
|
||||||
current: Arc<Mutex<Vec<String>>>,
|
current: Arc<Mutex<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hotkey {
|
impl Hotkey {
|
||||||
pub fn global() -> &'static Hotkey {
|
fn new() -> Self {
|
||||||
static HOTKEY: OnceCell<Hotkey> = OnceCell::new();
|
Self {
|
||||||
|
|
||||||
HOTKEY.get_or_init(|| Hotkey {
|
|
||||||
current: Arc::new(Mutex::new(Vec::new())),
|
current: Arc::new(Mutex::new(Vec::new())),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self) -> Result<()> {
|
/// Execute the function associated with a hotkey function enum
|
||||||
let verge = Config::verge();
|
fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) {
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
match function {
|
||||||
|
HotkeyFunction::OpenOrCloseDashboard => {
|
||||||
|
AsyncHandler::spawn(async move || {
|
||||||
|
crate::feat::open_or_close_dashboard().await;
|
||||||
|
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HotkeyFunction::ClashModeRule => {
|
||||||
|
AsyncHandler::spawn(async move || {
|
||||||
|
feat::change_clash_mode("rule".into()).await;
|
||||||
|
notify_event(
|
||||||
|
app_handle,
|
||||||
|
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HotkeyFunction::ClashModeGlobal => {
|
||||||
|
AsyncHandler::spawn(async move || {
|
||||||
|
feat::change_clash_mode("global".into()).await;
|
||||||
|
notify_event(
|
||||||
|
app_handle,
|
||||||
|
NotificationEvent::ClashModeChanged { mode: "Global" },
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HotkeyFunction::ClashModeDirect => {
|
||||||
|
AsyncHandler::spawn(async move || {
|
||||||
|
feat::change_clash_mode("direct".into()).await;
|
||||||
|
notify_event(
|
||||||
|
app_handle,
|
||||||
|
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HotkeyFunction::ToggleSystemProxy => {
|
||||||
|
AsyncHandler::spawn(async move || {
|
||||||
|
feat::toggle_system_proxy().await;
|
||||||
|
notify_event(app_handle, NotificationEvent::SystemProxyToggled).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HotkeyFunction::ToggleTunMode => {
|
||||||
|
AsyncHandler::spawn(async move || {
|
||||||
|
feat::toggle_tun_mode(None).await;
|
||||||
|
notify_event(app_handle, NotificationEvent::TunModeToggled).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HotkeyFunction::EntryLightweightMode => {
|
||||||
|
AsyncHandler::spawn(async move || {
|
||||||
|
entry_lightweight_mode().await;
|
||||||
|
notify_event(app_handle, NotificationEvent::LightweightModeEntered).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HotkeyFunction::Quit => {
|
||||||
|
AsyncHandler::spawn(async move || {
|
||||||
|
notify_event(app_handle, NotificationEvent::AppQuit).await;
|
||||||
|
feat::quit().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
HotkeyFunction::Hide => {
|
||||||
|
AsyncHandler::spawn(async move || {
|
||||||
|
feat::hide().await;
|
||||||
|
notify_event(app_handle, NotificationEvent::AppHidden).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
/// Register a system hotkey using enum
|
||||||
|
pub async fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
|
||||||
|
let hotkey_str = hotkey.to_string();
|
||||||
|
let function = hotkey.function();
|
||||||
|
self.register_hotkey_with_function(&hotkey_str, function)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
/// Unregister a system hotkey using enum
|
||||||
|
pub fn unregister_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
|
||||||
|
let hotkey_str = hotkey.to_string();
|
||||||
|
self.unregister(&hotkey_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a hotkey with function enum
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
pub async fn register_hotkey_with_function(
|
||||||
|
&self,
|
||||||
|
hotkey: &str,
|
||||||
|
function: HotkeyFunction,
|
||||||
|
) -> Result<()> {
|
||||||
|
let app_handle = handle::Handle::global()
|
||||||
|
.app_handle()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
|
||||||
|
let manager = app_handle.global_shortcut();
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Attempting to register hotkey: {} for function: {}",
|
||||||
|
hotkey,
|
||||||
|
function
|
||||||
|
);
|
||||||
|
|
||||||
|
if manager.is_registered(hotkey) {
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Hotkey {} was already registered, unregistering first",
|
||||||
|
hotkey
|
||||||
|
);
|
||||||
|
manager.unregister(hotkey)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_quit = matches!(function, HotkeyFunction::Quit);
|
||||||
|
|
||||||
|
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
|
||||||
|
let hotkey_event_owned = *hotkey_event;
|
||||||
|
let event_owned = event;
|
||||||
|
let function_owned = function;
|
||||||
|
let is_quit_owned = is_quit;
|
||||||
|
|
||||||
|
let app_handle_cloned = app_handle.clone();
|
||||||
|
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
if event_owned.state == ShortcutState::Pressed {
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Hotkey pressed: {:?}",
|
||||||
|
hotkey_event_owned
|
||||||
|
);
|
||||||
|
|
||||||
|
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
|
||||||
|
if let Some(window) = app_handle_cloned.get_webview_window("main")
|
||||||
|
&& window.is_focused().unwrap_or(false)
|
||||||
|
{
|
||||||
|
logging!(debug, Type::Hotkey, "Executing quit function");
|
||||||
|
Self::execute_function(function_owned, &app_handle_cloned);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||||
|
|
||||||
|
let is_enable_global_hotkey = Config::verge()
|
||||||
|
.await
|
||||||
|
.latest_ref()
|
||||||
|
.enable_global_hotkey
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if is_enable_global_hotkey {
|
||||||
|
Self::execute_function(function_owned, &app_handle_cloned);
|
||||||
|
} else {
|
||||||
|
use crate::utils::window_manager::WindowManager;
|
||||||
|
let is_visible = WindowManager::is_main_window_visible();
|
||||||
|
let is_focused = WindowManager::is_main_window_focused();
|
||||||
|
|
||||||
|
if is_focused && is_visible {
|
||||||
|
Self::execute_function(function_owned, &app_handle_cloned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
debug,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Successfully registered hotkey {} for {}",
|
||||||
|
hotkey,
|
||||||
|
function
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use unified singleton macro
|
||||||
|
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
|
||||||
|
|
||||||
|
impl Hotkey {
|
||||||
|
pub async fn init(&self) -> Result<()> {
|
||||||
|
let verge = Config::verge().await;
|
||||||
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
|
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
|
||||||
|
|
||||||
logging!(
|
logging!(
|
||||||
@@ -39,7 +305,10 @@ impl Hotkey {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(hotkeys) = verge.latest_ref().hotkeys.as_ref() {
|
// Extract hotkeys data before async operations
|
||||||
|
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
|
||||||
|
|
||||||
|
if let Some(hotkeys) = hotkeys {
|
||||||
logging!(
|
logging!(
|
||||||
debug,
|
debug,
|
||||||
Type::Hotkey,
|
Type::Hotkey,
|
||||||
@@ -63,7 +332,7 @@ impl Hotkey {
|
|||||||
key,
|
key,
|
||||||
func
|
func
|
||||||
);
|
);
|
||||||
if let Err(e) = self.register(key, func) {
|
if let Err(e) = self.register(key, func).await {
|
||||||
logging!(
|
logging!(
|
||||||
error,
|
error,
|
||||||
Type::Hotkey,
|
Type::Hotkey,
|
||||||
@@ -97,7 +366,7 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.current.lock().clone_from(hotkeys);
|
self.current.lock().clone_from(&hotkeys);
|
||||||
} else {
|
} else {
|
||||||
logging!(debug, Type::Hotkey, "No hotkeys configured");
|
logging!(debug, Type::Hotkey, "No hotkeys configured");
|
||||||
}
|
}
|
||||||
@@ -106,192 +375,34 @@ impl Hotkey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&self) -> Result<()> {
|
pub fn reset(&self) -> Result<()> {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global()
|
||||||
|
.app_handle()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
manager.unregister_all()?;
|
manager.unregister_all()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
/// Register a hotkey with string-based function (backward compatibility)
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
pub async fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||||
let manager = app_handle.global_shortcut();
|
let function = HotkeyFunction::from_str(func)?;
|
||||||
|
self.register_hotkey_with_function(hotkey, function).await
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Hotkey,
|
|
||||||
"Attempting to register hotkey: {} for function: {}",
|
|
||||||
hotkey,
|
|
||||||
func
|
|
||||||
);
|
|
||||||
|
|
||||||
if manager.is_registered(hotkey) {
|
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Hotkey,
|
|
||||||
"Hotkey {} was already registered, unregistering first",
|
|
||||||
hotkey
|
|
||||||
);
|
|
||||||
manager.unregister(hotkey)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let app_handle_clone = app_handle.clone();
|
|
||||||
let f: Box<dyn Fn() + Send + Sync> = match func.trim() {
|
|
||||||
"open_or_close_dashboard" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"=== Hotkey Dashboard Window Operation Start ==="
|
|
||||||
);
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
"Using unified WindowManager for hotkey operation (bypass debounce)"
|
|
||||||
);
|
|
||||||
|
|
||||||
crate::feat::open_or_close_dashboard_hotkey();
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Hotkey,
|
|
||||||
"=== Hotkey Dashboard Window Operation End ==="
|
|
||||||
);
|
|
||||||
notify_event(&app_handle, NotificationEvent::DashboardToggled);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"clash_mode_rule" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::change_clash_mode("rule".into());
|
|
||||||
notify_event(
|
|
||||||
&app_handle,
|
|
||||||
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"clash_mode_global" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::change_clash_mode("global".into());
|
|
||||||
notify_event(
|
|
||||||
&app_handle,
|
|
||||||
NotificationEvent::ClashModeChanged { mode: "Global" },
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"clash_mode_direct" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::change_clash_mode("direct".into());
|
|
||||||
notify_event(
|
|
||||||
&app_handle,
|
|
||||||
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"toggle_system_proxy" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::toggle_system_proxy();
|
|
||||||
notify_event(&app_handle, NotificationEvent::SystemProxyToggled);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"toggle_tun_mode" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::toggle_tun_mode(None);
|
|
||||||
notify_event(&app_handle, NotificationEvent::TunModeToggled);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"entry_lightweight_mode" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
entry_lightweight_mode();
|
|
||||||
notify_event(&app_handle, NotificationEvent::LightweightModeEntered);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"quit" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::quit();
|
|
||||||
notify_event(&app_handle, NotificationEvent::AppQuit);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"hide" => {
|
|
||||||
let app_handle = app_handle_clone.clone();
|
|
||||||
Box::new(move || {
|
|
||||||
feat::hide();
|
|
||||||
notify_event(&app_handle, NotificationEvent::AppHidden);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
|
||||||
bail!("invalid function \"{func}\"");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_quit = func.trim() == "quit";
|
|
||||||
|
|
||||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
|
||||||
if event.state == ShortcutState::Pressed {
|
|
||||||
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey);
|
|
||||||
|
|
||||||
if hotkey.key == Code::KeyQ && is_quit {
|
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
|
||||||
if window.is_focused().unwrap_or(false) {
|
|
||||||
logging!(debug, Type::Hotkey, "Executing quit function");
|
|
||||||
f();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
|
||||||
|
|
||||||
let is_enable_global_hotkey = Config::verge()
|
|
||||||
.latest_ref()
|
|
||||||
.enable_global_hotkey
|
|
||||||
.unwrap_or(true);
|
|
||||||
|
|
||||||
if is_enable_global_hotkey {
|
|
||||||
f();
|
|
||||||
} else {
|
|
||||||
use crate::utils::window_manager::WindowManager;
|
|
||||||
let is_visible = WindowManager::is_main_window_visible();
|
|
||||||
let is_focused = WindowManager::is_main_window_focused();
|
|
||||||
|
|
||||||
if is_focused && is_visible {
|
|
||||||
f();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
debug,
|
|
||||||
Type::Hotkey,
|
|
||||||
"Successfully registered hotkey {} for {}",
|
|
||||||
hotkey,
|
|
||||||
func
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unregister(&self, hotkey: &str) -> Result<()> {
|
pub fn unregister(&self, hotkey: &str) -> Result<()> {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global()
|
||||||
|
.app_handle()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
|
||||||
let manager = app_handle.global_shortcut();
|
let manager = app_handle.global_shortcut();
|
||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey);
|
logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
||||||
let mut current = self.current.lock();
|
// Extract current hotkeys before async operations
|
||||||
let old_map = Self::get_map_from_vec(¤t);
|
let current_hotkeys = self.current.lock().clone();
|
||||||
|
let old_map = Self::get_map_from_vec(¤t_hotkeys);
|
||||||
let new_map = Self::get_map_from_vec(&new_hotkeys);
|
let new_map = Self::get_map_from_vec(&new_hotkeys);
|
||||||
|
|
||||||
let (del, add) = Self::get_diff(old_map, new_map);
|
let (del, add) = Self::get_diff(old_map, new_map);
|
||||||
@@ -300,11 +411,12 @@ impl Hotkey {
|
|||||||
let _ = self.unregister(key);
|
let _ = self.unregister(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
add.iter().for_each(|(key, func)| {
|
for (key, func) in add.iter() {
|
||||||
logging_error!(Type::Hotkey, self.register(key, func));
|
logging_error!(Type::Hotkey, self.register(key, func).await);
|
||||||
});
|
}
|
||||||
|
|
||||||
*current = new_hotkeys;
|
// Update the current hotkeys after all async operations
|
||||||
|
*self.current.lock() = new_hotkeys;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +468,17 @@ impl Hotkey {
|
|||||||
|
|
||||||
impl Drop for Hotkey {
|
impl Drop for Hotkey {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = match handle::Handle::global().app_handle() {
|
||||||
|
Some(handle) => handle,
|
||||||
|
None => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
"Failed to get app handle during hotkey cleanup"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||||
logging!(
|
logging!(
|
||||||
error,
|
error,
|
||||||
|
|||||||
+220
-820
File diff suppressed because it is too large
Load Diff
+186
-177
@@ -1,9 +1,15 @@
|
|||||||
use crate::{logging, utils::logging::Type};
|
use crate::{logging, utils::logging::Type};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
|
use backoff::{Error as BackoffError, ExponentialBackoff};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use tokio::net::UnixStream;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use tokio::net::windows::named_pipe::ClientOptions;
|
||||||
|
|
||||||
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
|
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
|
||||||
r"\\.\pipe\clash-verge-service"
|
r"\\.\pipe\clash-verge-service"
|
||||||
@@ -110,170 +116,134 @@ pub fn verify_response_signature(response: &IpcResponse) -> Result<bool> {
|
|||||||
Ok(expected_signature == response.signature)
|
Ok(expected_signature == response.signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPC连接管理-win
|
fn create_backoff_strategy() -> ExponentialBackoff {
|
||||||
#[cfg(target_os = "windows")]
|
ExponentialBackoff {
|
||||||
|
initial_interval: Duration::from_millis(50),
|
||||||
|
max_interval: Duration::from_secs(1),
|
||||||
|
max_elapsed_time: Some(Duration::from_secs(3)),
|
||||||
|
multiplier: 1.5,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_ipc_request(
|
pub async fn send_ipc_request(
|
||||||
command: IpcCommand,
|
command: IpcCommand,
|
||||||
payload: serde_json::Value,
|
payload: serde_json::Value,
|
||||||
) -> Result<IpcResponse> {
|
) -> Result<IpcResponse> {
|
||||||
use std::{
|
|
||||||
ffi::CString,
|
|
||||||
fs::File,
|
|
||||||
io::{Read, Write},
|
|
||||||
os::windows::io::{FromRawHandle, RawHandle},
|
|
||||||
ptr,
|
|
||||||
};
|
|
||||||
use winapi::um::{
|
|
||||||
fileapi::{CreateFileA, OPEN_EXISTING},
|
|
||||||
handleapi::INVALID_HANDLE_VALUE,
|
|
||||||
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
|
|
||||||
};
|
|
||||||
|
|
||||||
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
|
|
||||||
|
|
||||||
let command_type = format!("{command:?}");
|
let command_type = format!("{command:?}");
|
||||||
|
|
||||||
let request = match create_signed_request(command, payload) {
|
let operation = || async {
|
||||||
Ok(req) => req,
|
match send_ipc_request_internal(command.clone(), payload.clone()).await {
|
||||||
Err(e) => {
|
Ok(response) => Ok(response),
|
||||||
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
Err(e) => {
|
||||||
return Err(e);
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Service,
|
||||||
|
true,
|
||||||
|
"IPC请求失败,准备重试: 命令={}, 错误={}",
|
||||||
|
command_type,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
Err(BackoffError::transient(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let request_json = serde_json::to_string(&request)?;
|
match backoff::future::retry(create_backoff_strategy(), operation).await {
|
||||||
|
Ok(response) => {
|
||||||
let result = tokio::task::spawn_blocking(move || -> Result<IpcResponse> {
|
// logging!(
|
||||||
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
|
// info,
|
||||||
Ok(name) => name,
|
// Type::Service,
|
||||||
Err(e) => {
|
// true,
|
||||||
logging!(error, Type::Service, true, "创建CString失败: {}", e);
|
// "IPC请求成功: 命令={}, 成功={}",
|
||||||
return Err(anyhow::anyhow!("创建CString失败: {}", e));
|
// command_type,
|
||||||
}
|
// response.success
|
||||||
};
|
// );
|
||||||
|
Ok(response)
|
||||||
let handle = unsafe {
|
}
|
||||||
CreateFileA(
|
Err(e) => {
|
||||||
c_pipe_name.as_ptr(),
|
|
||||||
GENERIC_READ | GENERIC_WRITE,
|
|
||||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
||||||
ptr::null_mut(),
|
|
||||||
OPEN_EXISTING,
|
|
||||||
0,
|
|
||||||
ptr::null_mut(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if handle == INVALID_HANDLE_VALUE {
|
|
||||||
let error = std::io::Error::last_os_error();
|
|
||||||
logging!(
|
logging!(
|
||||||
error,
|
error,
|
||||||
Type::Service,
|
Type::Service,
|
||||||
true,
|
true,
|
||||||
"连接到服务命名管道失败: {}",
|
"IPC请求最终失败,重试已耗尽: 命令={}, 错误={}",
|
||||||
error
|
command_type,
|
||||||
|
e
|
||||||
);
|
);
|
||||||
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error));
|
Err(anyhow::anyhow!("IPC请求重试失败: {}", e))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) };
|
// 内部IPC请求实现(不带重试)
|
||||||
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
|
async fn send_ipc_request_internal(
|
||||||
|
command: IpcCommand,
|
||||||
|
payload: serde_json::Value,
|
||||||
|
) -> Result<IpcResponse> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
send_ipc_request_windows(command, payload).await
|
||||||
|
}
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
{
|
||||||
|
send_ipc_request_unix(command, payload).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let request_bytes = request_json.as_bytes();
|
// IPC连接管理-win
|
||||||
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
#[cfg(target_os = "windows")]
|
||||||
|
async fn send_ipc_request_windows(
|
||||||
|
command: IpcCommand,
|
||||||
|
payload: serde_json::Value,
|
||||||
|
) -> Result<IpcResponse> {
|
||||||
|
let request = create_signed_request(command, payload)?;
|
||||||
|
let request_json = serde_json::to_string(&request)?;
|
||||||
|
let request_bytes = request_json.as_bytes();
|
||||||
|
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
||||||
|
|
||||||
if let Err(e) = pipe.write_all(&len_bytes) {
|
let mut pipe = match ClientOptions::new().open(IPC_SOCKET_NAME) {
|
||||||
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
Ok(p) => p,
|
||||||
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
Err(e) => {
|
||||||
|
logging!(error, Type::Service, true, "连接到服务命名管道失败: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", e));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Err(e) = pipe.write_all(request_bytes) {
|
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
|
||||||
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = pipe.flush() {
|
pipe.write_all(&len_bytes).await?;
|
||||||
logging!(error, Type::Service, true, "刷新管道失败: {}", e);
|
pipe.write_all(request_bytes).await?;
|
||||||
return Err(anyhow::anyhow!("刷新管道失败: {}", e));
|
pipe.flush().await?;
|
||||||
}
|
|
||||||
|
|
||||||
let mut response_len_bytes = [0u8; 4];
|
let mut response_len_bytes = [0u8; 4];
|
||||||
if let Err(e) = pipe.read_exact(&mut response_len_bytes) {
|
pipe.read_exact(&mut response_len_bytes).await?;
|
||||||
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
||||||
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
let mut response_bytes = vec![0u8; response_len];
|
||||||
|
pipe.read_exact(&mut response_bytes).await?;
|
||||||
|
|
||||||
let mut response_bytes = vec![0u8; response_len];
|
let response: IpcResponse = serde_json::from_slice(&response_bytes)
|
||||||
if let Err(e) = pipe.read_exact(&mut response_bytes) {
|
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
|
||||||
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
|
if !verify_response_signature(&response)? {
|
||||||
Ok(r) => r,
|
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
||||||
Err(e) => {
|
bail!("服务响应签名验证失败");
|
||||||
logging!(error, Type::Service, true, "服务响应解析失败: {}", e);
|
}
|
||||||
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match verify_response_signature(&response) {
|
Ok(response)
|
||||||
Ok(valid) => {
|
|
||||||
if !valid {
|
|
||||||
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
|
||||||
bail!("服务响应签名验证失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Service,
|
|
||||||
true,
|
|
||||||
"IPC请求完成: 命令={}, 成功={}",
|
|
||||||
command_type,
|
|
||||||
response.success
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPC连接管理-unix
|
// IPC连接管理-unix
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub async fn send_ipc_request(
|
async fn send_ipc_request_unix(
|
||||||
command: IpcCommand,
|
command: IpcCommand,
|
||||||
payload: serde_json::Value,
|
payload: serde_json::Value,
|
||||||
) -> Result<IpcResponse> {
|
) -> Result<IpcResponse> {
|
||||||
use std::os::unix::net::UnixStream;
|
let request = create_signed_request(command, payload)?;
|
||||||
|
|
||||||
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
|
|
||||||
|
|
||||||
let command_type = format!("{command:?}");
|
|
||||||
|
|
||||||
let request = match create_signed_request(command, payload) {
|
|
||||||
Ok(req) => req,
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let request_json = serde_json::to_string(&request)?;
|
let request_json = serde_json::to_string(&request)?;
|
||||||
|
|
||||||
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) {
|
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME).await {
|
||||||
Ok(s) => {
|
Ok(s) => s,
|
||||||
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
|
|
||||||
s
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
|
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
|
||||||
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
|
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
|
||||||
@@ -283,58 +253,97 @@ pub async fn send_ipc_request(
|
|||||||
let request_bytes = request_json.as_bytes();
|
let request_bytes = request_json.as_bytes();
|
||||||
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
|
||||||
|
|
||||||
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) {
|
stream.write_all(&len_bytes).await?;
|
||||||
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
|
stream.write_all(request_bytes).await?;
|
||||||
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
|
stream.flush().await?;
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
|
|
||||||
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 读取响应长度
|
||||||
let mut response_len_bytes = [0u8; 4];
|
let mut response_len_bytes = [0u8; 4];
|
||||||
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) {
|
stream.read_exact(&mut response_len_bytes).await?;
|
||||||
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
|
||||||
|
|
||||||
let mut response_bytes = vec![0u8; response_len];
|
let mut response_bytes = vec![0u8; response_len];
|
||||||
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) {
|
stream.read_exact(&mut response_bytes).await?;
|
||||||
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
|
|
||||||
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
|
let response: IpcResponse = serde_json::from_slice(&response_bytes)
|
||||||
|
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
|
||||||
|
|
||||||
|
if !verify_response_signature(&response)? {
|
||||||
|
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
||||||
|
bail!("服务响应签名验证失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,);
|
|
||||||
return Err(anyhow::anyhow!("解析响应失败: {}", e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match verify_response_signature(&response) {
|
|
||||||
Ok(valid) => {
|
|
||||||
if !valid {
|
|
||||||
logging!(error, Type::Service, true, "服务响应签名验证失败");
|
|
||||||
bail!("服务响应签名验证失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Service,
|
|
||||||
true,
|
|
||||||
"IPC请求完成: 命令={}, 成功={}",
|
|
||||||
command_type,
|
|
||||||
response.success
|
|
||||||
);
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_signed_request() {
|
||||||
|
let command = IpcCommand::GetVersion;
|
||||||
|
let payload = serde_json::json!({"test": "data"});
|
||||||
|
|
||||||
|
let result = create_signed_request(command, payload);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
if let Ok(request) = result {
|
||||||
|
assert!(!request.id.is_empty());
|
||||||
|
assert!(!request.signature.is_empty());
|
||||||
|
assert_eq!(request.command, IpcCommand::GetVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_and_verify_message() {
|
||||||
|
let test_message = "test message for signing";
|
||||||
|
|
||||||
|
let signature_result = sign_message(test_message);
|
||||||
|
assert!(signature_result.is_ok());
|
||||||
|
|
||||||
|
if let Ok(signature) = signature_result {
|
||||||
|
assert!(!signature.is_empty());
|
||||||
|
|
||||||
|
// 测试相同消息产生相同签名
|
||||||
|
if let Ok(signature2) = sign_message(test_message) {
|
||||||
|
assert_eq!(signature, signature2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verify_response_signature() {
|
||||||
|
let response = IpcResponse {
|
||||||
|
id: "test-id".to_string(),
|
||||||
|
success: true,
|
||||||
|
data: Some(serde_json::json!({"result": "success"})),
|
||||||
|
error: None,
|
||||||
|
signature: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建正确的签名
|
||||||
|
let verification_response = IpcResponse {
|
||||||
|
id: response.id.clone(),
|
||||||
|
success: response.success,
|
||||||
|
data: response.data.clone(),
|
||||||
|
error: response.error.clone(),
|
||||||
|
signature: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(message) = serde_json::to_string(&verification_response)
|
||||||
|
&& let Ok(correct_signature) = sign_message(&message)
|
||||||
|
{
|
||||||
|
let signed_response = IpcResponse {
|
||||||
|
signature: correct_signature,
|
||||||
|
..response
|
||||||
|
};
|
||||||
|
|
||||||
|
let verification_result = verify_response_signature(&signed_response);
|
||||||
|
assert!(verification_result.is_ok());
|
||||||
|
if let Ok(is_valid) = verification_result {
|
||||||
|
assert!(is_valid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
use crate::utils::autostart as startup_shortcut;
|
use crate::utils::autostart as startup_shortcut;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::{handle::Handle, EventDrivenProxyManager},
|
core::{EventDrivenProxyManager, handle::Handle},
|
||||||
logging, logging_error,
|
logging, logging_error, singleton_lazy,
|
||||||
utils::logging::Type,
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
use sysproxy::{Autoproxy, Sysproxy};
|
use sysproxy::{Autoproxy, Sysproxy};
|
||||||
@@ -25,16 +24,16 @@ static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;
|
|||||||
static DEFAULT_BYPASS: &str =
|
static DEFAULT_BYPASS: &str =
|
||||||
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
|
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
static DEFAULT_BYPASS: &str =
|
static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
|
||||||
|
|
||||||
fn get_bypass() -> String {
|
async fn get_bypass() -> String {
|
||||||
let use_default = Config::verge()
|
let use_default = Config::verge()
|
||||||
|
.await
|
||||||
.latest_ref()
|
.latest_ref()
|
||||||
.use_default_bypass
|
.use_default_bypass
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
let res = {
|
let res = {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge().await;
|
||||||
let verge = verge.latest_ref();
|
let verge = verge.latest_ref();
|
||||||
verge.system_proxy_bypass.clone()
|
verge.system_proxy_bypass.clone()
|
||||||
};
|
};
|
||||||
@@ -52,15 +51,19 @@ fn get_bypass() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sysopt {
|
impl Default for Sysopt {
|
||||||
pub fn global() -> &'static Sysopt {
|
fn default() -> Self {
|
||||||
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
|
Sysopt {
|
||||||
SYSOPT.get_or_init(|| Sysopt {
|
|
||||||
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||||
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use simplified singleton_lazy macro
|
||||||
|
singleton_lazy!(Sysopt, SYSOPT, Sysopt::default);
|
||||||
|
|
||||||
|
impl Sysopt {
|
||||||
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
||||||
// 使用事件驱动代理管理器
|
// 使用事件驱动代理管理器
|
||||||
let proxy_manager = EventDrivenProxyManager::global();
|
let proxy_manager = EventDrivenProxyManager::global();
|
||||||
@@ -74,14 +77,17 @@ impl Sysopt {
|
|||||||
pub async fn update_sysproxy(&self) -> Result<()> {
|
pub async fn update_sysproxy(&self) -> Result<()> {
|
||||||
let _lock = self.update_sysproxy.lock().await;
|
let _lock = self.update_sysproxy.lock().await;
|
||||||
|
|
||||||
let port = Config::verge()
|
let port = {
|
||||||
.latest_ref()
|
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
|
||||||
.verge_mixed_port
|
match verge_port {
|
||||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
Some(port) => port,
|
||||||
|
None => Config::clash().await.latest_ref().get_mixed_port(),
|
||||||
|
}
|
||||||
|
};
|
||||||
let pac_port = IVerge::get_singleton_port();
|
let pac_port = IVerge::get_singleton_port();
|
||||||
|
|
||||||
let (sys_enable, pac_enable, proxy_host) = {
|
let (sys_enable, pac_enable, proxy_host) = {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge().await;
|
||||||
let verge = verge.latest_ref();
|
let verge = verge.latest_ref();
|
||||||
(
|
(
|
||||||
verge.enable_system_proxy.unwrap_or(false),
|
verge.enable_system_proxy.unwrap_or(false),
|
||||||
@@ -99,7 +105,7 @@ impl Sysopt {
|
|||||||
enable: false,
|
enable: false,
|
||||||
host: proxy_host.clone(),
|
host: proxy_host.clone(),
|
||||||
port,
|
port,
|
||||||
bypass: get_bypass(),
|
bypass: get_bypass().await,
|
||||||
};
|
};
|
||||||
let mut auto = Autoproxy {
|
let mut auto = Autoproxy {
|
||||||
enable: false,
|
enable: false,
|
||||||
@@ -146,7 +152,9 @@ impl Sysopt {
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
let app_handle = Handle::global()
|
||||||
|
.app_handle()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
|
||||||
|
|
||||||
let binary_path = dirs::service_path()?;
|
let binary_path = dirs::service_path()?;
|
||||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||||
@@ -157,23 +165,27 @@ impl Sysopt {
|
|||||||
let shell = app_handle.shell();
|
let shell = app_handle.shell();
|
||||||
let output = if pac_enable {
|
let output = if pac_enable {
|
||||||
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
|
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
|
||||||
let output = shell
|
let sysproxy_str = sysproxy_exe
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
.as_path()
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
|
||||||
|
shell
|
||||||
|
.command(sysproxy_str)
|
||||||
.args(["pac", address.as_str()])
|
.args(["pac", address.as_str()])
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await?
|
||||||
.unwrap();
|
|
||||||
output
|
|
||||||
} else {
|
} else {
|
||||||
let address = format!("{proxy_host}:{port}");
|
let address = format!("{proxy_host}:{port}");
|
||||||
let bypass = get_bypass();
|
let bypass = get_bypass().await;
|
||||||
let output = shell
|
let sysproxy_str = sysproxy_exe
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
.as_path()
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
|
||||||
|
shell
|
||||||
|
.command(sysproxy_str)
|
||||||
.args(["global", address.as_str(), bypass.as_ref()])
|
.args(["global", address.as_str(), bypass.as_ref()])
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await?
|
||||||
.unwrap();
|
|
||||||
output
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
@@ -215,7 +227,9 @@ impl Sysopt {
|
|||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
let app_handle = Handle::global()
|
||||||
|
.app_handle()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
|
||||||
|
|
||||||
let binary_path = dirs::service_path()?;
|
let binary_path = dirs::service_path()?;
|
||||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||||
@@ -225,12 +239,15 @@ impl Sysopt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let shell = app_handle.shell();
|
let shell = app_handle.shell();
|
||||||
|
let sysproxy_str = sysproxy_exe
|
||||||
|
.as_path()
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
|
||||||
let output = shell
|
let output = shell
|
||||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
.command(sysproxy_str)
|
||||||
.args(["set", "1"])
|
.args(["set", "1"])
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
bail!("sysproxy exe run failed");
|
bail!("sysproxy exe run failed");
|
||||||
@@ -241,8 +258,8 @@ impl Sysopt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// update the startup
|
/// update the startup
|
||||||
pub fn update_launch(&self) -> Result<()> {
|
pub async fn update_launch(&self) -> Result<()> {
|
||||||
let enable_auto_launch = { Config::verge().latest_ref().enable_auto_launch };
|
let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch };
|
||||||
let is_enable = enable_auto_launch.unwrap_or(false);
|
let is_enable = enable_auto_launch.unwrap_or(false);
|
||||||
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
|
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
|
||||||
|
|
||||||
@@ -276,7 +293,10 @@ impl Sysopt {
|
|||||||
|
|
||||||
/// 尝试使用原来的自启动方法
|
/// 尝试使用原来的自启动方法
|
||||||
fn try_original_autostart_method(&self, is_enable: bool) {
|
fn try_original_autostart_method(&self, is_enable: bool) {
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
let Some(app_handle) = Handle::global().app_handle() else {
|
||||||
|
log::error!(target: "app", "App handle not available for autostart");
|
||||||
|
return;
|
||||||
|
};
|
||||||
let autostart_manager = app_handle.autolaunch();
|
let autostart_manager = app_handle.autolaunch();
|
||||||
|
|
||||||
if is_enable {
|
if is_enable {
|
||||||
@@ -303,7 +323,9 @@ impl Sysopt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 回退到原来的方法
|
// 回退到原来的方法
|
||||||
let app_handle = Handle::global().app_handle().unwrap();
|
let app_handle = Handle::global()
|
||||||
|
.app_handle()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
|
||||||
let autostart_manager = app_handle.autolaunch();
|
let autostart_manager = app_handle.autolaunch();
|
||||||
|
|
||||||
match autostart_manager.is_enabled() {
|
match autostart_manager.is_enabled() {
|
||||||
|
|||||||
+170
-157
@@ -1,9 +1,15 @@
|
|||||||
use crate::{config::Config, feat, logging, logging_error, utils::logging::Type};
|
use crate::{config::Config, feat, logging, logging_error, singleton, utils::logging::Type};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||||
use once_cell::sync::OnceCell;
|
use parking_lot::RwLock;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use std::{
|
||||||
use std::{collections::HashMap, sync::Arc};
|
collections::HashMap,
|
||||||
|
pin::Pin,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
type TaskID = u64;
|
type TaskID = u64;
|
||||||
|
|
||||||
@@ -22,94 +28,91 @@ pub struct Timer {
|
|||||||
/// save the current state - using RwLock for better read concurrency
|
/// save the current state - using RwLock for better read concurrency
|
||||||
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
|
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
|
||||||
|
|
||||||
/// increment id - kept as mutex since it's just a counter
|
/// increment id - atomic counter for better performance
|
||||||
pub timer_count: Arc<Mutex<TaskID>>,
|
pub timer_count: AtomicU64,
|
||||||
|
|
||||||
/// Flag to mark if timer is initialized - atomic for better performance
|
/// Flag to mark if timer is initialized - atomic for better performance
|
||||||
pub initialized: Arc<std::sync::atomic::AtomicBool>,
|
pub initialized: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timer {
|
// Use singleton macro
|
||||||
pub fn global() -> &'static Timer {
|
singleton!(Timer, TIMER_INSTANCE);
|
||||||
static TIMER: OnceCell<Timer> = OnceCell::new();
|
|
||||||
|
|
||||||
TIMER.get_or_init(|| Timer {
|
impl Timer {
|
||||||
|
fn new() -> Self {
|
||||||
|
Timer {
|
||||||
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
|
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
|
||||||
timer_map: Arc::new(RwLock::new(HashMap::new())),
|
timer_map: Arc::new(RwLock::new(HashMap::new())),
|
||||||
timer_count: Arc::new(Mutex::new(1)),
|
timer_count: AtomicU64::new(1),
|
||||||
initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
initialized: AtomicBool::new(false),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize timer with better error handling and atomic operations
|
/// Initialize timer with better error handling and atomic operations
|
||||||
pub fn init(&self) -> Result<()> {
|
pub async fn init(&self) -> Result<()> {
|
||||||
// Use compare_exchange for thread-safe initialization check
|
// Use compare_exchange for thread-safe initialization check
|
||||||
if self
|
if self
|
||||||
.initialized
|
.initialized
|
||||||
.compare_exchange(
|
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||||
false,
|
|
||||||
true,
|
|
||||||
std::sync::atomic::Ordering::SeqCst,
|
|
||||||
std::sync::atomic::Ordering::SeqCst,
|
|
||||||
)
|
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
logging!(debug, Type::Timer, "Timer already initialized, skipping...");
|
logging!(debug, Type::Timer, "Timer already initialized, skipping...");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
logging!(info, Type::Timer, true, "Initializing timer...");
|
|
||||||
|
|
||||||
// Initialize timer tasks
|
// Initialize timer tasks
|
||||||
if let Err(e) = self.refresh() {
|
if let Err(e) = self.refresh().await {
|
||||||
// Reset initialization flag on error
|
// Reset initialization flag on error
|
||||||
self.initialized
|
self.initialized.store(false, Ordering::SeqCst);
|
||||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
|
||||||
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
|
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let timer_map = self.timer_map.read();
|
// Log timer info first
|
||||||
logging!(
|
{
|
||||||
info,
|
let timer_map = self.timer_map.read();
|
||||||
Type::Timer,
|
|
||||||
"已注册的定时任务数量: {}",
|
|
||||||
timer_map.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
for (uid, task) in timer_map.iter() {
|
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::Timer,
|
Type::Timer,
|
||||||
"注册了定时任务 - uid={}, interval={}min, task_id={}",
|
"已注册的定时任务数量: {}",
|
||||||
uid,
|
timer_map.len()
|
||||||
task.interval_minutes,
|
|
||||||
task.task_id
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (uid, task) in timer_map.iter() {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Timer,
|
||||||
|
"注册了定时任务 - uid={}, interval={}min, task_id={}",
|
||||||
|
uid,
|
||||||
|
task.interval_minutes,
|
||||||
|
task.task_id
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cur_timestamp = chrono::Local::now().timestamp();
|
let cur_timestamp = chrono::Local::now().timestamp();
|
||||||
|
|
||||||
// Collect profiles that need immediate update
|
// Collect profiles that need immediate update
|
||||||
let profiles_to_update = if let Some(items) = Config::profiles().latest_ref().get_items() {
|
let profiles_to_update =
|
||||||
items
|
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
|
||||||
.iter()
|
items
|
||||||
.filter_map(|item| {
|
.iter()
|
||||||
let interval = item.option.as_ref()?.update_interval? as i64;
|
.filter_map(|item| {
|
||||||
let updated = item.updated? as i64;
|
let interval = item.option.as_ref()?.update_interval? as i64;
|
||||||
let uid = item.uid.as_ref()?;
|
let updated = item.updated? as i64;
|
||||||
|
let uid = item.uid.as_ref()?;
|
||||||
|
|
||||||
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
||||||
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
|
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
|
||||||
Some(uid.clone())
|
Some(uid.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Advance tasks outside of locks to minimize lock contention
|
// Advance tasks outside of locks to minimize lock contention
|
||||||
if !profiles_to_update.is_empty() {
|
if !profiles_to_update.is_empty() {
|
||||||
@@ -137,9 +140,9 @@ impl Timer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh timer tasks with better error handling
|
/// Refresh timer tasks with better error handling
|
||||||
pub fn refresh(&self) -> Result<()> {
|
pub async fn refresh(&self) -> Result<()> {
|
||||||
// Generate diff outside of lock to minimize lock contention
|
// Generate diff outside of lock to minimize lock contention
|
||||||
let diff_map = self.gen_diff();
|
let diff_map = self.gen_diff().await;
|
||||||
|
|
||||||
if diff_map.is_empty() {
|
if diff_map.is_empty() {
|
||||||
logging!(debug, Type::Timer, "No timer changes needed");
|
logging!(debug, Type::Timer, "No timer changes needed");
|
||||||
@@ -153,72 +156,80 @@ impl Timer {
|
|||||||
diff_map.len()
|
diff_map.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply changes while holding locks
|
// Apply changes - first collect operations to perform without holding locks
|
||||||
let mut timer_map = self.timer_map.write();
|
let mut operations_to_add: Vec<(String, TaskID, u64)> = Vec::new();
|
||||||
let mut delay_timer = self.delay_timer.write();
|
let _operations_to_remove: Vec<String> = Vec::new();
|
||||||
|
|
||||||
for (uid, diff) in diff_map {
|
// Perform sync operations while holding locks
|
||||||
match diff {
|
{
|
||||||
DiffFlag::Del(tid) => {
|
let mut timer_map = self.timer_map.write();
|
||||||
timer_map.remove(&uid);
|
let delay_timer = self.delay_timer.write();
|
||||||
if let Err(e) = delay_timer.remove_task(tid) {
|
|
||||||
logging!(
|
for (uid, diff) in diff_map {
|
||||||
warn,
|
match diff {
|
||||||
Type::Timer,
|
DiffFlag::Del(tid) => {
|
||||||
"Failed to remove task {} for uid {}: {}",
|
timer_map.remove(&uid);
|
||||||
tid,
|
if let Err(e) = delay_timer.remove_task(tid) {
|
||||||
uid,
|
logging!(
|
||||||
e
|
warn,
|
||||||
);
|
Type::Timer,
|
||||||
} else {
|
"Failed to remove task {} for uid {}: {}",
|
||||||
logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid);
|
tid,
|
||||||
|
uid,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DiffFlag::Add(tid, interval) => {
|
||||||
|
let task = TimerTask {
|
||||||
|
task_id: tid,
|
||||||
|
interval_minutes: interval,
|
||||||
|
last_run: chrono::Local::now().timestamp(),
|
||||||
|
};
|
||||||
|
|
||||||
|
timer_map.insert(uid.clone(), task);
|
||||||
|
operations_to_add.push((uid, tid, interval));
|
||||||
|
}
|
||||||
|
DiffFlag::Mod(tid, interval) => {
|
||||||
|
// Remove old task first
|
||||||
|
if let Err(e) = delay_timer.remove_task(tid) {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Timer,
|
||||||
|
"Failed to remove old task {} for uid {}: {}",
|
||||||
|
tid,
|
||||||
|
uid,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add the new one
|
||||||
|
let task = TimerTask {
|
||||||
|
task_id: tid,
|
||||||
|
interval_minutes: interval,
|
||||||
|
last_run: chrono::Local::now().timestamp(),
|
||||||
|
};
|
||||||
|
|
||||||
|
timer_map.insert(uid.clone(), task);
|
||||||
|
operations_to_add.push((uid, tid, interval));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DiffFlag::Add(tid, interval) => {
|
}
|
||||||
let task = TimerTask {
|
} // Locks are dropped here
|
||||||
task_id: tid,
|
|
||||||
interval_minutes: interval,
|
|
||||||
last_run: chrono::Local::now().timestamp(),
|
|
||||||
};
|
|
||||||
|
|
||||||
timer_map.insert(uid.clone(), task);
|
// Now perform async operations without holding locks
|
||||||
|
for (uid, tid, interval) in operations_to_add {
|
||||||
|
// Re-acquire locks for individual operations
|
||||||
|
let mut delay_timer = self.delay_timer.write();
|
||||||
|
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
||||||
|
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
|
||||||
|
|
||||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
// Rollback on failure - remove from timer_map
|
||||||
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
|
self.timer_map.write().remove(&uid);
|
||||||
timer_map.remove(&uid); // Rollback on failure
|
} else {
|
||||||
} else {
|
logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid);
|
||||||
logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DiffFlag::Mod(tid, interval) => {
|
|
||||||
// Remove old task first
|
|
||||||
if let Err(e) = delay_timer.remove_task(tid) {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Timer,
|
|
||||||
"Failed to remove old task {} for uid {}: {}",
|
|
||||||
tid,
|
|
||||||
uid,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then add the new one
|
|
||||||
let task = TimerTask {
|
|
||||||
task_id: tid,
|
|
||||||
interval_minutes: interval,
|
|
||||||
last_run: chrono::Local::now().timestamp(),
|
|
||||||
};
|
|
||||||
|
|
||||||
timer_map.insert(uid.clone(), task);
|
|
||||||
|
|
||||||
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
|
|
||||||
logging_error!(Type::Timer, "Failed to update task for uid {}: {}", uid, e);
|
|
||||||
timer_map.remove(&uid); // Rollback on failure
|
|
||||||
} else {
|
|
||||||
logging!(debug, Type::Timer, "Updated task {} for uid {}", tid, uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,24 +237,23 @@ impl Timer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate map of profile UIDs to update intervals
|
/// Generate map of profile UIDs to update intervals
|
||||||
fn gen_map(&self) -> HashMap<String, u64> {
|
async fn gen_map(&self) -> HashMap<String, u64> {
|
||||||
let mut new_map = HashMap::new();
|
let mut new_map = HashMap::new();
|
||||||
|
|
||||||
if let Some(items) = Config::profiles().latest_ref().get_items() {
|
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if let Some(option) = item.option.as_ref() {
|
if let Some(option) = item.option.as_ref()
|
||||||
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
|
&& let (Some(interval), Some(uid)) = (option.update_interval, &item.uid)
|
||||||
if interval > 0 {
|
&& interval > 0
|
||||||
logging!(
|
{
|
||||||
debug,
|
logging!(
|
||||||
Type::Timer,
|
debug,
|
||||||
"找到定时更新配置: uid={}, interval={}min",
|
Type::Timer,
|
||||||
uid,
|
"找到定时更新配置: uid={}, interval={}min",
|
||||||
interval
|
uid,
|
||||||
);
|
interval
|
||||||
new_map.insert(uid.clone(), interval);
|
);
|
||||||
}
|
new_map.insert(uid.clone(), interval);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,9 +268,9 @@ impl Timer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate differences between current and new timer configuration
|
/// Generate differences between current and new timer configuration
|
||||||
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
async fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
||||||
let mut diff_map = HashMap::new();
|
let mut diff_map = HashMap::new();
|
||||||
let new_map = self.gen_map();
|
let new_map = self.gen_map().await;
|
||||||
|
|
||||||
// Read lock for comparing current state
|
// Read lock for comparing current state
|
||||||
let timer_map = self.timer_map.read();
|
let timer_map = self.timer_map.read();
|
||||||
@@ -299,7 +309,8 @@ impl Timer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find new tasks to add
|
// Find new tasks to add
|
||||||
let mut next_id = *self.timer_count.lock();
|
let mut next_id = self.timer_count.load(Ordering::Relaxed);
|
||||||
|
let original_id = next_id;
|
||||||
|
|
||||||
for (uid, &interval) in new_map.iter() {
|
for (uid, &interval) in new_map.iter() {
|
||||||
if !timer_map.contains_key(uid) {
|
if !timer_map.contains_key(uid) {
|
||||||
@@ -316,8 +327,8 @@ impl Timer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update counter only if we added new tasks
|
// Update counter only if we added new tasks
|
||||||
if next_id > *self.timer_count.lock() {
|
if next_id > original_id {
|
||||||
*self.timer_count.lock() = next_id;
|
self.timer_count.store(next_id, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
|
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
|
||||||
@@ -348,9 +359,9 @@ impl Timer {
|
|||||||
.set_frequency_repeated_by_minutes(minutes)
|
.set_frequency_repeated_by_minutes(minutes)
|
||||||
.spawn_async_routine(move || {
|
.spawn_async_routine(move || {
|
||||||
let uid = uid.clone();
|
let uid = uid.clone();
|
||||||
async move {
|
Box::pin(async move {
|
||||||
Self::async_task(uid).await;
|
Self::async_task(uid).await;
|
||||||
}
|
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
|
||||||
})
|
})
|
||||||
.context("failed to create timer task")?;
|
.context("failed to create timer task")?;
|
||||||
|
|
||||||
@@ -362,21 +373,24 @@ impl Timer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get next update time for a profile
|
/// Get next update time for a profile
|
||||||
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> {
|
pub async fn get_next_update_time(&self, uid: &str) -> Option<i64> {
|
||||||
logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid);
|
logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid);
|
||||||
|
|
||||||
let timer_map = self.timer_map.read();
|
// First extract timer task data without holding the lock across await
|
||||||
let task = match timer_map.get(uid) {
|
let task_interval = {
|
||||||
Some(t) => t,
|
let timer_map = self.timer_map.read();
|
||||||
None => {
|
match timer_map.get(uid) {
|
||||||
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
|
Some(t) => t.interval_minutes,
|
||||||
return None;
|
None => {
|
||||||
|
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the profile updated timestamp
|
// Get the profile updated timestamp - now safe to await
|
||||||
let profiles_config = Config::profiles();
|
let config_profiles = Config::profiles().await;
|
||||||
let profiles = profiles_config.latest_ref();
|
let profiles = config_profiles.data_ref().clone();
|
||||||
let items = match profiles.get_items() {
|
let items = match profiles.get_items() {
|
||||||
Some(i) => i,
|
Some(i) => i,
|
||||||
None => {
|
None => {
|
||||||
@@ -396,8 +410,8 @@ impl Timer {
|
|||||||
let updated = profile.updated.unwrap_or(0) as i64;
|
let updated = profile.updated.unwrap_or(0) as i64;
|
||||||
|
|
||||||
// Calculate next update time
|
// Calculate next update time
|
||||||
if updated > 0 && task.interval_minutes > 0 {
|
if updated > 0 && task_interval > 0 {
|
||||||
let next_time = updated + (task.interval_minutes as i64 * 60);
|
let next_time = updated + (task_interval as i64 * 60);
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::Timer,
|
Type::Timer,
|
||||||
@@ -412,7 +426,7 @@ impl Timer {
|
|||||||
Type::Timer,
|
Type::Timer,
|
||||||
"更新时间或间隔无效,updated={}, interval={}",
|
"更新时间或间隔无效,updated={}, interval={}",
|
||||||
updated,
|
updated,
|
||||||
task.interval_minutes
|
task_interval
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -420,7 +434,6 @@ impl Timer {
|
|||||||
|
|
||||||
/// Emit update events for frontend notification
|
/// Emit update events for frontend notification
|
||||||
fn emit_update_event(_uid: &str, _is_start: bool) {
|
fn emit_update_event(_uid: &str, _is_start: bool) {
|
||||||
#[cfg(any(feature = "verge-dev", feature = "default"))]
|
|
||||||
{
|
{
|
||||||
if _is_start {
|
if _is_start {
|
||||||
super::handle::Handle::notify_profile_update_started(_uid.to_string());
|
super::handle::Handle::notify_profile_update_started(_uid.to_string());
|
||||||
@@ -438,7 +451,7 @@ impl Timer {
|
|||||||
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
|
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
|
||||||
Self::emit_update_event(&uid, true);
|
Self::emit_update_event(&uid, true);
|
||||||
|
|
||||||
let is_current = Config::profiles().latest_ref().current.as_ref() == Some(&uid);
|
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(&uid);
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::Timer,
|
Type::Timer,
|
||||||
|
|||||||
+528
-305
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
|||||||
#![cfg(target_os = "windows")]
|
#![cfg(target_os = "windows")]
|
||||||
|
|
||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result, bail};
|
||||||
use deelevate::{PrivilegeLevel, Token};
|
use deelevate::{PrivilegeLevel, Token};
|
||||||
use runas::Command as RunasCommand;
|
use runas::Command as RunasCommand;
|
||||||
use std::process::Command as StdCommand;
|
use std::process::Command as StdCommand;
|
||||||
|
|
||||||
pub async fn invoke_uwptools() -> Result<()> {
|
pub fn invoke_uwptools() -> Result<()> {
|
||||||
let resource_dir = dirs::app_resources_dir()?;
|
let resource_dir = dirs::app_resources_dir()?;
|
||||||
let tool_path = resource_dir.join("enableLoopback.exe");
|
let tool_path = resource_dir.join("enableLoopback.exe");
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// This function is exported for use by the Clash core
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
function main(config, _name) {
|
function main(config, _name) {
|
||||||
if (config.mode === "script") {
|
if (config.mode === "script") {
|
||||||
config.mode = "rule";
|
config.mode = "rule";
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// This function is exported for use by the Clash core
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
function main(config, _name) {
|
function main(config, _name) {
|
||||||
if (Array.isArray(config.proxies)) {
|
if (Array.isArray(config.proxies)) {
|
||||||
config.proxies.forEach((p, i) => {
|
config.proxies.forEach((p, i) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
config::PrfItem,
|
config::PrfItem,
|
||||||
utils::{dirs, help},
|
utils::{dirs, help},
|
||||||
};
|
};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -22,6 +22,7 @@ pub enum ChainType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum ChainSupport {
|
pub enum ChainSupport {
|
||||||
Clash,
|
Clash,
|
||||||
ClashMeta,
|
ClashMeta,
|
||||||
@@ -29,8 +30,49 @@ pub enum ChainSupport {
|
|||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&PrfItem> for Option<ChainItem> {
|
// impl From<&PrfItem> for Option<ChainItem> {
|
||||||
fn from(item: &PrfItem) -> Self {
|
// fn from(item: &PrfItem) -> Self {
|
||||||
|
// let itype = item.itype.as_ref()?.as_str();
|
||||||
|
// let file = item.file.clone()?;
|
||||||
|
// let uid = item.uid.clone().unwrap_or("".into());
|
||||||
|
// let path = dirs::app_profiles_dir().ok()?.join(file);
|
||||||
|
|
||||||
|
// if !path.exists() {
|
||||||
|
// return None;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// match itype {
|
||||||
|
// "script" => Some(ChainItem {
|
||||||
|
// uid,
|
||||||
|
// data: ChainType::Script(fs::read_to_string(path).ok()?),
|
||||||
|
// }),
|
||||||
|
// "merge" => Some(ChainItem {
|
||||||
|
// uid,
|
||||||
|
// data: ChainType::Merge(help::read_mapping(&path).ok()?),
|
||||||
|
// }),
|
||||||
|
// "rules" => Some(ChainItem {
|
||||||
|
// uid,
|
||||||
|
// data: ChainType::Rules(help::read_seq_map(&path).ok()?),
|
||||||
|
// }),
|
||||||
|
// "proxies" => Some(ChainItem {
|
||||||
|
// uid,
|
||||||
|
// data: ChainType::Proxies(help::read_seq_map(&path).ok()?),
|
||||||
|
// }),
|
||||||
|
// "groups" => Some(ChainItem {
|
||||||
|
// uid,
|
||||||
|
// data: ChainType::Groups(help::read_seq_map(&path).ok()?),
|
||||||
|
// }),
|
||||||
|
// _ => None,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Helper trait to allow async conversion
|
||||||
|
pub trait AsyncChainItemFrom {
|
||||||
|
async fn from_async(item: &PrfItem) -> Option<ChainItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncChainItemFrom for Option<ChainItem> {
|
||||||
|
async fn from_async(item: &PrfItem) -> Option<ChainItem> {
|
||||||
let itype = item.itype.as_ref()?.as_str();
|
let itype = item.itype.as_ref()?.as_str();
|
||||||
let file = item.file.clone()?;
|
let file = item.file.clone()?;
|
||||||
let uid = item.uid.clone().unwrap_or("".into());
|
let uid = item.uid.clone().unwrap_or("".into());
|
||||||
@@ -47,25 +89,33 @@ impl From<&PrfItem> for Option<ChainItem> {
|
|||||||
}),
|
}),
|
||||||
"merge" => Some(ChainItem {
|
"merge" => Some(ChainItem {
|
||||||
uid,
|
uid,
|
||||||
data: ChainType::Merge(help::read_mapping(&path).ok()?),
|
data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
|
||||||
}),
|
|
||||||
"rules" => Some(ChainItem {
|
|
||||||
uid,
|
|
||||||
data: ChainType::Rules(help::read_seq_map(&path).ok()?),
|
|
||||||
}),
|
|
||||||
"proxies" => Some(ChainItem {
|
|
||||||
uid,
|
|
||||||
data: ChainType::Proxies(help::read_seq_map(&path).ok()?),
|
|
||||||
}),
|
|
||||||
"groups" => Some(ChainItem {
|
|
||||||
uid,
|
|
||||||
data: ChainType::Groups(help::read_seq_map(&path).ok()?),
|
|
||||||
}),
|
}),
|
||||||
|
"rules" => {
|
||||||
|
let seq_map = help::read_seq_map(&path).await.ok()?;
|
||||||
|
Some(ChainItem {
|
||||||
|
uid,
|
||||||
|
data: ChainType::Rules(seq_map),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"proxies" => {
|
||||||
|
let seq_map = help::read_seq_map(&path).await.ok()?;
|
||||||
|
Some(ChainItem {
|
||||||
|
uid,
|
||||||
|
data: ChainType::Proxies(seq_map),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"groups" => {
|
||||||
|
let seq_map = help::read_seq_map(&path).await.ok()?;
|
||||||
|
Some(ChainItem {
|
||||||
|
uid,
|
||||||
|
data: ChainType::Groups(seq_map),
|
||||||
|
})
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChainItem {
|
impl ChainItem {
|
||||||
/// 内建支持一些脚本
|
/// 内建支持一些脚本
|
||||||
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
|
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
pub const HANDLE_FIELDS: [&str; 12] = [
|
pub const HANDLE_FIELDS: [&str; 12] = [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::use_lowercase;
|
use super::use_lowercase;
|
||||||
use serde_yaml::{self, Mapping, Value};
|
use serde_yaml_ng::{self, Mapping, Value};
|
||||||
|
|
||||||
fn deep_merge(a: &mut Value, b: &Value) {
|
fn deep_merge(a: &mut Value, b: &Value) {
|
||||||
match (a, b) {
|
match (a, b) {
|
||||||
@@ -18,9 +18,10 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
|
|||||||
|
|
||||||
deep_merge(&mut config, &Value::from(merge));
|
deep_merge(&mut config, &Value::from(merge));
|
||||||
|
|
||||||
let config = config.as_mapping().unwrap().clone();
|
config.as_mapping().cloned().unwrap_or_else(|| {
|
||||||
|
log::error!("Failed to convert merged config to mapping, using empty mapping");
|
||||||
config
|
Mapping::new()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -51,10 +52,10 @@ fn test_merge() -> anyhow::Result<()> {
|
|||||||
script1: test
|
script1: test
|
||||||
";
|
";
|
||||||
|
|
||||||
let merge = serde_yaml::from_str::<Mapping>(merge)?;
|
let merge = serde_yaml_ng::from_str::<Mapping>(merge)?;
|
||||||
let config = serde_yaml::from_str::<Mapping>(config)?;
|
let config = serde_yaml_ng::from_str::<Mapping>(config)?;
|
||||||
|
|
||||||
let _ = serde_yaml::to_string(&use_merge(merge, config))?;
|
let _ = serde_yaml_ng::to_string(&use_merge(merge, config))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+204
-91
@@ -7,7 +7,7 @@ mod tun;
|
|||||||
|
|
||||||
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
|
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
|
||||||
use crate::{config::Config, utils::tmpl};
|
use crate::{config::Config, utils::tmpl};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
type ResultLog = Vec<(String, String)>;
|
type ResultLog = Vec<(String, String)>;
|
||||||
@@ -16,10 +16,10 @@ type ResultLog = Vec<(String, String)>;
|
|||||||
/// 返回最终订阅、该订阅包含的键、和script执行的结果
|
/// 返回最终订阅、该订阅包含的键、和script执行的结果
|
||||||
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||||
// config.yaml 的订阅
|
// config.yaml 的订阅
|
||||||
let clash_config = { Config::clash().latest_ref().0.clone() };
|
let clash_config = { Config::clash().await.latest_ref().0.clone() };
|
||||||
|
|
||||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
|
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge().await;
|
||||||
let verge = verge.latest_ref();
|
let verge = verge.latest_ref();
|
||||||
(
|
(
|
||||||
Some(verge.get_valid_clash_core()),
|
Some(verge.get_valid_clash_core()),
|
||||||
@@ -32,18 +32,18 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
};
|
};
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let redir_enabled = {
|
let redir_enabled = {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge().await;
|
||||||
let verge = verge.latest_ref();
|
let verge = verge.latest_ref();
|
||||||
verge.verge_redir_enabled.unwrap_or(false)
|
verge.verge_redir_enabled.unwrap_or(false)
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let tproxy_enabled = {
|
let tproxy_enabled = {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge().await;
|
||||||
let verge = verge.latest_ref();
|
let verge = verge.latest_ref();
|
||||||
verge.verge_tproxy_enabled.unwrap_or(false)
|
verge.verge_tproxy_enabled.unwrap_or(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从profiles里拿东西
|
// 从profiles里拿东西 - 先收集需要的数据,然后释放锁
|
||||||
let (
|
let (
|
||||||
mut config,
|
mut config,
|
||||||
merge_item,
|
merge_item,
|
||||||
@@ -55,74 +55,172 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
global_script,
|
global_script,
|
||||||
profile_name,
|
profile_name,
|
||||||
) = {
|
) = {
|
||||||
let profiles = Config::profiles();
|
// 收集所有需要的数据,然后释放profiles锁
|
||||||
let profiles = profiles.latest_ref();
|
let (
|
||||||
|
current,
|
||||||
|
merge_uid,
|
||||||
|
script_uid,
|
||||||
|
rules_uid,
|
||||||
|
proxies_uid,
|
||||||
|
groups_uid,
|
||||||
|
_current_profile_uid,
|
||||||
|
name,
|
||||||
|
) = {
|
||||||
|
// 分离async调用和数据获取,避免借用检查问题
|
||||||
|
let current = {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles_clone = profiles.latest_ref().clone();
|
||||||
|
profiles_clone.current_mapping().await.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
let current = profiles.current_mapping().unwrap_or_default();
|
// 重新获取锁进行其他操作
|
||||||
let merge = profiles
|
let profiles = Config::profiles().await;
|
||||||
.get_item(&profiles.current_merge().unwrap_or_default())
|
let profiles_ref = profiles.latest_ref();
|
||||||
.ok()
|
|
||||||
.and_then(<Option<ChainItem>>::from)
|
|
||||||
.unwrap_or_else(|| ChainItem {
|
|
||||||
uid: "".into(),
|
|
||||||
data: ChainType::Merge(Mapping::new()),
|
|
||||||
});
|
|
||||||
let script = profiles
|
|
||||||
.get_item(&profiles.current_script().unwrap_or_default())
|
|
||||||
.ok()
|
|
||||||
.and_then(<Option<ChainItem>>::from)
|
|
||||||
.unwrap_or_else(|| ChainItem {
|
|
||||||
uid: "".into(),
|
|
||||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
|
||||||
});
|
|
||||||
let rules = profiles
|
|
||||||
.get_item(&profiles.current_rules().unwrap_or_default())
|
|
||||||
.ok()
|
|
||||||
.and_then(<Option<ChainItem>>::from)
|
|
||||||
.unwrap_or_else(|| ChainItem {
|
|
||||||
uid: "".into(),
|
|
||||||
data: ChainType::Rules(SeqMap::default()),
|
|
||||||
});
|
|
||||||
let proxies = profiles
|
|
||||||
.get_item(&profiles.current_proxies().unwrap_or_default())
|
|
||||||
.ok()
|
|
||||||
.and_then(<Option<ChainItem>>::from)
|
|
||||||
.unwrap_or_else(|| ChainItem {
|
|
||||||
uid: "".into(),
|
|
||||||
data: ChainType::Proxies(SeqMap::default()),
|
|
||||||
});
|
|
||||||
let groups = profiles
|
|
||||||
.get_item(&profiles.current_groups().unwrap_or_default())
|
|
||||||
.ok()
|
|
||||||
.and_then(<Option<ChainItem>>::from)
|
|
||||||
.unwrap_or_else(|| ChainItem {
|
|
||||||
uid: "".into(),
|
|
||||||
data: ChainType::Groups(SeqMap::default()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let global_merge = profiles
|
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
|
||||||
.get_item(&"Merge".to_string())
|
let script_uid = profiles_ref.current_script().unwrap_or_default();
|
||||||
.ok()
|
let rules_uid = profiles_ref.current_rules().unwrap_or_default();
|
||||||
.and_then(<Option<ChainItem>>::from)
|
let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
|
||||||
.unwrap_or_else(|| ChainItem {
|
let groups_uid = profiles_ref.current_groups().unwrap_or_default();
|
||||||
uid: "Merge".into(),
|
let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
|
||||||
data: ChainType::Merge(Mapping::new()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let global_script = profiles
|
let name = profiles_ref
|
||||||
.get_item(&"Script".to_string())
|
.get_item(¤t_profile_uid)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(<Option<ChainItem>>::from)
|
.and_then(|item| item.name.clone())
|
||||||
.unwrap_or_else(|| ChainItem {
|
.unwrap_or_default();
|
||||||
uid: "Script".into(),
|
|
||||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let name = profiles
|
(
|
||||||
.get_item(&profiles.get_current().unwrap_or_default())
|
current,
|
||||||
.ok()
|
merge_uid,
|
||||||
.and_then(|item| item.name.clone())
|
script_uid,
|
||||||
.unwrap_or_default();
|
rules_uid,
|
||||||
|
proxies_uid,
|
||||||
|
groups_uid,
|
||||||
|
current_profile_uid,
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 现在获取具体的items,此时profiles锁已经释放
|
||||||
|
let merge = {
|
||||||
|
let item = {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles = profiles.latest_ref();
|
||||||
|
profiles.get_item(&merge_uid).ok().cloned()
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
<Option<ChainItem>>::from_async(&item).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|| ChainItem {
|
||||||
|
uid: "".into(),
|
||||||
|
data: ChainType::Merge(Mapping::new()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let script = {
|
||||||
|
let item = {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles = profiles.latest_ref();
|
||||||
|
profiles.get_item(&script_uid).ok().cloned()
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
<Option<ChainItem>>::from_async(&item).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|| ChainItem {
|
||||||
|
uid: "".into(),
|
||||||
|
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let rules = {
|
||||||
|
let item = {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles = profiles.latest_ref();
|
||||||
|
profiles.get_item(&rules_uid).ok().cloned()
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
<Option<ChainItem>>::from_async(&item).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|| ChainItem {
|
||||||
|
uid: "".into(),
|
||||||
|
data: ChainType::Rules(SeqMap::default()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let proxies = {
|
||||||
|
let item = {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles = profiles.latest_ref();
|
||||||
|
profiles.get_item(&proxies_uid).ok().cloned()
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
<Option<ChainItem>>::from_async(&item).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|| ChainItem {
|
||||||
|
uid: "".into(),
|
||||||
|
data: ChainType::Proxies(SeqMap::default()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let groups = {
|
||||||
|
let item = {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles = profiles.latest_ref();
|
||||||
|
profiles.get_item(&groups_uid).ok().cloned()
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
<Option<ChainItem>>::from_async(&item).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|| ChainItem {
|
||||||
|
uid: "".into(),
|
||||||
|
data: ChainType::Groups(SeqMap::default()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let global_merge = {
|
||||||
|
let item = {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles = profiles.latest_ref();
|
||||||
|
profiles.get_item(&"Merge".to_string()).ok().cloned()
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
<Option<ChainItem>>::from_async(&item).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|| ChainItem {
|
||||||
|
uid: "Merge".into(),
|
||||||
|
data: ChainType::Merge(Mapping::new()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let global_script = {
|
||||||
|
let item = {
|
||||||
|
let profiles = Config::profiles().await;
|
||||||
|
let profiles = profiles.latest_ref();
|
||||||
|
profiles.get_item(&"Script".to_string()).ok().cloned()
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
<Option<ChainItem>>::from_async(&item).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap_or_else(|| ChainItem {
|
||||||
|
uid: "Script".into(),
|
||||||
|
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||||
|
});
|
||||||
|
|
||||||
(
|
(
|
||||||
current,
|
current,
|
||||||
@@ -234,7 +332,23 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.insert(key, value);
|
// 处理 external-controller 键的开关逻辑
|
||||||
|
if key.as_str() == Some("external-controller") {
|
||||||
|
let enable_external_controller = Config::verge()
|
||||||
|
.await
|
||||||
|
.latest_ref()
|
||||||
|
.enable_external_controller
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if enable_external_controller {
|
||||||
|
config.insert(key, value);
|
||||||
|
} else {
|
||||||
|
// 如果禁用了外部控制器,设置为空字符串
|
||||||
|
config.insert(key, "".into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.insert(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +373,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
config = use_tun(config, enable_tun).await;
|
config = use_tun(config, enable_tun);
|
||||||
config = use_sort(config);
|
config = use_sort(config);
|
||||||
|
|
||||||
// 应用独立的DNS配置(如果启用)
|
// 应用独立的DNS配置(如果启用)
|
||||||
@@ -270,27 +384,26 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
if let Ok(app_dir) = dirs::app_home_dir() {
|
if let Ok(app_dir) = dirs::app_home_dir() {
|
||||||
let dns_path = app_dir.join("dns_config.yaml");
|
let dns_path = app_dir.join("dns_config.yaml");
|
||||||
|
|
||||||
if dns_path.exists() {
|
if dns_path.exists()
|
||||||
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
|
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path)
|
||||||
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
|
||||||
// 处理hosts配置
|
{
|
||||||
if let Some(hosts_value) = dns_config.get("hosts") {
|
// 处理hosts配置
|
||||||
if hosts_value.is_mapping() {
|
if let Some(hosts_value) = dns_config.get("hosts")
|
||||||
config.insert("hosts".into(), hosts_value.clone());
|
&& hosts_value.is_mapping()
|
||||||
log::info!(target: "app", "apply hosts configuration");
|
{
|
||||||
}
|
config.insert("hosts".into(), hosts_value.clone());
|
||||||
}
|
log::info!(target: "app", "apply hosts configuration");
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(dns_value) = dns_config.get("dns") {
|
if let Some(dns_value) = dns_config.get("dns") {
|
||||||
if let Some(dns_mapping) = dns_value.as_mapping() {
|
if let Some(dns_mapping) = dns_value.as_mapping() {
|
||||||
config.insert("dns".into(), dns_mapping.clone().into());
|
config.insert("dns".into(), dns_mapping.clone().into());
|
||||||
log::info!(target: "app", "apply dns_config.yaml (dns section)");
|
log::info!(target: "app", "apply dns_config.yaml (dns section)");
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config.insert("dns".into(), dns_config.into());
|
|
||||||
log::info!(target: "app", "apply dns_config.yaml");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
config.insert("dns".into(), dns_config.into());
|
||||||
|
log::info!(target: "app", "apply dns_config.yaml");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,49 @@
|
|||||||
use super::use_lowercase;
|
use super::use_lowercase;
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
|
|
||||||
pub fn use_script(
|
pub fn use_script(
|
||||||
script: String,
|
script: String,
|
||||||
config: Mapping,
|
config: Mapping,
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<(Mapping, Vec<(String, String)>)> {
|
) -> Result<(Mapping, Vec<(String, String)>)> {
|
||||||
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
|
use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction};
|
||||||
use parking_lot::Mutex;
|
use std::{cell::RefCell, rc::Rc};
|
||||||
use std::sync::Arc;
|
|
||||||
let mut context = Context::default();
|
let mut context = Context::default();
|
||||||
|
|
||||||
let outputs = Arc::new(Mutex::new(vec![]));
|
let outputs = Rc::new(RefCell::new(vec![]));
|
||||||
|
|
||||||
let copy_outputs = outputs.clone();
|
let copy_outputs = Rc::clone(&outputs);
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = context.register_global_builtin_callable(
|
let _ = context.register_global_builtin_callable(
|
||||||
"__verge_log__".into(),
|
"__verge_log__".into(),
|
||||||
2,
|
2,
|
||||||
NativeFunction::from_closure(
|
NativeFunction::from_closure(
|
||||||
move |_: &JsValue, args: &[JsValue], context: &mut Context| {
|
move |_: &JsValue, args: &[JsValue], context: &mut Context| {
|
||||||
let level = args.first().unwrap().to_string(context)?;
|
let level = args.first().ok_or_else(|| {
|
||||||
let level = level.to_std_string().unwrap();
|
boa_engine::JsError::from_opaque(
|
||||||
let data = args.get(1).unwrap().to_string(context)?;
|
JsString::from("Missing level argument").into(),
|
||||||
let data = data.to_std_string().unwrap();
|
)
|
||||||
let mut out = copy_outputs.lock();
|
})?;
|
||||||
|
let level = level.to_string(context)?;
|
||||||
|
let level = level.to_std_string().map_err(|_| {
|
||||||
|
boa_engine::JsError::from_opaque(
|
||||||
|
JsString::from("Failed to convert level to string").into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let data = args.get(1).ok_or_else(|| {
|
||||||
|
boa_engine::JsError::from_opaque(
|
||||||
|
JsString::from("Missing data argument").into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let data = data.to_string(context)?;
|
||||||
|
let data = data.to_std_string().map_err(|_| {
|
||||||
|
boa_engine::JsError::from_opaque(
|
||||||
|
JsString::from("Failed to convert data to string").into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let mut out = copy_outputs.borrow_mut();
|
||||||
out.push((level, data));
|
out.push((level, data));
|
||||||
Ok(JsValue::undefined())
|
Ok(JsValue::undefined())
|
||||||
},
|
},
|
||||||
@@ -50,25 +68,29 @@ pub fn use_script(
|
|||||||
let safe_name = escape_js_string_for_single_quote(&name);
|
let safe_name = escape_js_string_for_single_quote(&name);
|
||||||
|
|
||||||
let code = format!(
|
let code = format!(
|
||||||
r#"try{{
|
r"try{{
|
||||||
{script};
|
{script};
|
||||||
JSON.stringify(main({config_str},'{safe_name}')||'')
|
JSON.stringify(main({config_str},'{safe_name}')||'')
|
||||||
}} catch(err) {{
|
}} catch(err) {{
|
||||||
`__error_flag__ ${{err.toString()}}`
|
`__error_flag__ ${{err.toString()}}`
|
||||||
}}"#
|
}}"
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
|
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
|
||||||
if !result.is_string() {
|
if !result.is_string() {
|
||||||
anyhow::bail!("main function should return object");
|
anyhow::bail!("main function should return object");
|
||||||
}
|
}
|
||||||
let result = result.to_string(&mut context).unwrap();
|
let result = result
|
||||||
let result = result.to_std_string().unwrap();
|
.to_string(&mut context)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to convert JS result to string: {}", e))?;
|
||||||
|
let result = result
|
||||||
|
.to_std_string()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Failed to convert JS string to std string"))?;
|
||||||
|
|
||||||
// 直接解析JSON结果,不做其他解析
|
// 直接解析JSON结果,不做其他解析
|
||||||
let res: Result<Mapping, Error> = parse_json_safely(&result);
|
let res: Result<Mapping, Error> = parse_json_safely(&result);
|
||||||
|
|
||||||
let mut out = outputs.lock();
|
let mut out = outputs.borrow_mut();
|
||||||
match res {
|
match res {
|
||||||
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -103,6 +125,8 @@ fn escape_js_string_for_single_quote(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
fn test_script() {
|
fn test_script() {
|
||||||
let script = r#"
|
let script = r#"
|
||||||
function main(config) {
|
function main(config) {
|
||||||
@@ -115,7 +139,7 @@ fn test_script() {
|
|||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let config = r#"
|
let config = r"
|
||||||
rules:
|
rules:
|
||||||
- 111
|
- 111
|
||||||
- 222
|
- 222
|
||||||
@@ -123,22 +147,21 @@ fn test_script() {
|
|||||||
enable: false
|
enable: false
|
||||||
dns:
|
dns:
|
||||||
enable: false
|
enable: false
|
||||||
"#;
|
";
|
||||||
|
|
||||||
let config = serde_yaml::from_str(config).unwrap();
|
let config = serde_yaml_ng::from_str(config).expect("Failed to parse test config YAML");
|
||||||
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
|
let (config, results) = use_script(script.into(), config, "".to_string())
|
||||||
|
.expect("Script execution should succeed in test");
|
||||||
|
|
||||||
let _ = serde_yaml::to_string(&config).unwrap();
|
let _ = serde_yaml_ng::to_string(&config).expect("Failed to serialize config to YAML");
|
||||||
let yaml_config_size = std::mem::size_of_val(&config);
|
let yaml_config_size = std::mem::size_of_val(&config);
|
||||||
dbg!(yaml_config_size);
|
|
||||||
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
|
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
|
||||||
dbg!(box_yaml_config_size);
|
|
||||||
dbg!(results);
|
|
||||||
assert!(box_yaml_config_size < yaml_config_size);
|
assert!(box_yaml_config_size < yaml_config_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试特殊字符转义功能
|
// 测试特殊字符转义功能
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
fn test_escape_unescape() {
|
fn test_escape_unescape() {
|
||||||
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
||||||
let escaped = escape_js_string_for_single_quote(test_string);
|
let escaped = escape_js_string_for_single_quote(test_string);
|
||||||
@@ -146,13 +169,14 @@ fn test_escape_unescape() {
|
|||||||
println!("Escaped: {escaped}");
|
println!("Escaped: {escaped}");
|
||||||
|
|
||||||
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
||||||
let parsed = parse_json_safely(json_str).unwrap();
|
let parsed = parse_json_safely(json_str).expect("Failed to parse test JSON safely");
|
||||||
|
|
||||||
assert!(parsed.contains_key("key"));
|
assert!(parsed.contains_key("key"));
|
||||||
assert!(parsed.contains_key("nested"));
|
assert!(parsed.contains_key("nested"));
|
||||||
|
|
||||||
let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#;
|
let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#;
|
||||||
let parsed_quoted = parse_json_safely(quoted_json_str).unwrap();
|
let parsed_quoted =
|
||||||
|
parse_json_safely(quoted_json_str).expect("Failed to parse quoted test JSON safely");
|
||||||
|
|
||||||
assert!(parsed_quoted.contains_key("key"));
|
assert!(parsed_quoted.contains_key("key"));
|
||||||
assert!(parsed_quoted.contains_key("nested"));
|
assert!(parsed_quoted.contains_key("nested"));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::{Mapping, Sequence, Value};
|
use serde_yaml_ng::{Mapping, Sequence, Value};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct SeqMap {
|
pub struct SeqMap {
|
||||||
@@ -44,39 +44,39 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
|||||||
config.insert(Value::String(field.into()), Value::Sequence(new_seq));
|
config.insert(Value::String(field.into()), Value::Sequence(new_seq));
|
||||||
|
|
||||||
// If this is proxies field, we also need to filter proxy-groups
|
// If this is proxies field, we also need to filter proxy-groups
|
||||||
if field == "proxies" {
|
if field == "proxies"
|
||||||
if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") {
|
&& let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups")
|
||||||
let mut new_groups = Sequence::new();
|
{
|
||||||
for group in groups {
|
let mut new_groups = Sequence::new();
|
||||||
if let Value::Mapping(group_map) = group {
|
for group in groups {
|
||||||
let mut new_group = group_map.clone();
|
if let Value::Mapping(group_map) = group {
|
||||||
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
|
let mut new_group = group_map.clone();
|
||||||
let filtered_proxies: Sequence = proxies
|
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
|
||||||
.iter()
|
let filtered_proxies: Sequence = proxies
|
||||||
.filter(|p| {
|
.iter()
|
||||||
if let Value::String(name) = p {
|
.filter(|p| {
|
||||||
!delete.contains(name)
|
if let Value::String(name) = p {
|
||||||
} else {
|
!delete.contains(name)
|
||||||
true
|
} else {
|
||||||
}
|
true
|
||||||
})
|
}
|
||||||
.cloned()
|
})
|
||||||
.collect();
|
.cloned()
|
||||||
new_group.insert(
|
.collect();
|
||||||
Value::String("proxies".into()),
|
new_group.insert(
|
||||||
Value::Sequence(filtered_proxies),
|
Value::String("proxies".into()),
|
||||||
);
|
Value::Sequence(filtered_proxies),
|
||||||
}
|
);
|
||||||
new_groups.push(Value::Mapping(new_group));
|
|
||||||
} else {
|
|
||||||
new_groups.push(group.clone());
|
|
||||||
}
|
}
|
||||||
|
new_groups.push(Value::Mapping(new_group));
|
||||||
|
} else {
|
||||||
|
new_groups.push(group.clone());
|
||||||
}
|
}
|
||||||
config.insert(
|
|
||||||
Value::String("proxy-groups".into()),
|
|
||||||
Value::Sequence(new_groups),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
config.insert(
|
||||||
|
Value::String("proxy-groups".into()),
|
||||||
|
Value::Sequence(new_groups),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
config
|
config
|
||||||
@@ -86,9 +86,11 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use serde_yaml::Value;
|
use serde_yaml_ng::Value;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
fn test_delete_proxy_and_references() {
|
fn test_delete_proxy_and_references() {
|
||||||
let config_str = r#"
|
let config_str = r#"
|
||||||
proxies:
|
proxies:
|
||||||
@@ -107,7 +109,8 @@ proxy-groups:
|
|||||||
proxies:
|
proxies:
|
||||||
- "proxy1"
|
- "proxy1"
|
||||||
"#;
|
"#;
|
||||||
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
|
let mut config: Mapping =
|
||||||
|
serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML");
|
||||||
|
|
||||||
let seq = SeqMap {
|
let seq = SeqMap {
|
||||||
prepend: Sequence::new(),
|
prepend: Sequence::new(),
|
||||||
@@ -118,38 +121,51 @@ proxy-groups:
|
|||||||
config = use_seq(seq, config, "proxies");
|
config = use_seq(seq, config, "proxies");
|
||||||
|
|
||||||
// Check if proxy1 is removed from proxies
|
// Check if proxy1 is removed from proxies
|
||||||
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
|
let proxies = config
|
||||||
|
.get("proxies")
|
||||||
|
.expect("proxies field should exist")
|
||||||
|
.as_sequence()
|
||||||
|
.expect("proxies should be a sequence");
|
||||||
assert_eq!(proxies.len(), 1);
|
assert_eq!(proxies.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
proxies[0]
|
proxies[0]
|
||||||
.as_mapping()
|
.as_mapping()
|
||||||
.unwrap()
|
.expect("proxy should be a mapping")
|
||||||
.get("name")
|
.get("name")
|
||||||
.unwrap()
|
.expect("proxy should have name")
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap(),
|
.expect("name should be string"),
|
||||||
"proxy2"
|
"proxy2"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if proxy1 is removed from all groups
|
// Check if proxy1 is removed from all groups
|
||||||
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
|
let groups = config
|
||||||
|
.get("proxy-groups")
|
||||||
|
.expect("proxy-groups field should exist")
|
||||||
|
.as_sequence()
|
||||||
|
.expect("proxy-groups should be a sequence");
|
||||||
let group1_proxies = groups[0]
|
let group1_proxies = groups[0]
|
||||||
.as_mapping()
|
.as_mapping()
|
||||||
.unwrap()
|
.expect("group should be a mapping")
|
||||||
.get("proxies")
|
.get("proxies")
|
||||||
.unwrap()
|
.expect("group should have proxies")
|
||||||
.as_sequence()
|
.as_sequence()
|
||||||
.unwrap();
|
.expect("group proxies should be a sequence");
|
||||||
let group2_proxies = groups[1]
|
let group2_proxies = groups[1]
|
||||||
.as_mapping()
|
.as_mapping()
|
||||||
.unwrap()
|
.expect("group should be a mapping")
|
||||||
.get("proxies")
|
.get("proxies")
|
||||||
.unwrap()
|
.expect("group should have proxies")
|
||||||
.as_sequence()
|
.as_sequence()
|
||||||
.unwrap();
|
.expect("group proxies should be a sequence");
|
||||||
|
|
||||||
assert_eq!(group1_proxies.len(), 1);
|
assert_eq!(group1_proxies.len(), 1);
|
||||||
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
|
assert_eq!(
|
||||||
|
group1_proxies[0]
|
||||||
|
.as_str()
|
||||||
|
.expect("proxy name should be string"),
|
||||||
|
"proxy2"
|
||||||
|
);
|
||||||
assert_eq!(group2_proxies.len(), 0);
|
assert_eq!(group2_proxies.len(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use crate::process::AsyncHandler;
|
||||||
|
|
||||||
macro_rules! revise {
|
macro_rules! revise {
|
||||||
($map: expr, $key: expr, $val: expr) => {
|
($map: expr, $key: expr, $val: expr) => {
|
||||||
@@ -18,7 +21,7 @@ macro_rules! append {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||||
let tun_key = Value::from("tun");
|
let tun_key = Value::from("tun");
|
||||||
let tun_val = config.get(&tun_key);
|
let tun_val = config.get(&tun_key);
|
||||||
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
||||||
@@ -59,8 +62,10 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
crate::utils::resolve::restore_public_dns().await;
|
AsyncHandler::spawn(move || async move {
|
||||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
crate::utils::resolve::dns::restore_public_dns().await;
|
||||||
|
crate::utils::resolve::dns::set_public_dns("223.6.6.6".to_string()).await;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +74,9 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
|||||||
} else {
|
} else {
|
||||||
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
|
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
crate::utils::resolve::restore_public_dns().await;
|
AsyncHandler::spawn(move || async move {
|
||||||
|
crate::utils::resolve::dns::restore_public_dns().await;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新TUN配置
|
// 更新TUN配置
|
||||||
|
|||||||
@@ -51,13 +51,15 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
|||||||
|
|
||||||
/// Restore WebDAV backup
|
/// Restore WebDAV backup
|
||||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||||
let verge = Config::verge();
|
let verge = Config::verge().await;
|
||||||
let verge_data = verge.latest_ref().clone();
|
let verge_data = verge.latest_ref().clone();
|
||||||
let webdav_url = verge_data.webdav_url.clone();
|
let webdav_url = verge_data.webdav_url.clone();
|
||||||
let webdav_username = verge_data.webdav_username.clone();
|
let webdav_username = verge_data.webdav_username.clone();
|
||||||
let webdav_password = verge_data.webdav_password.clone();
|
let webdav_password = verge_data.webdav_password.clone();
|
||||||
|
|
||||||
let backup_storage_path = app_home_dir().unwrap().join(&filename);
|
let backup_storage_path = app_home_dir()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
|
||||||
|
.join(&filename);
|
||||||
backup::WebDavClient::global()
|
backup::WebDavClient::global()
|
||||||
.download(filename, backup_storage_path.clone())
|
.download(filename, backup_storage_path.clone())
|
||||||
.await
|
.await
|
||||||
|
|||||||
+60
-48
@@ -1,49 +1,55 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
core::{handle, tray, CoreManager},
|
core::{CoreManager, handle, tray},
|
||||||
|
ipc::IpcManager,
|
||||||
logging_error,
|
logging_error,
|
||||||
module::mihomo::MihomoManager,
|
|
||||||
process::AsyncHandler,
|
process::AsyncHandler,
|
||||||
utils::{logging::Type, resolve},
|
utils::{logging::Type, resolve},
|
||||||
};
|
};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
/// Restart the Clash core
|
/// Restart the Clash core
|
||||||
pub fn restart_clash_core() {
|
pub async fn restart_clash_core() {
|
||||||
AsyncHandler::spawn(move || async move {
|
match CoreManager::global().restart_core().await {
|
||||||
match CoreManager::global().restart_core().await {
|
Ok(_) => {
|
||||||
Ok(_) => {
|
handle::Handle::refresh_clash();
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::notice_message("set_config::ok", "ok");
|
||||||
handle::Handle::notice_message("set_config::ok", "ok");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
|
||||||
log::error!(target:"app", "{err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
Err(err) => {
|
||||||
|
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||||
|
log::error!(target:"app", "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restart the application
|
/// Restart the application
|
||||||
pub fn restart_app() {
|
pub async fn restart_app() {
|
||||||
AsyncHandler::spawn(move || async move {
|
// logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||||
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
resolve::resolve_reset_async().await;
|
||||||
resolve::resolve_reset_async().await;
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
handle::Handle::global()
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
.app_handle()
|
||||||
tauri::process::restart(&app_handle.env());
|
.map(|app_handle| {
|
||||||
});
|
app_handle.restart();
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
logging_error!(
|
||||||
|
Type::System,
|
||||||
|
false,
|
||||||
|
"{}",
|
||||||
|
"Failed to get app handle for restart"
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn after_change_clash_mode() {
|
fn after_change_clash_mode() {
|
||||||
AsyncHandler::spawn(move || async {
|
AsyncHandler::spawn(move || async {
|
||||||
match MihomoManager::global().get_connections().await {
|
match IpcManager::global().get_connections().await {
|
||||||
Ok(connections) => {
|
Ok(connections) => {
|
||||||
if let Some(connections_array) = connections["connections"].as_array() {
|
if let Some(connections_array) = connections["connections"].as_array() {
|
||||||
for connection in connections_array {
|
for connection in connections_array {
|
||||||
if let Some(id) = connection["id"].as_str() {
|
if let Some(id) = connection["id"].as_str() {
|
||||||
let _ = MihomoManager::global().delete_connection(id).await;
|
let _ = IpcManager::global().delete_connection(id).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,37 +62,42 @@ fn after_change_clash_mode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Change Clash mode (rule/global/direct/script)
|
/// Change Clash mode (rule/global/direct/script)
|
||||||
pub fn change_clash_mode(mode: String) {
|
pub async fn change_clash_mode(mode: String) {
|
||||||
let mut mapping = Mapping::new();
|
let mut mapping = Mapping::new();
|
||||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||||
// Convert YAML mapping to JSON Value
|
// Convert YAML mapping to JSON Value
|
||||||
let json_value = serde_json::json!({
|
let json_value = serde_json::json!({
|
||||||
"mode": mode
|
"mode": mode
|
||||||
});
|
});
|
||||||
AsyncHandler::spawn(move || async move {
|
log::debug!(target: "app", "change clash mode to {mode}");
|
||||||
log::debug!(target: "app", "change clash mode to {mode}");
|
match IpcManager::global().patch_configs(json_value).await {
|
||||||
match MihomoManager::global().patch_configs(json_value).await {
|
Ok(_) => {
|
||||||
Ok(_) => {
|
// 更新订阅
|
||||||
// 更新订阅
|
Config::clash().await.data_mut().patch_config(mapping);
|
||||||
Config::clash().data_mut().patch_config(mapping);
|
|
||||||
|
|
||||||
if Config::clash().data_mut().save_config().is_ok() {
|
// 分离数据获取和异步调用
|
||||||
handle::Handle::refresh_clash();
|
let clash_data = Config::clash().await.data_mut().clone();
|
||||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
if clash_data.save_config().await.is_ok() {
|
||||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
handle::Handle::refresh_clash();
|
||||||
}
|
logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
|
||||||
|
logging_error!(
|
||||||
|
Type::Tray,
|
||||||
|
true,
|
||||||
|
tray::Tray::global().update_icon(None).await
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let is_auto_close_connection = Config::verge()
|
let is_auto_close_connection = Config::verge()
|
||||||
.data_mut()
|
.await
|
||||||
.auto_close_connection
|
.data_mut()
|
||||||
.unwrap_or(false);
|
.auto_close_connection
|
||||||
if is_auto_close_connection {
|
.unwrap_or(false);
|
||||||
after_change_clash_mode();
|
if is_auto_close_connection {
|
||||||
}
|
after_change_clash_mode();
|
||||||
}
|
}
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
|
||||||
}
|
}
|
||||||
});
|
Err(err) => log::error!(target: "app", "{err}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test connection delay to a URL
|
/// Test connection delay to a URL
|
||||||
@@ -95,6 +106,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
|||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
|
|
||||||
let tun_mode = Config::verge()
|
let tun_mode = Config::verge()
|
||||||
|
.await
|
||||||
.latest_ref()
|
.latest_ref()
|
||||||
.enable_tun_mode
|
.enable_tun_mode
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@@ -110,7 +122,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
|||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
let response = NetworkManager::global()
|
let response = NetworkManager::new()
|
||||||
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
|
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::{handle, hotkey, sysopt, tray, CoreManager},
|
core::{CoreManager, handle, hotkey, sysopt, tray},
|
||||||
logging_error,
|
logging_error,
|
||||||
module::lightweight,
|
module::lightweight,
|
||||||
utils::logging::Type,
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
|
|
||||||
/// Patch Clash configuration
|
/// Patch Clash configuration
|
||||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||||
Config::clash().draft_mut().patch_config(patch.clone());
|
Config::clash()
|
||||||
|
.await
|
||||||
|
.draft_mut()
|
||||||
|
.patch_config(patch.clone());
|
||||||
|
|
||||||
let res = {
|
let res = {
|
||||||
// 激活订阅
|
// 激活订阅
|
||||||
@@ -19,10 +22,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
|||||||
CoreManager::global().restart_core().await?;
|
CoreManager::global().restart_core().await?;
|
||||||
} else {
|
} else {
|
||||||
if patch.get("mode").is_some() {
|
if patch.get("mode").is_some() {
|
||||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
|
||||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
logging_error!(
|
||||||
|
Type::Tray,
|
||||||
|
true,
|
||||||
|
tray::Tray::global().update_icon(None).await
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Config::runtime().draft_mut().patch_config(patch);
|
Config::runtime().await.draft_mut().patch_config(patch);
|
||||||
CoreManager::global().update_config().await?;
|
CoreManager::global().update_config().await?;
|
||||||
}
|
}
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
@@ -30,12 +37,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
|||||||
};
|
};
|
||||||
match res {
|
match res {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
Config::clash().apply();
|
Config::clash().await.apply();
|
||||||
Config::clash().data_mut().save_config()?;
|
// 分离数据获取和异步调用
|
||||||
|
let clash_data = Config::clash().await.data_mut().clone();
|
||||||
|
clash_data.save_config().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
Config::clash().discard();
|
Config::clash().await.discard();
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +69,10 @@ enum UpdateFlags {
|
|||||||
|
|
||||||
/// Patch Verge configuration
|
/// Patch Verge configuration
|
||||||
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||||
Config::verge().draft_mut().patch_config(patch.clone());
|
Config::verge()
|
||||||
|
.await
|
||||||
|
.draft_mut()
|
||||||
|
.patch_config(patch.clone());
|
||||||
|
|
||||||
let tun_mode = patch.enable_tun_mode;
|
let tun_mode = patch.enable_tun_mode;
|
||||||
let auto_launch = patch.enable_auto_launch;
|
let auto_launch = patch.enable_auto_launch;
|
||||||
@@ -95,6 +107,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
let tray_event = patch.tray_event;
|
let tray_event = patch.tray_event;
|
||||||
let home_cards = patch.home_cards.clone();
|
let home_cards = patch.home_cards.clone();
|
||||||
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
|
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
|
||||||
|
let enable_external_controller = patch.enable_external_controller;
|
||||||
let res: std::result::Result<(), anyhow::Error> = {
|
let res: std::result::Result<(), anyhow::Error> = {
|
||||||
// Initialize with no flags set
|
// Initialize with no flags set
|
||||||
let mut update_flags: i32 = UpdateFlags::None as i32;
|
let mut update_flags: i32 = UpdateFlags::None as i32;
|
||||||
@@ -165,6 +178,11 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
update_flags |= UpdateFlags::LighteWeight as i32;
|
update_flags |= UpdateFlags::LighteWeight as i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理 external-controller 的开关
|
||||||
|
if enable_external_controller.is_some() {
|
||||||
|
update_flags |= UpdateFlags::RestartCore as i32;
|
||||||
|
}
|
||||||
|
|
||||||
// Process updates based on flags
|
// Process updates based on flags
|
||||||
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||||
Config::generate().await?;
|
Config::generate().await?;
|
||||||
@@ -175,33 +193,35 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
|
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
|
||||||
Config::verge().draft_mut().enable_global_hotkey = enable_global_hotkey;
|
Config::verge().await.draft_mut().enable_global_hotkey = enable_global_hotkey;
|
||||||
handle::Handle::refresh_verge();
|
handle::Handle::refresh_verge();
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
|
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
|
||||||
sysopt::Sysopt::global().update_launch()?;
|
sysopt::Sysopt::global().update_launch().await?;
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
|
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
|
||||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
|
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0
|
||||||
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
|
&& let Some(hotkeys) = patch.hotkeys
|
||||||
|
{
|
||||||
|
hotkey::Hotkey::global().update(hotkeys).await?;
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
|
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
|
||||||
tray::Tray::global().update_menu()?;
|
tray::Tray::global().update_menu().await?;
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
|
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
|
||||||
tray::Tray::global().update_icon(None)?;
|
tray::Tray::global().update_icon(None).await?;
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
|
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
|
||||||
tray::Tray::global().update_tooltip()?;
|
tray::Tray::global().update_tooltip().await?;
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
|
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
|
||||||
tray::Tray::global().update_click_behavior()?;
|
tray::Tray::global().update_click_behavior().await?;
|
||||||
}
|
}
|
||||||
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
|
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
|
||||||
if enable_auto_light_weight.unwrap() {
|
if enable_auto_light_weight.unwrap_or(false) {
|
||||||
lightweight::enable_auto_light_weight_mode();
|
lightweight::enable_auto_light_weight_mode().await;
|
||||||
} else {
|
} else {
|
||||||
lightweight::disable_auto_light_weight_mode();
|
lightweight::disable_auto_light_weight_mode();
|
||||||
}
|
}
|
||||||
@@ -211,15 +231,17 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
|||||||
};
|
};
|
||||||
match res {
|
match res {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
Config::verge().apply();
|
Config::verge().await.apply();
|
||||||
if !not_save_file {
|
if !not_save_file {
|
||||||
Config::verge().data_mut().save_file()?;
|
// 分离数据获取和异步调用
|
||||||
|
let verge_data = Config::verge().await.data_mut().clone();
|
||||||
|
verge_data.save_file().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
Config::verge().discard();
|
Config::verge().await.discard();
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
cmd,
|
cmd,
|
||||||
config::{Config, PrfItem, PrfOption},
|
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
|
||||||
core::{handle, CoreManager, *},
|
core::{CoreManager, handle, tray},
|
||||||
logging,
|
logging,
|
||||||
process::AsyncHandler,
|
|
||||||
utils::logging::Type,
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
/// Toggle proxy profile
|
/// Toggle proxy profile
|
||||||
pub fn toggle_proxy_profile(profile_index: String) {
|
pub async fn toggle_proxy_profile(profile_index: String) {
|
||||||
AsyncHandler::spawn(|| async move {
|
match cmd::patch_profiles_config_by_profile_index(profile_index).await {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
Ok(_) => {
|
||||||
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
let result = tray::Tray::global().update_menu().await;
|
||||||
Ok(_) => {
|
if let Err(err) = result {
|
||||||
let _ = tray::Tray::global().update_menu();
|
logging!(error, Type::Tray, true, "更新菜单失败: {}", err);
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "app", "{err}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
Err(err) => {
|
||||||
|
log::error!(target: "app", "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a profile
|
/// Update a profile
|
||||||
@@ -35,7 +34,7 @@ pub async fn update_profile(
|
|||||||
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
|
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
|
||||||
|
|
||||||
let url_opt = {
|
let url_opt = {
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles().await;
|
||||||
let profiles = profiles.latest_ref();
|
let profiles = profiles.latest_ref();
|
||||||
let item = profiles.get_item(&uid)?;
|
let item = profiles.get_item(&uid)?;
|
||||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||||
@@ -50,9 +49,14 @@ pub async fn update_profile(
|
|||||||
log::info!(target: "app",
|
log::info!(target: "app",
|
||||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||||
uid,
|
uid,
|
||||||
item.url.clone().unwrap()
|
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
|
||||||
);
|
);
|
||||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
Some((
|
||||||
|
item.url
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
|
||||||
|
item.option.clone(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,11 +69,13 @@ pub async fn update_profile(
|
|||||||
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||||
Ok(item) => {
|
Ok(item) => {
|
||||||
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles().await;
|
||||||
let mut profiles = profiles.draft_mut();
|
|
||||||
profiles.update_item(uid.clone(), item)?;
|
|
||||||
|
|
||||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
// 使用Send-safe helper函数
|
||||||
|
let result = profiles_draft_update_item_safe(uid.clone(), item).await;
|
||||||
|
result?;
|
||||||
|
|
||||||
|
let is_current = Some(uid.clone()) == profiles.latest_ref().get_current();
|
||||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||||
is_current && auto_refresh
|
is_current && auto_refresh
|
||||||
}
|
}
|
||||||
@@ -101,9 +107,10 @@ pub async fn update_profile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新到配置
|
// 更新到配置
|
||||||
let profiles = Config::profiles();
|
let profiles = Config::profiles().await;
|
||||||
let mut profiles = profiles.draft_mut();
|
|
||||||
profiles.update_item(uid.clone(), item.clone())?;
|
// 使用 Send-safe 方法进行数据操作
|
||||||
|
profiles_draft_update_item_safe(uid.clone(), item.clone()).await?;
|
||||||
|
|
||||||
// 获取配置名称用于通知
|
// 获取配置名称用于通知
|
||||||
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
|
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
|
||||||
@@ -111,7 +118,7 @@ pub async fn update_profile(
|
|||||||
// 发送通知告知用户自动更新使用了回退机制
|
// 发送通知告知用户自动更新使用了回退机制
|
||||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||||
|
|
||||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
let is_current = Some(uid.clone()) == profiles.data_ref().get_current();
|
||||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||||
is_current && auto_refresh
|
is_current && auto_refresh
|
||||||
}
|
}
|
||||||
@@ -136,6 +143,15 @@ pub async fn update_profile(
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
|
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
|
if let Err(err) = cmd::proxy::force_refresh_proxies().await {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Config,
|
||||||
|
true,
|
||||||
|
"[订阅更新] 代理组刷新失败: {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
|
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
|
||||||
|
|||||||
+67
-54
@@ -1,81 +1,94 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::handle,
|
core::handle,
|
||||||
process::AsyncHandler,
|
ipc::IpcManager,
|
||||||
|
logging,
|
||||||
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
use std::env;
|
use std::env;
|
||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
|
|
||||||
/// Toggle system proxy on/off
|
/// Toggle system proxy on/off
|
||||||
pub fn toggle_system_proxy() {
|
pub async fn toggle_system_proxy() {
|
||||||
let enable = Config::verge().draft_mut().enable_system_proxy;
|
// 获取当前系统代理状态
|
||||||
let enable = enable.unwrap_or(false);
|
let enable = {
|
||||||
let auto_close_connection = Config::verge()
|
let verge = Config::verge().await;
|
||||||
.data_mut()
|
|
||||||
.auto_close_connection
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
AsyncHandler::spawn(move || async move {
|
verge.latest_ref().enable_system_proxy.unwrap_or(false)
|
||||||
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
|
};
|
||||||
if enable && auto_close_connection {
|
// 获取自动关闭连接设置
|
||||||
if let Err(err) = crate::module::mihomo::MihomoManager::global()
|
let auto_close_connection = {
|
||||||
.close_all_connections()
|
let verge = Config::verge().await;
|
||||||
.await
|
|
||||||
{
|
|
||||||
log::error!(target: "app", "Failed to close all connections: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match super::patch_verge(
|
verge.latest_ref().auto_close_connection.unwrap_or(false)
|
||||||
IVerge {
|
};
|
||||||
enable_system_proxy: Some(!enable),
|
|
||||||
..IVerge::default()
|
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
|
||||||
},
|
if enable
|
||||||
false,
|
&& auto_close_connection
|
||||||
)
|
&& let Err(err) = IpcManager::global().close_all_connections().await
|
||||||
.await
|
{
|
||||||
{
|
log::error!(target: "app", "Failed to close all connections: {err}");
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
}
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
|
||||||
}
|
let patch_result = super::patch_verge(
|
||||||
});
|
IVerge {
|
||||||
|
enable_system_proxy: Some(!enable),
|
||||||
|
..IVerge::default()
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match patch_result {
|
||||||
|
Ok(_) => handle::Handle::refresh_verge(),
|
||||||
|
Err(err) => log::error!(target: "app", "{err}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle TUN mode on/off
|
/// Toggle TUN mode on/off
|
||||||
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||||
let enable = Config::verge().data_mut().enable_tun_mode;
|
let enable = Config::verge().await.data_mut().enable_tun_mode;
|
||||||
let enable = enable.unwrap_or(false);
|
let enable = enable.unwrap_or(false);
|
||||||
|
|
||||||
AsyncHandler::spawn(async move || {
|
match super::patch_verge(
|
||||||
match super::patch_verge(
|
IVerge {
|
||||||
IVerge {
|
enable_tun_mode: Some(!enable),
|
||||||
enable_tun_mode: Some(!enable),
|
..IVerge::default()
|
||||||
..IVerge::default()
|
},
|
||||||
},
|
not_save_file.unwrap_or(false),
|
||||||
not_save_file.unwrap_or(false),
|
)
|
||||||
)
|
.await
|
||||||
.await
|
{
|
||||||
{
|
Ok(_) => handle::Handle::refresh_verge(),
|
||||||
Ok(_) => handle::Handle::refresh_verge(),
|
Err(err) => log::error!(target: "app", "{err}"),
|
||||||
Err(err) => log::error!(target: "app", "{err}"),
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy proxy environment variables to clipboard
|
/// Copy proxy environment variables to clipboard
|
||||||
pub fn copy_clash_env() {
|
pub async fn copy_clash_env() {
|
||||||
// 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1
|
// 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1
|
||||||
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| {
|
let clash_verge_rev_ip = match env::var("CLASH_VERGE_REV_IP") {
|
||||||
Config::verge()
|
Ok(ip) => ip,
|
||||||
|
Err(_) => Config::verge()
|
||||||
|
.await
|
||||||
.latest_ref()
|
.latest_ref()
|
||||||
.proxy_host
|
.proxy_host
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "127.0.0.1".to_string())
|
.unwrap_or_else(|| "127.0.0.1".to_string()),
|
||||||
});
|
};
|
||||||
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let Some(app_handle) = handle::Handle::global().app_handle() else {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::System,
|
||||||
|
"Failed to get app handle for proxy operation"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
let port = {
|
let port = {
|
||||||
Config::verge()
|
Config::verge()
|
||||||
|
.await
|
||||||
.latest_ref()
|
.latest_ref()
|
||||||
.verge_mixed_port
|
.verge_mixed_port
|
||||||
.unwrap_or(7897)
|
.unwrap_or(7897)
|
||||||
@@ -84,7 +97,7 @@ pub fn copy_clash_env() {
|
|||||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
|
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
|
||||||
|
|
||||||
let cliboard = app_handle.clipboard();
|
let cliboard = app_handle.clipboard();
|
||||||
let env_type = { Config::verge().latest_ref().env_type.clone() };
|
let env_type = { Config::verge().await.latest_ref().env_type.clone() };
|
||||||
let env_type = match env_type {
|
let env_type = match env_type {
|
||||||
Some(env_type) => env_type,
|
Some(env_type) => env_type,
|
||||||
None => {
|
None => {
|
||||||
|
|||||||
+70
-102
@@ -1,73 +1,38 @@
|
|||||||
#[cfg(target_os = "macos")]
|
use crate::utils::window_manager::WindowManager;
|
||||||
use crate::AppHandleManager;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
core::{handle, sysopt, CoreManager},
|
core::{CoreManager, handle, sysopt},
|
||||||
|
ipc::IpcManager,
|
||||||
logging,
|
logging,
|
||||||
module::mihomo::MihomoManager,
|
module::lightweight,
|
||||||
utils::logging::Type,
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Open or close the dashboard window
|
/// Open or close the dashboard window
|
||||||
#[allow(dead_code)]
|
pub async fn open_or_close_dashboard() {
|
||||||
pub fn open_or_close_dashboard() {
|
open_or_close_dashboard_internal().await
|
||||||
open_or_close_dashboard_internal(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open or close the dashboard window (hotkey call, dispatched to main thread)
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn open_or_close_dashboard_hotkey() {
|
|
||||||
open_or_close_dashboard_internal(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal implementation for opening/closing dashboard
|
/// Internal implementation for opening/closing dashboard
|
||||||
fn open_or_close_dashboard_internal(bypass_debounce: bool) {
|
async fn open_or_close_dashboard_internal() {
|
||||||
use crate::process::AsyncHandler;
|
let _ = lightweight::exit_lightweight_mode().await;
|
||||||
use crate::utils::window_manager::WindowManager;
|
let result = WindowManager::toggle_main_window().await;
|
||||||
|
|
||||||
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
|
|
||||||
|
|
||||||
// 热键调用调度到主线程执行,避免 WebView 创建死锁
|
|
||||||
if bypass_debounce {
|
|
||||||
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
|
|
||||||
|
|
||||||
AsyncHandler::spawn(move || async move {
|
|
||||||
log::info!(target: "app", "主线程中执行热键窗口操作");
|
|
||||||
|
|
||||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
|
||||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
|
||||||
crate::module::lightweight::exit_lightweight_mode();
|
|
||||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
|
||||||
let result = WindowManager::show_main_window();
|
|
||||||
log::info!(target: "app", "Window operation result: {result:?}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = WindowManager::toggle_main_window();
|
|
||||||
log::info!(target: "app", "Window toggle result: {result:?}");
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
|
||||||
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
|
|
||||||
crate::module::lightweight::exit_lightweight_mode();
|
|
||||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
|
||||||
let result = WindowManager::show_main_window();
|
|
||||||
log::info!(target: "app", "Window operation result: {result:?}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = WindowManager::toggle_main_window();
|
|
||||||
log::info!(target: "app", "Window toggle result: {result:?}");
|
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 异步优化的应用退出函数
|
/// 异步优化的应用退出函数
|
||||||
pub fn quit() {
|
pub async fn quit() {
|
||||||
use crate::process::AsyncHandler;
|
|
||||||
logging!(debug, Type::System, true, "启动退出流程");
|
logging!(debug, Type::System, true, "启动退出流程");
|
||||||
|
|
||||||
// 获取应用句柄并设置退出标志
|
// 获取应用句柄并设置退出标志
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let Some(app_handle) = handle::Handle::global().app_handle() else {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::System,
|
||||||
|
"Failed to get app handle for quit operation"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
handle::Handle::global().set_is_exiting();
|
handle::Handle::global().set_is_exiting();
|
||||||
|
|
||||||
// 优先关闭窗口,提供立即反馈
|
// 优先关闭窗口,提供立即反馈
|
||||||
@@ -77,52 +42,54 @@ pub fn quit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用异步任务处理资源清理,避免阻塞
|
// 使用异步任务处理资源清理,避免阻塞
|
||||||
AsyncHandler::spawn(move || async move {
|
logging!(info, Type::System, true, "开始异步清理资源");
|
||||||
logging!(info, Type::System, true, "开始异步清理资源");
|
let cleanup_result = clean_async().await;
|
||||||
let cleanup_result = clean_async().await;
|
|
||||||
|
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::System,
|
Type::System,
|
||||||
true,
|
true,
|
||||||
"资源清理完成,退出代码: {}",
|
"资源清理完成,退出代码: {}",
|
||||||
if cleanup_result { 0 } else { 1 }
|
if cleanup_result { 0 } else { 1 }
|
||||||
);
|
);
|
||||||
app_handle.exit(if cleanup_result { 0 } else { 1 });
|
app_handle.exit(if cleanup_result { 0 } else { 1 });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn clean_async() -> bool {
|
async fn clean_async() -> bool {
|
||||||
use tokio::time::{timeout, Duration};
|
use tokio::time::{Duration, timeout};
|
||||||
|
|
||||||
logging!(info, Type::System, true, "开始执行异步清理操作...");
|
logging!(info, Type::System, true, "开始执行异步清理操作...");
|
||||||
|
|
||||||
// 1. 处理TUN模式
|
// 1. 处理TUN模式
|
||||||
let tun_task = async {
|
let tun_success = if Config::verge()
|
||||||
if Config::verge().data_mut().enable_tun_mode.unwrap_or(false) {
|
.await
|
||||||
let disable_tun = serde_json::json!({
|
.data_mut()
|
||||||
"tun": {
|
.enable_tun_mode
|
||||||
"enable": false
|
.unwrap_or(false)
|
||||||
}
|
{
|
||||||
});
|
let disable_tun = serde_json::json!({"tun": {"enable": false}});
|
||||||
match timeout(
|
match timeout(
|
||||||
Duration::from_secs(2),
|
Duration::from_secs(3),
|
||||||
MihomoManager::global().patch_configs(disable_tun),
|
IpcManager::global().patch_configs(disable_tun),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(Ok(_)) => {
|
||||||
log::info!(target: "app", "TUN模式已禁用");
|
log::info!(target: "app", "TUN模式已禁用");
|
||||||
true
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||||
}
|
true
|
||||||
Err(_) => {
|
}
|
||||||
log::warn!(target: "app", "禁用TUN模式超时");
|
Ok(Err(e)) => {
|
||||||
false
|
log::warn!(target: "app", "禁用TUN模式失败: {e}");
|
||||||
}
|
false
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!(target: "app", "禁用TUN模式超时");
|
||||||
|
false
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. 系统代理重置
|
// 2. 系统代理重置
|
||||||
@@ -163,7 +130,7 @@ async fn clean_async() -> bool {
|
|||||||
let dns_task = async {
|
let dns_task = async {
|
||||||
match timeout(
|
match timeout(
|
||||||
Duration::from_millis(1000),
|
Duration::from_millis(1000),
|
||||||
crate::utils::resolve::restore_public_dns(),
|
crate::utils::resolve::dns::restore_public_dns(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -178,8 +145,8 @@ async fn clean_async() -> bool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 并行执行所有清理任务
|
// 并行执行剩余清理任务
|
||||||
let (tun_success, proxy_success, core_success) = tokio::join!(tun_task, proxy_task, core_task);
|
let (proxy_success, core_success) = tokio::join!(proxy_task, core_task);
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let dns_success = dns_task.await;
|
let dns_success = dns_task.await;
|
||||||
@@ -192,7 +159,7 @@ async fn clean_async() -> bool {
|
|||||||
info,
|
info,
|
||||||
Type::System,
|
Type::System,
|
||||||
true,
|
true,
|
||||||
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
|
"异步关闭操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
|
||||||
tun_success,
|
tun_success,
|
||||||
proxy_success,
|
proxy_success,
|
||||||
core_success,
|
core_success,
|
||||||
@@ -209,7 +176,7 @@ pub fn clean() -> bool {
|
|||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
AsyncHandler::spawn(move || async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
logging!(info, Type::System, true, "开始执行清理操作...");
|
logging!(info, Type::System, true, "开始执行关闭操作...");
|
||||||
|
|
||||||
// 使用已有的异步清理函数
|
// 使用已有的异步清理函数
|
||||||
let cleanup_result = clean_async().await;
|
let cleanup_result = clean_async().await;
|
||||||
@@ -220,7 +187,7 @@ pub fn clean() -> bool {
|
|||||||
|
|
||||||
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
|
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
|
logging!(info, Type::System, true, "关闭操作完成,结果: {}", result);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -236,22 +203,23 @@ pub fn clean() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn hide() {
|
pub async fn hide() {
|
||||||
use crate::module::lightweight::add_light_weight_timer;
|
use crate::module::lightweight::add_light_weight_timer;
|
||||||
|
|
||||||
let enable_auto_light_weight_mode = Config::verge()
|
let enable_auto_light_weight_mode = Config::verge()
|
||||||
|
.await
|
||||||
.data_mut()
|
.data_mut()
|
||||||
.enable_auto_light_weight_mode
|
.enable_auto_light_weight_mode
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if enable_auto_light_weight_mode {
|
if enable_auto_light_weight_mode {
|
||||||
add_light_weight_timer();
|
add_light_weight_timer().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window()
|
||||||
if window.is_visible().unwrap_or(false) {
|
&& window.is_visible().unwrap_or(false)
|
||||||
let _ = window.hide();
|
{
|
||||||
}
|
let _ = window.hide();
|
||||||
}
|
}
|
||||||
AppHandleManager::global().set_activation_policy_accessory();
|
handle::Handle::global().set_activation_policy_accessory();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,370 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use kode_bridge::{
|
||||||
|
ClientConfig, IpcHttpClient, LegacyResponse,
|
||||||
|
errors::{AnyError, AnyResult},
|
||||||
|
};
|
||||||
|
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
logging, singleton_with_logging,
|
||||||
|
utils::{dirs::ipc_path, logging::Type},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定义用于URL路径的编码集合,只编码真正必要的字符
|
||||||
|
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||||
|
.add(b' ') // 空格
|
||||||
|
.add(b'/') // 斜杠
|
||||||
|
.add(b'?') // 问号
|
||||||
|
.add(b'#') // 井号
|
||||||
|
.add(b'&') // 和号
|
||||||
|
.add(b'%'); // 百分号
|
||||||
|
|
||||||
|
// Helper function to create AnyError from string
|
||||||
|
fn create_error(msg: impl Into<String>) -> AnyError {
|
||||||
|
Box::new(std::io::Error::other(msg.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IpcManager {
|
||||||
|
client: IpcHttpClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IpcManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
logging!(info, Type::Ipc, true, "Creating new IpcManager instance");
|
||||||
|
let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
|
||||||
|
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
|
||||||
|
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
|
||||||
|
});
|
||||||
|
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
|
||||||
|
let config = ClientConfig {
|
||||||
|
default_timeout: Duration::from_secs(5),
|
||||||
|
enable_pooling: false,
|
||||||
|
max_retries: 4,
|
||||||
|
retry_delay: Duration::from_millis(125),
|
||||||
|
max_concurrent_requests: 16,
|
||||||
|
max_requests_per_second: Some(64.0),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
let client = IpcHttpClient::with_config(ipc_path, config).unwrap();
|
||||||
|
Self { client }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IpcManager {
|
||||||
|
pub async fn request(
|
||||||
|
&self,
|
||||||
|
method: &str,
|
||||||
|
path: &str,
|
||||||
|
body: Option<&serde_json::Value>,
|
||||||
|
) -> AnyResult<LegacyResponse> {
|
||||||
|
self.client.request(method, path, body).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IpcManager {
|
||||||
|
pub async fn send_request(
|
||||||
|
&self,
|
||||||
|
method: &str,
|
||||||
|
path: &str,
|
||||||
|
body: Option<&serde_json::Value>,
|
||||||
|
) -> AnyResult<serde_json::Value> {
|
||||||
|
let response = IpcManager::global().request(method, path, body).await?;
|
||||||
|
match method {
|
||||||
|
"GET" => Ok(response.json()?),
|
||||||
|
"PATCH" => {
|
||||||
|
if response.status == 204 {
|
||||||
|
Ok(serde_json::json!({"code": 204}))
|
||||||
|
} else {
|
||||||
|
Ok(response.json()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"PUT" | "DELETE" => {
|
||||||
|
if response.status == 204 {
|
||||||
|
Ok(serde_json::json!({"code": 204}))
|
||||||
|
} else {
|
||||||
|
match response.json() {
|
||||||
|
Ok(json) => Ok(json),
|
||||||
|
Err(_) => Ok(serde_json::json!({
|
||||||
|
"code": response.status,
|
||||||
|
"message": response.body,
|
||||||
|
"error": "failed to parse response as JSON"
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => match response.json() {
|
||||||
|
Ok(json) => Ok(json),
|
||||||
|
Err(_) => Ok(serde_json::json!({
|
||||||
|
"code": response.status,
|
||||||
|
"message": response.body,
|
||||||
|
"error": "failed to parse response as JSON"
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础代理信息获取
|
||||||
|
pub async fn get_proxies(&self) -> AnyResult<serde_json::Value> {
|
||||||
|
let url = "/proxies";
|
||||||
|
self.send_request("GET", url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代理提供者信息获取
|
||||||
|
pub async fn get_providers_proxies(&self) -> AnyResult<serde_json::Value> {
|
||||||
|
let url = "/providers/proxies";
|
||||||
|
self.send_request("GET", url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接管理
|
||||||
|
pub async fn get_connections(&self) -> AnyResult<serde_json::Value> {
|
||||||
|
let url = "/connections";
|
||||||
|
self.send_request("GET", url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_connection(&self, id: &str) -> AnyResult<()> {
|
||||||
|
let encoded_id = utf8_percent_encode(id, URL_PATH_ENCODE_SET).to_string();
|
||||||
|
let url = format!("/connections/{encoded_id}");
|
||||||
|
let response = self.send_request("DELETE", &url, None).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error(
|
||||||
|
response["message"].as_str().unwrap_or("unknown error"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close_all_connections(&self) -> AnyResult<()> {
|
||||||
|
let url = "/connections";
|
||||||
|
let response = self.send_request("DELETE", url, None).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error")
|
||||||
|
.to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IpcManager {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn is_mihomo_running(&self) -> AnyResult<()> {
|
||||||
|
let url = "/version";
|
||||||
|
let _response = self.send_request("GET", url, None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn put_configs_force(&self, clash_config_path: &str) -> AnyResult<()> {
|
||||||
|
let url = "/configs?force=true";
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"path": clash_config_path,
|
||||||
|
});
|
||||||
|
let _response = self.send_request("PUT", url, Some(&payload)).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn patch_configs(&self, config: serde_json::Value) -> AnyResult<()> {
|
||||||
|
let url = "/configs";
|
||||||
|
let response = self.send_request("PATCH", url, Some(&config)).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error")
|
||||||
|
.to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_proxy_delay(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
test_url: Option<String>,
|
||||||
|
timeout: i32,
|
||||||
|
) -> AnyResult<serde_json::Value> {
|
||||||
|
let test_url =
|
||||||
|
test_url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
|
||||||
|
|
||||||
|
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
|
||||||
|
// 测速URL不再编码,直接传递
|
||||||
|
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
|
||||||
|
|
||||||
|
self.send_request("GET", &url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// 版本和配置相关
|
||||||
|
pub async fn get_version(&self) -> AnyResult<serde_json::Value> {
|
||||||
|
let url = "/version";
|
||||||
|
self.send_request("GET", url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_config(&self) -> AnyResult<serde_json::Value> {
|
||||||
|
let url = "/configs";
|
||||||
|
self.send_request("GET", url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_geo_data(&self) -> AnyResult<()> {
|
||||||
|
let url = "/configs/geo";
|
||||||
|
let response = self.send_request("POST", url, None).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error")
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upgrade_core(&self) -> AnyResult<()> {
|
||||||
|
let url = "/upgrade";
|
||||||
|
let response = self.send_request("POST", url, None).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error")
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规则相关
|
||||||
|
pub async fn get_rules(&self) -> AnyResult<serde_json::Value> {
|
||||||
|
let url = "/rules";
|
||||||
|
self.send_request("GET", url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_rule_providers(&self) -> AnyResult<serde_json::Value> {
|
||||||
|
let url = "/providers/rules";
|
||||||
|
self.send_request("GET", url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_rule_provider(&self, name: &str) -> AnyResult<()> {
|
||||||
|
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
|
||||||
|
let url = format!("/providers/rules/{encoded_name}");
|
||||||
|
let response = self.send_request("PUT", &url, None).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error")
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 代理相关
|
||||||
|
pub async fn update_proxy(&self, group: &str, proxy: &str) -> AnyResult<()> {
|
||||||
|
// 使用 percent-encoding 进行正确的 URL 编码
|
||||||
|
let encoded_group = utf8_percent_encode(group, URL_PATH_ENCODE_SET).to_string();
|
||||||
|
let url = format!("/proxies/{encoded_group}");
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"name": proxy
|
||||||
|
});
|
||||||
|
match self.send_request("PUT", &url, Some(&payload)).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
crate::utils::logging::Type::Ipc,
|
||||||
|
true,
|
||||||
|
"IPC: updateProxy encountered error: {} (ignored, always returning true)",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn proxy_provider_health_check(&self, name: &str) -> AnyResult<()> {
|
||||||
|
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
|
||||||
|
let url = format!("/providers/proxies/{encoded_name}/healthcheck");
|
||||||
|
let response = self.send_request("GET", &url, None).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error")
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_proxy_provider(&self, name: &str) -> AnyResult<()> {
|
||||||
|
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
|
||||||
|
let url = format!("/providers/proxies/{encoded_name}");
|
||||||
|
let response = self.send_request("PUT", &url, None).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error")
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟测试相关
|
||||||
|
pub async fn get_group_proxy_delays(
|
||||||
|
&self,
|
||||||
|
group_name: &str,
|
||||||
|
url: Option<String>,
|
||||||
|
timeout: i32,
|
||||||
|
) -> AnyResult<serde_json::Value> {
|
||||||
|
let test_url = url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
|
||||||
|
|
||||||
|
let encoded_group_name = utf8_percent_encode(group_name, URL_PATH_ENCODE_SET).to_string();
|
||||||
|
// 测速URL不再编码,直接传递
|
||||||
|
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
|
||||||
|
|
||||||
|
self.send_request("GET", &url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调试相关
|
||||||
|
pub async fn is_debug_enabled(&self) -> AnyResult<bool> {
|
||||||
|
let url = "/debug/pprof";
|
||||||
|
match self.send_request("GET", url, None).await {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn gc(&self) -> AnyResult<()> {
|
||||||
|
let url = "/debug/gc";
|
||||||
|
let response = self.send_request("PUT", url, None).await?;
|
||||||
|
if response["code"] == 204 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown error")
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use singleton macro with logging
|
||||||
|
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{collections::VecDeque, sync::Arc, time::Instant};
|
||||||
|
use tauri::async_runtime::JoinHandle;
|
||||||
|
use tokio::{sync::RwLock, time::Duration};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ipc::monitor::MonitorData,
|
||||||
|
logging,
|
||||||
|
process::AsyncHandler,
|
||||||
|
singleton_with_logging,
|
||||||
|
utils::{dirs::ipc_path, logging::Type},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_LOGS: usize = 1000; // Maximum number of logs to keep in memory
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct LogData {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub log_type: String,
|
||||||
|
pub payload: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LogItem {
|
||||||
|
pub log_type: String,
|
||||||
|
pub payload: String,
|
||||||
|
pub time: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogItem {
|
||||||
|
fn new(log_type: String, payload: String) -> Self {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
// Simple time formatting (HH:MM:SS)
|
||||||
|
let hours = (now / 3600) % 24;
|
||||||
|
let minutes = (now / 60) % 60;
|
||||||
|
let seconds = now % 60;
|
||||||
|
let time_str = format!("{hours:02}:{minutes:02}:{seconds:02}");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
log_type,
|
||||||
|
payload,
|
||||||
|
time: time_str,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CurrentLogs {
|
||||||
|
pub logs: VecDeque<LogItem>,
|
||||||
|
// pub level: String,
|
||||||
|
pub last_updated: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CurrentLogs {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
logs: VecDeque::with_capacity(MAX_LOGS),
|
||||||
|
// level: "info".to_string(),
|
||||||
|
last_updated: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonitorData for CurrentLogs {
|
||||||
|
fn mark_fresh(&mut self) {
|
||||||
|
self.last_updated = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_fresh_within(&self, duration: Duration) -> bool {
|
||||||
|
self.last_updated.elapsed() < duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs monitor with streaming support
|
||||||
|
pub struct LogsMonitor {
|
||||||
|
current: Arc<RwLock<CurrentLogs>>,
|
||||||
|
task_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
|
||||||
|
current_monitoring_level: Arc<RwLock<Option<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use singleton_with_logging macro
|
||||||
|
singleton_with_logging!(LogsMonitor, INSTANCE, "LogsMonitor");
|
||||||
|
|
||||||
|
impl LogsMonitor {
|
||||||
|
fn new() -> Self {
|
||||||
|
let current = Arc::new(RwLock::new(CurrentLogs::default()));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
current,
|
||||||
|
task_handle: Arc::new(RwLock::new(None)),
|
||||||
|
current_monitoring_level: Arc::new(RwLock::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_monitoring(&self, level: Option<String>) {
|
||||||
|
let filter_level = level.clone().unwrap_or_else(|| "info".to_string());
|
||||||
|
|
||||||
|
// Check if we're already monitoring the same level
|
||||||
|
// let level_changed = {
|
||||||
|
// let current_level = self.current_monitoring_level.read().await;
|
||||||
|
// if let Some(existing_level) = current_level.as_ref() {
|
||||||
|
// if existing_level == &filter_level {
|
||||||
|
// logging!(
|
||||||
|
// info,
|
||||||
|
// Type::Ipc,
|
||||||
|
// true,
|
||||||
|
// "LogsMonitor: Already monitoring level '{}', skipping duplicate request",
|
||||||
|
// filter_level
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// true // Level changed
|
||||||
|
// } else {
|
||||||
|
// true // First time or was stopped
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Stop existing monitoring task if level changed or first time
|
||||||
|
{
|
||||||
|
let mut handle = self.task_handle.write().await;
|
||||||
|
if let Some(task) = handle.take() {
|
||||||
|
task.abort();
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Ipc,
|
||||||
|
true,
|
||||||
|
"LogsMonitor: Stopped previous monitoring task (level changed)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to keep the logs cache even if the level changes,
|
||||||
|
// so we don't clear it here. The cache will be cleared only when the level changes
|
||||||
|
// and a new task is started. This allows us to keep logs from previous levels
|
||||||
|
// even if the level changes during monitoring.
|
||||||
|
// Clear logs cache when level changes to ensure fresh data
|
||||||
|
// if level_changed {
|
||||||
|
// let mut current = self.current.write().await;
|
||||||
|
// current.logs.clear();
|
||||||
|
// current.level = filter_level.clone();
|
||||||
|
// current.mark_fresh();
|
||||||
|
// logging!(
|
||||||
|
// info,
|
||||||
|
// Type::Ipc,
|
||||||
|
// true,
|
||||||
|
// "LogsMonitor: Cleared logs cache due to level change to '{}'",
|
||||||
|
// filter_level
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Update current monitoring level
|
||||||
|
{
|
||||||
|
let mut current_level = self.current_monitoring_level.write().await;
|
||||||
|
*current_level = Some(filter_level.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let monitor_current = Arc::clone(&self.current);
|
||||||
|
|
||||||
|
let task = AsyncHandler::spawn(move || async move {
|
||||||
|
loop {
|
||||||
|
// Get fresh IPC path and client for each connection attempt
|
||||||
|
let (_ipc_path_buf, client) = match Self::create_ipc_client() {
|
||||||
|
Ok((path, client)) => (path, client),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = if filter_level == "all" {
|
||||||
|
"/logs".to_string()
|
||||||
|
} else {
|
||||||
|
format!("/logs?level={filter_level}")
|
||||||
|
};
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Ipc,
|
||||||
|
true,
|
||||||
|
"LogsMonitor: Starting stream for {}",
|
||||||
|
url
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = client
|
||||||
|
.get(&url)
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.process_lines(|line| {
|
||||||
|
Self::process_log_line(line, Arc::clone(&monitor_current))
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the task handle
|
||||||
|
{
|
||||||
|
let mut handle = self.task_handle.write().await;
|
||||||
|
*handle = Some(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Ipc,
|
||||||
|
true,
|
||||||
|
"LogsMonitor: Started new monitoring task for level: {:?}",
|
||||||
|
level
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop_monitoring(&self) {
|
||||||
|
// Stop monitoring task but keep logs
|
||||||
|
{
|
||||||
|
let mut handle = self.task_handle.write().await;
|
||||||
|
if let Some(task) = handle.take() {
|
||||||
|
task.abort();
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Ipc,
|
||||||
|
true,
|
||||||
|
"LogsMonitor: Stopped monitoring task"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset monitoring level
|
||||||
|
{
|
||||||
|
let mut monitoring_level = self.current_monitoring_level.write().await;
|
||||||
|
*monitoring_level = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_ipc_client() -> Result<
|
||||||
|
(std::path::PathBuf, kode_bridge::IpcStreamClient),
|
||||||
|
Box<dyn std::error::Error + Send + Sync>,
|
||||||
|
> {
|
||||||
|
use kode_bridge::IpcStreamClient;
|
||||||
|
|
||||||
|
let ipc_path_buf = ipc_path()?;
|
||||||
|
let ipc_path = ipc_path_buf.to_str().ok_or("Invalid IPC path")?;
|
||||||
|
let client = IpcStreamClient::new(ipc_path)?;
|
||||||
|
Ok((ipc_path_buf, client))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_log_line(
|
||||||
|
line: &str,
|
||||||
|
current: Arc<RwLock<CurrentLogs>>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
if let Ok(log_data) = serde_json::from_str::<LogData>(line.trim()) {
|
||||||
|
// Server-side filtering via query parameters handles the level filtering
|
||||||
|
// We only need to accept all logs since filtering is done at the endpoint level
|
||||||
|
let log_item = LogItem::new(log_data.log_type, log_data.payload);
|
||||||
|
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
let mut logs = current.write().await;
|
||||||
|
|
||||||
|
// Add new log
|
||||||
|
logs.logs.push_back(log_item);
|
||||||
|
|
||||||
|
// Keep only the last 1000 logs
|
||||||
|
if logs.logs.len() > 1000 {
|
||||||
|
logs.logs.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.mark_fresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn current(&self) -> CurrentLogs {
|
||||||
|
self.current.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_logs(&self) {
|
||||||
|
let mut current = self.current.write().await;
|
||||||
|
current.logs.clear();
|
||||||
|
current.mark_fresh();
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Ipc,
|
||||||
|
true,
|
||||||
|
"LogsMonitor: Cleared frontend logs (monitoring continues)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_logs_as_json(&self) -> serde_json::Value {
|
||||||
|
let current = self.current().await;
|
||||||
|
|
||||||
|
// Simply return all cached logs since filtering is handled by start_monitoring
|
||||||
|
// and the cache is cleared when level changes
|
||||||
|
let logs: Vec<serde_json::Value> = current
|
||||||
|
.logs
|
||||||
|
.iter()
|
||||||
|
.map(|log| {
|
||||||
|
serde_json::json!({
|
||||||
|
"type": log.log_type,
|
||||||
|
"payload": log.payload,
|
||||||
|
"time": log.time
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
serde_json::Value::Array(logs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_logs_monitoring(level: Option<String>) {
|
||||||
|
LogsMonitor::global().start_monitoring(level).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop_logs_monitoring() {
|
||||||
|
LogsMonitor::global().stop_monitoring().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_logs() {
|
||||||
|
LogsMonitor::global().clear_logs().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_logs_json() -> serde_json::Value {
|
||||||
|
LogsMonitor::global().get_logs_as_json().await
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{sync::Arc, time::Instant};
|
||||||
|
use tokio::{sync::RwLock, time::Duration};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
|
||||||
|
process::AsyncHandler,
|
||||||
|
singleton_lazy_with_logging,
|
||||||
|
utils::format::fmt_bytes,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct MemoryData {
|
||||||
|
pub inuse: u64,
|
||||||
|
pub oslimit: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CurrentMemory {
|
||||||
|
pub inuse: u64,
|
||||||
|
pub oslimit: u64,
|
||||||
|
pub last_updated: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CurrentMemory {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inuse: 0,
|
||||||
|
oslimit: 0,
|
||||||
|
last_updated: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonitorData for CurrentMemory {
|
||||||
|
fn mark_fresh(&mut self) {
|
||||||
|
self.last_updated = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_fresh_within(&self, duration: Duration) -> bool {
|
||||||
|
self.last_updated.elapsed() < duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamingParser for CurrentMemory {
|
||||||
|
fn parse_and_update(
|
||||||
|
line: &str,
|
||||||
|
current: Arc<RwLock<Self>>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
if let Ok(memory) = serde_json::from_str::<MemoryData>(line.trim()) {
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
let mut current_guard = current.write().await;
|
||||||
|
current_guard.inuse = memory.inuse;
|
||||||
|
current_guard.oslimit = memory.oslimit;
|
||||||
|
current_guard.mark_fresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal memory monitor using the new architecture
|
||||||
|
pub struct MemoryMonitor {
|
||||||
|
monitor: IpcStreamMonitor<CurrentMemory>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MemoryMonitor {
|
||||||
|
fn default() -> Self {
|
||||||
|
MemoryMonitor {
|
||||||
|
monitor: IpcStreamMonitor::new(
|
||||||
|
"/memory".to_string(),
|
||||||
|
Duration::from_secs(10),
|
||||||
|
Duration::from_secs(2),
|
||||||
|
Duration::from_secs(10),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use simplified singleton_lazy_with_logging macro
|
||||||
|
singleton_lazy_with_logging!(
|
||||||
|
MemoryMonitor,
|
||||||
|
INSTANCE,
|
||||||
|
"MemoryMonitor",
|
||||||
|
MemoryMonitor::default
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MemoryMonitor {
|
||||||
|
pub async fn current(&self) -> CurrentMemory {
|
||||||
|
self.monitor.current().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_fresh(&self) -> bool {
|
||||||
|
self.monitor.is_fresh().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_current_memory() -> CurrentMemory {
|
||||||
|
MemoryMonitor::global().current().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_formatted_memory() -> (String, String, f64, bool) {
|
||||||
|
let monitor = MemoryMonitor::global();
|
||||||
|
let memory = monitor.current().await;
|
||||||
|
let is_fresh = monitor.is_fresh().await;
|
||||||
|
|
||||||
|
let usage_percent = if memory.oslimit > 0 {
|
||||||
|
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
fmt_bytes(memory.inuse),
|
||||||
|
fmt_bytes(memory.oslimit),
|
||||||
|
usage_percent,
|
||||||
|
is_fresh,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
pub mod general;
|
||||||
|
pub mod logs;
|
||||||
|
pub mod memory;
|
||||||
|
pub mod monitor;
|
||||||
|
pub mod traffic;
|
||||||
|
|
||||||
|
pub use general::IpcManager;
|
||||||
|
pub use logs::{clear_logs, get_logs_json, start_logs_monitoring, stop_logs_monitoring};
|
||||||
|
pub use memory::{get_current_memory, get_formatted_memory};
|
||||||
|
pub use traffic::{get_current_traffic, get_formatted_traffic};
|
||||||
|
|
||||||
|
pub struct Rate {
|
||||||
|
// pub up: usize,
|
||||||
|
// pub down: usize,
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
use kode_bridge::IpcStreamClient;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::{sync::RwLock, time::Duration};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
logging,
|
||||||
|
process::AsyncHandler,
|
||||||
|
utils::{dirs::ipc_path, logging::Type},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Generic base structure for IPC monitoring data with freshness tracking
|
||||||
|
pub trait MonitorData: Clone + Send + Sync + 'static {
|
||||||
|
/// Update the last_updated timestamp to now
|
||||||
|
fn mark_fresh(&mut self);
|
||||||
|
|
||||||
|
/// Check if data is fresh based on the given duration
|
||||||
|
fn is_fresh_within(&self, duration: Duration) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for parsing streaming data and updating monitor state
|
||||||
|
pub trait StreamingParser: MonitorData {
|
||||||
|
/// Parse a line of streaming data and update the current state
|
||||||
|
fn parse_and_update(
|
||||||
|
line: &str,
|
||||||
|
current: Arc<RwLock<Self>>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic IPC stream monitor that handles the common streaming pattern
|
||||||
|
pub struct IpcStreamMonitor<T>
|
||||||
|
where
|
||||||
|
T: MonitorData + StreamingParser + Default,
|
||||||
|
{
|
||||||
|
current: Arc<RwLock<T>>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
endpoint: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
timeout: Duration,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
retry_interval: Duration,
|
||||||
|
freshness_duration: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IpcStreamMonitor<T>
|
||||||
|
where
|
||||||
|
T: MonitorData + StreamingParser + Default,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
endpoint: String,
|
||||||
|
timeout: Duration,
|
||||||
|
retry_interval: Duration,
|
||||||
|
freshness_duration: Duration,
|
||||||
|
) -> Self {
|
||||||
|
let current = Arc::new(RwLock::new(T::default()));
|
||||||
|
let monitor_current = Arc::clone(¤t);
|
||||||
|
let endpoint_clone = endpoint.clone();
|
||||||
|
|
||||||
|
// Start the monitoring task
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
Self::streaming_task(monitor_current, endpoint_clone, timeout, retry_interval).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
current,
|
||||||
|
endpoint,
|
||||||
|
timeout,
|
||||||
|
retry_interval,
|
||||||
|
freshness_duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn current(&self) -> T {
|
||||||
|
self.current.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_fresh(&self) -> bool {
|
||||||
|
self.current
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.is_fresh_within(self.freshness_duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The core streaming task that can be specialized per monitor type
|
||||||
|
async fn streaming_task(
|
||||||
|
current: Arc<RwLock<T>>,
|
||||||
|
endpoint: String,
|
||||||
|
timeout: Duration,
|
||||||
|
retry_interval: Duration,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
let ipc_path_buf = match ipc_path() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
|
||||||
|
tokio::time::sleep(retry_interval).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
|
||||||
|
|
||||||
|
let client = match IpcStreamClient::new(ipc_path) {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
|
||||||
|
tokio::time::sleep(retry_interval).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = client
|
||||||
|
.get(&endpoint)
|
||||||
|
.timeout(timeout)
|
||||||
|
.process_lines(|line| T::parse_and_update(line, Arc::clone(¤t)))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
tokio::time::sleep(retry_interval).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{sync::Arc, time::Instant};
|
||||||
|
use tokio::{sync::RwLock, time::Duration};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
|
||||||
|
process::AsyncHandler,
|
||||||
|
singleton_lazy_with_logging,
|
||||||
|
utils::format::fmt_bytes,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct TrafficData {
|
||||||
|
pub up: u64,
|
||||||
|
pub down: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CurrentTraffic {
|
||||||
|
pub up_rate: u64,
|
||||||
|
pub down_rate: u64,
|
||||||
|
pub total_up: u64,
|
||||||
|
pub total_down: u64,
|
||||||
|
pub last_updated: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CurrentTraffic {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
up_rate: 0,
|
||||||
|
down_rate: 0,
|
||||||
|
total_up: 0,
|
||||||
|
total_down: 0,
|
||||||
|
last_updated: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonitorData for CurrentTraffic {
|
||||||
|
fn mark_fresh(&mut self) {
|
||||||
|
self.last_updated = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_fresh_within(&self, duration: Duration) -> bool {
|
||||||
|
self.last_updated.elapsed() < duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traffic monitoring state for calculating rates
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct TrafficMonitorState {
|
||||||
|
pub current: CurrentTraffic,
|
||||||
|
pub last_traffic: Option<TrafficData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonitorData for TrafficMonitorState {
|
||||||
|
fn mark_fresh(&mut self) {
|
||||||
|
self.current.mark_fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_fresh_within(&self, duration: Duration) -> bool {
|
||||||
|
self.current.is_fresh_within(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamingParser for TrafficMonitorState {
|
||||||
|
fn parse_and_update(
|
||||||
|
line: &str,
|
||||||
|
current: Arc<RwLock<Self>>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
if let Ok(traffic) = serde_json::from_str::<TrafficData>(line.trim()) {
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
let mut state_guard = current.write().await;
|
||||||
|
|
||||||
|
let (up_rate, down_rate) = state_guard
|
||||||
|
.last_traffic
|
||||||
|
.as_ref()
|
||||||
|
.map(|l| {
|
||||||
|
(
|
||||||
|
traffic.up.saturating_sub(l.up),
|
||||||
|
traffic.down.saturating_sub(l.down),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
|
||||||
|
state_guard.current = CurrentTraffic {
|
||||||
|
up_rate,
|
||||||
|
down_rate,
|
||||||
|
total_up: traffic.up,
|
||||||
|
total_down: traffic.down,
|
||||||
|
last_updated: Instant::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
state_guard.last_traffic = Some(traffic);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal traffic monitor using the new architecture
|
||||||
|
pub struct TrafficMonitor {
|
||||||
|
monitor: IpcStreamMonitor<TrafficMonitorState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TrafficMonitor {
|
||||||
|
fn default() -> Self {
|
||||||
|
TrafficMonitor {
|
||||||
|
monitor: IpcStreamMonitor::new(
|
||||||
|
"/traffic".to_string(),
|
||||||
|
Duration::from_secs(10),
|
||||||
|
Duration::from_secs(1),
|
||||||
|
Duration::from_secs(5),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use simplified singleton_lazy_with_logging macro
|
||||||
|
singleton_lazy_with_logging!(
|
||||||
|
TrafficMonitor,
|
||||||
|
INSTANCE,
|
||||||
|
"TrafficMonitor",
|
||||||
|
TrafficMonitor::default
|
||||||
|
);
|
||||||
|
|
||||||
|
impl TrafficMonitor {
|
||||||
|
pub async fn current(&self) -> CurrentTraffic {
|
||||||
|
self.monitor.current().await.current
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_fresh(&self) -> bool {
|
||||||
|
self.monitor.is_fresh().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_current_traffic() -> CurrentTraffic {
|
||||||
|
TrafficMonitor::global().current().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_formatted_traffic() -> (String, String, String, String, bool) {
|
||||||
|
let monitor = TrafficMonitor::global();
|
||||||
|
let traffic = monitor.current().await;
|
||||||
|
let is_fresh = monitor.is_fresh().await;
|
||||||
|
|
||||||
|
(
|
||||||
|
fmt_bytes(traffic.up_rate),
|
||||||
|
fmt_bytes(traffic.down_rate),
|
||||||
|
fmt_bytes(traffic.total_up),
|
||||||
|
fmt_bytes(traffic.total_down),
|
||||||
|
is_fresh,
|
||||||
|
)
|
||||||
|
}
|
||||||
+439
-316
@@ -1,226 +1,146 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
#![recursion_limit = "512"]
|
||||||
|
|
||||||
|
mod cache;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
mod core;
|
mod core;
|
||||||
mod enhance;
|
mod enhance;
|
||||||
mod feat;
|
mod feat;
|
||||||
|
mod ipc;
|
||||||
mod module;
|
mod module;
|
||||||
mod process;
|
mod process;
|
||||||
mod state;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use crate::utils::window_manager::WindowManager;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
core::handle,
|
||||||
core::hotkey,
|
core::hotkey,
|
||||||
process::AsyncHandler,
|
process::AsyncHandler,
|
||||||
utils::{resolve, resolve::resolve_scheme, server},
|
utils::{resolve, server},
|
||||||
};
|
};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::sync::Once;
|
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
use tokio::time::{timeout, Duration};
|
use tokio::time::{Duration, timeout};
|
||||||
use utils::logging::Type;
|
use utils::logging::Type;
|
||||||
|
|
||||||
/// A global singleton handle to the application.
|
/// Application initialization helper functions
|
||||||
pub struct AppHandleManager {
|
mod app_init {
|
||||||
inner: Mutex<Option<AppHandle>>,
|
use super::*;
|
||||||
init: Once,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppHandleManager {
|
/// Initialize singleton monitoring for other instances
|
||||||
/// Get the global instance of the app handle manager.
|
pub fn init_singleton_check() {
|
||||||
pub fn global() -> &'static Self {
|
AsyncHandler::spawn_blocking(move || async move {
|
||||||
static INSTANCE: AppHandleManager = AppHandleManager {
|
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
||||||
inner: Mutex::new(None),
|
match timeout(Duration::from_millis(500), server::check_singleton()).await {
|
||||||
init: Once::new(),
|
Ok(result) => {
|
||||||
};
|
if result.is_err() {
|
||||||
&INSTANCE
|
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
||||||
}
|
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||||
|
app_handle.exit(0);
|
||||||
/// Initialize the app handle manager with an app handle.
|
} else {
|
||||||
pub fn init(&self, handle: AppHandle) {
|
std::process::exit(0);
|
||||||
self.init.call_once(|| {
|
}
|
||||||
let mut app_handle = self.inner.lock();
|
} else {
|
||||||
*app_handle = Some(handle);
|
logging!(info, Type::Setup, true, "未检测到其他应用实例");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"单例检查超时,假定没有其他实例运行"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the app handle if it has been initialized.
|
/// Setup plugins for the Tauri builder
|
||||||
pub fn get(&self) -> Option<AppHandle> {
|
pub fn setup_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||||
self.inner.lock().clone()
|
#[allow(unused_mut)]
|
||||||
}
|
let mut builder = builder
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_fs::init())
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
|
.plugin(tauri_plugin_http::init());
|
||||||
|
|
||||||
/// Get the app handle, panics if it hasn't been initialized.
|
// Devtools plugin only in debug mode with feature tauri-dev
|
||||||
pub fn get_handle(&self) -> AppHandle {
|
// to avoid duplicated registering of logger since the devtools plugin also registers a logger
|
||||||
self.get().expect("AppHandle not initialized")
|
#[cfg(all(debug_assertions, not(feature = "tokio-trace"), feature = "tauri-dev"))]
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_activation_policy_regular(&self) {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
{
|
||||||
let app_handle = self.inner.lock();
|
builder = builder.plugin(tauri_plugin_devtools::init());
|
||||||
let app_handle = app_handle.as_ref().unwrap();
|
|
||||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
|
|
||||||
}
|
}
|
||||||
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_activation_policy_accessory(&self) {
|
/// Setup deep link handling
|
||||||
#[cfg(target_os = "macos")]
|
pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||||
{
|
{
|
||||||
let app_handle = self.inner.lock();
|
logging!(info, Type::Setup, true, "注册深层链接...");
|
||||||
let app_handle = app_handle.as_ref().unwrap();
|
app.deep_link().register_all()?;
|
||||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_activation_policy_prohibited(&self) {
|
app.deep_link().on_open_url(|event| {
|
||||||
#[cfg(target_os = "macos")]
|
let url = event.urls().first().map(|u| u.to_string());
|
||||||
{
|
if let Some(url) = url {
|
||||||
let app_handle = self.inner.lock();
|
AsyncHandler::spawn(|| async {
|
||||||
let app_handle = app_handle.as_ref().unwrap();
|
if let Err(e) = resolve::resolve_scheme(url).await {
|
||||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited);
|
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::panic)]
|
|
||||||
pub fn run() {
|
|
||||||
utils::network::NetworkManager::global().init();
|
|
||||||
|
|
||||||
let _ = utils::dirs::init_portable_flag();
|
|
||||||
|
|
||||||
// 异步单例检测
|
|
||||||
AsyncHandler::spawn(move || async move {
|
|
||||||
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
|
||||||
match timeout(Duration::from_secs(3), server::check_singleton()).await {
|
|
||||||
Ok(result) => {
|
|
||||||
if result.is_err() {
|
|
||||||
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
|
||||||
if let Some(app_handle) = AppHandleManager::global().get() {
|
|
||||||
app_handle.exit(0);
|
|
||||||
} else {
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logging!(info, Type::Setup, true, "未检测到其他应用实例");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
logging!(
|
|
||||||
warn,
|
|
||||||
Type::Setup,
|
|
||||||
true,
|
|
||||||
"单例检查超时,假定没有其他实例运行"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
let devtools = tauri_plugin_devtools::init();
|
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut builder = tauri::Builder::default()
|
|
||||||
.plugin(tauri_plugin_notification::init())
|
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
|
||||||
.plugin(tauri_plugin_process::init())
|
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
|
||||||
.plugin(tauri_plugin_fs::init())
|
|
||||||
.plugin(tauri_plugin_dialog::init())
|
|
||||||
.plugin(tauri_plugin_shell::init())
|
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
|
||||||
.manage(Mutex::new(state::lightweight::LightWeightState::default()))
|
|
||||||
.setup(|app| {
|
|
||||||
logging!(info, Type::Setup, true, "开始应用初始化...");
|
|
||||||
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
auto_start_plugin_builder = auto_start_plugin_builder
|
|
||||||
.macos_launcher(MacosLauncher::LaunchAgent)
|
|
||||||
.app_name(app.config().identifier.clone());
|
|
||||||
}
|
|
||||||
let _ = app.handle().plugin(auto_start_plugin_builder.build());
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
|
||||||
{
|
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
|
||||||
logging!(info, Type::Setup, true, "注册深层链接...");
|
|
||||||
logging_error!(Type::System, true, app.deep_link().register_all());
|
|
||||||
}
|
|
||||||
|
|
||||||
app.deep_link().on_open_url(|event| {
|
|
||||||
AsyncHandler::spawn(move || {
|
|
||||||
let url = event.urls().first().map(|u| u.to_string());
|
|
||||||
async move {
|
|
||||||
if let Some(url) = url {
|
|
||||||
logging_error!(Type::Setup, true, resolve_scheme(url).await);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// 窗口管理
|
|
||||||
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
|
|
||||||
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
|
||||||
.with_filename("window_state.json")
|
|
||||||
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
|
|
||||||
.build();
|
|
||||||
let _ = app.handle().plugin(window_state_plugin);
|
|
||||||
|
|
||||||
// 异步处理
|
|
||||||
let app_handle = app.handle().clone();
|
|
||||||
AsyncHandler::spawn(move || async move {
|
|
||||||
logging!(info, Type::Setup, true, "异步执行应用设置...");
|
|
||||||
match timeout(
|
|
||||||
Duration::from_secs(30),
|
|
||||||
resolve::resolve_setup_async(&app_handle),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
logging!(info, Type::Setup, true, "应用设置成功完成");
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
logging!(
|
|
||||||
error,
|
|
||||||
Type::Setup,
|
|
||||||
true,
|
|
||||||
"应用设置超时(30秒),继续执行后续流程"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
|
||||||
|
|
||||||
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
|
|
||||||
AppHandleManager::global().init(app.handle().clone());
|
|
||||||
|
|
||||||
logging!(info, Type::Setup, true, "初始化核心句柄...");
|
|
||||||
core::handle::Handle::global().init(app.handle());
|
|
||||||
|
|
||||||
logging!(info, Type::Setup, true, "初始化配置...");
|
|
||||||
if let Err(e) = utils::init::init_config() {
|
|
||||||
logging!(error, Type::Setup, true, "初始化配置失败: {}", e);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
logging!(info, Type::Setup, true, "初始化资源...");
|
Ok(())
|
||||||
if let Err(e) = utils::init::init_resources() {
|
}
|
||||||
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
/// Setup autostart plugin
|
||||||
Ok(())
|
pub fn setup_autostart(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
})
|
#[cfg(target_os = "macos")]
|
||||||
.invoke_handler(tauri::generate_handler![
|
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||||
// common
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
auto_start_plugin_builder = auto_start_plugin_builder
|
||||||
|
.macos_launcher(MacosLauncher::LaunchAgent)
|
||||||
|
.app_name(app.config().identifier.clone());
|
||||||
|
}
|
||||||
|
app.handle().plugin(auto_start_plugin_builder.build())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup window state management
|
||||||
|
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
|
||||||
|
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
||||||
|
.with_filename("window_state.json")
|
||||||
|
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
|
||||||
|
.build();
|
||||||
|
app.handle().plugin(window_state_plugin)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate all command handlers for the application
|
||||||
|
pub fn generate_handlers()
|
||||||
|
-> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||||
|
tauri::generate_handler![
|
||||||
|
// Common commands
|
||||||
cmd::get_sys_proxy,
|
cmd::get_sys_proxy,
|
||||||
cmd::get_auto_proxy,
|
cmd::get_auto_proxy,
|
||||||
cmd::open_app_dir,
|
cmd::open_app_dir,
|
||||||
@@ -231,28 +151,27 @@ pub fn run() {
|
|||||||
cmd::get_network_interfaces,
|
cmd::get_network_interfaces,
|
||||||
cmd::get_system_hostname,
|
cmd::get_system_hostname,
|
||||||
cmd::restart_app,
|
cmd::restart_app,
|
||||||
// 内核管理
|
// Core management
|
||||||
cmd::start_core,
|
cmd::start_core,
|
||||||
cmd::stop_core,
|
cmd::stop_core,
|
||||||
cmd::restart_core,
|
cmd::restart_core,
|
||||||
// 启动命令
|
// Application lifecycle
|
||||||
cmd::notify_ui_ready,
|
cmd::notify_ui_ready,
|
||||||
cmd::update_ui_stage,
|
cmd::update_ui_stage,
|
||||||
cmd::reset_ui_ready_state,
|
|
||||||
cmd::get_running_mode,
|
cmd::get_running_mode,
|
||||||
cmd::get_app_uptime,
|
cmd::get_app_uptime,
|
||||||
cmd::get_auto_launch_status,
|
cmd::get_auto_launch_status,
|
||||||
cmd::is_admin,
|
cmd::is_admin,
|
||||||
// 添加轻量模式相关命令
|
// Lightweight mode
|
||||||
cmd::entry_lightweight_mode,
|
cmd::entry_lightweight_mode,
|
||||||
cmd::exit_lightweight_mode,
|
cmd::exit_lightweight_mode,
|
||||||
// service 管理
|
// Service management
|
||||||
cmd::install_service,
|
cmd::install_service,
|
||||||
cmd::uninstall_service,
|
cmd::uninstall_service,
|
||||||
cmd::reinstall_service,
|
cmd::reinstall_service,
|
||||||
cmd::repair_service,
|
cmd::repair_service,
|
||||||
cmd::is_service_available,
|
cmd::is_service_available,
|
||||||
// clash
|
// Clash core commands
|
||||||
cmd::get_clash_info,
|
cmd::get_clash_info,
|
||||||
cmd::patch_clash_config,
|
cmd::patch_clash_config,
|
||||||
cmd::patch_clash_mode,
|
cmd::patch_clash_mode,
|
||||||
@@ -261,17 +180,51 @@ pub fn run() {
|
|||||||
cmd::get_runtime_yaml,
|
cmd::get_runtime_yaml,
|
||||||
cmd::get_runtime_exists,
|
cmd::get_runtime_exists,
|
||||||
cmd::get_runtime_logs,
|
cmd::get_runtime_logs,
|
||||||
|
cmd::get_runtime_proxy_chain_config,
|
||||||
|
cmd::update_proxy_chain_config_in_runtime,
|
||||||
cmd::invoke_uwp_tool,
|
cmd::invoke_uwp_tool,
|
||||||
cmd::copy_clash_env,
|
cmd::copy_clash_env,
|
||||||
cmd::get_proxies,
|
cmd::get_proxies,
|
||||||
cmd::force_refresh_proxies,
|
cmd::force_refresh_proxies,
|
||||||
cmd::get_providers_proxies,
|
cmd::get_providers_proxies,
|
||||||
|
cmd::sync_tray_proxy_selection,
|
||||||
|
cmd::update_proxy_and_sync,
|
||||||
cmd::save_dns_config,
|
cmd::save_dns_config,
|
||||||
cmd::apply_dns_config,
|
cmd::apply_dns_config,
|
||||||
cmd::check_dns_config_exists,
|
cmd::check_dns_config_exists,
|
||||||
cmd::get_dns_config_content,
|
cmd::get_dns_config_content,
|
||||||
cmd::validate_dns_config,
|
cmd::validate_dns_config,
|
||||||
// verge
|
cmd::get_clash_version,
|
||||||
|
cmd::get_clash_config,
|
||||||
|
cmd::force_refresh_clash_config,
|
||||||
|
cmd::update_geo_data,
|
||||||
|
cmd::upgrade_clash_core,
|
||||||
|
cmd::get_clash_rules,
|
||||||
|
cmd::update_proxy_choice,
|
||||||
|
cmd::get_proxy_providers,
|
||||||
|
cmd::get_rule_providers,
|
||||||
|
cmd::proxy_provider_health_check,
|
||||||
|
cmd::update_proxy_provider,
|
||||||
|
cmd::update_rule_provider,
|
||||||
|
cmd::get_clash_connections,
|
||||||
|
cmd::delete_clash_connection,
|
||||||
|
cmd::close_all_clash_connections,
|
||||||
|
cmd::get_group_proxy_delays,
|
||||||
|
cmd::is_clash_debug_enabled,
|
||||||
|
cmd::clash_gc,
|
||||||
|
// Logging and monitoring
|
||||||
|
cmd::get_clash_logs,
|
||||||
|
cmd::start_logs_monitoring,
|
||||||
|
cmd::stop_logs_monitoring,
|
||||||
|
cmd::clear_logs,
|
||||||
|
cmd::get_traffic_data,
|
||||||
|
cmd::get_memory_data,
|
||||||
|
cmd::get_formatted_traffic_data,
|
||||||
|
cmd::get_formatted_memory_data,
|
||||||
|
cmd::get_system_monitor_overview,
|
||||||
|
cmd::start_traffic_service,
|
||||||
|
cmd::stop_traffic_service,
|
||||||
|
// Verge configuration
|
||||||
cmd::get_verge_config,
|
cmd::get_verge_config,
|
||||||
cmd::patch_verge_config,
|
cmd::patch_verge_config,
|
||||||
cmd::test_delay,
|
cmd::test_delay,
|
||||||
@@ -281,7 +234,7 @@ pub fn run() {
|
|||||||
cmd::open_devtools,
|
cmd::open_devtools,
|
||||||
cmd::exit_app,
|
cmd::exit_app,
|
||||||
cmd::get_network_interfaces_info,
|
cmd::get_network_interfaces_info,
|
||||||
// profile
|
// Profile management
|
||||||
cmd::get_profiles,
|
cmd::get_profiles,
|
||||||
cmd::enhance_profiles,
|
cmd::enhance_profiles,
|
||||||
cmd::patch_profiles_config,
|
cmd::patch_profiles_config,
|
||||||
@@ -295,164 +248,334 @@ pub fn run() {
|
|||||||
cmd::read_profile_file,
|
cmd::read_profile_file,
|
||||||
cmd::save_profile_file,
|
cmd::save_profile_file,
|
||||||
cmd::get_next_update_time,
|
cmd::get_next_update_time,
|
||||||
// script validation
|
// Script validation
|
||||||
cmd::script_validate_notice,
|
cmd::script_validate_notice,
|
||||||
cmd::validate_script_file,
|
cmd::validate_script_file,
|
||||||
// clash api
|
// Clash API
|
||||||
cmd::clash_api_get_proxy_delay,
|
cmd::clash_api_get_proxy_delay,
|
||||||
// backup
|
// Backup and WebDAV
|
||||||
cmd::create_webdav_backup,
|
cmd::create_webdav_backup,
|
||||||
cmd::save_webdav_config,
|
cmd::save_webdav_config,
|
||||||
cmd::list_webdav_backup,
|
cmd::list_webdav_backup,
|
||||||
cmd::delete_webdav_backup,
|
cmd::delete_webdav_backup,
|
||||||
cmd::restore_webdav_backup,
|
cmd::restore_webdav_backup,
|
||||||
// export diagnostic info for issue reporting
|
// Diagnostics and system info
|
||||||
cmd::export_diagnostic_info,
|
cmd::export_diagnostic_info,
|
||||||
// get system info for display
|
|
||||||
cmd::get_system_info,
|
cmd::get_system_info,
|
||||||
// media unlock checker
|
// Media unlock checker
|
||||||
cmd::get_unlock_items,
|
cmd::get_unlock_items,
|
||||||
cmd::check_media_unlock,
|
cmd::check_media_unlock,
|
||||||
// light-weight model
|
]
|
||||||
cmd::entry_lightweight_mode,
|
}
|
||||||
]);
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
pub fn run() {
|
||||||
|
// Setup singleton check
|
||||||
|
app_init::init_singleton_check();
|
||||||
|
|
||||||
|
let _ = utils::dirs::init_portable_flag();
|
||||||
|
|
||||||
|
// Set Linux environment variable
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
builder = builder.plugin(devtools);
|
unsafe {
|
||||||
|
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_uppercase();
|
||||||
|
let is_kde_desktop = desktop_env.contains("KDE");
|
||||||
|
let is_plasma_desktop = desktop_env.contains("PLASMA");
|
||||||
|
|
||||||
|
if is_kde_desktop || is_plasma_desktop {
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("GTK_CSD", "0");
|
||||||
|
}
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"KDE detected: Disabled GTK CSD for better titlebar stability."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Macos Application Menu
|
// Create and configure the Tauri builder
|
||||||
#[cfg(target_os = "macos")]
|
let builder = app_init::setup_plugins(tauri::Builder::default())
|
||||||
{
|
.setup(|app| {
|
||||||
// Temporary Achived due to cannot CMD+C/V/A
|
logging!(info, Type::Setup, true, "开始应用初始化...");
|
||||||
}
|
|
||||||
|
|
||||||
let app = builder
|
// Setup autostart plugin
|
||||||
.build(tauri::generate_context!())
|
if let Err(e) = app_init::setup_autostart(app) {
|
||||||
.expect("error while running tauri application");
|
logging!(error, Type::Setup, true, "Failed to setup autostart: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
app.run(|app_handle, e| match e {
|
// Setup deep links
|
||||||
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
if let Err(e) = app_init::setup_deep_links(app) {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"Failed to setup deep links: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup window state management
|
||||||
|
if let Err(e) = app_init::setup_window_state(app) {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"Failed to setup window state: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
|
||||||
|
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
||||||
|
|
||||||
|
resolve::resolve_setup_handle(app_handle);
|
||||||
|
resolve::resolve_setup_async();
|
||||||
|
resolve::resolve_setup_sync();
|
||||||
|
|
||||||
|
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(app_init::generate_handlers());
|
||||||
|
|
||||||
|
/// Event handling helper functions
|
||||||
|
mod event_handlers {
|
||||||
|
use crate::core::handle;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Handle application ready/resumed events
|
||||||
|
pub fn handle_ready_resumed(app_handle: &AppHandle) {
|
||||||
logging!(info, Type::System, true, "应用就绪或恢复");
|
logging!(info, Type::System, true, "应用就绪或恢复");
|
||||||
AppHandleManager::global().init(app_handle.clone());
|
handle::Handle::global().init(app_handle.clone());
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
if let Some(window) = AppHandleManager::global()
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
.get_handle()
|
|
||||||
.get_webview_window("main")
|
|
||||||
{
|
|
||||||
logging!(info, Type::Window, true, "设置macOS窗口标题");
|
logging!(info, Type::Window, true, "设置macOS窗口标题");
|
||||||
let _ = window.set_title("Clash Verge");
|
let _ = window.set_title("Clash Verge");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle application reopen events (macOS)
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
tauri::RunEvent::Reopen {
|
pub async fn handle_reopen(app_handle: &AppHandle, has_visible_windows: bool) {
|
||||||
has_visible_windows,
|
logging!(
|
||||||
..
|
info,
|
||||||
} => {
|
Type::System,
|
||||||
|
true,
|
||||||
|
"处理 macOS 应用重新打开事件: has_visible_windows={}",
|
||||||
|
has_visible_windows
|
||||||
|
);
|
||||||
|
|
||||||
|
handle::Handle::global().init(app_handle.clone());
|
||||||
|
|
||||||
if !has_visible_windows {
|
if !has_visible_windows {
|
||||||
AppHandleManager::global().set_activation_policy_regular();
|
// 当没有可见窗口时,设置为 regular 模式并显示主窗口
|
||||||
}
|
handle::Handle::global().set_activation_policy_regular();
|
||||||
AppHandleManager::global().init(app_handle.clone());
|
|
||||||
}
|
logging!(info, Type::System, true, "没有可见窗口,尝试显示主窗口");
|
||||||
tauri::RunEvent::ExitRequested { api, code, .. } => {
|
|
||||||
if code.is_none() {
|
let result = WindowManager::show_main_window().await;
|
||||||
api.prevent_exit();
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::System,
|
||||||
|
true,
|
||||||
|
"窗口显示操作完成,结果: {:?}",
|
||||||
|
result
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logging!(info, Type::System, true, "已有可见窗口,无需额外操作");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tauri::RunEvent::Exit => {
|
|
||||||
// avoid duplicate cleanup
|
/// Handle window close requests
|
||||||
|
pub fn handle_window_close(api: &tauri::WindowEvent) {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
handle::Handle::global().set_activation_policy_accessory();
|
||||||
|
|
||||||
if core::handle::Handle::global().is_exiting() {
|
if core::handle::Handle::global().is_exiting() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
feat::clean();
|
|
||||||
}
|
log::info!(target: "app", "closing window...");
|
||||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
if let tauri::WindowEvent::CloseRequested { api, .. } = api {
|
||||||
if label == "main" {
|
api.prevent_close();
|
||||||
match event {
|
if let Some(window) = core::handle::Handle::global().get_window() {
|
||||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
let _ = window.hide();
|
||||||
#[cfg(target_os = "macos")]
|
} else {
|
||||||
AppHandleManager::global().set_activation_policy_accessory();
|
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
|
||||||
if core::handle::Handle::global().is_exiting() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log::info!(target: "app", "closing window...");
|
|
||||||
api.prevent_close();
|
|
||||||
if let Some(window) = core::handle::Handle::global().get_window() {
|
|
||||||
let _ = window.hide();
|
|
||||||
} else {
|
|
||||||
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tauri::WindowEvent::Focused(true) => {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
logging_error!(
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
hotkey::Hotkey::global().register("CMD+Q", "quit")
|
|
||||||
);
|
|
||||||
logging_error!(
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
hotkey::Hotkey::global().register("CMD+W", "hide")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let is_enable_global_hotkey = Config::verge()
|
|
||||||
.latest_ref()
|
|
||||||
.enable_global_hotkey
|
|
||||||
.unwrap_or(true);
|
|
||||||
if !is_enable_global_hotkey {
|
|
||||||
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().init())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tauri::WindowEvent::Focused(false) => {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
logging_error!(
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
hotkey::Hotkey::global().unregister("CMD+Q")
|
|
||||||
);
|
|
||||||
logging_error!(
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
hotkey::Hotkey::global().unregister("CMD+W")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let is_enable_global_hotkey = Config::verge()
|
|
||||||
.latest_ref()
|
|
||||||
.enable_global_hotkey
|
|
||||||
.unwrap_or(true);
|
|
||||||
if !is_enable_global_hotkey {
|
|
||||||
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().reset())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tauri::WindowEvent::Destroyed => {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
logging_error!(
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
hotkey::Hotkey::global().unregister("CMD+Q")
|
|
||||||
);
|
|
||||||
logging_error!(
|
|
||||||
Type::Hotkey,
|
|
||||||
true,
|
|
||||||
hotkey::Hotkey::global().unregister("CMD+W")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
|
/// Handle window focus events
|
||||||
|
pub fn handle_window_focus(focused: bool) {
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
let is_enable_global_hotkey = Config::verge()
|
||||||
|
.await
|
||||||
|
.latest_ref()
|
||||||
|
.enable_global_hotkey
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if focused {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
use crate::core::hotkey::SystemHotkey;
|
||||||
|
if let Err(e) = hotkey::Hotkey::global()
|
||||||
|
.register_system_hotkey(SystemHotkey::CmdQ)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
logging!(error, Type::Hotkey, true, "Failed to register CMD+Q: {}", e);
|
||||||
|
}
|
||||||
|
if let Err(e) = hotkey::Hotkey::global()
|
||||||
|
.register_system_hotkey(SystemHotkey::CmdW)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
logging!(error, Type::Hotkey, true, "Failed to register CMD+W: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_enable_global_hotkey
|
||||||
|
&& let Err(e) = hotkey::Hotkey::global().init().await
|
||||||
|
{
|
||||||
|
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unfocused state
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
use crate::core::hotkey::SystemHotkey;
|
||||||
|
if let Err(e) =
|
||||||
|
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
|
||||||
|
{
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Failed to unregister CMD+Q: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Err(e) =
|
||||||
|
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
|
||||||
|
{
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Failed to unregister CMD+W: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_enable_global_hotkey && let Err(e) = hotkey::Hotkey::global().reset() {
|
||||||
|
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle window destroyed events
|
||||||
|
pub fn handle_window_destroyed() {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
use crate::core::hotkey::SystemHotkey;
|
||||||
|
if let Err(e) =
|
||||||
|
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
|
||||||
|
{
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Failed to unregister CMD+Q on destroy: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Err(e) =
|
||||||
|
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
|
||||||
|
{
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Hotkey,
|
||||||
|
true,
|
||||||
|
"Failed to unregister CMD+W on destroy: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the application
|
||||||
|
let app = builder
|
||||||
|
.build(tauri::generate_context!())
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Setup,
|
||||||
|
true,
|
||||||
|
"Failed to build Tauri application: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.run(|app_handle, e| {
|
||||||
|
match e {
|
||||||
|
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
||||||
|
event_handlers::handle_ready_resumed(app_handle);
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
tauri::RunEvent::Reopen {
|
||||||
|
has_visible_windows,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
AsyncHandler::spawn(move || async move {
|
||||||
|
event_handlers::handle_reopen(&app_handle, has_visible_windows).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tauri::RunEvent::ExitRequested { api, code, .. } => {
|
||||||
|
if code.is_none() {
|
||||||
|
api.prevent_exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tauri::RunEvent::Exit => {
|
||||||
|
// Avoid duplicate cleanup
|
||||||
|
if core::handle::Handle::global().is_exiting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
feat::clean();
|
||||||
|
}
|
||||||
|
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||||
|
if label == "main" {
|
||||||
|
match event {
|
||||||
|
tauri::WindowEvent::CloseRequested { .. } => {
|
||||||
|
event_handlers::handle_window_close(&event);
|
||||||
|
}
|
||||||
|
tauri::WindowEvent::Focused(focused) => {
|
||||||
|
event_handlers::handle_window_focus(focused);
|
||||||
|
}
|
||||||
|
tauri::WindowEvent::Destroyed => {
|
||||||
|
event_handlers::handle_window_destroyed();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(feature = "tokio-trace")]
|
||||||
|
console_subscriber::init();
|
||||||
app_lib::run();
|
app_lib::run();
|
||||||
}
|
}
|
||||||
|
|||||||
+190
-141
@@ -1,109 +1,146 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
cache::CacheProxy,
|
||||||
config::Config,
|
config::Config,
|
||||||
core::{handle, timer::Timer, tray::Tray},
|
core::{handle, timer::Timer, tray::Tray},
|
||||||
log_err, logging,
|
log_err, logging,
|
||||||
state::lightweight::LightWeightState,
|
process::AsyncHandler,
|
||||||
utils::logging::Type,
|
utils::logging::Type,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use crate::logging_error;
|
use crate::logging_error;
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use crate::AppHandleManager;
|
|
||||||
|
|
||||||
|
use crate::utils::window_manager::WindowManager;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use delay_timer::prelude::TaskBuilder;
|
use delay_timer::prelude::TaskBuilder;
|
||||||
use parking_lot::Mutex;
|
use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use tauri::Listener;
|
||||||
use tauri::{Listener, Manager};
|
|
||||||
|
|
||||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||||
|
|
||||||
// 添加退出轻量模式的锁,防止并发调用
|
#[repr(u8)]
|
||||||
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum LightweightState {
|
||||||
fn with_lightweight_status<F, R>(f: F) -> R
|
Normal = 0,
|
||||||
where
|
In = 1,
|
||||||
F: FnOnce(&mut LightWeightState) -> R,
|
Exiting = 2,
|
||||||
{
|
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
|
||||||
let state = app_handle.state::<Mutex<LightWeightState>>();
|
|
||||||
let mut guard = state.lock();
|
|
||||||
f(&mut guard)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_once_auto_lightweight() {
|
impl From<u8> for LightweightState {
|
||||||
LightWeightState::default().run_once_time(|| {
|
fn from(v: u8) -> Self {
|
||||||
let is_silent_start = Config::verge()
|
match v {
|
||||||
.latest_ref()
|
1 => LightweightState::In,
|
||||||
.enable_silent_start
|
2 => LightweightState::Exiting,
|
||||||
.unwrap_or(false);
|
_ => LightweightState::Normal,
|
||||||
let enable_auto = Config::verge()
|
|
||||||
.data_mut()
|
|
||||||
.enable_auto_light_weight_mode
|
|
||||||
.unwrap_or(false);
|
|
||||||
if enable_auto && is_silent_start {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Lightweight,
|
|
||||||
true,
|
|
||||||
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
|
|
||||||
);
|
|
||||||
set_lightweight_mode(false);
|
|
||||||
enable_auto_light_weight_mode();
|
|
||||||
|
|
||||||
// 触发托盘更新
|
|
||||||
if let Err(e) = Tray::global().update_part() {
|
|
||||||
log::warn!("Failed to update tray: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn auto_lightweight_mode_init() {
|
|
||||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
|
||||||
let _ = app_handle.state::<Mutex<LightWeightState>>();
|
|
||||||
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false);
|
|
||||||
let enable_auto =
|
|
||||||
{ Config::verge().latest_ref().enable_auto_light_weight_mode }.unwrap_or(false);
|
|
||||||
|
|
||||||
if enable_auto && !is_silent_start {
|
|
||||||
logging!(
|
|
||||||
info,
|
|
||||||
Type::Lightweight,
|
|
||||||
true,
|
|
||||||
"非静默启动直接挂载自动进入轻量模式监听器!"
|
|
||||||
);
|
|
||||||
set_lightweight_mode(true);
|
|
||||||
enable_auto_light_weight_mode();
|
|
||||||
|
|
||||||
// 确保托盘状态更新
|
|
||||||
if let Err(e) = Tray::global().update_part() {
|
|
||||||
log::warn!("Failed to update tray: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LightweightState {
|
||||||
|
fn as_u8(self) -> u8 {
|
||||||
|
self as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
|
||||||
|
|
||||||
|
static WINDOW_CLOSE_HANDLER: AtomicU32 = AtomicU32::new(0);
|
||||||
|
static WEBVIEW_FOCUS_HANDLER: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
|
fn set_state(new: LightweightState) {
|
||||||
|
LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release);
|
||||||
|
match new {
|
||||||
|
LightweightState::Normal => {
|
||||||
|
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
|
||||||
|
}
|
||||||
|
LightweightState::In => {
|
||||||
|
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||||
|
}
|
||||||
|
LightweightState::Exiting => {
|
||||||
|
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_state() -> LightweightState {
|
||||||
|
LIGHTWEIGHT_STATE.load(Ordering::Acquire).into()
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否处于轻量模式
|
// 检查是否处于轻量模式
|
||||||
pub fn is_in_lightweight_mode() -> bool {
|
pub fn is_in_lightweight_mode() -> bool {
|
||||||
with_lightweight_status(|state| state.is_lightweight)
|
get_state() == LightweightState::In
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置轻量模式状态
|
// 设置轻量模式状态(仅 Normal <-> In)
|
||||||
pub fn set_lightweight_mode(value: bool) {
|
async fn set_lightweight_mode(value: bool) {
|
||||||
with_lightweight_status(|state| {
|
let current = get_state();
|
||||||
state.set_lightweight_mode(value);
|
if value && current != LightweightState::In {
|
||||||
});
|
set_state(LightweightState::In);
|
||||||
|
} else if !value && current != LightweightState::Normal {
|
||||||
|
set_state(LightweightState::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
// 触发托盘更新
|
// 只有在状态可用时才触发托盘更新
|
||||||
if let Err(e) = Tray::global().update_part() {
|
if let Err(e) = Tray::global().update_part().await {
|
||||||
log::warn!("Failed to update tray: {e}");
|
log::warn!("Failed to update tray: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enable_auto_light_weight_mode() {
|
pub async fn run_once_auto_lightweight() {
|
||||||
Timer::global().init().unwrap();
|
let verge_config = Config::verge().await;
|
||||||
|
let enable_auto = verge_config
|
||||||
|
.data_mut()
|
||||||
|
.enable_auto_light_weight_mode
|
||||||
|
.unwrap_or(false);
|
||||||
|
let is_silent_start = verge_config
|
||||||
|
.latest_ref()
|
||||||
|
.enable_silent_start
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !(enable_auto && is_silent_start) {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
true,
|
||||||
|
"不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_lightweight_mode(true).await;
|
||||||
|
enable_auto_light_weight_mode().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn auto_lightweight_mode_init() -> Result<()> {
|
||||||
|
let is_silent_start =
|
||||||
|
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
|
||||||
|
let enable_auto = {
|
||||||
|
Config::verge()
|
||||||
|
.await
|
||||||
|
.latest_ref()
|
||||||
|
.enable_auto_light_weight_mode
|
||||||
|
}
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if enable_auto && !is_silent_start {
|
||||||
|
logging!(
|
||||||
|
info,
|
||||||
|
Type::Lightweight,
|
||||||
|
true,
|
||||||
|
"非静默启动直接挂载自动进入轻量模式监听器!"
|
||||||
|
);
|
||||||
|
set_state(LightweightState::Normal);
|
||||||
|
enable_auto_light_weight_mode().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn enable_auto_light_weight_mode() {
|
||||||
|
if let Err(e) = Timer::global().init().await {
|
||||||
|
logging!(error, Type::Lightweight, "Failed to initialize timer: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
|
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
|
||||||
setup_window_close_listener();
|
setup_window_close_listener();
|
||||||
setup_webview_focus_listener();
|
setup_webview_focus_listener();
|
||||||
@@ -113,83 +150,82 @@ pub fn disable_auto_light_weight_mode() {
|
|||||||
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
|
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
|
||||||
let _ = cancel_light_weight_timer();
|
let _ = cancel_light_weight_timer();
|
||||||
cancel_window_close_listener();
|
cancel_window_close_listener();
|
||||||
|
cancel_webview_focus_listener();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entry_lightweight_mode() {
|
pub async fn entry_lightweight_mode() -> bool {
|
||||||
use crate::utils::window_manager::WindowManager;
|
// 尝试从 Normal -> In
|
||||||
|
if LIGHTWEIGHT_STATE
|
||||||
let result = WindowManager::hide_main_window();
|
.compare_exchange(
|
||||||
logging!(
|
LightweightState::Normal as u8,
|
||||||
info,
|
LightweightState::In as u8,
|
||||||
Type::Lightweight,
|
Ordering::Acquire,
|
||||||
true,
|
Ordering::Relaxed,
|
||||||
"轻量模式隐藏窗口结果: {:?}",
|
)
|
||||||
result
|
.is_err()
|
||||||
);
|
{
|
||||||
|
logging!(info, Type::Lightweight, true, "无需进入轻量模式,跳过调用");
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
return false;
|
||||||
if let Some(webview) = window.get_webview_window("main") {
|
|
||||||
let _ = webview.destroy();
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
AppHandleManager::global().set_activation_policy_accessory();
|
|
||||||
}
|
}
|
||||||
set_lightweight_mode(true);
|
|
||||||
|
WindowManager::destroy_main_window();
|
||||||
|
|
||||||
|
set_lightweight_mode(true).await;
|
||||||
let _ = cancel_light_weight_timer();
|
let _ = cancel_light_weight_timer();
|
||||||
|
|
||||||
// 更新托盘显示
|
// 回到 In
|
||||||
let _tray = crate::core::tray::Tray::global();
|
set_state(LightweightState::In);
|
||||||
|
|
||||||
|
CacheProxy::global().clean_default_keys();
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加从轻量模式恢复的函数
|
// 添加从轻量模式恢复的函数
|
||||||
pub fn exit_lightweight_mode() {
|
pub async fn exit_lightweight_mode() -> bool {
|
||||||
// 使用原子操作检查是否已经在退出过程中,防止并发调用
|
// 尝试从 In -> Exiting
|
||||||
if EXITING_LIGHTWEIGHT
|
if LIGHTWEIGHT_STATE
|
||||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
.compare_exchange(
|
||||||
|
LightweightState::In as u8,
|
||||||
|
LightweightState::Exiting as u8,
|
||||||
|
Ordering::Acquire,
|
||||||
|
Ordering::Relaxed,
|
||||||
|
)
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::Lightweight,
|
Type::Lightweight,
|
||||||
true,
|
true,
|
||||||
"轻量模式退出操作已在进行中,跳过重复调用"
|
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
|
||||||
);
|
);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用defer确保无论如何都会重置标志
|
WindowManager::show_main_window().await;
|
||||||
let _guard = scopeguard::guard((), |_| {
|
|
||||||
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 确保当前确实处于轻量模式才执行退出操作
|
set_lightweight_mode(false).await;
|
||||||
if !is_in_lightweight_mode() {
|
let _ = cancel_light_weight_timer();
|
||||||
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
set_lightweight_mode(false);
|
// 回到 Normal
|
||||||
|
set_state(LightweightState::Normal);
|
||||||
|
|
||||||
// macOS激活策略
|
logging!(info, Type::Lightweight, true, "轻量模式退出完成");
|
||||||
#[cfg(target_os = "macos")]
|
true
|
||||||
AppHandleManager::global().set_activation_policy_regular();
|
|
||||||
|
|
||||||
// 重置UI就绪状态
|
|
||||||
crate::utils::resolve::reset_ui_ready();
|
|
||||||
|
|
||||||
// 更新托盘显示
|
|
||||||
let _tray = crate::core::tray::Tray::global();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn add_light_weight_timer() {
|
pub async fn add_light_weight_timer() {
|
||||||
logging_error!(Type::Lightweight, setup_light_weight_timer());
|
logging_error!(Type::Lightweight, setup_light_weight_timer().await);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_window_close_listener() -> u32 {
|
fn setup_window_close_listener() {
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
let handler = window.listen("tauri://close-requested", move |_event| {
|
let handler = window.listen("tauri://close-requested", move |_event| {
|
||||||
let _ = setup_light_weight_timer();
|
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||||
|
if let Err(e) = setup_light_weight_timer().await {
|
||||||
|
log::warn!("Failed to setup light weight timer: {e}");
|
||||||
|
}
|
||||||
|
}));
|
||||||
logging!(
|
logging!(
|
||||||
info,
|
info,
|
||||||
Type::Lightweight,
|
Type::Lightweight,
|
||||||
@@ -197,12 +233,22 @@ fn setup_window_close_listener() -> u32 {
|
|||||||
"监听到关闭请求,开始轻量模式计时"
|
"监听到关闭请求,开始轻量模式计时"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return handler;
|
|
||||||
|
WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release);
|
||||||
}
|
}
|
||||||
0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_webview_focus_listener() -> u32 {
|
fn cancel_window_close_listener() {
|
||||||
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
|
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
|
||||||
|
if handler != 0 {
|
||||||
|
window.unlisten(handler);
|
||||||
|
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_webview_focus_listener() {
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
let handler = window.listen("tauri://focus", move |_event| {
|
let handler = window.listen("tauri://focus", move |_event| {
|
||||||
log_err!(cancel_light_weight_timer());
|
log_err!(cancel_light_weight_timer());
|
||||||
@@ -212,31 +258,34 @@ fn setup_webview_focus_listener() -> u32 {
|
|||||||
"监听到窗口获得焦点,取消轻量模式计时"
|
"监听到窗口获得焦点,取消轻量模式计时"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return handler;
|
|
||||||
|
WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release);
|
||||||
}
|
}
|
||||||
0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel_window_close_listener() {
|
fn cancel_webview_focus_listener() {
|
||||||
if let Some(window) = handle::Handle::global().get_window() {
|
if let Some(window) = handle::Handle::global().get_window() {
|
||||||
window.unlisten(setup_window_close_listener());
|
let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
|
||||||
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
|
if handler != 0 {
|
||||||
|
window.unlisten(handler);
|
||||||
|
logging!(info, Type::Lightweight, true, "取消了窗口焦点监听");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_light_weight_timer() -> Result<()> {
|
async fn setup_light_weight_timer() -> Result<()> {
|
||||||
Timer::global().init()?;
|
Timer::global().init().await?;
|
||||||
let once_by_minutes = Config::verge()
|
let once_by_minutes = Config::verge()
|
||||||
|
.await
|
||||||
.latest_ref()
|
.latest_ref()
|
||||||
.auto_light_weight_minutes
|
.auto_light_weight_minutes
|
||||||
.unwrap_or(10);
|
.unwrap_or(10);
|
||||||
|
|
||||||
// 获取task_id
|
// 获取task_id
|
||||||
let task_id = {
|
let task_id = {
|
||||||
let mut timer_count = Timer::global().timer_count.lock();
|
Timer::global()
|
||||||
let id = *timer_count;
|
.timer_count
|
||||||
*timer_count += 1;
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||||
id
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建任务
|
// 创建任务
|
||||||
@@ -246,7 +295,7 @@ fn setup_light_weight_timer() -> Result<()> {
|
|||||||
.set_frequency_once_by_minutes(once_by_minutes)
|
.set_frequency_once_by_minutes(once_by_minutes)
|
||||||
.spawn_async_routine(move || async move {
|
.spawn_async_routine(move || async move {
|
||||||
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
|
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
|
||||||
entry_lightweight_mode();
|
entry_lightweight_mode().await;
|
||||||
})
|
})
|
||||||
.context("failed to create timer task")?;
|
.context("failed to create timer task")?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
use crate::config::Config;
|
|
||||||
use mihomo_api;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use parking_lot::{Mutex, RwLock};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use tauri::http::HeaderMap;
|
|
||||||
|
|
||||||
// 缓存的最大有效期(5秒)
|
|
||||||
const CACHE_TTL: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq)]
|
|
||||||
pub struct Rate {
|
|
||||||
pub up: u64,
|
|
||||||
pub down: u64,
|
|
||||||
}
|
|
||||||
// 缓存MihomoManager实例
|
|
||||||
struct MihomoCache {
|
|
||||||
manager: mihomo_api::MihomoManager,
|
|
||||||
created_at: Instant,
|
|
||||||
server: String,
|
|
||||||
}
|
|
||||||
// 使用RwLock替代Mutex,允许多个读取操作并发进行
|
|
||||||
pub struct MihomoManager {
|
|
||||||
mihomo_cache: RwLock<Option<MihomoCache>>,
|
|
||||||
create_lock: Mutex<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MihomoManager {
|
|
||||||
fn __global() -> &'static MihomoManager {
|
|
||||||
static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager {
|
|
||||||
mihomo_cache: RwLock::new(None),
|
|
||||||
create_lock: Mutex::new(()),
|
|
||||||
});
|
|
||||||
&INSTANCE
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn global() -> mihomo_api::MihomoManager {
|
|
||||||
let instance = MihomoManager::__global();
|
|
||||||
|
|
||||||
// 尝试从缓存读取(只需读锁)
|
|
||||||
{
|
|
||||||
let cache = instance.mihomo_cache.read();
|
|
||||||
if let Some(cache_entry) = &*cache {
|
|
||||||
let (current_server, _) = MihomoManager::get_clash_client_info()
|
|
||||||
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
|
|
||||||
|
|
||||||
// 检查缓存是否有效
|
|
||||||
if cache_entry.server == current_server
|
|
||||||
&& cache_entry.created_at.elapsed() < CACHE_TTL
|
|
||||||
{
|
|
||||||
return cache_entry.manager.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存无效,获取创建锁
|
|
||||||
let _create_guard = instance.create_lock.lock();
|
|
||||||
|
|
||||||
// 再次检查缓存(双重检查锁定模式)
|
|
||||||
{
|
|
||||||
let cache = instance.mihomo_cache.read();
|
|
||||||
if let Some(cache_entry) = &*cache {
|
|
||||||
let (current_server, _) = MihomoManager::get_clash_client_info()
|
|
||||||
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
|
|
||||||
|
|
||||||
if cache_entry.server == current_server
|
|
||||||
&& cache_entry.created_at.elapsed() < CACHE_TTL
|
|
||||||
{
|
|
||||||
return cache_entry.manager.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新实例
|
|
||||||
let (current_server, headers) = MihomoManager::get_clash_client_info()
|
|
||||||
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
|
|
||||||
let manager = mihomo_api::MihomoManager::new(current_server.clone(), headers);
|
|
||||||
|
|
||||||
// 更新缓存
|
|
||||||
{
|
|
||||||
let mut cache = instance.mihomo_cache.write();
|
|
||||||
*cache = Some(MihomoCache {
|
|
||||||
manager: manager.clone(),
|
|
||||||
created_at: Instant::now(),
|
|
||||||
server: current_server,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
manager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MihomoManager {
|
|
||||||
pub fn get_clash_client_info() -> Option<(String, HeaderMap)> {
|
|
||||||
let client = { Config::clash().latest_ref().get_client_info() };
|
|
||||||
let server = format!("http://{}", client.server);
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
|
||||||
if let Some(secret) = client.secret {
|
|
||||||
let secret = format!("Bearer {secret}").parse().unwrap();
|
|
||||||
headers.insert("Authorization", secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((server, headers))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已移除未使用的 get_clash_client_info_or_default 和 get_traffic_ws_url 方法
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
pub mod lightweight;
|
pub mod lightweight;
|
||||||
pub mod mihomo;
|
|
||||||
pub mod sysinfo;
|
pub mod sysinfo;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
cmd::system,
|
cmd::system,
|
||||||
core::{handle, CoreManager},
|
core::{CoreManager, handle},
|
||||||
};
|
};
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
@@ -20,7 +20,13 @@ impl Debug for PlatformSpecification {
|
|||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}",
|
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}",
|
||||||
self.system_name, self.system_version, self.system_kernel_version, self.system_arch, self.verge_version, self.running_mode, self.is_admin
|
self.system_name,
|
||||||
|
self.system_version,
|
||||||
|
self.system_kernel_version,
|
||||||
|
self.system_arch,
|
||||||
|
self.verge_version,
|
||||||
|
self.running_mode,
|
||||||
|
self.is_admin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +38,17 @@ impl PlatformSpecification {
|
|||||||
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
|
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
|
||||||
let system_arch = System::cpu_arch();
|
let system_arch = System::cpu_arch();
|
||||||
|
|
||||||
let handler = handle::Handle::global().app_handle().unwrap();
|
let Some(handler) = handle::Handle::global().app_handle() else {
|
||||||
|
return Self {
|
||||||
|
system_name,
|
||||||
|
system_version,
|
||||||
|
system_kernel_version,
|
||||||
|
system_arch,
|
||||||
|
verge_version: "unknown".into(),
|
||||||
|
running_mode: "NotRunning".to_string(),
|
||||||
|
is_admin: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
let verge_version = handler.package_info().version.to_string();
|
let verge_version = handler.package_info().version.to_string();
|
||||||
|
|
||||||
// 使用默认值避免在同步上下文中执行异步操作
|
// 使用默认值避免在同步上下文中执行异步操作
|
||||||
@@ -52,10 +68,10 @@ impl PlatformSpecification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 异步方法来获取完整的系统信息
|
// 异步方法来获取完整的系统信息
|
||||||
pub async fn new_async() -> Self {
|
pub fn new_sync() -> Self {
|
||||||
let mut info = Self::new();
|
let mut info = Self::new();
|
||||||
|
|
||||||
let running_mode = CoreManager::global().get_running_mode().await;
|
let running_mode = CoreManager::global().get_running_mode();
|
||||||
info.running_mode = running_mode.to_string();
|
info.running_mode = running_mode.to_string();
|
||||||
|
|
||||||
info
|
info
|
||||||
|
|||||||
@@ -1,14 +1,84 @@
|
|||||||
|
#[cfg(feature = "tokio-trace")]
|
||||||
|
use std::any::type_name;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
#[cfg(feature = "tokio-trace")]
|
||||||
|
use std::panic::Location;
|
||||||
use tauri::{async_runtime, async_runtime::JoinHandle};
|
use tauri::{async_runtime, async_runtime::JoinHandle};
|
||||||
|
|
||||||
pub struct AsyncHandler;
|
pub struct AsyncHandler;
|
||||||
|
|
||||||
impl AsyncHandler {
|
impl AsyncHandler {
|
||||||
|
pub fn handle() -> async_runtime::RuntimeHandle {
|
||||||
|
async_runtime::handle()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn spawn<F, Fut>(f: F) -> JoinHandle<()>
|
pub fn spawn<F, Fut>(f: F) -> JoinHandle<()>
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Fut + Send + 'static,
|
F: FnOnce() -> Fut + Send + 'static,
|
||||||
Fut: Future<Output = ()> + Send + 'static,
|
Fut: Future<Output = ()> + Send + 'static,
|
||||||
{
|
{
|
||||||
|
#[cfg(feature = "tokio-trace")]
|
||||||
|
Self::log_task_info(&f);
|
||||||
async_runtime::spawn(f())
|
async_runtime::spawn(f())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn spawn_blocking<T, F>(f: F) -> JoinHandle<T>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> T + Send + 'static,
|
||||||
|
T: Send + 'static,
|
||||||
|
{
|
||||||
|
#[cfg(feature = "tokio-trace")]
|
||||||
|
Self::log_task_info(&f);
|
||||||
|
async_runtime::spawn_blocking(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[track_caller]
|
||||||
|
pub fn block_on<Fut>(fut: Fut) -> Fut::Output
|
||||||
|
where
|
||||||
|
Fut: Future + Send + 'static,
|
||||||
|
{
|
||||||
|
#[cfg(feature = "tokio-trace")]
|
||||||
|
Self::log_task_info(&fut);
|
||||||
|
async_runtime::block_on(fut)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio-trace")]
|
||||||
|
#[track_caller]
|
||||||
|
fn log_task_info<F>(f: &F)
|
||||||
|
where
|
||||||
|
F: ?Sized,
|
||||||
|
{
|
||||||
|
const TRACE_SPECIAL_SIZE: [usize; 3] = [0, 4, 24];
|
||||||
|
let size = std::mem::size_of_val(f);
|
||||||
|
if TRACE_SPECIAL_SIZE.contains(&size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let location = Location::caller();
|
||||||
|
let type_str = type_name::<F>();
|
||||||
|
let size_str = format!("{} bytes", size);
|
||||||
|
let loc_str = format!(
|
||||||
|
"{}:{}:{}",
|
||||||
|
location.file(),
|
||||||
|
location.line(),
|
||||||
|
location.column()
|
||||||
|
);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐"
|
||||||
|
);
|
||||||
|
println!("│ {:<18} │ {:<80} │", "Field", "Value");
|
||||||
|
println!(
|
||||||
|
"├────────────────────┼─────────────────────────────────────────────────────────────────────────────┤"
|
||||||
|
);
|
||||||
|
println!("│ {:<18} │ {:<80} │", "Type of task", type_str);
|
||||||
|
println!("│ {:<18} │ {:<80} │", "Size of task", size_str);
|
||||||
|
println!("│ {:<18} │ {:<80} │", "Called from", loc_str);
|
||||||
|
println!(
|
||||||
|
"└────────────────────┴─────────────────────────────────────────────────────────────────────────────┘"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
use std::sync::{Arc, Once, OnceLock};
|
|
||||||
|
|
||||||
use crate::{logging, utils::logging::Type};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct LightWeightState {
|
|
||||||
#[allow(unused)]
|
|
||||||
once: Arc<Once>,
|
|
||||||
pub is_lightweight: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LightWeightState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
once: Arc::new(Once::new()),
|
|
||||||
is_lightweight: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn run_once_time<F>(&self, f: F)
|
|
||||||
where
|
|
||||||
F: FnOnce() + Send + 'static,
|
|
||||||
{
|
|
||||||
self.once.call_once(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
|
|
||||||
self.is_lightweight = value;
|
|
||||||
if value {
|
|
||||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
|
||||||
} else {
|
|
||||||
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LightWeightState {
|
|
||||||
fn default() -> Self {
|
|
||||||
static INSTANCE: OnceLock<LightWeightState> = OnceLock::new();
|
|
||||||
INSTANCE.get_or_init(LightWeightState::new).clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
|
|
||||||
// https://tauri.app/develop/state-management/#do-you-need-arc
|
|
||||||
|
|
||||||
pub mod lightweight;
|
|
||||||
pub mod proxy;
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
use std::time::{Duration, Instant};
|
|
||||||
pub struct CacheEntry {
|
|
||||||
pub value: Arc<Value>,
|
|
||||||
pub expires_at: Instant,
|
|
||||||
}
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::OnceCell;
|
|
||||||
|
|
||||||
pub struct ProxyRequestCache {
|
|
||||||
pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProxyRequestCache {
|
|
||||||
pub fn global() -> &'static Self {
|
|
||||||
static INSTANCE: once_cell::sync::OnceCell<ProxyRequestCache> =
|
|
||||||
once_cell::sync::OnceCell::new();
|
|
||||||
INSTANCE.get_or_init(|| ProxyRequestCache {
|
|
||||||
map: DashMap::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn make_key(prefix: &str, id: &str) -> String {
|
|
||||||
format!("{prefix}:{id}")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<Value>
|
|
||||||
where
|
|
||||||
F: Fn() -> Fut,
|
|
||||||
Fut: std::future::Future<Output = Value>,
|
|
||||||
{
|
|
||||||
let now = Instant::now();
|
|
||||||
let key_cloned = key.clone();
|
|
||||||
let cell = self
|
|
||||||
.map
|
|
||||||
.entry(key)
|
|
||||||
.or_insert_with(|| Arc::new(OnceCell::new()))
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
if let Some(entry) = cell.get() {
|
|
||||||
if entry.expires_at > now {
|
|
||||||
return Arc::clone(&entry.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(entry) = cell.get() {
|
|
||||||
if entry.expires_at <= now {
|
|
||||||
self.map
|
|
||||||
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
|
|
||||||
let new_cell = Arc::new(OnceCell::new());
|
|
||||||
self.map.insert(key_cloned.clone(), new_cell.clone());
|
|
||||||
return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = fetch_fn().await;
|
|
||||||
let entry = CacheEntry {
|
|
||||||
value: Arc::new(value),
|
|
||||||
expires_at: Instant::now() + ttl,
|
|
||||||
};
|
|
||||||
let _ = cell.set(entry);
|
|
||||||
Arc::clone(&cell.get().unwrap().value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{Result, anyhow};
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user