mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 22:00:56 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -294,6 +294,15 @@ jobs:
|
||||
sudo apt-get update
|
||||
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
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
@@ -176,6 +176,15 @@ jobs:
|
||||
sudo apt-get update
|
||||
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
|
||||
name: Install pnpm
|
||||
with:
|
||||
@@ -216,7 +225,7 @@ jobs:
|
||||
prerelease: true
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
includeUpdaterJson: true
|
||||
# includeUpdaterJson: true
|
||||
|
||||
autobuild-arm-linux:
|
||||
name: Autobuild ARM Linux
|
||||
@@ -420,7 +429,7 @@ jobs:
|
||||
with:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
includeUpdaterJson: true
|
||||
# includeUpdaterJson: true
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
|
||||
@@ -42,21 +42,25 @@ jobs:
|
||||
sudo apt-get update
|
||||
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
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
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
|
||||
run: pnpm run web:build
|
||||
env:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
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
|
||||
@@ -141,6 +141,7 @@ jobs:
|
||||
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
|
||||
@@ -186,6 +187,15 @@ jobs:
|
||||
sudo apt-get update
|
||||
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
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -224,8 +234,6 @@ jobs:
|
||||
args: --target ${{ matrix.target }}
|
||||
includeUpdaterJson: true
|
||||
|
||||
|
||||
|
||||
release-for-linux-arm:
|
||||
name: Release Build for Linux ARM
|
||||
needs: [check_tag_version]
|
||||
@@ -521,7 +529,13 @@ jobs:
|
||||
notify-telegram:
|
||||
name: Notify Telegram
|
||||
runs-on: ubuntu-latest
|
||||
needs: [update_tag, release-update, release-update-for-fixed-webview2, submit-to-winget]
|
||||
needs:
|
||||
[
|
||||
update_tag,
|
||||
release-update,
|
||||
release-update-for-fixed-webview2,
|
||||
submit-to-winget,
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,3 +1,42 @@
|
||||
## v2.4.1
|
||||
|
||||
### 🏆 重大改进
|
||||
|
||||
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.13**
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 优化热键响应速度,提升快捷键操作体验
|
||||
- 改进服务管理响应性,减少系统服务操作等待时间
|
||||
- 提升文件和配置处理性能
|
||||
- 优化任务管理和日志记录效率
|
||||
- 优化异步内存管理,减少内存占用并提升多任务处理效率
|
||||
- 优化启动阶段初始化性能
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复应用在某些操作中可能出现的响应延迟问题
|
||||
- 修复任务管理中的潜在并发问题
|
||||
- 修复通过托盘重启应用无法恢复
|
||||
- 修复订阅在某些情况下无法导入
|
||||
- 修复无法新建订阅时使用远程链接
|
||||
- 修复卸载服务后的 tun 开关状态问题
|
||||
- 修复页面快速切换订阅时导致崩溃
|
||||
- 修复丢失工作目录时无法恢复环境
|
||||
- 修复从轻量模式恢复导致崩溃
|
||||
|
||||
### 👙 界面样式
|
||||
|
||||
- 统一代理设置样式
|
||||
|
||||
### 🗑️ 移除内容
|
||||
|
||||
- 移除启动阶段自动清理过期订阅
|
||||
|
||||
## v2.4.0
|
||||
|
||||
**发行代号:融**
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
plugins: { js },
|
||||
extends: ["js/recommended"],
|
||||
languageOptions: { globals: globals.browser },
|
||||
},
|
||||
tseslint.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
]);
|
||||
+22
-22
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
@@ -24,6 +24,7 @@
|
||||
"publish-version": "node scripts/publish-version.mjs",
|
||||
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
||||
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
|
||||
"lint": "eslint src",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
@@ -37,26 +38,23 @@
|
||||
"@mui/icons-material": "^7.3.1",
|
||||
"@mui/lab": "7.0.0-beta.16",
|
||||
"@mui/material": "^7.3.1",
|
||||
"@mui/x-data-grid": "^8.10.2",
|
||||
"@mui/x-data-grid": "^8.11.0",
|
||||
"@tauri-apps/api": "2.8.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.3",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.1",
|
||||
"@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-window-state": "^2.4.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.9.3",
|
||||
"ahooks": "^3.9.4",
|
||||
"axios": "^1.11.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"cli-color": "^2.0.4",
|
||||
"dayjs": "1.11.13",
|
||||
"dayjs": "1.11.16",
|
||||
"foxact": "^0.2.49",
|
||||
"glob": "^11.0.3",
|
||||
"i18next": "^25.4.0",
|
||||
"i18next": "^25.4.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -64,15 +62,13 @@
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"react": "19.1.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "19.1.1",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "15.7.0",
|
||||
"react-i18next": "15.7.3",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.59.0",
|
||||
"react-router-dom": "7.8.1",
|
||||
"react-router-dom": "7.8.2",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.3.6",
|
||||
@@ -82,28 +78,32 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@tauri-apps/cli": "2.8.1",
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@tauri-apps/cli": "2.8.3",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react": "5.0.1",
|
||||
"@vitejs/plugin-react": "5.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"commander": "^14.0.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.3.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"jiti": "^2.5.1",
|
||||
"meta-json-schema": "^1.19.12",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-organize-imports": "^4.2.0",
|
||||
"sass": "^1.90.0",
|
||||
"sass": "^1.91.0",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.41.0",
|
||||
"vite": "^7.1.3",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.13.2"
|
||||
|
||||
Generated
+3896
-396
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -38,5 +38,5 @@
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"ignoreDeps": ["serde_yaml", "sysinfo", "zip", "criterion"]
|
||||
"ignoreDeps": ["criterion"]
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ async function run() {
|
||||
execSync(`git tag ${tag}`, { stdio: "inherit" });
|
||||
execSync(`git push origin ${tag}`, { stdio: "inherit" });
|
||||
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import { execSync } from "child_process";
|
||||
function getGitShortCommit() {
|
||||
try {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
|
||||
return "nogit";
|
||||
}
|
||||
@@ -59,7 +59,7 @@ function getLatestTauriCommit() {
|
||||
.toString()
|
||||
.trim();
|
||||
return execSync(`git rev-parse --short ${fullHash}`).toString().trim();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.warn(
|
||||
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
|
||||
);
|
||||
|
||||
+10
-5
@@ -36,7 +36,7 @@ async function sendTelegramNotification() {
|
||||
releaseContent = readFileSync("release.txt", "utf-8");
|
||||
log_info("成功读取 release.txt 文件");
|
||||
} catch (error) {
|
||||
log_error("无法读取 release.txt,使用默认发布说明");
|
||||
log_error("无法读取 release.txt,使用默认发布说明", error);
|
||||
releaseContent = "更多新功能现已支持,详细更新日志请查看发布页面。";
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ async function sendTelegramNotification() {
|
||||
} else {
|
||||
let processedLine = line.replace(
|
||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||
'<a href="$2">$1</a>',
|
||||
(match, text, url) => {
|
||||
const encodedUrl = encodeURI(url);
|
||||
return `<a href="${encodedUrl}">${text}</a>`;
|
||||
},
|
||||
);
|
||||
processedLine = processedLine.replace(
|
||||
/\*\*([^*]+)\*\*/g,
|
||||
@@ -71,7 +74,8 @@ async function sendTelegramNotification() {
|
||||
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
|
||||
|
||||
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
|
||||
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${version}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
|
||||
const encodedVersion = encodeURIComponent(version);
|
||||
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
|
||||
|
||||
// 发送到 Telegram
|
||||
try {
|
||||
@@ -82,7 +86,7 @@ async function sendTelegramNotification() {
|
||||
text: content,
|
||||
link_preview_options: {
|
||||
is_disabled: false,
|
||||
url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${version}`,
|
||||
url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}`,
|
||||
prefer_large_media: true,
|
||||
},
|
||||
parse_mode: "HTML",
|
||||
@@ -93,6 +97,7 @@ async function sendTelegramNotification() {
|
||||
log_error(
|
||||
`❌ Telegram 通知发送失败到 ${chatId}:`,
|
||||
error.response?.data || error.message,
|
||||
error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -100,6 +105,6 @@ async function sendTelegramNotification() {
|
||||
|
||||
// 执行函数
|
||||
sendTelegramNotification().catch((error) => {
|
||||
log_error("脚本执行失败:", error.message);
|
||||
log_error("脚本执行失败:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ const UPDATE_LOG = "UPDATELOG.md";
|
||||
export async function resolveUpdateLog(tag) {
|
||||
const cwd = process.cwd();
|
||||
|
||||
const reTitle = /^## v[\d\.]+/;
|
||||
const reTitle = /^## v[\d.]+/;
|
||||
const reEnd = /^---/;
|
||||
|
||||
const file = path.join(cwd, UPDATE_LOG);
|
||||
@@ -54,7 +54,7 @@ export async function resolveUpdateLogDefault() {
|
||||
|
||||
const data = await fsp.readFile(file, "utf-8");
|
||||
|
||||
const reTitle = /^## v[\d\.]+/;
|
||||
const reTitle = /^## v[\d.]+/;
|
||||
const reEnd = /^---/;
|
||||
|
||||
let isCapturing = false;
|
||||
|
||||
Generated
+591
-446
File diff suppressed because it is too large
Load Diff
+18
-13
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -25,15 +25,15 @@ dunce = "1.0.5"
|
||||
log4rs = "1.3.0"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.41"
|
||||
sysinfo = { version = "=0.35.2", features = ["network", "system"] }
|
||||
sysinfo = { version = "0.37.0", features = ["network", "system"] }
|
||||
boa_engine = "0.20.0"
|
||||
serde_json = "1.0.143"
|
||||
serde_yaml = "0.9.34"
|
||||
serde_yaml_ng = "0.10.0"
|
||||
once_cell = "1.21.3"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3.1"
|
||||
percent-encoding = "2.3.2"
|
||||
tokio = { version = "1.47.1", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
@@ -41,10 +41,10 @@ tokio = { version = "1.47.1", features = [
|
||||
"sync",
|
||||
] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
reqwest = { version = "0.12.23", features = ["json", "rustls-tls", "cookies"] }
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
|
||||
regex = "1.11.2"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
tauri = { version = "2.8.2", features = [
|
||||
tauri = { version = "2.8.4", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
@@ -52,16 +52,16 @@ tauri = { version = "2.8.2", features = [
|
||||
"image-png",
|
||||
] }
|
||||
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.3"
|
||||
tauri-plugin-fs = "2.4.2"
|
||||
tauri-plugin-process = "2.3.0"
|
||||
tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-deep-link = "2.4.1"
|
||||
tauri-plugin-deep-link = "2.4.2"
|
||||
tauri-plugin-devtools = "2.0.1"
|
||||
tauri-plugin-window-state = "2.4.0"
|
||||
zip = "=4.2.0"
|
||||
reqwest_dav = "0.2.1"
|
||||
zip = "4.5.0"
|
||||
reqwest_dav = "0.2.2"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
getrandom = "0.3.3"
|
||||
@@ -73,11 +73,16 @@ hmac = "0.12.1"
|
||||
sha2 = "0.10.9"
|
||||
hex = "0.4.3"
|
||||
scopeguard = "1.2.0"
|
||||
kode-bridge = "0.2.1-rc1"
|
||||
kode-bridge = "0.2.1-rc2"
|
||||
dashmap = "6.1.0"
|
||||
tauri-plugin-notification = "2.3.0"
|
||||
tauri-plugin-notification = "2.3.1"
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
tokio-stream = "0.1.17"
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"text-decoding",
|
||||
"parking_lot",
|
||||
] }
|
||||
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
|
||||
+12
-12
@@ -4,18 +4,18 @@ use crate::{
|
||||
utils::{dirs, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use tauri::Manager;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
/// 打开应用程序所在目录
|
||||
#[tauri::command]
|
||||
pub fn open_app_dir() -> CmdResult<()> {
|
||||
pub async fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
/// 打开核心所在目录
|
||||
#[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 = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
wrap_err!(open::that(core_dir))
|
||||
@@ -23,7 +23,7 @@ pub fn open_core_dir() -> CmdResult<()> {
|
||||
|
||||
/// 打开日志目录
|
||||
#[tauri::command]
|
||||
pub fn open_logs_dir() -> CmdResult<()> {
|
||||
pub async fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||
wrap_err!(open::that(log_dir))
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
|
||||
/// 打开/关闭开发者工具
|
||||
#[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 !window.is_devtools_open() {
|
||||
window.open_devtools();
|
||||
@@ -48,14 +48,14 @@ pub fn open_devtools(app_handle: tauri::AppHandle) {
|
||||
|
||||
/// 退出应用
|
||||
#[tauri::command]
|
||||
pub fn exit_app() {
|
||||
feat::quit();
|
||||
pub async fn exit_app() {
|
||||
feat::quit().await;
|
||||
}
|
||||
|
||||
/// 重启应用
|
||||
#[tauri::command]
|
||||
pub async fn restart_app() -> CmdResult<()> {
|
||||
feat::restart_app();
|
||||
feat::restart_app().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
#[tauri::command]
|
||||
pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
log::info!(target: "app", "前端UI已准备就绪");
|
||||
crate::utils::resolve::mark_ui_ready();
|
||||
crate::utils::resolve::ui::mark_ui_ready();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
log::info!(target: "app", "UI加载阶段更新: {stage}");
|
||||
|
||||
use crate::utils::resolve::UiReadyStage;
|
||||
use crate::utils::resolve::ui::UiReadyStage;
|
||||
|
||||
let stage_enum = match stage.as_str() {
|
||||
"NotStarted" => UiReadyStage::NotStarted,
|
||||
@@ -232,7 +232,7 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -240,6 +240,6 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
#[tauri::command]
|
||||
pub fn reset_ui_ready_state() -> CmdResult<()> {
|
||||
log::info!(target: "app", "重置UI就绪状态");
|
||||
crate::utils::resolve::reset_ui_ready();
|
||||
crate::utils::resolve::ui::reset_ui_ready();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+107
-113
@@ -1,31 +1,33 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, CoreManager},
|
||||
};
|
||||
use crate::{
|
||||
config::*,
|
||||
core::*,
|
||||
feat,
|
||||
ipc::{self, IpcManager},
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
state::proxy::ProxyRequestCache,
|
||||
utils::logging::Type,
|
||||
wrap_err,
|
||||
};
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use std::time::Duration;
|
||||
|
||||
const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
/// 复制Clash环境变量
|
||||
#[tauri::command]
|
||||
pub fn copy_clash_env() -> CmdResult {
|
||||
feat::copy_clash_env();
|
||||
pub async fn copy_clash_env() -> CmdResult {
|
||||
feat::copy_clash_env().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取Clash信息
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest_ref().get_client_info())
|
||||
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().await.latest_ref().get_client_info())
|
||||
}
|
||||
|
||||
/// 修改Clash配置
|
||||
@@ -37,7 +39,7 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
/// 修改Clash模式
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
feat::change_clash_mode(payload);
|
||||
feat::change_clash_mode(payload).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -127,15 +129,22 @@ pub async fn clash_api_get_proxy_delay(
|
||||
/// 测试URL延迟
|
||||
#[tauri::command]
|
||||
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配置到单独文件
|
||||
#[tauri::command]
|
||||
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
use crate::utils::dirs;
|
||||
use serde_yaml;
|
||||
use std::fs;
|
||||
use serde_yaml_ng;
|
||||
use tokio::fs;
|
||||
|
||||
// 获取DNS配置文件路径
|
||||
let dns_path = dirs::app_home_dir()
|
||||
@@ -143,8 +152,10 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
.join("dns_config.yaml");
|
||||
|
||||
// 保存DNS配置到文件
|
||||
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
||||
fs::write(&dns_path, yaml_str).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)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
|
||||
|
||||
Ok(())
|
||||
@@ -152,111 +163,92 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
|
||||
/// 应用或撤销DNS配置
|
||||
#[tauri::command]
|
||||
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, CoreManager},
|
||||
utils::dirs,
|
||||
};
|
||||
|
||||
// 使用spawn来处理异步操作
|
||||
AsyncHandler::spawn(move || async move {
|
||||
if apply {
|
||||
// 读取DNS配置文件
|
||||
let dns_path = match dirs::app_home_dir() {
|
||||
Ok(path) => path.join("dns_config.yaml"),
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "Failed to get home dir: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if apply {
|
||||
// 读取DNS配置文件
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
if !dns_path.exists() {
|
||||
logging!(warn, Type::Config, "DNS config file not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "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) => {
|
||||
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
logging!(info, Type::Config, "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() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to regenerate config with DNS: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 然后应用新配置
|
||||
if let Err(err) = CoreManager::global().update_config().await {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to apply config with DNS: {err}"
|
||||
);
|
||||
} else {
|
||||
logging!(info, Type::Config, "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
} else {
|
||||
// 当关闭DNS设置时,不需要对配置进行任何修改
|
||||
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"DNS settings disabled, regenerating config"
|
||||
);
|
||||
|
||||
// 重新生成配置
|
||||
if let Err(err) = Config::generate() {
|
||||
logging!(error, Type::Config, "Failed to regenerate config: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用新配置
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Config, "Config regenerated successfully");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to apply regenerated config: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
if !dns_path.exists() {
|
||||
logging!(warn, Type::Config, "DNS config file not found");
|
||||
return Err("DNS config file not found".into());
|
||||
}
|
||||
});
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -277,17 +269,19 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
|
||||
#[tauri::command]
|
||||
pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||
use crate::utils::dirs;
|
||||
use std::fs;
|
||||
use tokio::fs;
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.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());
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ use super::CmdResult;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||
lightweight::entry_lightweight_mode();
|
||||
lightweight::entry_lightweight_mode().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn exit_lightweight_mode() -> CmdResult {
|
||||
lightweight::exit_lightweight_mode();
|
||||
lightweight::exit_lightweight_mode().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::wrap_err;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml_ng::Mapping;
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
@@ -31,7 +31,7 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
let current = proxy_manager.get_auto_proxy_cached();
|
||||
let current = proxy_manager.get_auto_proxy_cached().await;
|
||||
// 异步请求更新,立即返回缓存数据
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let _ = proxy_manager.get_auto_proxy_async().await;
|
||||
|
||||
+189
-245
@@ -1,6 +1,12 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
config::{Config, IProfiles, PrfItem, PrfOption},
|
||||
config::{
|
||||
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, Config, IProfiles, PrfItem, PrfOption,
|
||||
},
|
||||
core::{handle, timer::Timer, tray::Tray, CoreManager},
|
||||
feat, logging,
|
||||
process::AsyncHandler,
|
||||
@@ -8,92 +14,62 @@ use crate::{
|
||||
utils::{dirs, help, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
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_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]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
// 策略1: 尝试快速获取latest数据
|
||||
let latest_result = tokio::time::timeout(
|
||||
Duration::from_millis(500),
|
||||
AsyncHandler::spawn_blocking(move || {
|
||||
let profiles = Config::profiles();
|
||||
let latest = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: latest.current.clone(),
|
||||
items: latest.items.clone(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
let latest_result = tokio::time::timeout(Duration::from_millis(500), async {
|
||||
let profiles = Config::profiles().await;
|
||||
let latest = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: latest.current.clone(),
|
||||
items: latest.items.clone(),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match latest_result {
|
||||
Ok(Ok(profiles)) => {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Ok(Err(join_err)) => {
|
||||
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
|
||||
}
|
||||
}
|
||||
|
||||
// 策略2: 如果快速获取失败,尝试获取data()
|
||||
let data_result = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
AsyncHandler::spawn_blocking(move || {
|
||||
let profiles = Config::profiles();
|
||||
let data = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: data.current.clone(),
|
||||
items: data.items.clone(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
let data_result = tokio::time::timeout(Duration::from_secs(2), async {
|
||||
let profiles = Config::profiles().await;
|
||||
let data = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: data.current.clone(),
|
||||
items: data.items.clone(),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match data_result {
|
||||
Ok(Ok(profiles)) => {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Ok(Err(join_err)) => {
|
||||
Err(join_err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"获取draft配置任务失败: {}",
|
||||
"获取draft配置任务失败或超时: {}",
|
||||
join_err
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
|
||||
}
|
||||
}
|
||||
|
||||
// 策略3: fallback,尝试重新创建配置
|
||||
@@ -104,26 +80,19 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
"所有获取配置策略都失败,尝试fallback"
|
||||
);
|
||||
|
||||
match AsyncHandler::spawn_blocking(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![]),
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(IProfiles::new().await)
|
||||
}
|
||||
|
||||
/// 增强配置文件
|
||||
#[tauri::command]
|
||||
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();
|
||||
Ok(())
|
||||
}
|
||||
@@ -133,82 +102,76 @@ pub async fn enhance_profiles() -> CmdResult {
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url);
|
||||
|
||||
// 使用超时保护避免长时间阻塞
|
||||
let import_result = tokio::time::timeout(
|
||||
Duration::from_secs(60), // 60秒超时
|
||||
async {
|
||||
let item = PrfItem::from_url(&url, None, None, option).await?;
|
||||
logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置");
|
||||
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 pre_count = Config::profiles()
|
||||
.latest_ref()
|
||||
.items
|
||||
.as_ref()
|
||||
.map_or(0, |items| items.len());
|
||||
let profiles = Config::profiles().await;
|
||||
let pre_count = profiles
|
||||
.latest_ref()
|
||||
.items
|
||||
.as_ref()
|
||||
.map_or(0, |items| items.len());
|
||||
|
||||
Config::profiles().data_mut().append_item(item.clone())?;
|
||||
let result = profiles_append_item_safe(item.clone()).await;
|
||||
result?;
|
||||
|
||||
// 验证导入是否成功
|
||||
let post_count = Config::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!("配置导入后数量未增加"));
|
||||
}
|
||||
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,
|
||||
"[导入订阅] 配置保存成功,数量: {} -> {}",
|
||||
pre_count,
|
||||
post_count
|
||||
"[导入订阅] 发送配置变更通知: {}",
|
||||
uid
|
||||
);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
}
|
||||
|
||||
// 立即发送配置变更通知
|
||||
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, "[导入订阅] 配置文件保存成功");
|
||||
|
||||
// 异步保存配置文件并发送全局通知
|
||||
let uid_clone = item.uid.clone();
|
||||
crate::process::AsyncHandler::spawn(move || async move {
|
||||
// 在异步块中重新获取锁,避免跨await问题
|
||||
let save_result = { Config::profiles().data_mut().save_file() };
|
||||
|
||||
if let Err(e) = save_result {
|
||||
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);
|
||||
}
|
||||
// 发送全局配置更新通知
|
||||
if let Some(uid) = uid_clone {
|
||||
// 延迟发送,确保文件已完全写入
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match import_result {
|
||||
@@ -227,36 +190,62 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新排序配置文件
|
||||
/// 调整profile的顺序
|
||||
#[tauri::command]
|
||||
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]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data_mut().append_item(item))
|
||||
match profiles_append_item_with_filedata_safe(item, file_data).await {
|
||||
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]
|
||||
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]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let should_update = wrap_err!({ Config::profiles().data_mut().delete_item(index) })?;
|
||||
|
||||
// 删除后自动清理冗余文件
|
||||
let _ = Config::profiles().latest_ref().auto_cleanup();
|
||||
// 使用Send-safe helper函数
|
||||
let should_update = wrap_err!(profiles_delete_item_safe(index).await)?;
|
||||
|
||||
if should_update {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "{}", e);
|
||||
return Err(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -264,6 +253,12 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
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 target_profile = profiles.current.clone();
|
||||
@@ -277,35 +272,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
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);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
@@ -320,7 +286,7 @@ 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);
|
||||
|
||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||
@@ -330,7 +296,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
|
||||
// 获取目标配置文件路径
|
||||
let config_file_result = {
|
||||
let profiles_config = Config::profiles();
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_data = profiles_config.latest_ref();
|
||||
match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
@@ -375,7 +341,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
match file_read_result {
|
||||
Ok(Ok(content)) => {
|
||||
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
|
||||
serde_yaml::from_str::<serde_yaml::Value>(&content)
|
||||
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -446,18 +412,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
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配置
|
||||
logging!(
|
||||
info,
|
||||
@@ -469,7 +423,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
|
||||
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);
|
||||
@@ -482,7 +436,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
Config::profiles().await.discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -514,7 +468,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
Config::profiles().await.discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -525,7 +479,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
"配置更新成功,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().apply();
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||
@@ -535,20 +489,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() {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||
}
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_menu() {
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
if let Err(e) = Tray::global().update_menu().await {
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
|
||||
// 保存配置文件
|
||||
if let Err(e) = Config::profiles().data_mut().save_file() {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
// 保存配置文件
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
||||
}
|
||||
|
||||
// 立即通知前端配置变更
|
||||
if let Some(current) = ¤t_value {
|
||||
@@ -563,13 +515,12 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
}
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置切换完成").await;
|
||||
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(true)
|
||||
}
|
||||
Ok(Ok((false, error_msg))) => {
|
||||
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().discard();
|
||||
Config::profiles().await.discard();
|
||||
// 如果验证失败,恢复到之前的配置
|
||||
if let Some(prev_profile) = current_profile {
|
||||
logging!(
|
||||
@@ -586,13 +537,14 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 静默恢复,不触发验证
|
||||
wrap_err!({
|
||||
Config::profiles()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
})?;
|
||||
Config::profiles().apply();
|
||||
Config::profiles().await.apply();
|
||||
|
||||
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}");
|
||||
}
|
||||
});
|
||||
@@ -602,9 +554,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置验证失败").await;
|
||||
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
@@ -616,11 +566,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
e,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
Config::profiles().await.discard();
|
||||
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)
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -634,7 +583,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
timeout_msg,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
Config::profiles().await.discard();
|
||||
|
||||
if let Some(prev_profile) = current_profile {
|
||||
logging!(
|
||||
@@ -651,16 +600,15 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
};
|
||||
wrap_err!({
|
||||
Config::profiles()
|
||||
.await
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
})?;
|
||||
Config::profiles().apply();
|
||||
Config::profiles().await.apply();
|
||||
}
|
||||
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置更新超时").await;
|
||||
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
@@ -668,10 +616,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config_by_profile_index(
|
||||
_app_handle: tauri::AppHandle,
|
||||
profile_index: String,
|
||||
) -> CmdResult<bool> {
|
||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<bool> {
|
||||
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
|
||||
|
||||
let profiles = IProfiles {
|
||||
@@ -683,28 +628,26 @@ pub async fn patch_profiles_config_by_profile_index(
|
||||
|
||||
/// 修改某个profile item的
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
// 保存修改前检查是否有更新 update_interval
|
||||
let update_interval_changed =
|
||||
if let Ok(old_profile) = Config::profiles().latest_ref().get_item(&index) {
|
||||
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);
|
||||
old_interval != new_interval
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let profiles = Config::profiles().await;
|
||||
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 new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
old_interval != new_interval
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 保存修改
|
||||
wrap_err!(Config::profiles()
|
||||
.data_mut()
|
||||
.patch_item(index.clone(), profile))?;
|
||||
wrap_err!(profiles_patch_item_safe(index.clone(), profile).await)?;
|
||||
|
||||
// 如果更新间隔变更,异步刷新定时器
|
||||
if update_interval_changed {
|
||||
let index_clone = index.clone();
|
||||
crate::process::AsyncHandler::spawn(move || async move {
|
||||
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);
|
||||
} else {
|
||||
// 刷新成功后发送自定义事件,不触发配置重载
|
||||
@@ -718,9 +661,10 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
|
||||
/// 查看配置文件
|
||||
#[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 file = {
|
||||
wrap_err!(Config::profiles().latest_ref().get_item(&index))?
|
||||
wrap_err!(profiles.latest_ref().get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
@@ -731,23 +675,23 @@ pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
ret_err!("the file not found");
|
||||
}
|
||||
|
||||
wrap_err!(help::open_file(app_handle, path))
|
||||
wrap_err!(help::open_file(path))
|
||||
}
|
||||
|
||||
/// 读取配置文件内容
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest_ref();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_ref();
|
||||
let item = wrap_err!(profiles_ref.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 获取下一次更新时间
|
||||
#[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 next_time = timer.get_next_update_time(&uid);
|
||||
let next_time = timer.get_next_update_time(&uid).await;
|
||||
Ok(next_time)
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, wrap_err};
|
||||
use anyhow::Context;
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 获取运行时配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest_ref().config.clone())
|
||||
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().await.latest_ref().config.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时YAML配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
pub async fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_ref();
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(
|
||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||
|config| serde_yaml_ng::to_string(config).context("failed to convert config to yaml")
|
||||
))
|
||||
}
|
||||
|
||||
/// 获取运行时存在的键
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest_ref().exists_keys.clone())
|
||||
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().await.latest_ref().exists_keys.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时日志
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest_ref().chain_logs.clone())
|
||||
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().await.latest_ref().chain_logs.clone())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
utils::{dirs, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use std::fs;
|
||||
use tokio::fs;
|
||||
|
||||
/// 保存profiles的配置
|
||||
#[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 profiles = Config::profiles();
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_guard = profiles.latest_ref();
|
||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||
// 确定是否为merge类型文件
|
||||
@@ -30,7 +30,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
|
||||
// 保存新的配置文件
|
||||
let file_data = file_data.ok_or("file_data is None")?;
|
||||
wrap_err!(fs::write(&file_path, &file_data))?;
|
||||
wrap_err!(fs::write(&file_path, &file_data).await)?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
logging!(
|
||||
@@ -88,7 +88,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
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());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
@@ -103,7 +103,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
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")
|
||||
@@ -177,7 +177,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
wrap_err!(fs::write(&file_path, original_content).await)?;
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,19 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
async fn execute_service_operation_sync<F, E>(service_op: F, op_type: &str) -> CmdResult
|
||||
async fn execute_service_operation_sync<F, Fut, E>(service_op: F, op_type: &str) -> CmdResult
|
||||
where
|
||||
F: FnOnce() -> Result<(), E>,
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: std::future::Future<Output = Result<(), E>>,
|
||||
E: ToString + std::fmt::Debug,
|
||||
{
|
||||
if let Err(e) = service_op() {
|
||||
if let Err(e) = service_op().await {
|
||||
let emsg = format!("{} {} failed: {}", op_type, "Service", e.to_string());
|
||||
return Err(t(emsg.as_str()));
|
||||
return Err(t(emsg.as_str()).await);
|
||||
}
|
||||
if CoreManager::global().restart_core().await.is_err() {
|
||||
let emsg = format!("{} {} failed", "Restart", "Core");
|
||||
return Err(t(emsg.as_str()));
|
||||
return Err(t(emsg.as_str()).await);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,10 +3,14 @@ use crate::{config::*, feat, wrap_err};
|
||||
|
||||
/// 获取Verge配置
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.latest_ref().clone();
|
||||
Ok(IVergeResponse::from(*verge_data))
|
||||
pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge().await;
|
||||
let verge_data = {
|
||||
let ref_data = verge.latest_ref();
|
||||
ref_data.clone()
|
||||
};
|
||||
let verge_response = IVergeResponse::from(*verge_data);
|
||||
Ok(verge_response)
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
|
||||
@@ -11,11 +11,17 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge().draft_mut().patch_config(patch.clone());
|
||||
Config::verge().apply();
|
||||
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()
|
||||
.await
|
||||
.map_err(|err| err.to_string())?;
|
||||
core::backup::WebDavClient::global().reset();
|
||||
Ok(())
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::utils::dirs::{ipc_path, path_to_str};
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
str::FromStr,
|
||||
@@ -13,9 +13,16 @@ use std::{
|
||||
pub struct IClashTemp(pub Mapping);
|
||||
|
||||
impl IClashTemp {
|
||||
pub fn new() -> Self {
|
||||
pub async fn new() -> Self {
|
||||
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) => {
|
||||
template.0.keys().for_each(|key| {
|
||||
if !map.contains_key(key) {
|
||||
@@ -135,12 +142,13 @@ impl IClashTemp {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_config(&self) -> Result<()> {
|
||||
pub async fn save_config(&self) -> Result<()> {
|
||||
help::save_yaml(
|
||||
&dirs::clash_path()?,
|
||||
&self.0,
|
||||
Some("# Generated by Clash Verge"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn get_mixed_port(&self) -> u16 {
|
||||
@@ -280,9 +288,10 @@ impl IClashTemp {
|
||||
Self::guard_server_ctrl(config)
|
||||
}
|
||||
|
||||
pub fn guard_external_controller_with_setting(config: &Mapping) -> String {
|
||||
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);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use crate::{
|
||||
config::PrfItem,
|
||||
config::{profiles_append_item_safe, PrfItem},
|
||||
core::{handle, CoreManager},
|
||||
enhance, logging,
|
||||
process::AsyncHandler,
|
||||
utils::{dirs, help, logging::Type},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
@@ -22,64 +21,65 @@ pub struct Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn global() -> &'static Config {
|
||||
static CONFIG: OnceCell<Config> = OnceCell::new();
|
||||
|
||||
CONFIG.get_or_init(|| Config {
|
||||
clash_config: Draft::from(Box::new(IClashTemp::new())),
|
||||
verge_config: Draft::from(Box::new(IVerge::new())),
|
||||
profiles_config: Draft::from(Box::new(IProfiles::new())),
|
||||
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
||||
})
|
||||
pub async fn global() -> &'static Config {
|
||||
static CONFIG: OnceCell<Config> = OnceCell::const_new();
|
||||
CONFIG
|
||||
.get_or_init(|| async {
|
||||
Config {
|
||||
clash_config: Draft::from(Box::new(IClashTemp::new().await)),
|
||||
verge_config: Draft::from(Box::new(IVerge::new().await)),
|
||||
profiles_config: Draft::from(Box::new(IProfiles::new().await)),
|
||||
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn clash() -> Draft<Box<IClashTemp>> {
|
||||
Self::global().clash_config.clone()
|
||||
pub async fn clash() -> Draft<Box<IClashTemp>> {
|
||||
Self::global().await.clash_config.clone()
|
||||
}
|
||||
|
||||
pub fn verge() -> Draft<Box<IVerge>> {
|
||||
Self::global().verge_config.clone()
|
||||
pub async fn verge() -> Draft<Box<IVerge>> {
|
||||
Self::global().await.verge_config.clone()
|
||||
}
|
||||
|
||||
pub fn profiles() -> Draft<Box<IProfiles>> {
|
||||
Self::global().profiles_config.clone()
|
||||
pub async fn profiles() -> Draft<Box<IProfiles>> {
|
||||
Self::global().await.profiles_config.clone()
|
||||
}
|
||||
|
||||
pub fn runtime() -> Draft<Box<IRuntime>> {
|
||||
Self::global().runtime_config.clone()
|
||||
pub async fn runtime() -> Draft<Box<IRuntime>> {
|
||||
Self::global().await.runtime_config.clone()
|
||||
}
|
||||
|
||||
/// 初始化订阅
|
||||
pub async fn init_config() -> Result<()> {
|
||||
if Self::profiles()
|
||||
.await
|
||||
.latest_ref()
|
||||
.get_item(&"Merge".to_string())
|
||||
.is_err()
|
||||
{
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
|
||||
Self::profiles()
|
||||
.data_mut()
|
||||
.append_item(merge_item.clone())?;
|
||||
profiles_append_item_safe(merge_item.clone()).await?;
|
||||
}
|
||||
if Self::profiles()
|
||||
.await
|
||||
.latest_ref()
|
||||
.get_item(&"Script".to_string())
|
||||
.is_err()
|
||||
{
|
||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||
Self::profiles()
|
||||
.data_mut()
|
||||
.append_item(script_item.clone())?;
|
||||
profiles_append_item_safe(script_item.clone()).await?;
|
||||
}
|
||||
// 生成运行时配置
|
||||
if let Err(err) = Self::generate() {
|
||||
if let Err(err) = Self::generate().await {
|
||||
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "生成运行时配置成功");
|
||||
}
|
||||
|
||||
// 生成运行时配置文件并验证
|
||||
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() {
|
||||
// 验证配置文件
|
||||
@@ -96,7 +96,8 @@ impl Config {
|
||||
error_msg
|
||||
);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::boot_error", &error_msg)?;
|
||||
.use_default_config("config_validate::boot_error", &error_msg)
|
||||
.await?;
|
||||
Some(("config_validate::boot_error", error_msg))
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "配置验证成功");
|
||||
@@ -106,50 +107,53 @@ impl Config {
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::process_terminated", "")?;
|
||||
.use_default_config("config_validate::process_terminated", "")
|
||||
.await?;
|
||||
Some(("config_validate::process_terminated", String::new()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
||||
CoreManager::global().use_default_config("config_validate::error", "")?;
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::error", "")
|
||||
.await?;
|
||||
Some(("config_validate::error", String::new()))
|
||||
};
|
||||
|
||||
// 在单独的任务中发送通知
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
handle::Handle::notice_message(msg_type, &msg_content);
|
||||
});
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
handle::Handle::notice_message(msg_type, &msg_content);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将订阅丢到对应的文件中
|
||||
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||
pub async fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||
let path = match typ {
|
||||
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
||||
ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG),
|
||||
};
|
||||
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest_ref();
|
||||
let runtime = Config::runtime().await;
|
||||
let config = runtime
|
||||
.latest_ref()
|
||||
.config
|
||||
.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)
|
||||
}
|
||||
|
||||
/// 生成订阅存好
|
||||
pub fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance();
|
||||
pub async fn generate() -> Result<()> {
|
||||
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),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
|
||||
@@ -4,7 +4,7 @@ mod config;
|
||||
mod draft;
|
||||
mod encrypt;
|
||||
mod prfitem;
|
||||
mod profiles;
|
||||
pub mod profiles;
|
||||
mod runtime;
|
||||
mod verge;
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@ use crate::utils::{
|
||||
tmpl,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use std::{fs, time::Duration};
|
||||
|
||||
use super::Config;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct PrfItem {
|
||||
pub uid: Option<String>,
|
||||
@@ -163,7 +160,7 @@ impl PrfItem {
|
||||
"local" => {
|
||||
let name = item.name.unwrap_or("Local File".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}\""),
|
||||
}
|
||||
@@ -171,7 +168,7 @@ impl PrfItem {
|
||||
|
||||
/// ## Local type
|
||||
/// create a new item from name/desc
|
||||
pub fn from_local(
|
||||
pub async fn from_local(
|
||||
name: String,
|
||||
desc: String,
|
||||
file_data: Option<String>,
|
||||
@@ -189,37 +186,27 @@ impl PrfItem {
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(merge_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(script_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(rules_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(proxies_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(groups_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
Ok(PrfItem {
|
||||
@@ -278,7 +265,7 @@ impl PrfItem {
|
||||
};
|
||||
|
||||
// 使用网络管理器发送请求
|
||||
let resp = match NetworkManager::global()
|
||||
let resp = match NetworkManager::new()
|
||||
.get_with_interrupt(
|
||||
url,
|
||||
proxy_type,
|
||||
@@ -296,7 +283,7 @@ impl PrfItem {
|
||||
};
|
||||
|
||||
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}")
|
||||
}
|
||||
|
||||
@@ -362,13 +349,13 @@ impl PrfItem {
|
||||
let uid = help::get_uid("R");
|
||||
let file = format!("{uid}.yaml");
|
||||
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"
|
||||
let data = data.trim_start_matches('\u{feff}');
|
||||
|
||||
// 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")?;
|
||||
|
||||
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
|
||||
@@ -377,37 +364,27 @@ impl PrfItem {
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(merge_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(script_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(rules_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(proxies_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(groups_item.clone())?;
|
||||
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use super::{prfitem::PrfItem, PrfOption};
|
||||
use crate::utils::{dirs, help};
|
||||
use crate::{
|
||||
logging_error,
|
||||
process::AsyncHandler,
|
||||
utils::{dirs, help, logging::Type},
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::{collections::HashSet, fs, io::Write};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use std::collections::HashSet;
|
||||
use tokio::fs;
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -32,22 +37,28 @@ macro_rules! patch {
|
||||
}
|
||||
|
||||
impl IProfiles {
|
||||
pub fn new() -> Self {
|
||||
match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) {
|
||||
Ok(mut profiles) => {
|
||||
if profiles.items.is_none() {
|
||||
profiles.items = Some(vec![]);
|
||||
}
|
||||
// compatible with the old old old version
|
||||
if let Some(items) = profiles.items.as_mut() {
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d"));
|
||||
pub async fn new() -> Self {
|
||||
match dirs::profiles_path() {
|
||||
Ok(path) => match help::read_yaml::<Self>(&path).await {
|
||||
Ok(mut profiles) => {
|
||||
if profiles.items.is_none() {
|
||||
profiles.items = Some(vec![]);
|
||||
}
|
||||
// compatible with the old old old version
|
||||
if let Some(items) = profiles.items.as_mut() {
|
||||
for item in items.iter_mut() {
|
||||
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) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
@@ -62,12 +73,13 @@ impl IProfiles {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
pub async fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(
|
||||
&dirs::profiles_path()?,
|
||||
self,
|
||||
Some("# Profiles Config for Clash Verge"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 只修改current,valid和chain
|
||||
@@ -115,7 +127,7 @@ impl IProfiles {
|
||||
/// append new item
|
||||
/// if the file_data is some
|
||||
/// 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() {
|
||||
bail!("the uid should not be null");
|
||||
}
|
||||
@@ -133,9 +145,8 @@ impl IProfiles {
|
||||
})?;
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||
.write(file_data.as_bytes())
|
||||
fs::write(&path, file_data.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
}
|
||||
|
||||
@@ -153,11 +164,11 @@ impl IProfiles {
|
||||
items.push(item)
|
||||
}
|
||||
|
||||
self.save_file()
|
||||
self.save_file().await
|
||||
}
|
||||
|
||||
/// 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 old_index = None;
|
||||
let mut new_index = None;
|
||||
@@ -178,11 +189,11 @@ impl IProfiles {
|
||||
let item = items.remove(old_idx);
|
||||
items.insert(new_idx, item);
|
||||
self.items = Some(items);
|
||||
self.save_file()
|
||||
self.save_file().await
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
for each in items.iter_mut() {
|
||||
@@ -198,7 +209,7 @@ impl IProfiles {
|
||||
patch!(each, item, option);
|
||||
|
||||
self.items = Some(items);
|
||||
return self.save_file();
|
||||
return self.save_file().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +219,7 @@ impl IProfiles {
|
||||
|
||||
/// be used to update the remote item
|
||||
/// 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() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
@@ -237,9 +248,8 @@ impl IProfiles {
|
||||
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||
.write(file_data.as_bytes())
|
||||
fs::write(&path, file_data.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
}
|
||||
|
||||
@@ -248,12 +258,12 @@ impl IProfiles {
|
||||
}
|
||||
}
|
||||
|
||||
self.save_file()
|
||||
self.save_file().await
|
||||
}
|
||||
|
||||
/// delete item
|
||||
/// 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 = current.clone();
|
||||
let item = self.get_item(&uid)?;
|
||||
@@ -279,10 +289,19 @@ impl IProfiles {
|
||||
}
|
||||
if let Some(index) = index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
let result = fs::remove_file(path.clone()).await;
|
||||
if let Err(err) = result {
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
false,
|
||||
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -296,10 +315,19 @@ impl IProfiles {
|
||||
}
|
||||
if let Some(index) = merge_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
let result = fs::remove_file(path.clone()).await;
|
||||
if let Err(err) = result {
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
false,
|
||||
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -313,10 +341,19 @@ impl IProfiles {
|
||||
}
|
||||
if let Some(index) = script_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
let result = fs::remove_file(path.clone()).await;
|
||||
if let Err(err) = result {
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
false,
|
||||
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -330,10 +367,19 @@ impl IProfiles {
|
||||
}
|
||||
if let Some(index) = rules_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
let result = fs::remove_file(path.clone()).await;
|
||||
if let Err(err) = result {
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
false,
|
||||
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -347,10 +393,19 @@ impl IProfiles {
|
||||
}
|
||||
if let Some(index) = proxies_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
let result = fs::remove_file(path.clone()).await;
|
||||
if let Err(err) = result {
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
false,
|
||||
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -364,10 +419,19 @@ impl IProfiles {
|
||||
}
|
||||
if let Some(index) = groups_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let _ = dirs::app_profiles_dir().map(async move |path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
let result = fs::remove_file(path.clone()).await;
|
||||
if let Err(err) = result {
|
||||
logging_error!(
|
||||
Type::Config,
|
||||
false,
|
||||
"[配置文件删除] 删除文件 {} 失败: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -386,12 +450,12 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
self.items = Some(items);
|
||||
self.save_file()?;
|
||||
self.save_file().await?;
|
||||
Ok(current == uid)
|
||||
}
|
||||
|
||||
/// 获取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()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
@@ -399,7 +463,7 @@ impl IProfiles {
|
||||
Some(file) => dirs::app_profiles_dir()?.join(file),
|
||||
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}\"");
|
||||
}
|
||||
@@ -671,23 +735,95 @@ impl IProfiles {
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auto_cleanup(&self) -> Result<()> {
|
||||
match self.cleanup_orphaned_files() {
|
||||
Ok(result) => {
|
||||
if !result.deleted_files.is_empty() {
|
||||
log::info!(
|
||||
target: "app",
|
||||
"自动清理完成,删除了 {} 个冗余文件",
|
||||
result.deleted_files.len()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "自动清理失败: {e}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
// 特殊的Send-safe helper函数,完全避免跨await持有guard
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn profiles_append_item_with_filedata_safe(
|
||||
item: PrfItem,
|
||||
file_data: Option<String>,
|
||||
) -> Result<()> {
|
||||
AsyncHandler::spawn_blocking(move || {
|
||||
AsyncHandler::handle().block_on(async {
|
||||
let item = PrfItem::from(item, file_data).await?;
|
||||
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_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 serde::{Deserialize, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use std::collections::HashMap;
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IRuntime {
|
||||
|
||||
@@ -237,9 +237,9 @@ impl IVerge {
|
||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
||||
|
||||
/// 验证并修正配置文件中的clash_core值
|
||||
pub fn validate_and_fix_config() -> Result<()> {
|
||||
pub async fn validate_and_fix_config() -> Result<()> {
|
||||
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,
|
||||
Err(_) => Self::template(),
|
||||
};
|
||||
@@ -273,7 +273,7 @@ impl IVerge {
|
||||
// 修正后保存配置
|
||||
if needs_fix {
|
||||
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!(
|
||||
info,
|
||||
Type::Config,
|
||||
@@ -281,7 +281,7 @@ impl IVerge {
|
||||
"配置文件修正完成,需要重新加载配置"
|
||||
);
|
||||
|
||||
Self::reload_config_after_fix(config)?;
|
||||
Self::reload_config_after_fix(config).await?;
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
@@ -296,10 +296,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;
|
||||
|
||||
let config_draft = Config::verge();
|
||||
let config_draft = Config::verge().await;
|
||||
*config_draft.draft_mut() = Box::new(updated_config.clone());
|
||||
config_draft.apply();
|
||||
|
||||
@@ -335,9 +335,15 @@ impl IVerge {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||
Ok(config) => config,
|
||||
pub async fn new() -> Self {
|
||||
match dirs::verge_path() {
|
||||
Ok(path) => match help::read_yaml::<IVerge>(&path).await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
@@ -408,8 +414,8 @@ impl IVerge {
|
||||
}
|
||||
|
||||
/// Save IVerge App Config
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
|
||||
pub async fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")).await
|
||||
}
|
||||
|
||||
/// patch verge config
|
||||
|
||||
@@ -74,11 +74,14 @@ impl WebDavClient {
|
||||
|
||||
// 获取或创建配置
|
||||
let config = {
|
||||
let mut lock = self.config.lock();
|
||||
if let Some(cfg) = lock.as_ref() {
|
||||
cfg.clone()
|
||||
// 首先检查是否已有配置
|
||||
let existing_config = self.config.lock().as_ref().cloned();
|
||||
|
||||
if let Some(cfg) = existing_config {
|
||||
cfg
|
||||
} else {
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
// 释放锁后获取异步配置
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
if verge.webdav_url.is_none()
|
||||
|| verge.webdav_username.is_none()
|
||||
|| verge.webdav_password.is_none()
|
||||
@@ -97,7 +100,8 @@ impl WebDavClient {
|
||||
password: verge.webdav_password.unwrap_or_default(),
|
||||
};
|
||||
|
||||
*lock = Some(config.clone());
|
||||
// 重新获取锁并存储配置
|
||||
*self.config.lock() = Some(config.clone());
|
||||
config
|
||||
}
|
||||
};
|
||||
@@ -260,14 +264,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
|
||||
|
||||
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() {
|
||||
obj.remove("webdav_username");
|
||||
obj.remove("webdav_password");
|
||||
obj.remove("webdav_url");
|
||||
}
|
||||
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.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
|
||||
|
||||
+50
-47
@@ -137,25 +137,25 @@ impl CoreManager {
|
||||
Ok(false)
|
||||
}
|
||||
/// 使用默认配置
|
||||
pub 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);
|
||||
*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![],
|
||||
chain_logs: Default::default(),
|
||||
});
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest_ref().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
)?;
|
||||
help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
Ok(())
|
||||
}
|
||||
/// 验证运行时配置
|
||||
pub async fn validate_config(&self) -> Result<(bool, String)> {
|
||||
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)?;
|
||||
self.validate_config_internal(config_path).await
|
||||
}
|
||||
@@ -248,7 +248,7 @@ impl CoreManager {
|
||||
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);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
|
||||
@@ -317,7 +317,7 @@ impl CoreManager {
|
||||
};
|
||||
// 对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(_) => {
|
||||
logging!(info, Type::Config, true, "YAML语法检查通过");
|
||||
Ok((true, String::new()))
|
||||
@@ -388,7 +388,7 @@ impl CoreManager {
|
||||
|
||||
// 1. 先生成新的配置内容
|
||||
logging!(info, Type::Config, true, "生成新的配置内容");
|
||||
Config::generate()?;
|
||||
Config::generate().await?;
|
||||
|
||||
// 2. 验证配置
|
||||
match self.validate_config().await {
|
||||
@@ -396,18 +396,18 @@ impl CoreManager {
|
||||
logging!(info, Type::Config, true, "配置验证通过");
|
||||
// 4. 验证通过后,生成正式的运行时配置
|
||||
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);
|
||||
Ok((true, "something".into()))
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
|
||||
Config::runtime().discard();
|
||||
Config::runtime().await.discard();
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
|
||||
Config::runtime().discard();
|
||||
Config::runtime().await.discard();
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
@@ -420,13 +420,13 @@ impl CoreManager {
|
||||
});
|
||||
match IpcManager::global().put_configs_force(run_path_str?).await {
|
||||
Ok(_) => {
|
||||
Config::runtime().apply();
|
||||
Config::runtime().await.apply();
|
||||
logging!(info, Type::Core, true, "Configuration updated successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
Config::runtime().discard();
|
||||
Config::runtime().await.discard();
|
||||
logging_error!(Type::Core, true, "Failed to update configuration: {}", msg);
|
||||
Err(msg)
|
||||
}
|
||||
@@ -735,13 +735,13 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_core_by_sidecar(&self) -> Result<()> {
|
||||
async fn start_core_by_sidecar(&self) -> Result<()> {
|
||||
logging!(trace, 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()
|
||||
.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 service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
|
||||
@@ -815,7 +815,7 @@ impl CoreManager {
|
||||
impl CoreManager {
|
||||
async fn start_core_by_service(&self) -> Result<()> {
|
||||
logging!(trace, 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?;
|
||||
self.set_running_mode(RunningMode::Service);
|
||||
Ok(())
|
||||
@@ -845,7 +845,7 @@ impl CoreManager {
|
||||
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() {
|
||||
if let Err(e) = service::reinstall_service().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
@@ -868,11 +868,11 @@ impl CoreManager {
|
||||
e
|
||||
);
|
||||
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
|
||||
let mut state = service::ServiceState::get();
|
||||
let mut state = service::ServiceState::get().await;
|
||||
if !state.prefer_sidecar {
|
||||
state.prefer_sidecar = true;
|
||||
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
|
||||
if let Err(save_err) = state.save() {
|
||||
if let Err(save_err) = state.save().await {
|
||||
logging!(
|
||||
error,
|
||||
Type::Core,
|
||||
@@ -941,7 +941,7 @@ impl CoreManager {
|
||||
"核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑"
|
||||
);
|
||||
|
||||
let service_state = service::ServiceState::get();
|
||||
let service_state = service::ServiceState::get().await;
|
||||
|
||||
if service_state.prefer_sidecar {
|
||||
logging!(
|
||||
@@ -950,7 +950,7 @@ impl CoreManager {
|
||||
true,
|
||||
"用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动"
|
||||
);
|
||||
self.start_core_by_sidecar()?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
|
||||
// 后续的 Tray::global().subscribe_traffic().await 仍然会执行
|
||||
} else {
|
||||
@@ -962,13 +962,13 @@ impl CoreManager {
|
||||
true,
|
||||
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
|
||||
);
|
||||
match service::install_service() {
|
||||
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()?;
|
||||
new_state.save().await?;
|
||||
|
||||
if service::is_service_available().await.is_ok() {
|
||||
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动");
|
||||
@@ -981,12 +981,12 @@ impl CoreManager {
|
||||
true,
|
||||
"新安装的服务启动失败,回退到Sidecar模式"
|
||||
);
|
||||
let mut final_state = service::ServiceState::get();
|
||||
let mut final_state = service::ServiceState::get().await;
|
||||
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()?;
|
||||
final_state.save().await?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
@@ -995,14 +995,14 @@ impl CoreManager {
|
||||
true,
|
||||
"服务安装成功但未能连接/立即可用,回退到Sidecar模式"
|
||||
);
|
||||
let mut final_state = service::ServiceState::get();
|
||||
let mut final_state = service::ServiceState::get().await;
|
||||
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()?;
|
||||
final_state.save().await?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1012,8 +1012,8 @@ impl CoreManager {
|
||||
prefer_sidecar: true,
|
||||
..Default::default()
|
||||
};
|
||||
new_state.save()?;
|
||||
self.start_core_by_sidecar()?;
|
||||
new_state.save().await?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1026,7 +1026,7 @@ impl CoreManager {
|
||||
true,
|
||||
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式"
|
||||
);
|
||||
let mut final_state = service::ServiceState::get();
|
||||
let mut final_state = service::ServiceState::get().await;
|
||||
if !final_state.prefer_sidecar {
|
||||
logging!(
|
||||
warn,
|
||||
@@ -1040,9 +1040,9 @@ impl CoreManager {
|
||||
"Service startup failed or unavailable before sidecar fallback"
|
||||
.to_string()
|
||||
}));
|
||||
final_state.save()?;
|
||||
final_state.save().await?;
|
||||
}
|
||||
self.start_core_by_sidecar()?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1067,13 +1067,13 @@ impl CoreManager {
|
||||
pub async fn start_core(&self) -> Result<()> {
|
||||
if service::is_service_available().await.is_ok() {
|
||||
if service::check_service_needs_reinstall().await {
|
||||
service::reinstall_service()?;
|
||||
service::reinstall_service().await?;
|
||||
}
|
||||
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
|
||||
self.start_core_by_service().await?;
|
||||
} else {
|
||||
// 服务不可用,检查用户偏好
|
||||
let service_state = service::ServiceState::get();
|
||||
let service_state = service::ServiceState::get().await;
|
||||
if service_state.prefer_sidecar {
|
||||
logging!(
|
||||
info,
|
||||
@@ -1081,10 +1081,10 @@ impl CoreManager {
|
||||
true,
|
||||
"服务不可用,根据用户偏好使用Sidecar模式"
|
||||
);
|
||||
self.start_core_by_sidecar()?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
} else {
|
||||
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
|
||||
self.start_core_by_sidecar()?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1125,11 +1125,14 @@ impl CoreManager {
|
||||
return Err(error_message);
|
||||
}
|
||||
|
||||
Config::verge().draft_mut().clash_core = clash_core.clone();
|
||||
Config::verge().apply();
|
||||
logging_error!(Type::Core, true, Config::verge().latest_ref().save_file());
|
||||
Config::verge().await.draft_mut().clash_core = clash_core.clone();
|
||||
Config::verge().await.apply();
|
||||
|
||||
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();
|
||||
logging_error!(Type::Core, true, "{}", msg);
|
||||
msg
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::time::{sleep, timeout, Duration};
|
||||
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||
@@ -99,7 +99,8 @@ impl EventDrivenProxyManager {
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
let (query_tx, query_rx) = mpsc::unbounded_channel();
|
||||
|
||||
Self::start_event_loop(Arc::clone(&state), event_rx, query_rx);
|
||||
let state_clone = Arc::clone(&state);
|
||||
AsyncHandler::spawn(move || Self::start_event_loop(state_clone, event_rx, query_rx));
|
||||
|
||||
Self {
|
||||
state,
|
||||
@@ -109,8 +110,8 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
|
||||
/// 获取自动代理配置(缓存)
|
||||
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
|
||||
self.state.read().auto_proxy.clone()
|
||||
pub async fn get_auto_proxy_cached(&self) -> Autoproxy {
|
||||
self.state.read().await.auto_proxy.clone()
|
||||
}
|
||||
|
||||
/// 异步获取最新的自动代理配置
|
||||
@@ -120,14 +121,14 @@ impl EventDrivenProxyManager {
|
||||
|
||||
if self.query_sender.send(query).is_err() {
|
||||
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 {
|
||||
Ok(Ok(result)) => result,
|
||||
_ => {
|
||||
log::warn!(target: "app", "查询超时,返回缓存数据");
|
||||
self.get_auto_proxy_cached()
|
||||
self.get_auto_proxy_cached().await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,36 +173,34 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_event_loop(
|
||||
pub async fn start_event_loop(
|
||||
state: Arc<RwLock<ProxyState>>,
|
||||
event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
|
||||
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
||||
) {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
log::info!(target: "app", "事件驱动代理管理器启动");
|
||||
log::info!(target: "app", "事件驱动代理管理器启动");
|
||||
|
||||
// 将 mpsc 接收器包装成 Stream,避免每次循环创建 future
|
||||
let mut event_stream = UnboundedReceiverStream::new(event_rx);
|
||||
let mut query_stream = UnboundedReceiverStream::new(query_rx);
|
||||
// 将 mpsc 接收器包装成 Stream,避免每次循环创建 future
|
||||
let mut event_stream = UnboundedReceiverStream::new(event_rx);
|
||||
let mut query_stream = UnboundedReceiverStream::new(query_rx);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(event) = event_stream.next() => {
|
||||
log::debug!(target: "app", "处理代理事件: {event:?}");
|
||||
Self::handle_event(&state, event).await;
|
||||
}
|
||||
Some(query) = query_stream.next() => {
|
||||
let result = Self::handle_query(&state).await;
|
||||
let _ = query.response_tx.send(result);
|
||||
}
|
||||
else => {
|
||||
// 两个通道都关闭时退出
|
||||
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
|
||||
break;
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(event) = event_stream.next() => {
|
||||
log::debug!(target: "app", "处理代理事件: {event:?}");
|
||||
Self::handle_event(&state, event).await;
|
||||
}
|
||||
Some(query) = query_stream.next() => {
|
||||
let result = Self::handle_query(&state).await;
|
||||
let _ = query.response_tx.send(result);
|
||||
}
|
||||
else => {
|
||||
// 两个通道都关闭时退出
|
||||
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
|
||||
@@ -235,7 +234,8 @@ impl EventDrivenProxyManager {
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.auto_proxy = auto_proxy.clone();
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
auto_proxy
|
||||
}
|
||||
@@ -243,7 +243,7 @@ impl EventDrivenProxyManager {
|
||||
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
||||
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 sys_proxy = Self::get_sys_proxy_with_timeout().await;
|
||||
|
||||
@@ -253,7 +253,8 @@ impl EventDrivenProxyManager {
|
||||
s.auto_proxy = auto_proxy;
|
||||
s.sys_proxy = sys_proxy;
|
||||
s.is_healthy = true;
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
||||
}
|
||||
@@ -261,12 +262,13 @@ impl EventDrivenProxyManager {
|
||||
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::debug!(target: "app", "更新代理配置");
|
||||
|
||||
let config = Self::get_proxy_config();
|
||||
let config = Self::get_proxy_config().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_enabled = config.sys_enabled;
|
||||
s.pac_enabled = config.pac_enabled;
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
if config.guard_enabled && config.sys_enabled {
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
@@ -275,7 +277,7 @@ impl EventDrivenProxyManager {
|
||||
|
||||
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let (sys_enabled, pac_enabled) = {
|
||||
let s = state.read();
|
||||
let s = state.read().await;
|
||||
(s.sys_enabled, s.pac_enabled)
|
||||
};
|
||||
|
||||
@@ -294,11 +296,12 @@ impl EventDrivenProxyManager {
|
||||
|
||||
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
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| {
|
||||
s.auto_proxy = current.clone();
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
if !current.enable || current.url != expected.url {
|
||||
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
|
||||
@@ -312,17 +315,19 @@ impl EventDrivenProxyManager {
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.is_healthy = restored.enable && restored.url == expected.url;
|
||||
s.auto_proxy = restored;
|
||||
});
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
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| {
|
||||
s.sys_proxy = current.clone();
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
if !current.enable || current.host != expected.host || current.port != expected.port {
|
||||
log::info!(target: "app", "系统代理设置异常,正在恢复...");
|
||||
@@ -338,22 +343,23 @@ impl EventDrivenProxyManager {
|
||||
&& restored.host == expected.host
|
||||
&& restored.port == expected.port;
|
||||
s.sys_proxy = restored;
|
||||
});
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "启用系统代理");
|
||||
|
||||
let pac_enabled = state.read().pac_enabled;
|
||||
let pac_enabled = state.read().await.pac_enabled;
|
||||
|
||||
if pac_enabled {
|
||||
let expected = Self::get_expected_pac_config();
|
||||
let expected = Self::get_expected_pac_config().await;
|
||||
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
|
||||
log::error!(target: "app", "启用PAC代理失败: {}", e);
|
||||
}
|
||||
} else {
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
let expected = Self::get_expected_sys_proxy().await;
|
||||
if let Err(e) = Self::restore_sys_proxy(&expected).await {
|
||||
log::error!(target: "app", "启用系统代理失败: {}", e);
|
||||
}
|
||||
@@ -382,7 +388,7 @@ impl EventDrivenProxyManager {
|
||||
let disabled_sys = Sysproxy::default();
|
||||
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;
|
||||
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
|
||||
log::error!(target: "app", "切换到PAC模式失败: {}", e);
|
||||
}
|
||||
@@ -390,13 +396,13 @@ impl EventDrivenProxyManager {
|
||||
let disabled_auto = Autoproxy::default();
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -423,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
|
||||
F: FnOnce(&mut ProxyState),
|
||||
{
|
||||
let mut state_guard = state.write();
|
||||
let mut state_guard = state.write().await;
|
||||
update_fn(&mut state_guard);
|
||||
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 verge_config = Config::verge();
|
||||
let verge_config = Config::verge().await;
|
||||
let verge = verge_config.latest_ref();
|
||||
(
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
@@ -449,9 +455,9 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expected_pac_config() -> Autoproxy {
|
||||
async fn get_expected_pac_config() -> Autoproxy {
|
||||
let proxy_host = {
|
||||
let verge_config = Config::verge();
|
||||
let verge_config = Config::verge().await;
|
||||
let verge = verge_config.latest_ref();
|
||||
verge
|
||||
.proxy_host
|
||||
@@ -465,28 +471,25 @@ impl EventDrivenProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expected_sys_proxy() -> Sysproxy {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest_ref();
|
||||
let port = verge
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
let proxy_host = verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||
async fn get_expected_sys_proxy() -> Sysproxy {
|
||||
let verge_config = Config::verge().await;
|
||||
let verge_mixed_port = verge_config.latest_ref().verge_mixed_port;
|
||||
let proxy_host = verge_config.latest_ref().proxy_host.clone();
|
||||
|
||||
let port = verge_mixed_port.unwrap_or(Config::clash().await.latest_ref().get_mixed_port());
|
||||
let proxy_host = proxy_host.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||
|
||||
Sysproxy {
|
||||
enable: true,
|
||||
host: proxy_host,
|
||||
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 verge_config = Config::verge();
|
||||
let verge_config = Config::verge().await;
|
||||
let verge = verge_config.latest_ref();
|
||||
(
|
||||
verge.use_default_bypass.unwrap_or(true),
|
||||
|
||||
@@ -282,10 +282,10 @@ impl Handle {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn init(&self, app_handle: &AppHandle) {
|
||||
pub fn init(&self, app_handle: AppHandle) {
|
||||
{
|
||||
let mut handle = self.app_handle.write();
|
||||
*handle = Some(app_handle.clone());
|
||||
*handle = Some(app_handle);
|
||||
}
|
||||
|
||||
let mut system_opt = self.notification_system.write();
|
||||
@@ -294,6 +294,7 @@ impl Handle {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取 AppHandle
|
||||
pub fn app_handle(&self) -> Option<AppHandle> {
|
||||
self.app_handle.read().clone()
|
||||
}
|
||||
@@ -521,3 +522,54 @@ impl Handle {
|
||||
*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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+114
-90
@@ -1,3 +1,4 @@
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::utils::notification::{notify_event, NotificationEvent};
|
||||
use crate::{
|
||||
config::Config, core::handle, feat, logging, logging_error,
|
||||
@@ -6,7 +7,7 @@ use crate::{
|
||||
use anyhow::{bail, Result};
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
|
||||
use tauri::Manager;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||
|
||||
/// Enum representing all available hotkey functions
|
||||
@@ -103,83 +104,86 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
/// Execute the function associated with a hotkey function enum
|
||||
fn execute_function(function: HotkeyFunction, app_handle: &tauri::AppHandle) {
|
||||
fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) {
|
||||
let app_handle = app_handle.clone();
|
||||
match function {
|
||||
HotkeyFunction::OpenOrCloseDashboard => {
|
||||
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);
|
||||
AsyncHandler::spawn(async move || {
|
||||
crate::feat::open_or_close_dashboard_hotkey().await;
|
||||
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ClashModeRule => {
|
||||
feat::change_clash_mode("rule".into());
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
||||
);
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::change_clash_mode("rule".into()).await;
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ClashModeGlobal => {
|
||||
feat::change_clash_mode("global".into());
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Global" },
|
||||
);
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::change_clash_mode("global".into()).await;
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Global" },
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ClashModeDirect => {
|
||||
feat::change_clash_mode("direct".into());
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
||||
);
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::change_clash_mode("direct".into()).await;
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ToggleSystemProxy => {
|
||||
feat::toggle_system_proxy();
|
||||
notify_event(app_handle, NotificationEvent::SystemProxyToggled);
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::toggle_system_proxy().await;
|
||||
notify_event(app_handle, NotificationEvent::SystemProxyToggled).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::ToggleTunMode => {
|
||||
feat::toggle_tun_mode(None);
|
||||
notify_event(app_handle, NotificationEvent::TunModeToggled);
|
||||
AsyncHandler::spawn(async move || {
|
||||
feat::toggle_tun_mode(None).await;
|
||||
notify_event(app_handle, NotificationEvent::TunModeToggled).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::EntryLightweightMode => {
|
||||
entry_lightweight_mode();
|
||||
notify_event(app_handle, NotificationEvent::LightweightModeEntered);
|
||||
AsyncHandler::spawn(async move || {
|
||||
entry_lightweight_mode().await;
|
||||
notify_event(app_handle, NotificationEvent::LightweightModeEntered).await;
|
||||
});
|
||||
}
|
||||
HotkeyFunction::Quit => {
|
||||
notify_event(app_handle, NotificationEvent::AppQuit);
|
||||
feat::quit();
|
||||
AsyncHandler::spawn(async move || {
|
||||
notify_event(app_handle, NotificationEvent::AppQuit).await;
|
||||
feat::quit().await;
|
||||
});
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
HotkeyFunction::Hide => {
|
||||
feat::hide();
|
||||
notify_event(app_handle, NotificationEvent::AppHidden);
|
||||
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 fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
|
||||
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")]
|
||||
@@ -190,7 +194,8 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
/// Register a hotkey with function enum
|
||||
pub fn register_hotkey_with_function(
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn register_hotkey_with_function(
|
||||
&self,
|
||||
hotkey: &str,
|
||||
function: HotkeyFunction,
|
||||
@@ -218,41 +223,55 @@ impl Hotkey {
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
let app_handle_clone = app_handle.clone();
|
||||
let is_quit = matches!(function, HotkeyFunction::Quit);
|
||||
|
||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
|
||||
if event.state == ShortcutState::Pressed {
|
||||
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey_event);
|
||||
let hotkey_event_owned = *hotkey_event;
|
||||
let event_owned = event;
|
||||
let function_owned = function;
|
||||
let is_quit_owned = is_quit;
|
||||
|
||||
if hotkey_event.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");
|
||||
Self::execute_function(function, &app_handle_clone);
|
||||
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") {
|
||||
if 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()
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
|
||||
if is_enable_global_hotkey {
|
||||
Self::execute_function(function, &app_handle_clone);
|
||||
} else {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let is_visible = WindowManager::is_main_window_visible();
|
||||
let is_focused = WindowManager::is_main_window_focused();
|
||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||
|
||||
if is_focused && is_visible {
|
||||
Self::execute_function(function, &app_handle_clone);
|
||||
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!(
|
||||
@@ -270,8 +289,8 @@ impl Hotkey {
|
||||
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
|
||||
|
||||
impl Hotkey {
|
||||
pub fn init(&self) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let verge = Config::verge().await;
|
||||
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
|
||||
|
||||
logging!(
|
||||
@@ -286,7 +305,10 @@ impl Hotkey {
|
||||
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!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
@@ -310,7 +332,7 @@ impl Hotkey {
|
||||
key,
|
||||
func
|
||||
);
|
||||
if let Err(e) = self.register(key, func) {
|
||||
if let Err(e) = self.register(key, func).await {
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
@@ -344,7 +366,7 @@ impl Hotkey {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.current.lock().clone_from(hotkeys);
|
||||
self.current.lock().clone_from(&hotkeys);
|
||||
} else {
|
||||
logging!(debug, Type::Hotkey, "No hotkeys configured");
|
||||
}
|
||||
@@ -362,9 +384,9 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
/// Register a hotkey with string-based function (backward compatibility)
|
||||
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||
pub async fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||
let function = HotkeyFunction::from_str(func)?;
|
||||
self.register_hotkey_with_function(hotkey, function)
|
||||
self.register_hotkey_with_function(hotkey, function).await
|
||||
}
|
||||
|
||||
pub fn unregister(&self, hotkey: &str) -> Result<()> {
|
||||
@@ -377,9 +399,10 @@ impl Hotkey {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
||||
let mut current = self.current.lock();
|
||||
let old_map = Self::get_map_from_vec(¤t);
|
||||
pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
||||
// Extract current hotkeys before async operations
|
||||
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 (del, add) = Self::get_diff(old_map, new_map);
|
||||
@@ -388,11 +411,12 @@ impl Hotkey {
|
||||
let _ = self.unregister(key);
|
||||
});
|
||||
|
||||
add.iter().for_each(|(key, func)| {
|
||||
logging_error!(Type::Hotkey, self.register(key, func));
|
||||
});
|
||||
for (key, func) in add.iter() {
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
+46
-216
@@ -13,7 +13,7 @@ use std::{
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
const REQUIRED_SERVICE_VERSION: &str = "1.1.0"; // 定义所需的服务版本号
|
||||
const REQUIRED_SERVICE_VERSION: &str = "1.1.1"; // 定义所需的服务版本号
|
||||
|
||||
// 限制重装时间和次数的常量
|
||||
const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期
|
||||
@@ -31,22 +31,26 @@ pub struct ServiceState {
|
||||
|
||||
impl ServiceState {
|
||||
// 获取当前的服务状态
|
||||
pub fn get() -> Self {
|
||||
if let Some(state) = Config::verge().latest_ref().service_state.clone() {
|
||||
pub async fn get() -> Self {
|
||||
if let Some(state) = Config::verge().await.latest_ref().service_state.clone() {
|
||||
return state;
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// 保存服务状态
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let config = Config::verge();
|
||||
pub async fn save(&self) -> Result<()> {
|
||||
let config = Config::verge().await;
|
||||
let mut latest = config.latest_ref().clone();
|
||||
latest.service_state = Some(self.clone());
|
||||
*config.draft_mut() = latest;
|
||||
config.apply();
|
||||
let result = config.latest_ref().save_file();
|
||||
result
|
||||
|
||||
// 先获取数据,再异步保存,避免跨await持有锁
|
||||
let verge_data = config.latest_ref().clone();
|
||||
drop(config); // 显式释放锁
|
||||
|
||||
verge_data.save_file().await
|
||||
}
|
||||
|
||||
// 更新安装信息
|
||||
@@ -112,7 +116,7 @@ pub struct JsonResponse {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn uninstall_service() -> Result<()> {
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "uninstall service");
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
@@ -146,7 +150,7 @@ pub fn uninstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn install_service() -> Result<()> {
|
||||
pub async fn install_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "install service");
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
@@ -180,11 +184,11 @@ pub fn install_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn reinstall_service() -> Result<()> {
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
let mut service_state = ServiceState::get();
|
||||
let mut service_state = ServiceState::get().await;
|
||||
|
||||
// 检查是否允许重装
|
||||
if !service_state.can_reinstall() {
|
||||
@@ -198,7 +202,7 @@ pub fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service() {
|
||||
if let Err(err) = uninstall_service().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
@@ -209,26 +213,27 @@ pub fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service() {
|
||||
match install_service().await {
|
||||
Ok(_) => {
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
service_state.last_error = None;
|
||||
service_state.save()?;
|
||||
service_state.save().await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
service_state.save().await?;
|
||||
bail!(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn uninstall_service() -> Result<()> {
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "uninstall service");
|
||||
use users::get_effective_uid;
|
||||
|
||||
@@ -268,7 +273,7 @@ pub fn uninstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn install_service() -> Result<()> {
|
||||
pub async fn install_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "install service");
|
||||
use users::get_effective_uid;
|
||||
|
||||
@@ -308,11 +313,11 @@ pub fn install_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn reinstall_service() -> Result<()> {
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
let mut service_state = ServiceState::get();
|
||||
let mut service_state = ServiceState::get().await;
|
||||
|
||||
// 检查是否允许重装
|
||||
if !service_state.can_reinstall() {
|
||||
@@ -326,7 +331,7 @@ pub fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service() {
|
||||
if let Err(err) = uninstall_service().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
@@ -337,26 +342,26 @@ pub fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service() {
|
||||
match install_service().await {
|
||||
Ok(_) => {
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
service_state.last_error = None;
|
||||
service_state.save()?;
|
||||
service_state.save().await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
service_state.save().await?;
|
||||
bail!(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn uninstall_service() -> Result<()> {
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
use crate::utils::i18n::t;
|
||||
|
||||
logging!(info, Type::Service, true, "uninstall service");
|
||||
@@ -370,7 +375,7 @@ pub fn uninstall_service() -> Result<()> {
|
||||
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||
|
||||
let prompt = t("Service Administrator Prompt");
|
||||
let prompt = t("Service Administrator Prompt").await;
|
||||
let command = format!(
|
||||
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||
);
|
||||
@@ -392,7 +397,7 @@ pub fn uninstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn install_service() -> Result<()> {
|
||||
pub async fn install_service() -> Result<()> {
|
||||
use crate::utils::i18n::t;
|
||||
|
||||
logging!(info, Type::Service, true, "install service");
|
||||
@@ -406,7 +411,7 @@ pub fn install_service() -> Result<()> {
|
||||
|
||||
let install_shell: String = install_path.to_string_lossy().into_owned();
|
||||
|
||||
let prompt = t("Service Administrator Prompt");
|
||||
let prompt = t("Service Administrator Prompt").await;
|
||||
let command = format!(
|
||||
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
|
||||
);
|
||||
@@ -428,11 +433,11 @@ pub fn install_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn reinstall_service() -> Result<()> {
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
let mut service_state = ServiceState::get();
|
||||
let mut service_state = ServiceState::get().await;
|
||||
|
||||
// 检查是否允许重装
|
||||
if !service_state.can_reinstall() {
|
||||
@@ -446,7 +451,7 @@ pub fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service() {
|
||||
if let Err(err) = uninstall_service().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
@@ -457,19 +462,19 @@ pub fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service() {
|
||||
match install_service().await {
|
||||
Ok(_) => {
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
service_state.last_error = None;
|
||||
service_state.save()?;
|
||||
service_state.save().await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
service_state.save().await?;
|
||||
bail!(error)
|
||||
}
|
||||
}
|
||||
@@ -676,7 +681,7 @@ pub async fn check_service_version() -> Result<String> {
|
||||
pub async fn check_service_needs_reinstall() -> bool {
|
||||
logging!(info, Type::Service, true, "开始检查服务是否需要重装");
|
||||
|
||||
let service_state = ServiceState::get();
|
||||
let service_state = ServiceState::get().await;
|
||||
|
||||
if !service_state.can_reinstall() {
|
||||
log::info!(target: "app", "服务重装检查: 处于冷却期或已达最大尝试次数");
|
||||
@@ -741,7 +746,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
|
||||
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
|
||||
|
||||
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
|
||||
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
|
||||
|
||||
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
|
||||
let clash_bin = format!("{clash_core}{bin_ext}");
|
||||
@@ -850,7 +855,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
if !version_check {
|
||||
log::info!(target: "app", "服务版本不匹配,尝试重装");
|
||||
|
||||
let service_state = ServiceState::get();
|
||||
let service_state = ServiceState::get().await;
|
||||
if !service_state.can_reinstall() {
|
||||
log::warn!(target: "app", "由于限制无法重装服务");
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
@@ -861,7 +866,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
}
|
||||
|
||||
log::info!(target: "app", "开始重装服务");
|
||||
if let Err(err) = reinstall_service() {
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {err}");
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
@@ -887,7 +892,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
if check_service_needs_reinstall().await {
|
||||
log::info!(target: "app", "服务需要重装");
|
||||
|
||||
if let Err(err) = reinstall_service() {
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {err}");
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
@@ -967,15 +972,15 @@ pub async fn is_service_available() -> Result<()> {
|
||||
}
|
||||
|
||||
/// 强制重装服务(UI修复按钮)
|
||||
pub fn force_reinstall_service() -> Result<()> {
|
||||
pub async fn force_reinstall_service() -> Result<()> {
|
||||
log::info!(target: "app", "用户请求强制重装服务");
|
||||
|
||||
let service_state = ServiceState::default();
|
||||
service_state.save()?;
|
||||
service_state.save().await?;
|
||||
|
||||
log::info!(target: "app", "已重置服务状态,开始执行重装");
|
||||
|
||||
match reinstall_service() {
|
||||
match reinstall_service().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务重装成功");
|
||||
Ok(())
|
||||
@@ -986,178 +991,3 @@ pub fn force_reinstall_service() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
/// 彻底诊断服务状态,检查安装状态、IPC通信和服务版本
|
||||
pub async fn diagnose_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "============= 开始服务诊断 =============");
|
||||
|
||||
// 1. 检查服务文件是否存在
|
||||
let service_path = dirs::service_path();
|
||||
match service_path {
|
||||
Ok(path) => {
|
||||
let service_exists = path.exists();
|
||||
logging!(info, Type::Service, true, "服务可执行文件路径: {:?}, 存在: {}", path, service_exists);
|
||||
|
||||
if !service_exists {
|
||||
logging!(error, Type::Service, true, "服务可执行文件不存在,需要重新安装");
|
||||
bail!("服务可执行文件不存在,需要重新安装");
|
||||
}
|
||||
|
||||
// 检查服务版本文件
|
||||
let version_file = path.with_file_name("version.txt");
|
||||
if version_file.exists() {
|
||||
match std::fs::read_to_string(&version_file) {
|
||||
Ok(content) => {
|
||||
logging!(info, Type::Service, true, "服务版本文件内容: {}", content.trim());
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Service, true, "读取服务版本文件失败: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(warn, Type::Service, true, "服务版本文件不存在: {:?}", version_file);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "获取服务路径失败: {}", e);
|
||||
bail!("获取服务路径失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查IPC通信 - 命名管道/Unix套接字
|
||||
let socket_path = if cfg!(windows) {
|
||||
r"\\.\pipe\clash-verge-service"
|
||||
} else {
|
||||
"/tmp/clash-verge-service.sock"
|
||||
};
|
||||
|
||||
logging!(info, Type::Service, true, "IPC通信路径: {}", socket_path);
|
||||
|
||||
if !cfg!(windows) {
|
||||
// Unix系统检查套接字文件是否存在
|
||||
let socket_exists = std::path::Path::new(socket_path).exists();
|
||||
logging!(info, Type::Service, true, "Unix套接字文件是否存在: {}", socket_exists);
|
||||
|
||||
if !socket_exists {
|
||||
logging!(warn, Type::Service, true, "Unix套接字文件不存在,服务可能未运行");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 尝试通过IPC检查服务状态
|
||||
logging!(info, Type::Service, true, "尝试通过IPC通信检查服务状态...");
|
||||
match check_service().await {
|
||||
Ok(resp) => {
|
||||
logging!(info, Type::Service, true, "服务状态检查成功: code={}, msg={}", resp.code, resp.msg);
|
||||
|
||||
// 4. 检查服务版本
|
||||
match check_service_version().await {
|
||||
Ok(version) => {
|
||||
logging!(info, Type::Service, true, "服务版本: {}, 要求版本: {}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
|
||||
if version != REQUIRED_SERVICE_VERSION {
|
||||
logging!(warn, Type::Service, true, "服务版本不匹配,建议重装服务");
|
||||
} else {
|
||||
logging!(info, Type::Service, true, "服务版本匹配");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Service, true, "检查服务版本失败: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Service, true, "服务状态检查失败: {}", err);
|
||||
|
||||
// 5. 检查系统服务状态 - Windows专用
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::process::Command;
|
||||
logging!(info, Type::Service, true, "尝试检查Windows服务状态...");
|
||||
|
||||
let output = Command::new("sc")
|
||||
.args(["query", "clash_verge_service"])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let contains_running = stdout.contains("RUNNING");
|
||||
|
||||
logging!(info, Type::Service, true, "Windows服务查询结果: {}",
|
||||
if contains_running { "正在运行" } else { "未运行" });
|
||||
|
||||
if !contains_running {
|
||||
logging!(info, Type::Service, true, "服务输出: {}", stdout);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "检查Windows服务状态失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// macOS专用
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
logging!(info, Type::Service, true, "尝试检查macOS服务状态...");
|
||||
|
||||
let output = Command::new("launchctl")
|
||||
.args(["list", "io.github.clash-verge-rev.clash-verge-rev.service"])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
|
||||
if out.status.success() {
|
||||
logging!(info, Type::Service, true, "macOS服务正在运行");
|
||||
logging!(debug, Type::Service, true, "服务详情: {}", stdout);
|
||||
} else {
|
||||
logging!(warn, Type::Service, true, "macOS服务未运行");
|
||||
if !stderr.is_empty() {
|
||||
logging!(info, Type::Service, true, "错误信息: {}", stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "检查macOS服务状态失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Linux专用
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::process::Command;
|
||||
logging!(info, Type::Service, true, "尝试检查Linux服务状态...");
|
||||
|
||||
let output = Command::new("systemctl")
|
||||
.args(["status", "clash_verge_service"])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let is_active = stdout.contains("Active: active (running)");
|
||||
|
||||
logging!(info, Type::Service, true, "Linux服务状态: {}",
|
||||
if is_active { "活跃运行中" } else { "未运行" });
|
||||
|
||||
if !is_active {
|
||||
logging!(info, Type::Service, true, "服务状态详情: {}", stdout);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Service, true, "检查Linux服务状态失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logging!(info, Type::Service, true, "============= 服务诊断完成 =============");
|
||||
Ok(())
|
||||
} */
|
||||
|
||||
@@ -27,13 +27,14 @@ 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>";
|
||||
|
||||
fn get_bypass() -> String {
|
||||
async fn get_bypass() -> String {
|
||||
let use_default = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.use_default_bypass
|
||||
.unwrap_or(true);
|
||||
let res = {
|
||||
let verge = Config::verge();
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
verge.system_proxy_bypass.clone()
|
||||
};
|
||||
@@ -77,14 +78,17 @@ impl Sysopt {
|
||||
pub async fn update_sysproxy(&self) -> Result<()> {
|
||||
let _lock = self.update_sysproxy.lock().await;
|
||||
|
||||
let port = Config::verge()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
let port = {
|
||||
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
|
||||
match verge_port {
|
||||
Some(port) => port,
|
||||
None => Config::clash().await.latest_ref().get_mixed_port(),
|
||||
}
|
||||
};
|
||||
let pac_port = IVerge::get_singleton_port();
|
||||
|
||||
let (sys_enable, pac_enable, proxy_host) = {
|
||||
let verge = Config::verge();
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
(
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
@@ -102,7 +106,7 @@ impl Sysopt {
|
||||
enable: false,
|
||||
host: proxy_host.clone(),
|
||||
port,
|
||||
bypass: get_bypass(),
|
||||
bypass: get_bypass().await,
|
||||
};
|
||||
let mut auto = Autoproxy {
|
||||
enable: false,
|
||||
@@ -173,7 +177,7 @@ impl Sysopt {
|
||||
.await?
|
||||
} else {
|
||||
let address = format!("{proxy_host}:{port}");
|
||||
let bypass = get_bypass();
|
||||
let bypass = get_bypass().await;
|
||||
let sysproxy_str = sysproxy_exe
|
||||
.as_path()
|
||||
.to_str()
|
||||
@@ -255,8 +259,8 @@ impl Sysopt {
|
||||
}
|
||||
|
||||
/// update the startup
|
||||
pub fn update_launch(&self) -> Result<()> {
|
||||
let enable_auto_launch = { Config::verge().latest_ref().enable_auto_launch };
|
||||
pub async fn update_launch(&self) -> Result<()> {
|
||||
let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch };
|
||||
let is_enable = enable_auto_launch.unwrap_or(false);
|
||||
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
|
||||
|
||||
|
||||
+131
-118
@@ -4,6 +4,7 @@ use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc,
|
||||
@@ -48,7 +49,7 @@ impl Timer {
|
||||
}
|
||||
|
||||
/// 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
|
||||
if self
|
||||
.initialized
|
||||
@@ -59,57 +60,59 @@ impl Timer {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
logging!(info, Type::Timer, true, "Initializing timer...");
|
||||
|
||||
// Initialize timer tasks
|
||||
if let Err(e) = self.refresh() {
|
||||
if let Err(e) = self.refresh().await {
|
||||
// Reset initialization flag on error
|
||||
self.initialized.store(false, Ordering::SeqCst);
|
||||
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let timer_map = self.timer_map.read();
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"已注册的定时任务数量: {}",
|
||||
timer_map.len()
|
||||
);
|
||||
|
||||
for (uid, task) in timer_map.iter() {
|
||||
// Log timer info first
|
||||
{
|
||||
let timer_map = self.timer_map.read();
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
"注册了定时任务 - uid={}, interval={}min, task_id={}",
|
||||
uid,
|
||||
task.interval_minutes,
|
||||
task.task_id
|
||||
"已注册的定时任务数量: {}",
|
||||
timer_map.len()
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
// Collect profiles that need immediate update
|
||||
let profiles_to_update = if let Some(items) = Config::profiles().latest_ref().get_items() {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let interval = item.option.as_ref()?.update_interval? as i64;
|
||||
let updated = item.updated? as i64;
|
||||
let uid = item.uid.as_ref()?;
|
||||
let profiles_to_update =
|
||||
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let interval = item.option.as_ref()?.update_interval? as i64;
|
||||
let updated = item.updated? as i64;
|
||||
let uid = item.uid.as_ref()?;
|
||||
|
||||
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
||||
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
|
||||
Some(uid.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if interval > 0 && cur_timestamp - updated >= interval * 60 {
|
||||
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
|
||||
Some(uid.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Advance tasks outside of locks to minimize lock contention
|
||||
if !profiles_to_update.is_empty() {
|
||||
@@ -137,9 +140,9 @@ impl Timer {
|
||||
}
|
||||
|
||||
/// 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
|
||||
let diff_map = self.gen_diff();
|
||||
let diff_map = self.gen_diff().await;
|
||||
|
||||
if diff_map.is_empty() {
|
||||
logging!(debug, Type::Timer, "No timer changes needed");
|
||||
@@ -153,72 +156,80 @@ impl Timer {
|
||||
diff_map.len()
|
||||
);
|
||||
|
||||
// Apply changes while holding locks
|
||||
let mut timer_map = self.timer_map.write();
|
||||
let mut delay_timer = self.delay_timer.write();
|
||||
// Apply changes - first collect operations to perform without holding locks
|
||||
let mut operations_to_add: Vec<(String, TaskID, u64)> = Vec::new();
|
||||
let _operations_to_remove: Vec<String> = Vec::new();
|
||||
|
||||
for (uid, diff) in diff_map {
|
||||
match diff {
|
||||
DiffFlag::Del(tid) => {
|
||||
timer_map.remove(&uid);
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"Failed to remove task {} for uid {}: {}",
|
||||
tid,
|
||||
uid,
|
||||
e
|
||||
);
|
||||
} else {
|
||||
logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid);
|
||||
// Perform sync operations while holding locks
|
||||
{
|
||||
let mut timer_map = self.timer_map.write();
|
||||
let delay_timer = self.delay_timer.write();
|
||||
|
||||
for (uid, diff) in diff_map {
|
||||
match diff {
|
||||
DiffFlag::Del(tid) => {
|
||||
timer_map.remove(&uid);
|
||||
if let Err(e) = delay_timer.remove_task(tid) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Timer,
|
||||
"Failed to remove task {} for 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 {
|
||||
task_id: tid,
|
||||
interval_minutes: interval,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
}
|
||||
} // Locks are dropped here
|
||||
|
||||
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) {
|
||||
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
|
||||
timer_map.remove(&uid); // Rollback on failure
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Rollback on failure - remove from timer_map
|
||||
self.timer_map.write().remove(&uid);
|
||||
} else {
|
||||
logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,10 +237,10 @@ impl Timer {
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
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() {
|
||||
if let Some(option) = item.option.as_ref() {
|
||||
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
|
||||
@@ -258,9 +269,9 @@ impl Timer {
|
||||
}
|
||||
|
||||
/// 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 new_map = self.gen_map();
|
||||
let new_map = self.gen_map().await;
|
||||
|
||||
// Read lock for comparing current state
|
||||
let timer_map = self.timer_map.read();
|
||||
@@ -349,9 +360,9 @@ impl Timer {
|
||||
.set_frequency_repeated_by_minutes(minutes)
|
||||
.spawn_async_routine(move || {
|
||||
let uid = uid.clone();
|
||||
async move {
|
||||
Box::pin(async move {
|
||||
Self::async_task(uid).await;
|
||||
}
|
||||
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
@@ -363,21 +374,23 @@ impl Timer {
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
let timer_map = self.timer_map.read();
|
||||
let task = match timer_map.get(uid) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
|
||||
return None;
|
||||
// First extract timer task data without holding the lock across await
|
||||
let task_interval = {
|
||||
let timer_map = self.timer_map.read();
|
||||
match timer_map.get(uid) {
|
||||
Some(t) => t.interval_minutes,
|
||||
None => {
|
||||
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get the profile updated timestamp
|
||||
let profiles_config = Config::profiles();
|
||||
let profiles = profiles_config.latest_ref();
|
||||
// Get the profile updated timestamp - now safe to await
|
||||
let profiles = { Config::profiles().await.clone().data_ref() }.clone();
|
||||
let items = match profiles.get_items() {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
@@ -397,8 +410,8 @@ impl Timer {
|
||||
let updated = profile.updated.unwrap_or(0) as i64;
|
||||
|
||||
// Calculate next update time
|
||||
if updated > 0 && task.interval_minutes > 0 {
|
||||
let next_time = updated + (task.interval_minutes as i64 * 60);
|
||||
if updated > 0 && task_interval > 0 {
|
||||
let next_time = updated + (task_interval as i64 * 60);
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
@@ -413,7 +426,7 @@ impl Timer {
|
||||
Type::Timer,
|
||||
"更新时间或间隔无效,updated={}, interval={}",
|
||||
updated,
|
||||
task.interval_minutes
|
||||
task_interval
|
||||
);
|
||||
None
|
||||
}
|
||||
@@ -439,7 +452,7 @@ impl Timer {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
|
||||
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!(
|
||||
info,
|
||||
Type::Timer,
|
||||
|
||||
+272
-220
@@ -3,17 +3,20 @@ use tauri::tray::TrayIconBuilder;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::ipc::Rate;
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{
|
||||
cmd,
|
||||
config::Config,
|
||||
feat, logging,
|
||||
module::lightweight::is_in_lightweight_mode,
|
||||
singleton_lazy,
|
||||
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
|
||||
utils::{dirs::find_target_icons, i18n::t},
|
||||
Type,
|
||||
};
|
||||
|
||||
use super::handle;
|
||||
use anyhow::Result;
|
||||
use futures::future::join_all;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
fs,
|
||||
@@ -26,8 +29,6 @@ use tauri::{
|
||||
AppHandle, Wry,
|
||||
};
|
||||
|
||||
use super::handle;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TrayState {}
|
||||
|
||||
@@ -67,8 +68,8 @@ pub struct Tray {
|
||||
}
|
||||
|
||||
impl TrayState {
|
||||
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
pub async fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
|
||||
if is_common_tray_icon {
|
||||
if let Ok(Some(common_icon_path)) = find_target_icons("common") {
|
||||
@@ -102,8 +103,8 @@ impl TrayState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
|
||||
if is_sysproxy_tray_icon {
|
||||
if let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") {
|
||||
@@ -137,8 +138,8 @@ impl TrayState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_tun_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
|
||||
if is_tun_tray_icon {
|
||||
if let Ok(Some(tun_icon_path)) = find_target_icons("tun") {
|
||||
@@ -185,16 +186,20 @@ impl Default for Tray {
|
||||
singleton_lazy!(Tray, TRAY, Tray::default);
|
||||
|
||||
impl Tray {
|
||||
pub fn init(&self) -> Result<()> {
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray initialization"))?;
|
||||
self.create_tray_from_handle(&app_handle).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘点击行为
|
||||
pub fn update_click_behavior(&self) -> Result<()> {
|
||||
pub async fn update_click_behavior(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
|
||||
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
||||
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
let tray = app_handle
|
||||
.tray_by_id("main")
|
||||
@@ -207,7 +212,7 @@ impl Tray {
|
||||
}
|
||||
|
||||
/// 更新托盘菜单
|
||||
pub fn update_menu(&self) -> Result<()> {
|
||||
pub async fn update_menu(&self) -> Result<()> {
|
||||
// 调整最小更新间隔,确保状态及时刷新
|
||||
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
@@ -244,7 +249,7 @@ impl Tray {
|
||||
// 设置更新状态
|
||||
self.menu_updating.store(true, Ordering::Release);
|
||||
|
||||
let result = self.update_menu_internal(&app_handle);
|
||||
let result = self.update_menu_internal(&app_handle).await;
|
||||
|
||||
{
|
||||
let mut last_update = self.last_menu_update.lock();
|
||||
@@ -255,12 +260,13 @@ impl Tray {
|
||||
result
|
||||
}
|
||||
|
||||
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
let mode = {
|
||||
Config::clash()
|
||||
.await
|
||||
.latest_ref()
|
||||
.0
|
||||
.get("mode")
|
||||
@@ -269,6 +275,7 @@ impl Tray {
|
||||
.to_owned()
|
||||
};
|
||||
let profile_uid_and_name = Config::profiles()
|
||||
.await
|
||||
.data_mut()
|
||||
.all_profile_uid_and_name()
|
||||
.unwrap_or_default();
|
||||
@@ -276,14 +283,17 @@ impl Tray {
|
||||
|
||||
match app_handle.tray_by_id("main") {
|
||||
Some(tray) => {
|
||||
let _ = tray.set_menu(Some(create_tray_menu(
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
profile_uid_and_name,
|
||||
is_lightweight_mode,
|
||||
)?));
|
||||
let _ = tray.set_menu(Some(
|
||||
create_tray_menu(
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
profile_uid_and_name,
|
||||
is_lightweight_mode,
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
log::debug!(target: "app", "托盘菜单更新成功");
|
||||
Ok(())
|
||||
}
|
||||
@@ -296,7 +306,7 @@ impl Tray {
|
||||
|
||||
/// 更新托盘图标
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
pub async fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
@@ -313,15 +323,15 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
(true, true) => TrayState::get_tun_tray_icon(),
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
||||
(false, true) => TrayState::get_tun_tray_icon(),
|
||||
(false, false) => TrayState::get_common_tray_icon(),
|
||||
(true, true) => TrayState::get_tun_tray_icon().await,
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon().await,
|
||||
(false, true) => TrayState::get_tun_tray_icon().await,
|
||||
(false, false) => TrayState::get_common_tray_icon().await,
|
||||
};
|
||||
|
||||
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
@@ -333,7 +343,7 @@ impl Tray {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
pub async fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
@@ -350,15 +360,15 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) {
|
||||
(true, true) => TrayState::get_tun_tray_icon(),
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon(),
|
||||
(false, true) => TrayState::get_tun_tray_icon(),
|
||||
(false, false) => TrayState::get_common_tray_icon(),
|
||||
(true, true) => TrayState::get_tun_tray_icon().await,
|
||||
(true, false) => TrayState::get_sysproxy_tray_icon().await,
|
||||
(false, true) => TrayState::get_tun_tray_icon().await,
|
||||
(false, false) => TrayState::get_common_tray_icon().await,
|
||||
};
|
||||
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
@@ -366,7 +376,7 @@ impl Tray {
|
||||
}
|
||||
|
||||
/// 更新托盘显示状态的函数
|
||||
pub fn update_tray_display(&self) -> Result<()> {
|
||||
pub async fn update_tray_display(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
|
||||
@@ -375,13 +385,13 @@ impl Tray {
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
|
||||
|
||||
// 更新菜单
|
||||
self.update_menu()?;
|
||||
self.update_menu().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘提示
|
||||
pub fn update_tooltip(&self) -> Result<()> {
|
||||
pub async fn update_tooltip(&self) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
@@ -390,15 +400,7 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let version = match VERSION.get() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let verge = Config::verge().await.latest_ref().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
@@ -410,25 +412,33 @@ impl Tray {
|
||||
};
|
||||
|
||||
let mut current_profile_name = "None".to_string();
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest_ref();
|
||||
if let Some(current_profile_uid) = profiles.get_current() {
|
||||
if let Ok(profile) = profiles.get_item(¤t_profile_uid) {
|
||||
current_profile_name = match &profile.name {
|
||||
Some(profile_name) => profile_name.to_string(),
|
||||
None => current_profile_name,
|
||||
};
|
||||
{
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
if let Some(current_profile_uid) = profiles.get_current() {
|
||||
if let Ok(profile) = profiles.get_item(¤t_profile_uid) {
|
||||
current_profile_name = match &profile.name {
|
||||
Some(profile_name) => profile_name.to_string(),
|
||||
None => current_profile_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Get localized strings before using them
|
||||
let sys_proxy_text = t("SysProxy").await;
|
||||
let tun_text = t("TUN").await;
|
||||
let profile_text = t("Profile").await;
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
if let Some(tray) = app_handle.tray_by_id("main") {
|
||||
let _ = tray.set_tooltip(Some(&format!(
|
||||
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
|
||||
t("SysProxy"),
|
||||
sys_proxy_text,
|
||||
switch_map[system_proxy],
|
||||
t("TUN"),
|
||||
tun_text,
|
||||
switch_map[tun_mode],
|
||||
t("Profile"),
|
||||
profile_text,
|
||||
current_profile_name
|
||||
)));
|
||||
} else {
|
||||
@@ -438,24 +448,20 @@ impl Tray {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_part(&self) -> Result<()> {
|
||||
self.update_menu()?;
|
||||
self.update_icon(None)?;
|
||||
self.update_tooltip()?;
|
||||
pub async fn update_part(&self) -> Result<()> {
|
||||
// self.update_menu().await?;
|
||||
// 更新轻量模式显示状态
|
||||
self.update_tray_display()?;
|
||||
self.update_tray_display().await?;
|
||||
self.update_icon(None).await?;
|
||||
self.update_tooltip().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 取消订阅 traffic 数据
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn unsubscribe_traffic(&self) {}
|
||||
|
||||
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
log::info!(target: "app", "正在从AppHandle创建系统托盘");
|
||||
|
||||
// 获取图标
|
||||
let icon_bytes = TrayState::get_common_tray_icon().1;
|
||||
let icon_bytes = TrayState::get_common_tray_icon().await.1;
|
||||
let icon = tauri::image::Image::from_bytes(&icon_bytes)?;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -463,6 +469,13 @@ impl Tray {
|
||||
.icon(icon)
|
||||
.icon_as_template(false);
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
let show_menu_on_left_click = {
|
||||
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
tray_event.as_str() == "tray_menu"
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let mut builder = TrayIconBuilder::with_id("main")
|
||||
.icon(icon)
|
||||
@@ -470,47 +483,55 @@ impl Tray {
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
{
|
||||
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
if tray_event.as_str() != "tray_menu" {
|
||||
if !show_menu_on_left_click {
|
||||
builder = builder.show_menu_on_left_click(false);
|
||||
}
|
||||
}
|
||||
|
||||
let tray = builder.build(app_handle)?;
|
||||
|
||||
tray.on_tray_icon_event(|_, event| {
|
||||
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
log::debug!(target: "app","tray event: {tray_event:?}");
|
||||
tray.on_tray_icon_event(|_app_handle, event| {
|
||||
AsyncHandler::spawn(|| async move {
|
||||
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
log::debug!(target: "app", "tray event: {tray_event:?}");
|
||||
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
// 添加防抖检查,防止快速连击
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
match tray_event.as_str() {
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"main_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "Tray点击事件: 显示主窗口");
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
// 添加防抖检查,防止快速连击
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
let fut: Pin<Box<dyn Future<Output = ()> + Send>> = match tray_event.as_str() {
|
||||
"system_proxy" => Box::pin(async move {
|
||||
feat::toggle_system_proxy().await;
|
||||
}),
|
||||
"tun_mode" => Box::pin(async move {
|
||||
feat::toggle_tun_mode(None).await;
|
||||
}),
|
||||
"main_window" => Box::pin(async move {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "Tray点击事件: 显示主窗口");
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
|
||||
crate::module::lightweight::exit_lightweight_mode().await;
|
||||
}
|
||||
let result = WindowManager::show_main_window().await;
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
}),
|
||||
_ => Box::pin(async move {}),
|
||||
};
|
||||
fut.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
tray.on_menu_event(on_menu_event);
|
||||
log::info!(target: "app", "系统托盘创建成功");
|
||||
@@ -518,18 +539,18 @@ impl Tray {
|
||||
}
|
||||
|
||||
// 托盘统一的状态更新函数
|
||||
pub fn update_all_states(&self) -> Result<()> {
|
||||
pub async fn update_all_states(&self) -> Result<()> {
|
||||
// 确保所有状态更新完成
|
||||
self.update_menu()?;
|
||||
self.update_icon(None)?;
|
||||
self.update_tooltip()?;
|
||||
self.update_tray_display()?;
|
||||
self.update_tray_display().await?;
|
||||
// self.update_menu().await?;
|
||||
self.update_icon(None).await?;
|
||||
self.update_tooltip().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tray_menu(
|
||||
async fn create_tray_menu(
|
||||
app_handle: &AppHandle,
|
||||
mode: Option<&str>,
|
||||
system_proxy_enabled: bool,
|
||||
@@ -539,10 +560,10 @@ fn create_tray_menu(
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let mode = mode.unwrap_or("");
|
||||
|
||||
let unknown_version = String::from("unknown");
|
||||
let version = VERSION.get().unwrap_or(&unknown_version);
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let hotkeys = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.hotkeys
|
||||
.as_ref()
|
||||
@@ -559,23 +580,54 @@ fn create_tray_menu(
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
|
||||
.iter()
|
||||
.map(|(profile_uid, profile_name)| {
|
||||
let is_current_profile = Config::profiles()
|
||||
.data_mut()
|
||||
.is_current_profile_index(profile_uid.to_string());
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
format!("profiles_{profile_uid}"),
|
||||
t(profile_name),
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
let profile_menu_items: Vec<CheckMenuItem<Wry>> = {
|
||||
let futures = profile_uid_and_name
|
||||
.iter()
|
||||
.map(|(profile_uid, profile_name)| {
|
||||
let app_handle = app_handle.clone();
|
||||
let profile_uid = profile_uid.clone();
|
||||
let profile_name = profile_name.clone();
|
||||
async move {
|
||||
let is_current_profile = Config::profiles()
|
||||
.await
|
||||
.data_mut()
|
||||
.is_current_profile_index(profile_uid.to_string());
|
||||
CheckMenuItem::with_id(
|
||||
&app_handle,
|
||||
format!("profiles_{profile_uid}"),
|
||||
t(&profile_name).await,
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
)
|
||||
}
|
||||
});
|
||||
let results = join_all(futures).await;
|
||||
results.into_iter().collect::<Result<Vec<_>, _>>()?
|
||||
};
|
||||
|
||||
// Pre-fetch all localized strings
|
||||
let dashboard_text = t("Dashboard").await;
|
||||
let rule_mode_text = t("Rule Mode").await;
|
||||
let global_mode_text = t("Global Mode").await;
|
||||
let direct_mode_text = t("Direct Mode").await;
|
||||
let profiles_text = t("Profiles").await;
|
||||
let system_proxy_text = t("System Proxy").await;
|
||||
let tun_mode_text = t("TUN Mode").await;
|
||||
let lightweight_mode_text = t("LightWeight Mode").await;
|
||||
let copy_env_text = t("Copy Env").await;
|
||||
let conf_dir_text = t("Conf Dir").await;
|
||||
let core_dir_text = t("Core Dir").await;
|
||||
let logs_dir_text = t("Logs Dir").await;
|
||||
let open_dir_text = t("Open Dir").await;
|
||||
let restart_clash_text = t("Restart Clash Core").await;
|
||||
let restart_app_text = t("Restart App").await;
|
||||
let verge_version_text = t("Verge Version").await;
|
||||
let more_text = t("More").await;
|
||||
let exit_text = t("Exit").await;
|
||||
|
||||
// Convert to references only when needed
|
||||
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
.collect();
|
||||
@@ -583,7 +635,7 @@ fn create_tray_menu(
|
||||
let open_window = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_window",
|
||||
t("Dashboard"),
|
||||
dashboard_text,
|
||||
true,
|
||||
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
|
||||
)?;
|
||||
@@ -591,7 +643,7 @@ fn create_tray_menu(
|
||||
let rule_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"rule_mode",
|
||||
t("Rule Mode"),
|
||||
rule_mode_text,
|
||||
true,
|
||||
mode == "rule",
|
||||
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
|
||||
@@ -600,7 +652,7 @@ fn create_tray_menu(
|
||||
let global_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"global_mode",
|
||||
t("Global Mode"),
|
||||
global_mode_text,
|
||||
true,
|
||||
mode == "global",
|
||||
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
|
||||
@@ -609,7 +661,7 @@ fn create_tray_menu(
|
||||
let direct_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"direct_mode",
|
||||
t("Direct Mode"),
|
||||
direct_mode_text,
|
||||
true,
|
||||
mode == "direct",
|
||||
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
|
||||
@@ -618,15 +670,15 @@ fn create_tray_menu(
|
||||
let profiles = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"profiles",
|
||||
t("Profiles"),
|
||||
profiles_text,
|
||||
true,
|
||||
&profile_menu_items,
|
||||
&profile_menu_items_refs,
|
||||
)?;
|
||||
|
||||
let system_proxy = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"system_proxy",
|
||||
t("System Proxy"),
|
||||
system_proxy_text,
|
||||
true,
|
||||
system_proxy_enabled,
|
||||
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
|
||||
@@ -635,7 +687,7 @@ fn create_tray_menu(
|
||||
let tun_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"tun_mode",
|
||||
t("TUN Mode"),
|
||||
tun_mode_text,
|
||||
true,
|
||||
tun_mode_enabled,
|
||||
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
|
||||
@@ -644,18 +696,18 @@ fn create_tray_menu(
|
||||
let lighteweight_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
"entry_lightweight_mode",
|
||||
t("LightWeight Mode"),
|
||||
lightweight_mode_text,
|
||||
true,
|
||||
is_lightweight_mode,
|
||||
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
|
||||
)?;
|
||||
|
||||
let copy_env = &MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>)?;
|
||||
let copy_env = &MenuItem::with_id(app_handle, "copy_env", copy_env_text, true, None::<&str>)?;
|
||||
|
||||
let open_app_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_app_dir",
|
||||
t("Conf Dir"),
|
||||
conf_dir_text,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
@@ -663,7 +715,7 @@ fn create_tray_menu(
|
||||
let open_core_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_core_dir",
|
||||
t("Core Dir"),
|
||||
core_dir_text,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
@@ -671,7 +723,7 @@ fn create_tray_menu(
|
||||
let open_logs_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"open_logs_dir",
|
||||
t("Logs Dir"),
|
||||
logs_dir_text,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
@@ -679,7 +731,7 @@ fn create_tray_menu(
|
||||
let open_dir = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"open_dir",
|
||||
t("Open Dir"),
|
||||
open_dir_text,
|
||||
true,
|
||||
&[open_app_dir, open_core_dir, open_logs_dir],
|
||||
)?;
|
||||
@@ -687,7 +739,7 @@ fn create_tray_menu(
|
||||
let restart_clash = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_clash",
|
||||
t("Restart Clash Core"),
|
||||
restart_clash_text,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
@@ -695,7 +747,7 @@ fn create_tray_menu(
|
||||
let restart_app = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"restart_app",
|
||||
t("Restart App"),
|
||||
restart_app_text,
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
@@ -703,7 +755,7 @@ fn create_tray_menu(
|
||||
let app_version = &MenuItem::with_id(
|
||||
app_handle,
|
||||
"app_version",
|
||||
format!("{} {version}", t("Verge Version")),
|
||||
format!("{} {version}", verge_version_text),
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
@@ -711,12 +763,12 @@ fn create_tray_menu(
|
||||
let more = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
"more",
|
||||
t("More"),
|
||||
more_text,
|
||||
true,
|
||||
&[restart_clash, restart_app, app_version],
|
||||
)?;
|
||||
|
||||
let quit = &MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q"))?;
|
||||
let quit = &MenuItem::with_id(app_handle, "quit", exit_text, true, Some("CmdOrControl+Q"))?;
|
||||
|
||||
let separator = &PredefinedMenuItem::separator(app_handle)?;
|
||||
|
||||
@@ -745,80 +797,80 @@ fn create_tray_menu(
|
||||
}
|
||||
|
||||
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
match event.id.as_ref() {
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5];
|
||||
logging!(
|
||||
info,
|
||||
Type::ProxyMode,
|
||||
true,
|
||||
"Switch Proxy Mode To: {}",
|
||||
mode
|
||||
);
|
||||
feat::change_clash_mode(mode.into());
|
||||
}
|
||||
"open_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
||||
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
AsyncHandler::spawn(|| async move {
|
||||
match event.id.as_ref() {
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5]; // Removing the "_mode" suffix
|
||||
logging!(
|
||||
info,
|
||||
Type::ProxyMode,
|
||||
true,
|
||||
"Switch Proxy Mode To: {}",
|
||||
mode
|
||||
);
|
||||
feat::change_clash_mode(mode.into()).await; // Await async function
|
||||
}
|
||||
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
}
|
||||
"system_proxy" => {
|
||||
feat::toggle_system_proxy();
|
||||
}
|
||||
"tun_mode" => {
|
||||
feat::toggle_tun_mode(None);
|
||||
}
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => {
|
||||
let _ = cmd::open_app_dir();
|
||||
}
|
||||
"open_core_dir" => {
|
||||
let _ = cmd::open_core_dir();
|
||||
}
|
||||
"open_logs_dir" => {
|
||||
let _ = cmd::open_logs_dir();
|
||||
}
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"entry_lightweight_mode" => {
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
|
||||
if was_lightweight {
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
} else {
|
||||
crate::module::lightweight::entry_lightweight_mode();
|
||||
}
|
||||
|
||||
if was_lightweight {
|
||||
"open_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
feat::quit();
|
||||
}
|
||||
id if id.starts_with("profiles_") => {
|
||||
let profile_index = &id["profiles_".len()..];
|
||||
feat::toggle_proxy_profile(profile_index.into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
log::info!(target: "app", "托盘菜单点击: 打开窗口");
|
||||
|
||||
if let Err(e) = Tray::global().update_all_states() {
|
||||
log::warn!(target: "app", "更新托盘状态失败: {e}");
|
||||
}
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出");
|
||||
crate::module::lightweight::exit_lightweight_mode().await; // Await async function
|
||||
}
|
||||
let result = WindowManager::show_main_window().await; // Await async function
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
}
|
||||
"system_proxy" => {
|
||||
feat::toggle_system_proxy().await; // Await async function
|
||||
}
|
||||
"tun_mode" => {
|
||||
feat::toggle_tun_mode(None).await; // Await async function
|
||||
}
|
||||
"copy_env" => feat::copy_clash_env().await, // Await async function
|
||||
"open_app_dir" => {
|
||||
let _ = cmd::open_app_dir().await; // Await async function
|
||||
}
|
||||
"open_core_dir" => {
|
||||
let _ = cmd::open_core_dir().await; // Await async function
|
||||
}
|
||||
"open_logs_dir" => {
|
||||
let _ = cmd::open_logs_dir().await; // Await async function
|
||||
}
|
||||
"restart_clash" => feat::restart_clash_core().await, // Await async function
|
||||
"restart_app" => feat::restart_app().await, // Await async function
|
||||
"entry_lightweight_mode" => {
|
||||
if !should_handle_tray_click() {
|
||||
return;
|
||||
}
|
||||
|
||||
let was_lightweight = crate::module::lightweight::is_in_lightweight_mode();
|
||||
if was_lightweight {
|
||||
crate::module::lightweight::exit_lightweight_mode().await; // Await async function
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let result = WindowManager::show_main_window().await; // Await async function
|
||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
|
||||
} else {
|
||||
crate::module::lightweight::entry_lightweight_mode().await; // Remove .await as it's not async
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
feat::quit().await; // Await async function
|
||||
}
|
||||
id if id.starts_with("profiles_") => {
|
||||
let profile_index = &id["profiles_".len()..];
|
||||
feat::toggle_proxy_profile(profile_index.into()).await; // Await async function
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Ensure tray state update is awaited and properly handled
|
||||
if let Err(e) = Tray::global().update_all_states().await {
|
||||
log::warn!(target: "app", "更新托盘状态失败: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
if (config.mode === "script") {
|
||||
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) {
|
||||
if (Array.isArray(config.proxies)) {
|
||||
config.proxies.forEach((p, i) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
config::PrfItem,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -22,6 +22,7 @@ pub enum ChainType {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub enum ChainSupport {
|
||||
Clash,
|
||||
ClashMeta,
|
||||
@@ -29,8 +30,49 @@ pub enum ChainSupport {
|
||||
All,
|
||||
}
|
||||
|
||||
impl From<&PrfItem> for Option<ChainItem> {
|
||||
fn from(item: &PrfItem) -> Self {
|
||||
// impl From<&PrfItem> for Option<ChainItem> {
|
||||
// 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 file = item.file.clone()?;
|
||||
let uid = item.uid.clone().unwrap_or("".into());
|
||||
@@ -47,25 +89,33 @@ impl From<&PrfItem> for Option<ChainItem> {
|
||||
}),
|
||||
"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()?),
|
||||
data: ChainType::Merge(help::read_mapping(&path).await.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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl 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;
|
||||
|
||||
pub const HANDLE_FIELDS: [&str; 12] = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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) {
|
||||
match (a, b) {
|
||||
@@ -54,10 +54,10 @@ fn test_merge() -> anyhow::Result<()> {
|
||||
script1: test
|
||||
";
|
||||
|
||||
let merge = serde_yaml::from_str::<Mapping>(merge)?;
|
||||
let config = serde_yaml::from_str::<Mapping>(config)?;
|
||||
let merge = serde_yaml_ng::from_str::<Mapping>(merge)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
+173
-72
@@ -7,19 +7,19 @@ mod tun;
|
||||
|
||||
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
|
||||
use crate::{config::Config, utils::tmpl};
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
type ResultLog = Vec<(String, String)>;
|
||||
|
||||
/// Enhance mode
|
||||
/// 返回最终订阅、该订阅包含的键、和script执行的结果
|
||||
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// 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 verge = Config::verge();
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
(
|
||||
Some(verge.get_valid_clash_core()),
|
||||
@@ -32,18 +32,18 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_enabled = {
|
||||
let verge = Config::verge();
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
verge.verge_redir_enabled.unwrap_or(false)
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_enabled = {
|
||||
let verge = Config::verge();
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
verge.verge_tproxy_enabled.unwrap_or(false)
|
||||
};
|
||||
|
||||
// 从profiles里拿东西
|
||||
// 从profiles里拿东西 - 先收集需要的数据,然后释放锁
|
||||
let (
|
||||
mut config,
|
||||
merge_item,
|
||||
@@ -55,74 +55,172 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
global_script,
|
||||
profile_name,
|
||||
) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest_ref();
|
||||
// 收集所有需要的数据,然后释放profiles锁
|
||||
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
|
||||
.get_item(&profiles.current_merge().unwrap_or_default())
|
||||
.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 profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_ref();
|
||||
|
||||
let global_merge = profiles
|
||||
.get_item(&"Merge".to_string())
|
||||
.ok()
|
||||
.and_then(<Option<ChainItem>>::from)
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "Merge".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
});
|
||||
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
|
||||
let script_uid = profiles_ref.current_script().unwrap_or_default();
|
||||
let rules_uid = profiles_ref.current_rules().unwrap_or_default();
|
||||
let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
|
||||
let groups_uid = profiles_ref.current_groups().unwrap_or_default();
|
||||
let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
|
||||
|
||||
let global_script = profiles
|
||||
.get_item(&"Script".to_string())
|
||||
.ok()
|
||||
.and_then(<Option<ChainItem>>::from)
|
||||
.unwrap_or_else(|| ChainItem {
|
||||
uid: "Script".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
});
|
||||
let name = profiles_ref
|
||||
.get_item(¤t_profile_uid)
|
||||
.ok()
|
||||
.and_then(|item| item.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let name = profiles
|
||||
.get_item(&profiles.get_current().unwrap_or_default())
|
||||
.ok()
|
||||
.and_then(|item| item.name.clone())
|
||||
.unwrap_or_default();
|
||||
(
|
||||
current,
|
||||
merge_uid,
|
||||
script_uid,
|
||||
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,
|
||||
@@ -237,6 +335,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// 处理 external-controller 键的开关逻辑
|
||||
if key.as_str() == Some("external-controller") {
|
||||
let enable_external_controller = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.enable_external_controller
|
||||
.unwrap_or(false);
|
||||
@@ -287,7 +386,9 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
|
||||
if dns_path.exists() {
|
||||
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
|
||||
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
||||
if 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") {
|
||||
if hosts_value.is_mapping() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::use_lowercase;
|
||||
use anyhow::{Error, Result};
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml_ng::Mapping;
|
||||
|
||||
pub fn use_script(
|
||||
script: String,
|
||||
@@ -149,11 +149,11 @@ fn test_script() {
|
||||
enable: false
|
||||
";
|
||||
|
||||
let config = serde_yaml::from_str(config).expect("Failed to parse test config YAML");
|
||||
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())
|
||||
.expect("Script execution should succeed in test");
|
||||
|
||||
let _ = serde_yaml::to_string(&config).expect("Failed to serialize config to YAML");
|
||||
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 box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
|
||||
assert!(box_yaml_config_size < yaml_config_size);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::{Mapping, Sequence, Value};
|
||||
use serde_yaml_ng::{Mapping, Sequence, Value};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SeqMap {
|
||||
@@ -86,7 +86,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[allow(unused_imports)]
|
||||
use serde_yaml::Value;
|
||||
use serde_yaml_ng::Value;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
@@ -110,7 +110,7 @@ proxy-groups:
|
||||
- "proxy1"
|
||||
"#;
|
||||
let mut config: Mapping =
|
||||
serde_yaml::from_str(config_str).expect("Failed to parse test config YAML");
|
||||
serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML");
|
||||
|
||||
let seq = SeqMap {
|
||||
prepend: Sequence::new(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::process::AsyncHandler;
|
||||
@@ -63,8 +63,8 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AsyncHandler::spawn(move || async move {
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
|
||||
#[cfg(target_os = "macos")]
|
||||
AsyncHandler::spawn(move || async move {
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
crate::utils::resolve::dns::restore_public_dns().await;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
|
||||
/// Restore WebDAV backup
|
||||
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 webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
|
||||
+55
-54
@@ -6,45 +6,40 @@ use crate::{
|
||||
process::AsyncHandler,
|
||||
utils::{logging::Type, resolve},
|
||||
};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use tauri::Manager;
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
|
||||
/// Restart the Clash core
|
||||
pub fn restart_clash_core() {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
match CoreManager::global().restart_core().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
handle::Handle::notice_message("set_config::ok", "ok");
|
||||
}
|
||||
Err(err) => {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target:"app", "{err}");
|
||||
}
|
||||
pub async fn restart_clash_core() {
|
||||
match CoreManager::global().restart_core().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
handle::Handle::notice_message("set_config::ok", "ok");
|
||||
}
|
||||
});
|
||||
Err(err) => {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target:"app", "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Restart the application
|
||||
pub fn restart_app() {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
// logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||
resolve::resolve_reset_async().await;
|
||||
pub async fn restart_app() {
|
||||
// logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||
resolve::resolve_reset_async().await;
|
||||
|
||||
handle::Handle::global()
|
||||
.app_handle()
|
||||
.map(|app_handle| {
|
||||
tauri::process::restart(&app_handle.env());
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
logging_error!(
|
||||
Type::System,
|
||||
false,
|
||||
"{}",
|
||||
"Failed to get app handle for restart"
|
||||
);
|
||||
});
|
||||
});
|
||||
handle::Handle::global()
|
||||
.app_handle()
|
||||
.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() {
|
||||
@@ -67,37 +62,42 @@ fn after_change_clash_mode() {
|
||||
}
|
||||
|
||||
/// 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();
|
||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||
// Convert YAML mapping to JSON Value
|
||||
let json_value = serde_json::json!({
|
||||
"mode": mode
|
||||
});
|
||||
AsyncHandler::spawn(move || async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
match IpcManager::global().patch_configs(json_value).await {
|
||||
Ok(_) => {
|
||||
// 更新订阅
|
||||
Config::clash().data_mut().patch_config(mapping);
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
match IpcManager::global().patch_configs(json_value).await {
|
||||
Ok(_) => {
|
||||
// 更新订阅
|
||||
Config::clash().await.data_mut().patch_config(mapping);
|
||||
|
||||
if Config::clash().data_mut().save_config().is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
||||
}
|
||||
// 分离数据获取和异步调用
|
||||
let clash_data = Config::clash().await.data_mut().clone();
|
||||
if clash_data.save_config().await.is_ok() {
|
||||
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()
|
||||
.data_mut()
|
||||
.auto_close_connection
|
||||
.unwrap_or(false);
|
||||
if is_auto_close_connection {
|
||||
after_change_clash_mode();
|
||||
}
|
||||
let is_auto_close_connection = Config::verge()
|
||||
.await
|
||||
.data_mut()
|
||||
.auto_close_connection
|
||||
.unwrap_or(false);
|
||||
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
|
||||
@@ -106,6 +106,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
use tokio::time::Instant;
|
||||
|
||||
let tun_mode = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.enable_tun_mode
|
||||
.unwrap_or(false);
|
||||
@@ -121,7 +122,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let response = NetworkManager::global()
|
||||
let response = NetworkManager::new()
|
||||
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -6,23 +6,30 @@ use crate::{
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use serde_yaml::Mapping;
|
||||
use serde_yaml_ng::Mapping;
|
||||
|
||||
/// Patch Clash configuration
|
||||
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 = {
|
||||
// 激活订阅
|
||||
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
|
||||
Config::generate()?;
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
} else {
|
||||
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_icon(None));
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
|
||||
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?;
|
||||
}
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -30,12 +37,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::clash().apply();
|
||||
Config::clash().data_mut().save_config()?;
|
||||
Config::clash().await.apply();
|
||||
// 分离数据获取和异步调用
|
||||
let clash_data = Config::clash().await.data_mut().clone();
|
||||
clash_data.save_config().await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::clash().discard();
|
||||
Config::clash().await.discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
@@ -60,7 +69,10 @@ enum UpdateFlags {
|
||||
|
||||
/// Patch Verge configuration
|
||||
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 auto_launch = patch.enable_auto_launch;
|
||||
@@ -173,7 +185,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
|
||||
// Process updates based on flags
|
||||
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||
Config::generate()?;
|
||||
Config::generate().await?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
||||
@@ -181,35 +193,35 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
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();
|
||||
}
|
||||
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 {
|
||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
|
||||
if let Some(hotkeys) = patch.hotkeys {
|
||||
hotkey::Hotkey::global().update(hotkeys)?;
|
||||
hotkey::Hotkey::global().update(hotkeys).await?;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
tray::Tray::global().update_icon(None)?;
|
||||
tray::Tray::global().update_icon(None).await?;
|
||||
}
|
||||
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 {
|
||||
tray::Tray::global().update_click_behavior()?;
|
||||
tray::Tray::global().update_click_behavior().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
|
||||
if enable_auto_light_weight.unwrap_or(false) {
|
||||
lightweight::enable_auto_light_weight_mode();
|
||||
lightweight::enable_auto_light_weight_mode().await;
|
||||
} else {
|
||||
lightweight::disable_auto_light_weight_mode();
|
||||
}
|
||||
@@ -219,15 +231,17 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
};
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::verge().apply();
|
||||
Config::verge().await.apply();
|
||||
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(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::verge().discard();
|
||||
Config::verge().await.discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
use crate::{
|
||||
cmd,
|
||||
config::{Config, PrfItem, PrfOption},
|
||||
core::{handle, CoreManager, *},
|
||||
config::{profiles::profiles_draft_update_item_safe, Config, PrfItem, PrfOption},
|
||||
core::{handle, tray, CoreManager},
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
/// Toggle proxy profile
|
||||
pub fn toggle_proxy_profile(profile_index: String) {
|
||||
AsyncHandler::spawn(|| async move {
|
||||
let Some(app_handle) = handle::Handle::global().app_handle() else {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to get app handle for profile toggle"
|
||||
);
|
||||
return;
|
||||
};
|
||||
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
||||
Ok(_) => {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
pub async fn toggle_proxy_profile(profile_index: String) {
|
||||
match cmd::patch_profiles_config_by_profile_index(profile_index).await {
|
||||
Ok(_) => {
|
||||
let result = tray::Tray::global().update_menu().await;
|
||||
if let Err(err) = result {
|
||||
logging!(error, Type::Tray, true, "更新菜单失败: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a profile
|
||||
@@ -42,7 +34,7 @@ pub async fn update_profile(
|
||||
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
|
||||
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles = profiles.latest_ref();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
@@ -77,11 +69,13 @@ pub async fn update_profile(
|
||||
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||
Ok(item) => {
|
||||
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.draft_mut();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
let profiles = Config::profiles().await;
|
||||
|
||||
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}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
@@ -113,9 +107,10 @@ pub async fn update_profile(
|
||||
}
|
||||
|
||||
// 更新到配置
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.draft_mut();
|
||||
profiles.update_item(uid.clone(), item.clone())?;
|
||||
let profiles = Config::profiles().await;
|
||||
|
||||
// 使用 Send-safe 方法进行数据操作
|
||||
profiles_draft_update_item_safe(uid.clone(), item.clone()).await?;
|
||||
|
||||
// 获取配置名称用于通知
|
||||
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
|
||||
@@ -123,7 +118,7 @@ pub async fn update_profile(
|
||||
// 发送通知告知用户自动更新使用了回退机制
|
||||
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}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
|
||||
+54
-49
@@ -3,75 +3,79 @@ use crate::{
|
||||
core::handle,
|
||||
ipc::IpcManager,
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use std::env;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
/// Toggle system proxy on/off
|
||||
pub fn toggle_system_proxy() {
|
||||
let enable = Config::verge().draft_mut().enable_system_proxy;
|
||||
let enable = enable.unwrap_or(false);
|
||||
let auto_close_connection = Config::verge()
|
||||
.data_mut()
|
||||
.auto_close_connection
|
||||
.unwrap_or(false);
|
||||
pub async fn toggle_system_proxy() {
|
||||
// 获取当前系统代理状态
|
||||
let enable = {
|
||||
let verge = Config::verge().await;
|
||||
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false);
|
||||
enable
|
||||
};
|
||||
// 获取自动关闭连接设置
|
||||
let auto_close_connection = {
|
||||
let verge = Config::verge().await;
|
||||
let auto_close = verge.latest_ref().auto_close_connection.unwrap_or(false);
|
||||
auto_close
|
||||
};
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
|
||||
if enable && auto_close_connection {
|
||||
if let Err(err) = IpcManager::global().close_all_connections().await {
|
||||
log::error!(target: "app", "Failed to close all connections: {err}");
|
||||
}
|
||||
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
|
||||
if enable && auto_close_connection {
|
||||
if let Err(err) = IpcManager::global().close_all_connections().await {
|
||||
log::error!(target: "app", "Failed to close all connections: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
match super::patch_verge(
|
||||
IVerge {
|
||||
enable_system_proxy: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
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
|
||||
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
let enable = Config::verge().data_mut().enable_tun_mode;
|
||||
pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
let enable = Config::verge().await.data_mut().enable_tun_mode;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
AsyncHandler::spawn(async move || {
|
||||
match super::patch_verge(
|
||||
IVerge {
|
||||
enable_tun_mode: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
not_save_file.unwrap_or(false),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
match super::patch_verge(
|
||||
IVerge {
|
||||
enable_tun_mode: Some(!enable),
|
||||
..IVerge::default()
|
||||
},
|
||||
not_save_file.unwrap_or(false),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy proxy environment variables to clipboard
|
||||
pub fn copy_clash_env() {
|
||||
pub async fn copy_clash_env() {
|
||||
// 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1
|
||||
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| {
|
||||
Config::verge()
|
||||
let clash_verge_rev_ip = match env::var("CLASH_VERGE_REV_IP") {
|
||||
Ok(ip) => ip,
|
||||
Err(_) => Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||
});
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string()),
|
||||
};
|
||||
|
||||
let Some(app_handle) = handle::Handle::global().app_handle() else {
|
||||
logging!(
|
||||
@@ -83,6 +87,7 @@ pub fn copy_clash_env() {
|
||||
};
|
||||
let port = {
|
||||
Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(7897)
|
||||
@@ -91,7 +96,7 @@ pub fn copy_clash_env() {
|
||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
|
||||
|
||||
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 {
|
||||
Some(env_type) => env_type,
|
||||
None => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, sysopt, CoreManager},
|
||||
@@ -10,19 +8,18 @@ use crate::{
|
||||
|
||||
/// Open or close the dashboard window
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard() {
|
||||
open_or_close_dashboard_internal(false)
|
||||
pub async fn open_or_close_dashboard() {
|
||||
open_or_close_dashboard_internal(false).await
|
||||
}
|
||||
|
||||
/// 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)
|
||||
pub async fn open_or_close_dashboard_hotkey() {
|
||||
open_or_close_dashboard_internal(true).await
|
||||
}
|
||||
|
||||
/// Internal implementation for opening/closing dashboard
|
||||
fn open_or_close_dashboard_internal(bypass_debounce: bool) {
|
||||
use crate::process::AsyncHandler;
|
||||
async fn open_or_close_dashboard_internal(bypass_debounce: bool) {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
|
||||
@@ -31,39 +28,36 @@ fn open_or_close_dashboard_internal(bypass_debounce: bool) {
|
||||
if bypass_debounce {
|
||||
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
log::info!(target: "app", "主线程中执行热键窗口操作");
|
||||
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;
|
||||
}
|
||||
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().await;
|
||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||
let result = WindowManager::show_main_window().await;
|
||||
log::info!(target: "app", "Window operation result: {result:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
let result = WindowManager::toggle_main_window();
|
||||
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||
});
|
||||
let result = WindowManager::toggle_main_window().await;
|
||||
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();
|
||||
crate::module::lightweight::exit_lightweight_mode().await;
|
||||
log::info!(target: "app", "Creating new window after exiting lightweight mode");
|
||||
let result = WindowManager::show_main_window();
|
||||
let result = WindowManager::show_main_window().await;
|
||||
log::info!(target: "app", "Window operation result: {result:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
let result = WindowManager::toggle_main_window();
|
||||
let result = WindowManager::toggle_main_window().await;
|
||||
log::info!(target: "app", "Window toggle result: {result:?}");
|
||||
}
|
||||
|
||||
/// 异步优化的应用退出函数
|
||||
pub fn quit() {
|
||||
use crate::process::AsyncHandler;
|
||||
pub async fn quit() {
|
||||
logging!(debug, Type::System, true, "启动退出流程");
|
||||
|
||||
// 获取应用句柄并设置退出标志
|
||||
@@ -84,19 +78,17 @@ pub fn quit() {
|
||||
}
|
||||
|
||||
// 使用异步任务处理资源清理,避免阻塞
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::System, true, "开始异步清理资源");
|
||||
let cleanup_result = clean_async().await;
|
||||
logging!(info, Type::System, true, "开始异步清理资源");
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"资源清理完成,退出代码: {}",
|
||||
if cleanup_result { 0 } else { 1 }
|
||||
);
|
||||
app_handle.exit(if cleanup_result { 0 } else { 1 });
|
||||
});
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"资源清理完成,退出代码: {}",
|
||||
if cleanup_result { 0 } else { 1 }
|
||||
);
|
||||
app_handle.exit(if cleanup_result { 0 } else { 1 });
|
||||
}
|
||||
|
||||
async fn clean_async() -> bool {
|
||||
@@ -105,7 +97,12 @@ async fn clean_async() -> bool {
|
||||
logging!(info, Type::System, true, "开始执行异步清理操作...");
|
||||
|
||||
// 1. 处理TUN模式
|
||||
let tun_success = if Config::verge().data_mut().enable_tun_mode.unwrap_or(false) {
|
||||
let tun_success = if Config::verge()
|
||||
.await
|
||||
.data_mut()
|
||||
.enable_tun_mode
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let disable_tun = serde_json::json!({"tun": {"enable": false}});
|
||||
match timeout(
|
||||
Duration::from_secs(3),
|
||||
@@ -169,7 +166,7 @@ async fn clean_async() -> bool {
|
||||
let dns_task = async {
|
||||
match timeout(
|
||||
Duration::from_millis(1000),
|
||||
crate::utils::resolve::restore_public_dns(),
|
||||
crate::utils::resolve::dns::restore_public_dns(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -242,16 +239,17 @@ pub fn clean() -> bool {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn hide() {
|
||||
pub async fn hide() {
|
||||
use crate::module::lightweight::add_light_weight_timer;
|
||||
|
||||
let enable_auto_light_weight_mode = Config::verge()
|
||||
.await
|
||||
.data_mut()
|
||||
.enable_auto_light_weight_mode
|
||||
.unwrap_or(false);
|
||||
|
||||
if enable_auto_light_weight_mode {
|
||||
add_light_weight_timer();
|
||||
add_light_weight_timer().await;
|
||||
}
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
@@ -259,5 +257,5 @@ pub fn hide() {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
handle::Handle::global().set_activation_policy_accessory();
|
||||
}
|
||||
|
||||
+126
-278
@@ -1,3 +1,6 @@
|
||||
#![allow(non_snake_case)]
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
mod cmd;
|
||||
pub mod config;
|
||||
mod core;
|
||||
@@ -11,9 +14,10 @@ mod utils;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
use crate::{
|
||||
core::handle,
|
||||
core::hotkey,
|
||||
process::AsyncHandler,
|
||||
utils::{resolve, resolve::resolve_scheme, server},
|
||||
utils::{resolve, server},
|
||||
};
|
||||
use config::Config;
|
||||
use parking_lot::Mutex;
|
||||
@@ -26,118 +30,6 @@ use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use utils::logging::Type;
|
||||
|
||||
/// A global singleton handle to the application.
|
||||
pub struct AppHandleManager {
|
||||
handle: Mutex<Option<AppHandle>>,
|
||||
}
|
||||
|
||||
impl AppHandleManager {
|
||||
/// Create a new AppHandleManager instance
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
handle: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the app handle manager with an app handle.
|
||||
pub fn init(&self, handle: AppHandle) {
|
||||
let mut app_handle = self.handle.lock();
|
||||
if app_handle.is_none() {
|
||||
*app_handle = Some(handle);
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"AppHandleManager initialized with handle"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the app handle if it has been initialized.
|
||||
pub fn get(&self) -> Option<AppHandle> {
|
||||
self.handle.lock().clone()
|
||||
}
|
||||
|
||||
/// Get the app handle, panics if it hasn't been initialized.
|
||||
pub fn get_handle(&self) -> AppHandle {
|
||||
if let Some(handle) = self.get() {
|
||||
handle
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
"AppHandle not initialized - ensure init() was called first"
|
||||
);
|
||||
std::process::exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the app handle has been initialized.
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
self.handle.lock().is_some()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn set_activation_policy(&self, policy: tauri::ActivationPolicy) -> Result<(), String> {
|
||||
let app_handle = self.handle.lock();
|
||||
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) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
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) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Accessory) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to set accessory activation policy: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_prohibited(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Prohibited) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to set prohibited activation policy: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use unified singleton macro
|
||||
singleton_with_logging!(AppHandleManager, INSTANCE, "AppHandleManager");
|
||||
|
||||
/// Application initialization helper functions
|
||||
mod app_init {
|
||||
use super::*;
|
||||
@@ -146,11 +38,11 @@ mod app_init {
|
||||
pub fn init_singleton_check() {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
||||
match timeout(Duration::from_secs(3), server::check_singleton()).await {
|
||||
match timeout(Duration::from_millis(500), server::check_singleton()).await {
|
||||
Ok(result) => {
|
||||
if result.is_err() {
|
||||
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
||||
if let Some(app_handle) = AppHandleManager::global().get() {
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
app_handle.exit(0);
|
||||
} else {
|
||||
std::process::exit(0);
|
||||
@@ -202,16 +94,14 @@ mod app_init {
|
||||
}
|
||||
|
||||
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 {
|
||||
if let Err(e) = resolve_scheme(url).await {
|
||||
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
|
||||
}
|
||||
let url = event.urls().first().map(|u| u.to_string());
|
||||
if let Some(url) = url {
|
||||
tokio::task::spawn_local(async move {
|
||||
if let Err(e) = resolve::resolve_scheme(url).await {
|
||||
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -245,49 +135,6 @@ mod app_init {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize core components asynchronously
|
||||
pub fn init_core_async(app_handle: tauri::AppHandle) {
|
||||
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秒),继续执行后续流程"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Initialize core components synchronously
|
||||
pub fn init_core_sync(app_handle: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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, "初始化配置...");
|
||||
utils::init::init_config()?;
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化资源...");
|
||||
utils::init::init_resources()?;
|
||||
|
||||
logging!(info, Type::Setup, true, "核心组件初始化完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate all command handlers for the application
|
||||
pub fn generate_handlers(
|
||||
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
@@ -422,11 +269,8 @@ pub fn run() {
|
||||
// Setup singleton check
|
||||
app_init::init_singleton_check();
|
||||
|
||||
// Initialize network manager
|
||||
utils::network::NetworkManager::global().init();
|
||||
|
||||
// Initialize portable flag
|
||||
let _ = utils::dirs::init_portable_flag();
|
||||
// We don't need init_portable_flag here anymore due to the init_config will do the things
|
||||
// let _ = utils::dirs::init_portable_flag();
|
||||
|
||||
// Set Linux environment variable
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -436,16 +280,17 @@ pub fn run() {
|
||||
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_default()
|
||||
.to_uppercase();
|
||||
let session_env = std::env::var("XDG_SESSION_TYPE").unwrap_or_default();
|
||||
let is_kde_desktop = desktop_env.contains("KDE");
|
||||
let is_plasma_desktop = desktop_env.contains("PLASMA");
|
||||
let is_wayland_session = session_env.contains("wayland");
|
||||
|
||||
if is_kde_desktop || is_plasma_desktop {
|
||||
std::env::set_var("GTK_CSD", "0");
|
||||
if is_kde_desktop && is_wayland_session {
|
||||
std::env::set_var("GDK_BACKEND", "x11");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"KDE detected: Disabled GTK CSD for better titlebar stability."
|
||||
"KDE Wayland detected: Switched to X11 backend for better titlebar stability."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -482,22 +327,13 @@ pub fn run() {
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize core components asynchronously
|
||||
app_init::init_core_async(app.handle().clone());
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
||||
|
||||
// Initialize core components synchronously
|
||||
if let Err(e) = app_init::init_core_sync(app.handle()) {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to initialize core components: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
logging!(info, Type::Setup, true, "异步执行应用设置...");
|
||||
resolve::resolve_setup_sync(app_handle);
|
||||
resolve::resolve_setup_async();
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||
Ok(())
|
||||
@@ -506,12 +342,14 @@ pub fn run() {
|
||||
|
||||
/// 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: &tauri::AppHandle) {
|
||||
pub fn handle_ready_resumed(app_handle: &AppHandle) {
|
||||
logging!(info, Type::System, true, "应用就绪或恢复");
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
handle::Handle::global().init(app_handle.clone());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -524,7 +362,7 @@ pub fn run() {
|
||||
|
||||
/// Handle application reopen events (macOS)
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn handle_reopen(app_handle: &tauri::AppHandle, has_visible_windows: bool) {
|
||||
pub async fn handle_reopen(app_handle: &AppHandle, has_visible_windows: bool) {
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
@@ -533,15 +371,15 @@ pub fn run() {
|
||||
has_visible_windows
|
||||
);
|
||||
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
handle::Handle::global().init(app_handle.clone());
|
||||
|
||||
if !has_visible_windows {
|
||||
// 当没有可见窗口时,设置为 regular 模式并显示主窗口
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
handle::Handle::global().set_activation_policy_regular();
|
||||
|
||||
logging!(info, Type::System, true, "没有可见窗口,尝试显示主窗口");
|
||||
|
||||
let result = WindowManager::show_main_window();
|
||||
let result = WindowManager::show_main_window().await;
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
@@ -557,7 +395,7 @@ pub fn run() {
|
||||
/// Handle window close requests
|
||||
pub fn handle_window_close(api: &tauri::WindowEvent) {
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
handle::Handle::global().set_activation_policy_accessory();
|
||||
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
@@ -576,68 +414,73 @@ pub fn run() {
|
||||
|
||||
/// Handle window focus events
|
||||
pub fn handle_window_focus(focused: bool) {
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
|
||||
if focused {
|
||||
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 {
|
||||
if 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().register_system_hotkey(SystemHotkey::CmdQ)
|
||||
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
|
||||
{
|
||||
logging!(error, Type::Hotkey, true, "Failed to register CMD+Q: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Failed to unregister CMD+Q: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
if let Err(e) =
|
||||
hotkey::Hotkey::global().register_system_hotkey(SystemHotkey::CmdW)
|
||||
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
|
||||
{
|
||||
logging!(error, Type::Hotkey, true, "Failed to register CMD+W: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Failed to unregister CMD+W: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_enable_global_hotkey {
|
||||
if let Err(e) = hotkey::Hotkey::global().init() {
|
||||
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
|
||||
if let Err(e) = hotkey::Hotkey::global().reset() {
|
||||
logging!(error, Type::Hotkey, true, "Failed to reset 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 {
|
||||
if let Err(e) = hotkey::Hotkey::global().reset() {
|
||||
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Handle window destroyed events
|
||||
@@ -685,45 +528,50 @@ pub fn run() {
|
||||
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,
|
||||
..
|
||||
} => {
|
||||
event_handlers::handle_reopen(app_handle, has_visible_windows);
|
||||
}
|
||||
tauri::RunEvent::ExitRequested { api, code, .. } => {
|
||||
if code.is_none() {
|
||||
api.prevent_exit();
|
||||
app.run(|app_handle, e| {
|
||||
match e {
|
||||
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
||||
event_handlers::handle_ready_resumed(app_handle);
|
||||
}
|
||||
}
|
||||
tauri::RunEvent::Exit => {
|
||||
// Avoid duplicate cleanup
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
#[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;
|
||||
});
|
||||
}
|
||||
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();
|
||||
}
|
||||
_ => {}
|
||||
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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ use crate::{
|
||||
config::Config,
|
||||
core::{handle, timer::Timer, tray::Tray},
|
||||
log_err, logging,
|
||||
process::AsyncHandler,
|
||||
state::lightweight::LightWeightState,
|
||||
utils::logging::Type,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::logging_error;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::TaskBuilder;
|
||||
@@ -41,35 +40,45 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_once_auto_lightweight() {
|
||||
LightWeightState::default().run_once_time(|| {
|
||||
let is_silent_start = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_silent_start
|
||||
.unwrap_or(false);
|
||||
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();
|
||||
pub async fn run_once_auto_lightweight() {
|
||||
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 let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
if !(enable_auto && is_silent_start) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
|
||||
);
|
||||
|
||||
if with_lightweight_status(|_| ()).is_some() {
|
||||
set_lightweight_mode(false).await;
|
||||
enable_auto_light_weight_mode().await;
|
||||
|
||||
if let Err(e) = Tray::global().update_part().await {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auto_lightweight_mode_init() {
|
||||
pub async fn auto_lightweight_mode_init() -> Result<()> {
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
// Check if state is available before accessing it
|
||||
if app_handle.try_state::<Mutex<LightWeightState>>().is_none() {
|
||||
@@ -79,12 +88,18 @@ pub fn auto_lightweight_mode_init() {
|
||||
true,
|
||||
"LightWeightState 尚未初始化,跳过自动轻量模式初始化"
|
||||
);
|
||||
return;
|
||||
return Err(anyhow::anyhow!("LightWeightState has not been initialized"));
|
||||
}
|
||||
|
||||
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);
|
||||
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!(
|
||||
@@ -93,15 +108,18 @@ pub fn auto_lightweight_mode_init() {
|
||||
true,
|
||||
"非静默启动直接挂载自动进入轻量模式监听器!"
|
||||
);
|
||||
set_lightweight_mode(true);
|
||||
enable_auto_light_weight_mode();
|
||||
set_lightweight_mode(true).await;
|
||||
enable_auto_light_weight_mode().await;
|
||||
|
||||
// 确保托盘状态更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
if let Err(e) = Tray::global().update_part().await {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 检查是否处于轻量模式
|
||||
@@ -110,21 +128,21 @@ pub fn is_in_lightweight_mode() -> bool {
|
||||
}
|
||||
|
||||
// 设置轻量模式状态
|
||||
pub fn set_lightweight_mode(value: bool) {
|
||||
pub async fn set_lightweight_mode(value: bool) {
|
||||
if with_lightweight_status(|state| {
|
||||
state.set_lightweight_mode(value);
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
// 只有在状态可用时才触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
if let Err(e) = Tray::global().update_part().await {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_auto_light_weight_mode() {
|
||||
if let Err(e) = Timer::global().init() {
|
||||
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;
|
||||
}
|
||||
@@ -139,7 +157,7 @@ pub fn disable_auto_light_weight_mode() {
|
||||
cancel_window_close_listener();
|
||||
}
|
||||
|
||||
pub fn entry_lightweight_mode() {
|
||||
pub async fn entry_lightweight_mode() {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
let result = WindowManager::hide_main_window();
|
||||
@@ -156,9 +174,9 @@ pub fn entry_lightweight_mode() {
|
||||
let _ = webview.destroy();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
handle::Handle::global().set_activation_policy_accessory();
|
||||
}
|
||||
set_lightweight_mode(true);
|
||||
set_lightweight_mode(true).await;
|
||||
let _ = cancel_light_weight_timer();
|
||||
|
||||
// 更新托盘显示
|
||||
@@ -166,7 +184,7 @@ pub fn entry_lightweight_mode() {
|
||||
}
|
||||
|
||||
// 添加从轻量模式恢复的函数
|
||||
pub fn exit_lightweight_mode() {
|
||||
pub async fn exit_lightweight_mode() {
|
||||
// 使用原子操作检查是否已经在退出过程中,防止并发调用
|
||||
if EXITING_LIGHTWEIGHT
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
@@ -192,28 +210,32 @@ pub fn exit_lightweight_mode() {
|
||||
return;
|
||||
}
|
||||
|
||||
set_lightweight_mode(false);
|
||||
set_lightweight_mode(false).await;
|
||||
|
||||
// macOS激活策略
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
handle::Handle::global().set_activation_policy_regular();
|
||||
|
||||
// 重置UI就绪状态
|
||||
crate::utils::resolve::reset_ui_ready();
|
||||
crate::utils::resolve::ui::reset_ui_ready();
|
||||
|
||||
// 更新托盘显示
|
||||
let _tray = crate::core::tray::Tray::global();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn add_light_weight_timer() {
|
||||
logging_error!(Type::Lightweight, setup_light_weight_timer());
|
||||
pub async fn add_light_weight_timer() {
|
||||
logging_error!(Type::Lightweight, setup_light_weight_timer().await);
|
||||
}
|
||||
|
||||
fn setup_window_close_listener() -> u32 {
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
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!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
@@ -248,9 +270,10 @@ fn cancel_window_close_listener() {
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_light_weight_timer() -> Result<()> {
|
||||
Timer::global().init()?;
|
||||
async fn setup_light_weight_timer() -> Result<()> {
|
||||
Timer::global().init().await?;
|
||||
let once_by_minutes = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.auto_light_weight_minutes
|
||||
.unwrap_or(10);
|
||||
@@ -269,7 +292,7 @@ fn setup_light_weight_timer() -> Result<()> {
|
||||
.set_frequency_once_by_minutes(once_by_minutes)
|
||||
.spawn_async_routine(move || async move {
|
||||
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
|
||||
entry_lightweight_mode();
|
||||
entry_lightweight_mode().await;
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
|
||||
@@ -1,22 +1,77 @@
|
||||
#[cfg(feature = "tokio-trace")]
|
||||
use std::any::type_name;
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "tokio-trace")]
|
||||
use std::panic::Location;
|
||||
use tauri::{async_runtime, async_runtime::JoinHandle};
|
||||
|
||||
pub struct AsyncHandler;
|
||||
|
||||
impl AsyncHandler {
|
||||
pub fn handle() -> async_runtime::RuntimeHandle {
|
||||
async_runtime::handle()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn spawn<F, Fut>(f: F) -> JoinHandle<()>
|
||||
where
|
||||
F: FnOnce() -> Fut + Send + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
#[cfg(feature = "tokio-trace")]
|
||||
Self::log_task_info(&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)
|
||||
}
|
||||
|
||||
#[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!("└────────────────────┴─────────────────────────────────────────────────────────────────────────────┘");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,6 @@ impl LightWeightState {
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
||||
@@ -27,46 +27,56 @@ impl ProxyRequestCache {
|
||||
|
||||
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>,
|
||||
F: Fn() -> Fut + Send + 'static,
|
||||
Fut: std::future::Future<Output = Value> + Send + 'static,
|
||||
{
|
||||
let now = Instant::now();
|
||||
let key_cloned = key.clone();
|
||||
let cell = self
|
||||
.map
|
||||
.entry(key)
|
||||
.or_insert_with(|| Arc::new(OnceCell::new()))
|
||||
.clone();
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
let key_cloned = key.clone();
|
||||
|
||||
if let Some(entry) = cell.get() {
|
||||
if entry.expires_at > now {
|
||||
return Arc::clone(&entry.value);
|
||||
}
|
||||
}
|
||||
// Get or create the cell
|
||||
let cell = self
|
||||
.map
|
||||
.entry(key_cloned.clone())
|
||||
.or_insert_with(|| Arc::new(OnceCell::new()))
|
||||
.clone();
|
||||
|
||||
if let Some(entry) = cell.get() {
|
||||
if entry.expires_at <= now {
|
||||
// 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));
|
||||
let new_cell = Arc::new(OnceCell::new());
|
||||
self.map.insert(key_cloned.clone(), Arc::clone(&new_cell));
|
||||
return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await;
|
||||
continue; // Retry with fresh cell
|
||||
}
|
||||
|
||||
// Try to set a new value
|
||||
let value = fetch_fn().await;
|
||||
let entry = 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let value = fetch_fn().await;
|
||||
let entry = CacheEntry {
|
||||
value: Arc::new(value),
|
||||
expires_at: Instant::now() + ttl,
|
||||
};
|
||||
let _ = cell.set(entry);
|
||||
// Safe to unwrap here as we just set the value
|
||||
Arc::clone(
|
||||
&cell
|
||||
.get()
|
||||
.unwrap_or_else(|| unreachable!("Cell value should exist after set"))
|
||||
.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-24
@@ -2,37 +2,32 @@ use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use nanoid::nanoid;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::{fs, path::PathBuf, str::FromStr};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
/// read data from yaml as struct T
|
||||
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
if !path.exists() {
|
||||
pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
|
||||
bail!("file not found \"{}\"", path.display());
|
||||
}
|
||||
|
||||
let yaml_str = fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
let yaml_str = tokio::fs::read_to_string(path).await?;
|
||||
|
||||
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
|
||||
format!(
|
||||
"failed to read the file with yaml format \"{}\"",
|
||||
path.display()
|
||||
)
|
||||
})
|
||||
Ok(serde_yaml_ng::from_str::<T>(&yaml_str)?)
|
||||
}
|
||||
|
||||
/// read mapping from yaml
|
||||
pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||
if !path.exists() {
|
||||
pub async fn read_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
|
||||
bail!("file not found \"{}\"", path.display());
|
||||
}
|
||||
|
||||
let yaml_str = fs::read_to_string(path)
|
||||
let yaml_str = tokio::fs::read_to_string(path)
|
||||
.await
|
||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
|
||||
// YAML语法检查
|
||||
match serde_yaml::from_str::<serde_yaml::Value>(&yaml_str) {
|
||||
match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&yaml_str) {
|
||||
Ok(mut val) => {
|
||||
val.apply_merge()
|
||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
@@ -60,16 +55,18 @@ pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||
}
|
||||
|
||||
/// read mapping from yaml fix #165
|
||||
pub fn read_seq_map(path: &PathBuf) -> Result<SeqMap> {
|
||||
let val: SeqMap = read_yaml(path)?;
|
||||
|
||||
Ok(val)
|
||||
pub async fn read_seq_map(path: &PathBuf) -> Result<SeqMap> {
|
||||
read_yaml(path).await
|
||||
}
|
||||
|
||||
/// save the data to the file
|
||||
/// can set `prefix` string to add some comments
|
||||
pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
|
||||
let data_str = serde_yaml::to_string(data)?;
|
||||
pub async fn save_yaml<T: Serialize + Sync>(
|
||||
path: &PathBuf,
|
||||
data: &T,
|
||||
prefix: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let data_str = serde_yaml_ng::to_string(data)?;
|
||||
|
||||
let yaml_str = match prefix {
|
||||
Some(prefix) => format!("{prefix}\n\n{data_str}"),
|
||||
@@ -77,7 +74,8 @@ pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -
|
||||
};
|
||||
|
||||
let path_str = path.as_os_str().to_string_lossy().to_string();
|
||||
fs::write(path, yaml_str.as_bytes())
|
||||
tokio::fs::write(path, yaml_str.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("failed to save file \"{path_str}\""))
|
||||
}
|
||||
|
||||
@@ -120,7 +118,7 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
/// open file
|
||||
pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||
pub fn open_file(path: PathBuf) -> Result<()> {
|
||||
open::that_detached(path.as_os_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -57,8 +57,11 @@ fn get_system_language() -> String {
|
||||
.unwrap_or_else(|| DEFAULT_LANGUAGE.to_string())
|
||||
}
|
||||
|
||||
pub fn t(key: &str) -> String {
|
||||
pub async fn t(key: &str) -> String {
|
||||
let key = key.to_string(); // own the string
|
||||
|
||||
let current_lang = Config::verge()
|
||||
.await
|
||||
.latest_ref()
|
||||
.language
|
||||
.as_deref()
|
||||
@@ -67,7 +70,7 @@ pub fn t(key: &str) -> String {
|
||||
|
||||
if let Some(text) = TRANSLATIONS
|
||||
.get(¤t_lang)
|
||||
.and_then(|trans| trans.get(key))
|
||||
.and_then(|trans| trans.get(&key))
|
||||
.and_then(|val| val.as_str())
|
||||
{
|
||||
return text.to_string();
|
||||
@@ -76,12 +79,12 @@ pub fn t(key: &str) -> String {
|
||||
if current_lang != DEFAULT_LANGUAGE {
|
||||
if let Some(text) = TRANSLATIONS
|
||||
.get(DEFAULT_LANGUAGE)
|
||||
.and_then(|trans| trans.get(key))
|
||||
.and_then(|trans| trans.get(&key))
|
||||
.and_then(|val| val.as_str())
|
||||
{
|
||||
return text.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
key.to_string()
|
||||
key
|
||||
}
|
||||
|
||||
+193
-83
@@ -1,7 +1,9 @@
|
||||
use crate::{
|
||||
config::*,
|
||||
core::handle,
|
||||
utils::{dirs, help},
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::{dirs, help, logging::Type},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use chrono::{Local, TimeZone};
|
||||
@@ -11,21 +13,19 @@ use log4rs::{
|
||||
config::{Appender, Logger, Root},
|
||||
encode::pattern::PatternEncoder,
|
||||
};
|
||||
use std::{
|
||||
fs::{self, DirEntry},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::fs;
|
||||
use tokio::fs::DirEntry;
|
||||
|
||||
/// initialize this instance's log file
|
||||
fn init_log() -> Result<()> {
|
||||
async fn init_log() -> Result<()> {
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
if !log_dir.exists() {
|
||||
let _ = fs::create_dir_all(&log_dir);
|
||||
let _ = tokio::fs::create_dir_all(&log_dir).await;
|
||||
}
|
||||
|
||||
let log_level = Config::verge().latest_ref().get_log_level();
|
||||
let log_level = Config::verge().await.latest_ref().get_log_level();
|
||||
if log_level == LevelFilter::Off {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -66,14 +66,14 @@ fn init_log() -> Result<()> {
|
||||
}
|
||||
|
||||
/// 删除log文件
|
||||
pub fn delete_log() -> Result<()> {
|
||||
pub async fn delete_log() -> Result<()> {
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
if !log_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auto_log_clean = {
|
||||
let verge = Config::verge();
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
verge.auto_log_clean.unwrap_or(0)
|
||||
};
|
||||
@@ -87,7 +87,13 @@ pub fn delete_log() -> Result<()> {
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
log::info!(target: "app", "try to delete log files, day: {day}");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"try to delete log files, day: {}",
|
||||
day
|
||||
);
|
||||
|
||||
// %Y-%m-%d to NaiveDateTime
|
||||
let parse_time_str = |s: &str| {
|
||||
@@ -106,7 +112,7 @@ pub fn delete_log() -> Result<()> {
|
||||
Ok(time)
|
||||
};
|
||||
|
||||
let process_file = |file: DirEntry| -> Result<()> {
|
||||
let process_file = async move |file: DirEntry| -> Result<()> {
|
||||
let file_name = file.file_name();
|
||||
let file_name = file_name.to_str().unwrap_or_default();
|
||||
|
||||
@@ -121,31 +127,33 @@ pub fn delete_log() -> Result<()> {
|
||||
let duration = now.signed_duration_since(file_time);
|
||||
if duration.num_days() > day {
|
||||
let file_path = file.path();
|
||||
let _ = fs::remove_file(file_path);
|
||||
log::info!(target: "app", "delete log file: {file_name}");
|
||||
let _ = fs::remove_file(file_path).await;
|
||||
logging!(info, Type::Setup, true, "delete log file: {}", file_name);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
for file in fs::read_dir(&log_dir)?.flatten() {
|
||||
let _ = process_file(file);
|
||||
let mut log_read_dir = fs::read_dir(&log_dir).await?;
|
||||
while let Some(entry) = log_read_dir.next_entry().await? {
|
||||
std::mem::drop(process_file(entry).await);
|
||||
}
|
||||
|
||||
let service_log_dir = log_dir.join("service");
|
||||
for file in fs::read_dir(service_log_dir)?.flatten() {
|
||||
let _ = process_file(file);
|
||||
let mut service_log_read_dir = fs::read_dir(service_log_dir).await?;
|
||||
while let Some(entry) = service_log_read_dir.next_entry().await? {
|
||||
std::mem::drop(process_file(entry).await);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 初始化DNS配置文件
|
||||
fn init_dns_config() -> Result<()> {
|
||||
use serde_yaml::Value;
|
||||
async fn init_dns_config() -> Result<()> {
|
||||
use serde_yaml_ng::Value;
|
||||
|
||||
// 创建DNS子配置
|
||||
let dns_config = serde_yaml::Mapping::from_iter([
|
||||
let dns_config = serde_yaml_ng::Mapping::from_iter([
|
||||
("enable".into(), Value::Bool(true)),
|
||||
("listen".into(), Value::String(":53".into())),
|
||||
("enhanced-mode".into(), Value::String("fake-ip".into())),
|
||||
@@ -197,7 +205,7 @@ fn init_dns_config() -> Result<()> {
|
||||
("fallback".into(), Value::Sequence(vec![])),
|
||||
(
|
||||
"nameserver-policy".into(),
|
||||
Value::Mapping(serde_yaml::Mapping::new()),
|
||||
Value::Mapping(serde_yaml_ng::Mapping::new()),
|
||||
),
|
||||
(
|
||||
"proxy-server-nameserver".into(),
|
||||
@@ -211,7 +219,7 @@ fn init_dns_config() -> Result<()> {
|
||||
("direct-nameserver-follow-policy".into(), Value::Bool(false)),
|
||||
(
|
||||
"fallback-filter".into(),
|
||||
Value::Mapping(serde_yaml::Mapping::from_iter([
|
||||
Value::Mapping(serde_yaml_ng::Mapping::from_iter([
|
||||
("geoip".into(), Value::Bool(true)),
|
||||
("geoip-code".into(), Value::String("CN".into())),
|
||||
(
|
||||
@@ -234,9 +242,12 @@ fn init_dns_config() -> Result<()> {
|
||||
]);
|
||||
|
||||
// 获取默认DNS和host配置
|
||||
let default_dns_config = serde_yaml::Mapping::from_iter([
|
||||
let default_dns_config = serde_yaml_ng::Mapping::from_iter([
|
||||
("dns".into(), Value::Mapping(dns_config)),
|
||||
("hosts".into(), Value::Mapping(serde_yaml::Mapping::new())),
|
||||
(
|
||||
"hosts".into(),
|
||||
Value::Mapping(serde_yaml_ng::Mapping::new()),
|
||||
),
|
||||
]);
|
||||
|
||||
// 检查DNS配置文件是否存在
|
||||
@@ -244,77 +255,147 @@ fn init_dns_config() -> Result<()> {
|
||||
let dns_path = app_dir.join("dns_config.yaml");
|
||||
|
||||
if !dns_path.exists() {
|
||||
log::info!(target: "app", "Creating default DNS config file");
|
||||
logging!(info, Type::Setup, true, "Creating default DNS config file");
|
||||
help::save_yaml(
|
||||
&dns_path,
|
||||
&default_dns_config,
|
||||
Some("# Clash Verge DNS Config"),
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 确保目录结构存在
|
||||
async fn ensure_directories() -> Result<()> {
|
||||
let directories = [
|
||||
("app_home", dirs::app_home_dir()?),
|
||||
("app_profiles", dirs::app_profiles_dir()?),
|
||||
("app_logs", dirs::app_logs_dir()?),
|
||||
];
|
||||
|
||||
for (name, dir) in directories {
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir).await.map_err(|e| {
|
||||
anyhow::anyhow!("Failed to create {} directory {:?}: {}", name, dir, e)
|
||||
})?;
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Created {} directory: {:?}",
|
||||
name,
|
||||
dir
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 初始化配置文件
|
||||
async fn initialize_config_files() -> Result<()> {
|
||||
if let Ok(path) = dirs::clash_path() {
|
||||
if !path.exists() {
|
||||
let template = IClashTemp::template().0;
|
||||
help::save_yaml(&path, &template, Some("# Clash Verge"))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?;
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Created clash config at {:?}",
|
||||
path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(path) = dirs::verge_path() {
|
||||
if !path.exists() {
|
||||
let template = IVerge::template();
|
||||
help::save_yaml(&path, &template, Some("# Clash Verge"))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?;
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Created verge config at {:?}",
|
||||
path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(path) = dirs::profiles_path() {
|
||||
if !path.exists() {
|
||||
let template = IProfiles::template();
|
||||
help::save_yaml(&path, &template, Some("# Clash Verge"))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Created profiles config at {:?}",
|
||||
path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证并修正verge配置
|
||||
IVerge::validate_and_fix_config()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to validate verge config: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize all the config files
|
||||
/// before tauri setup
|
||||
pub fn init_config() -> Result<()> {
|
||||
pub async fn init_config() -> Result<()> {
|
||||
let _ = dirs::init_portable_flag();
|
||||
let _ = init_log();
|
||||
let _ = delete_log();
|
||||
|
||||
crate::log_err!(dirs::app_home_dir().map(|app_dir| {
|
||||
if !app_dir.exists() {
|
||||
let _ = fs::create_dir_all(&app_dir);
|
||||
if let Err(e) = init_log().await {
|
||||
eprintln!("Failed to initialize logging: {}", e);
|
||||
}
|
||||
|
||||
ensure_directories().await?;
|
||||
|
||||
initialize_config_files().await?;
|
||||
|
||||
AsyncHandler::spawn(|| async {
|
||||
if let Err(e) = delete_log().await {
|
||||
logging!(warn, Type::Setup, true, "Failed to clean old logs: {}", e);
|
||||
}
|
||||
}));
|
||||
logging!(info, Type::Setup, true, "后台日志清理任务完成");
|
||||
});
|
||||
|
||||
crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| {
|
||||
if !profiles_dir.exists() {
|
||||
let _ = fs::create_dir_all(&profiles_dir);
|
||||
}
|
||||
}));
|
||||
|
||||
crate::log_err!(dirs::clash_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Vergeasu"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
crate::log_err!(dirs::verge_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
// 验证并修正verge.yaml中的clash_core配置
|
||||
crate::log_err!(IVerge::validate_and_fix_config());
|
||||
|
||||
crate::log_err!(dirs::profiles_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
// 初始化DNS配置文件
|
||||
let _ = init_dns_config();
|
||||
if let Err(e) = init_dns_config().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"DNS config initialization failed: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// initialize app resources
|
||||
/// after tauri setup
|
||||
pub fn init_resources() -> Result<()> {
|
||||
pub async fn init_resources() -> Result<()> {
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let res_dir = dirs::app_resources_dir()?;
|
||||
|
||||
if !app_dir.exists() {
|
||||
let _ = fs::create_dir_all(&app_dir);
|
||||
std::mem::drop(fs::create_dir_all(&app_dir).await);
|
||||
}
|
||||
if !res_dir.exists() {
|
||||
let _ = fs::create_dir_all(&res_dir);
|
||||
std::mem::drop(fs::create_dir_all(&res_dir).await);
|
||||
}
|
||||
|
||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
||||
@@ -324,36 +405,65 @@ pub fn init_resources() -> Result<()> {
|
||||
for file in file_list.iter() {
|
||||
let src_path = res_dir.join(file);
|
||||
let dest_path = app_dir.join(file);
|
||||
log::debug!(target: "app", "src_path: {src_path:?}, dest_path: {dest_path:?}");
|
||||
logging!(
|
||||
debug,
|
||||
Type::Setup,
|
||||
true,
|
||||
"src_path: {:?}, dest_path: {:?}",
|
||||
src_path,
|
||||
dest_path
|
||||
);
|
||||
|
||||
let handle_copy = |dest: &PathBuf| {
|
||||
match fs::copy(&src_path, dest) {
|
||||
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
|
||||
let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
|
||||
match fs::copy(&src, &dest).await {
|
||||
Ok(_) => {
|
||||
logging!(debug, Type::Setup, true, "resources copied '{}'", file);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "failed to copy resources '{file}' to '{dest:?}', {err}")
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"failed to copy resources '{}' to '{:?}', {}",
|
||||
file,
|
||||
dest,
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if src_path.exists() && !dest_path.exists() {
|
||||
handle_copy(&dest_path);
|
||||
handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());
|
||||
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());
|
||||
let src_modified = fs::metadata(&src_path).await.and_then(|m| m.modified());
|
||||
let dest_modified = fs::metadata(&dest_path).await.and_then(|m| m.modified());
|
||||
|
||||
match (src_modified, dest_modified) {
|
||||
(Ok(src_modified), Ok(dest_modified)) => {
|
||||
if src_modified > dest_modified {
|
||||
handle_copy(&dest_path);
|
||||
handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
|
||||
} else {
|
||||
log::debug!(target: "app", "skipping resource copy '{file}'");
|
||||
logging!(
|
||||
debug,
|
||||
Type::Setup,
|
||||
true,
|
||||
"skipping resource copy '{}'",
|
||||
file
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::debug!(target: "app", "failed to get modified '{file}'");
|
||||
handle_copy(&dest_path);
|
||||
logging!(
|
||||
debug,
|
||||
Type::Setup,
|
||||
true,
|
||||
"failed to get modified '{}'",
|
||||
file
|
||||
);
|
||||
handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -413,7 +523,7 @@ pub async fn startup_script() -> Result<()> {
|
||||
};
|
||||
|
||||
let script_path = {
|
||||
let verge = Config::verge();
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_ref();
|
||||
verge.startup_script.clone().unwrap_or("".to_string())
|
||||
};
|
||||
|
||||
@@ -78,15 +78,27 @@ macro_rules! trace_err {
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
macro_rules! wrap_err {
|
||||
($stat: expr) => {
|
||||
// Case 1: Future<Result<T, E>>
|
||||
($stat:expr, async) => {{
|
||||
match $stat.await {
|
||||
Ok(a) => Ok(a),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{}", err);
|
||||
Err(err.to_string())
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
// Case 2: Result<T, E>
|
||||
($stat:expr) => {{
|
||||
match $stat {
|
||||
Ok(a) => Ok(a),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{}", err.to_string());
|
||||
Err(format!("{}", err.to_string()))
|
||||
log::error!(target: "app", "{}", err);
|
||||
Err(err.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
||||
+159
-315
@@ -1,142 +1,97 @@
|
||||
use anyhow::Result;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest::{Client, ClientBuilder, Proxy, RequestBuilder, Response};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Once,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
use isahc::http::{
|
||||
header::{HeaderMap, HeaderValue, USER_AGENT},
|
||||
StatusCode, Uri,
|
||||
};
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use isahc::prelude::*;
|
||||
use isahc::{config::SslOption, HttpClient};
|
||||
use std::sync::Once;
|
||||
use std::time::{Duration, Instant};
|
||||
use sysproxy::Sysproxy;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::{config::Config, logging, process::AsyncHandler, singleton_lazy, utils::logging::Type};
|
||||
use crate::config::Config;
|
||||
|
||||
// HTTP2 相关
|
||||
const H2_CONNECTION_WINDOW_SIZE: u32 = 1024 * 1024;
|
||||
const H2_STREAM_WINDOW_SIZE: u32 = 1024 * 1024;
|
||||
const H2_MAX_FRAME_SIZE: u32 = 16 * 1024;
|
||||
const H2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const H2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const POOL_MAX_IDLE_PER_HOST: usize = 5;
|
||||
const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
/// 网络管理器
|
||||
pub struct NetworkManager {
|
||||
runtime: Arc<Runtime>,
|
||||
self_proxy_client: Mutex<Option<Client>>,
|
||||
system_proxy_client: Mutex<Option<Client>>,
|
||||
no_proxy_client: Mutex<Option<Client>>,
|
||||
init: Once,
|
||||
last_connection_error: Mutex<Option<(Instant, String)>>,
|
||||
connection_error_count: AtomicUsize,
|
||||
#[derive(Debug)]
|
||||
pub struct HttpResponse {
|
||||
status: StatusCode,
|
||||
headers: HeaderMap,
|
||||
body: String,
|
||||
}
|
||||
|
||||
// Use singleton_lazy macro to replace lazy_static!
|
||||
singleton_lazy!(NetworkManager, NETWORK_MANAGER, NetworkManager::new);
|
||||
impl HttpResponse {
|
||||
pub fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
|
||||
Self {
|
||||
status,
|
||||
headers,
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> StatusCode {
|
||||
self.status
|
||||
}
|
||||
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
pub fn text_with_charset(&self) -> Result<&str> {
|
||||
Ok(&self.body)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ProxyType {
|
||||
None,
|
||||
Localhost,
|
||||
System,
|
||||
}
|
||||
|
||||
pub struct NetworkManager {
|
||||
self_proxy_client: Mutex<Option<HttpClient>>,
|
||||
system_proxy_client: Mutex<Option<HttpClient>>,
|
||||
no_proxy_client: Mutex<Option<HttpClient>>,
|
||||
init: Once,
|
||||
last_connection_error: Mutex<Option<(Instant, String)>>,
|
||||
connection_error_count: Mutex<usize>,
|
||||
}
|
||||
|
||||
impl NetworkManager {
|
||||
fn new() -> Self {
|
||||
// 创建专用的异步运行时,线程数限制为4个
|
||||
let runtime = match Builder::new_multi_thread()
|
||||
.worker_threads(4)
|
||||
.thread_name("clash-verge-network")
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
{
|
||||
Ok(runtime) => runtime,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to create network runtime: {}. Using fallback single-threaded runtime.",
|
||||
e
|
||||
);
|
||||
// Fallback to current thread runtime
|
||||
match Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.thread_name("clash-verge-network-fallback")
|
||||
.build()
|
||||
{
|
||||
Ok(fallback_runtime) => fallback_runtime,
|
||||
Err(fallback_err) => {
|
||||
log::error!(
|
||||
"Failed to create fallback runtime: {}. This is critical.",
|
||||
fallback_err
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NetworkManager {
|
||||
runtime: Arc::new(runtime),
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
self_proxy_client: Mutex::new(None),
|
||||
system_proxy_client: Mutex::new(None),
|
||||
no_proxy_client: Mutex::new(None),
|
||||
init: Once::new(),
|
||||
last_connection_error: Mutex::new(None),
|
||||
connection_error_count: AtomicUsize::new(0),
|
||||
connection_error_count: Mutex::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化网络客户端
|
||||
pub fn init(&self) {
|
||||
self.init.call_once(|| {
|
||||
self.runtime.spawn(async {
|
||||
logging!(info, Type::Network, true, "初始化网络管理器");
|
||||
|
||||
// 创建无代理客户端
|
||||
let no_proxy_client = match ClientBuilder::new()
|
||||
.use_rustls_tls()
|
||||
.no_proxy()
|
||||
.pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
|
||||
.pool_idle_timeout(POOL_IDLE_TIMEOUT)
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
true,
|
||||
"Failed to build no_proxy client: {}",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut no_proxy_guard = NetworkManager::global().no_proxy_client.lock();
|
||||
*no_proxy_guard = Some(no_proxy_client);
|
||||
|
||||
logging!(info, Type::Network, true, "网络管理器初始化完成");
|
||||
});
|
||||
});
|
||||
self.init.call_once(|| {});
|
||||
}
|
||||
|
||||
fn record_connection_error(&self, error: &str) {
|
||||
let mut last_error = self.last_connection_error.lock();
|
||||
async fn record_connection_error(&self, error: &str) {
|
||||
let mut last_error = self.last_connection_error.lock().await;
|
||||
*last_error = Some((Instant::now(), error.to_string()));
|
||||
|
||||
self.connection_error_count.fetch_add(1, Ordering::Relaxed);
|
||||
let mut count = self.connection_error_count.lock().await;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
fn should_reset_clients(&self) -> bool {
|
||||
let error_count = self.connection_error_count.load(Ordering::Relaxed);
|
||||
let last_error = self.last_connection_error.lock();
|
||||
async fn should_reset_clients(&self) -> bool {
|
||||
let count = *self.connection_error_count.lock().await;
|
||||
let last_error_guard = self.last_connection_error.lock().await;
|
||||
|
||||
if error_count > 5 {
|
||||
if count > 5 {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some((time, _)) = *last_error {
|
||||
if time.elapsed() < Duration::from_secs(30) && error_count > 2 {
|
||||
if let Some((time, _)) = &*last_error_guard {
|
||||
if time.elapsed() < Duration::from_secs(30) && count > 2 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -144,191 +99,91 @@ impl NetworkManager {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn reset_clients(&self) {
|
||||
logging!(info, Type::Network, true, "正在重置所有HTTP客户端");
|
||||
{
|
||||
let mut client = self.self_proxy_client.lock();
|
||||
*client = None;
|
||||
}
|
||||
{
|
||||
let mut client = self.system_proxy_client.lock();
|
||||
*client = None;
|
||||
}
|
||||
{
|
||||
let mut client = self.no_proxy_client.lock();
|
||||
*client = None;
|
||||
}
|
||||
self.connection_error_count.store(0, Ordering::Relaxed);
|
||||
pub async fn reset_clients(&self) {
|
||||
*self.self_proxy_client.lock().await = None;
|
||||
*self.system_proxy_client.lock().await = None;
|
||||
*self.no_proxy_client.lock().await = None;
|
||||
*self.connection_error_count.lock().await = 0;
|
||||
}
|
||||
|
||||
/// 创建带有自定义选项的HTTP请求
|
||||
pub fn create_request(
|
||||
fn build_client(
|
||||
&self,
|
||||
proxy_uri: Option<Uri>,
|
||||
default_headers: HeaderMap,
|
||||
accept_invalid_certs: bool,
|
||||
timeout_secs: Option<u64>,
|
||||
) -> Result<HttpClient> {
|
||||
let proxy_uri_clone = proxy_uri.clone();
|
||||
let headers_clone = default_headers.clone();
|
||||
let client = {
|
||||
let mut builder = HttpClient::builder();
|
||||
|
||||
builder = match proxy_uri_clone {
|
||||
Some(uri) => builder.proxy(Some(uri)),
|
||||
None => builder.proxy(None),
|
||||
};
|
||||
|
||||
for (name, value) in headers_clone.iter() {
|
||||
builder = builder.default_header(name, value);
|
||||
}
|
||||
|
||||
if accept_invalid_certs {
|
||||
builder = builder.ssl_options(SslOption::DANGER_ACCEPT_INVALID_CERTS);
|
||||
}
|
||||
|
||||
if let Some(secs) = timeout_secs {
|
||||
builder = builder.timeout(Duration::from_secs(secs));
|
||||
}
|
||||
|
||||
Ok(builder.build()?)
|
||||
};
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
pub async fn create_request(
|
||||
&self,
|
||||
url: &str,
|
||||
proxy_type: ProxyType,
|
||||
timeout_secs: Option<u64>,
|
||||
user_agent: Option<String>,
|
||||
accept_invalid_certs: bool,
|
||||
) -> RequestBuilder {
|
||||
if self.should_reset_clients() {
|
||||
self.reset_clients();
|
||||
}
|
||||
|
||||
let mut builder = ClientBuilder::new()
|
||||
.use_rustls_tls()
|
||||
.pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
|
||||
.pool_idle_timeout(POOL_IDLE_TIMEOUT)
|
||||
.connect_timeout(DEFAULT_CONNECT_TIMEOUT)
|
||||
.http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE)
|
||||
.http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE)
|
||||
.http2_adaptive_window(true)
|
||||
.http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL))
|
||||
.http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT)
|
||||
.http2_max_frame_size(H2_MAX_FRAME_SIZE)
|
||||
.tcp_keepalive(Some(Duration::from_secs(10)))
|
||||
.http2_max_header_list_size(16 * 1024);
|
||||
|
||||
if let Some(timeout) = timeout_secs {
|
||||
builder = builder.timeout(Duration::from_secs(timeout));
|
||||
} else {
|
||||
builder = builder.timeout(DEFAULT_REQUEST_TIMEOUT);
|
||||
}
|
||||
|
||||
match proxy_type {
|
||||
ProxyType::None => {
|
||||
builder = builder.no_proxy();
|
||||
}
|
||||
) -> Result<HttpClient> {
|
||||
let proxy_uri = match proxy_type {
|
||||
ProxyType::None => None,
|
||||
ProxyType::Localhost => {
|
||||
let port = Config::verge()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
|
||||
let port = {
|
||||
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
|
||||
match verge_port {
|
||||
Some(port) => port,
|
||||
None => Config::clash().await.latest_ref().get_mixed_port(),
|
||||
}
|
||||
};
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
if let Ok(proxy) = Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
proxy_scheme.parse::<Uri>().ok()
|
||||
}
|
||||
ProxyType::System => {
|
||||
use sysproxy::Sysproxy;
|
||||
|
||||
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
|
||||
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
||||
|
||||
if let Ok(proxy) = Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder = builder.danger_accept_invalid_certs(accept_invalid_certs);
|
||||
|
||||
if let Some(ua) = user_agent {
|
||||
builder = builder.user_agent(ua);
|
||||
} else {
|
||||
use crate::utils::resolve::VERSION;
|
||||
|
||||
let version = match VERSION.get() {
|
||||
Some(v) => format!("clash-verge/v{v}"),
|
||||
None => "clash-verge/unknown".to_string(),
|
||||
};
|
||||
|
||||
builder = builder.user_agent(version);
|
||||
}
|
||||
|
||||
let client = match builder.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
true,
|
||||
"Failed to build custom HTTP client: {}",
|
||||
e
|
||||
);
|
||||
// Return a simple no-proxy client as fallback
|
||||
match ClientBuilder::new()
|
||||
.use_rustls_tls()
|
||||
.no_proxy()
|
||||
.timeout(DEFAULT_REQUEST_TIMEOUT)
|
||||
.build()
|
||||
{
|
||||
Ok(fallback_client) => fallback_client,
|
||||
Err(fallback_err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
true,
|
||||
"Failed to create fallback client: {}",
|
||||
fallback_err
|
||||
);
|
||||
self.record_connection_error(&format!(
|
||||
"Critical client build failure: {}",
|
||||
fallback_err
|
||||
));
|
||||
// Return a minimal client that will likely fail but won't panic
|
||||
ClientBuilder::new().build().unwrap_or_else(|_| {
|
||||
// If even the most basic client fails, this is truly critical
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
proxy_scheme.parse::<Uri>().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
client.get(url)
|
||||
}
|
||||
|
||||
/* /// 执行GET请求,添加错误跟踪
|
||||
pub async fn get(
|
||||
&self,
|
||||
url: &str,
|
||||
proxy_type: ProxyType,
|
||||
timeout_secs: Option<u64>,
|
||||
user_agent: Option<String>,
|
||||
accept_invalid_certs: bool,
|
||||
) -> Result<Response> {
|
||||
let request = self.create_request(
|
||||
url,
|
||||
proxy_type,
|
||||
timeout_secs,
|
||||
user_agent,
|
||||
accept_invalid_certs,
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
USER_AGENT,
|
||||
HeaderValue::from_str(
|
||||
&user_agent
|
||||
.unwrap_or_else(|| format!("clash-verge/v{}", env!("CARGO_PKG_VERSION"))),
|
||||
)?,
|
||||
);
|
||||
|
||||
let timeout_duration = timeout_secs.unwrap_or(30);
|
||||
let client = self.build_client(proxy_uri, headers, accept_invalid_certs, timeout_secs)?;
|
||||
|
||||
match tokio::time::timeout(Duration::from_secs(timeout_duration), request.send()).await {
|
||||
Ok(result) => match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(e) => {
|
||||
self.record_connection_error(&e.to_string());
|
||||
Err(anyhow::anyhow!("Failed to send HTTP request: {}", e))
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
self.record_connection_error("Request timeout");
|
||||
Err(anyhow::anyhow!(
|
||||
"HTTP request timed out after {} seconds",
|
||||
timeout_duration
|
||||
))
|
||||
}
|
||||
}
|
||||
} */
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn get_with_interrupt(
|
||||
&self,
|
||||
@@ -337,49 +192,38 @@ impl NetworkManager {
|
||||
timeout_secs: Option<u64>,
|
||||
user_agent: Option<String>,
|
||||
accept_invalid_certs: bool,
|
||||
) -> Result<Response> {
|
||||
let request = self.create_request(
|
||||
url,
|
||||
proxy_type,
|
||||
timeout_secs,
|
||||
user_agent,
|
||||
accept_invalid_certs,
|
||||
);
|
||||
) -> Result<HttpResponse> {
|
||||
if self.should_reset_clients().await {
|
||||
self.reset_clients().await;
|
||||
}
|
||||
|
||||
let timeout_duration = timeout_secs.unwrap_or(20);
|
||||
let client = self
|
||||
.create_request(proxy_type, timeout_secs, user_agent, accept_invalid_certs)
|
||||
.await?;
|
||||
|
||||
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(20));
|
||||
let url_owned = url.to_string();
|
||||
|
||||
let url_clone = url.to_string();
|
||||
let watchdog = AsyncHandler::spawn(move || async move {
|
||||
tokio::time::sleep(Duration::from_secs(timeout_duration)).await;
|
||||
let _ = cancel_tx.send(());
|
||||
logging!(warn, Type::Network, true, "请求超时取消: {}", url_clone);
|
||||
});
|
||||
|
||||
let result = tokio::select! {
|
||||
result = request.send() => result,
|
||||
_ = cancel_rx => {
|
||||
self.record_connection_error(&format!("Request interrupted for: {url}"));
|
||||
return Err(anyhow::anyhow!("Request interrupted after {} seconds", timeout_duration));
|
||||
let response = match timeout(timeout_duration, async {
|
||||
let mut response = client.get_async(&url_owned).await?;
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let body = response.text().await?;
|
||||
Ok::<_, anyhow::Error>(HttpResponse::new(status, headers, body))
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(res) => res?,
|
||||
Err(_) => {
|
||||
self.record_connection_error(&format!("Request interrupted: {}", url))
|
||||
.await;
|
||||
return Err(anyhow::anyhow!(
|
||||
"Request interrupted after {}s",
|
||||
timeout_duration.as_secs()
|
||||
));
|
||||
}
|
||||
};
|
||||
watchdog.abort();
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(e) => {
|
||||
self.record_connection_error(&e.to_string());
|
||||
Err(anyhow::anyhow!("Failed to send HTTP request: {}", e))
|
||||
}
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// 代理类型
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ProxyType {
|
||||
None,
|
||||
Localhost,
|
||||
System,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::utils::i18n::t;
|
||||
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
|
||||
@@ -23,48 +25,54 @@ fn notify(app: &AppHandle, title: &str, body: &str) {
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn notify_event(app: &AppHandle, event: NotificationEvent) {
|
||||
use crate::utils::i18n::t;
|
||||
pub async fn notify_event<'a>(app: AppHandle, event: NotificationEvent<'a>) {
|
||||
match event {
|
||||
NotificationEvent::DashboardToggled => {
|
||||
notify(app, &t("DashboardToggledTitle"), &t("DashboardToggledBody"));
|
||||
notify(
|
||||
&app,
|
||||
&t("DashboardToggledTitle").await,
|
||||
&t("DashboardToggledBody").await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::ClashModeChanged { mode } => {
|
||||
notify(
|
||||
app,
|
||||
&t("ClashModeChangedTitle"),
|
||||
&t_with_args("ClashModeChangedBody", mode),
|
||||
&app,
|
||||
&t("ClashModeChangedTitle").await,
|
||||
&t_with_args("ClashModeChangedBody", mode).await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::SystemProxyToggled => {
|
||||
notify(
|
||||
app,
|
||||
&t("SystemProxyToggledTitle"),
|
||||
&t("SystemProxyToggledBody"),
|
||||
&app,
|
||||
&t("SystemProxyToggledTitle").await,
|
||||
&t("SystemProxyToggledBody").await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::TunModeToggled => {
|
||||
notify(app, &t("TunModeToggledTitle"), &t("TunModeToggledBody"));
|
||||
notify(
|
||||
&app,
|
||||
&t("TunModeToggledTitle").await,
|
||||
&t("TunModeToggledBody").await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::LightweightModeEntered => {
|
||||
notify(
|
||||
app,
|
||||
&t("LightweightModeEnteredTitle"),
|
||||
&t("LightweightModeEnteredBody"),
|
||||
&app,
|
||||
&t("LightweightModeEnteredTitle").await,
|
||||
&t("LightweightModeEnteredBody").await,
|
||||
);
|
||||
}
|
||||
NotificationEvent::AppQuit => {
|
||||
notify(app, &t("AppQuitTitle"), &t("AppQuitBody"));
|
||||
notify(&app, &t("AppQuitTitle").await, &t("AppQuitBody").await);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
NotificationEvent::AppHidden => {
|
||||
notify(app, &t("AppHiddenTitle"), &t("AppHiddenBody"));
|
||||
notify(&app, &t("AppHiddenTitle").await, &t("AppHiddenBody").await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数,带参数的i18n
|
||||
fn t_with_args(key: &str, mode: &str) -> String {
|
||||
use crate::utils::i18n::t;
|
||||
t(key).replace("{mode}", mode)
|
||||
async fn t_with_args(key: &str, mode: &str) -> String {
|
||||
t(key).await.replace("{mode}", mode)
|
||||
}
|
||||
|
||||
@@ -1,753 +0,0 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::{Config, PrfItem},
|
||||
core::*,
|
||||
logging, logging_error,
|
||||
module::lightweight::{self, auto_lightweight_mode_init},
|
||||
process::AsyncHandler,
|
||||
utils::{init, logging::Type, server},
|
||||
wrap_err,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use scopeguard;
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use tauri::Url;
|
||||
//#[cfg(not(target_os = "linux"))]
|
||||
// use window_shadows::set_shadow;
|
||||
|
||||
pub static VERSION: OnceCell<String> = OnceCell::new();
|
||||
|
||||
// 定义默认窗口尺寸常量
|
||||
const DEFAULT_WIDTH: u32 = 940;
|
||||
const DEFAULT_HEIGHT: u32 = 700;
|
||||
|
||||
// 添加全局UI准备就绪标志
|
||||
static UI_READY: OnceCell<RwLock<bool>> = OnceCell::new();
|
||||
|
||||
// 窗口创建锁,防止并发创建窗口
|
||||
static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
|
||||
|
||||
// UI就绪阶段状态枚举
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum UiReadyStage {
|
||||
NotStarted,
|
||||
Loading,
|
||||
DomReady,
|
||||
ResourcesLoaded,
|
||||
Ready,
|
||||
}
|
||||
|
||||
// UI就绪详细状态
|
||||
#[derive(Debug)]
|
||||
struct UiReadyState {
|
||||
stage: RwLock<UiReadyStage>,
|
||||
}
|
||||
|
||||
impl Default for UiReadyState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stage: RwLock::new(UiReadyStage::NotStarted),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取UI就绪状态细节
|
||||
static UI_READY_STATE: OnceCell<UiReadyState> = OnceCell::new();
|
||||
|
||||
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
|
||||
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
|
||||
}
|
||||
|
||||
fn get_ui_ready() -> &'static RwLock<bool> {
|
||||
UI_READY.get_or_init(|| RwLock::new(false))
|
||||
}
|
||||
|
||||
fn get_ui_ready_state() -> &'static UiReadyState {
|
||||
UI_READY_STATE.get_or_init(UiReadyState::default)
|
||||
}
|
||||
|
||||
// 更新UI准备阶段
|
||||
pub fn update_ui_ready_stage(stage: UiReadyStage) {
|
||||
let state = get_ui_ready_state();
|
||||
let mut stage_lock = state.stage.write();
|
||||
|
||||
*stage_lock = stage;
|
||||
// 如果是最终阶段,标记UI完全就绪
|
||||
if stage == UiReadyStage::Ready {
|
||||
mark_ui_ready();
|
||||
}
|
||||
}
|
||||
|
||||
// 标记UI已准备就绪
|
||||
pub fn mark_ui_ready() {
|
||||
let mut ready = get_ui_ready().write();
|
||||
*ready = true;
|
||||
logging!(info, Type::Window, true, "UI已标记为完全就绪");
|
||||
}
|
||||
|
||||
// 重置UI就绪状态
|
||||
pub fn reset_ui_ready() {
|
||||
{
|
||||
let mut ready = get_ui_ready().write();
|
||||
*ready = false;
|
||||
}
|
||||
{
|
||||
let state = get_ui_ready_state();
|
||||
let mut stage = state.stage.write();
|
||||
*stage = UiReadyStage::NotStarted;
|
||||
}
|
||||
logging!(info, Type::Window, true, "UI就绪状态已重置");
|
||||
}
|
||||
|
||||
/// 异步方式处理启动后的额外任务
|
||||
pub async fn resolve_setup_async(app_handle: &AppHandle) {
|
||||
let start_time = std::time::Instant::now();
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"开始执行异步设置任务... 线程ID: {:?}",
|
||||
std::thread::current().id()
|
||||
);
|
||||
|
||||
if VERSION.get().is_none() {
|
||||
let version = app_handle.package_info().version.to_string();
|
||||
VERSION.get_or_init(|| {
|
||||
logging!(info, Type::Setup, true, "初始化版本信息: {}", version);
|
||||
version.clone()
|
||||
});
|
||||
}
|
||||
|
||||
logging_error!(Type::Setup, true, init::init_scheme());
|
||||
|
||||
logging_error!(Type::Setup, true, init::startup_script().await);
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"开始初始化配置... 线程ID: {:?}",
|
||||
std::thread::current().id()
|
||||
);
|
||||
logging_error!(Type::Config, true, Config::init_config().await);
|
||||
logging!(info, Type::Config, true, "配置初始化完成");
|
||||
|
||||
// 启动时清理冗余的 Profile 文件
|
||||
logging!(info, Type::Setup, true, "开始清理冗余的Profile文件...");
|
||||
|
||||
match Config::profiles().latest_ref().auto_cleanup() {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
logging!(trace, Type::Core, true, "启动核心管理器...");
|
||||
logging_error!(Type::Core, true, CoreManager::global().init().await);
|
||||
|
||||
log::trace!(target: "app", "启动内嵌服务器...");
|
||||
server::embed_server();
|
||||
|
||||
logging!(trace, Type::Core, true, "启动 IPC 监控服务...");
|
||||
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().init());
|
||||
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
logging!(info, Type::Tray, true, "创建系统托盘...");
|
||||
let result = tray::Tray::global().create_tray_from_handle(&app_handle);
|
||||
if result.is_ok() {
|
||||
logging!(info, Type::Tray, true, "系统托盘创建成功");
|
||||
} else if let Err(e) = result {
|
||||
logging!(error, Type::Tray, true, "系统托盘创建失败: {}", e);
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::Tray,
|
||||
true,
|
||||
"无法创建系统托盘: app_handle不存在"
|
||||
);
|
||||
}
|
||||
|
||||
// 更新系统代理
|
||||
logging_error!(
|
||||
Type::System,
|
||||
true,
|
||||
sysopt::Sysopt::global().update_sysproxy().await
|
||||
);
|
||||
logging_error!(
|
||||
Type::System,
|
||||
true,
|
||||
sysopt::Sysopt::global().init_guard_sysproxy()
|
||||
);
|
||||
|
||||
// 创建窗口
|
||||
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if is_silent_start {
|
||||
use crate::AppHandleManager;
|
||||
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
}
|
||||
}
|
||||
create_window(!is_silent_start);
|
||||
|
||||
// 初始化定时器
|
||||
logging_error!(Type::System, true, timer::Timer::global().init());
|
||||
|
||||
// 自动进入轻量模式
|
||||
auto_lightweight_mode_init();
|
||||
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_part());
|
||||
|
||||
logging!(trace, Type::System, true, "初始化热键...");
|
||||
logging_error!(Type::System, true, hotkey::Hotkey::global().init());
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"异步设置任务完成,耗时: {:?}",
|
||||
elapsed
|
||||
);
|
||||
|
||||
// 如果初始化时间过长,记录警告
|
||||
if elapsed.as_secs() > 10 {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"异步设置任务耗时较长({:?})",
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// reset system proxy (异步)
|
||||
pub async fn resolve_reset_async() {
|
||||
#[cfg(target_os = "macos")]
|
||||
logging!(info, Type::Tray, true, "Unsubscribing from traffic updates");
|
||||
#[cfg(target_os = "macos")]
|
||||
tray::Tray::global().unsubscribe_traffic();
|
||||
|
||||
logging_error!(
|
||||
Type::System,
|
||||
true,
|
||||
sysopt::Sysopt::global().reset_sysproxy().await
|
||||
);
|
||||
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging!(info, Type::System, true, "Restoring system DNS settings");
|
||||
restore_public_dns().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the main window
|
||||
pub fn create_window(is_show: bool) -> bool {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"开始创建/显示主窗口, is_show={}",
|
||||
is_show
|
||||
);
|
||||
|
||||
if !is_show {
|
||||
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
|
||||
lightweight::set_lightweight_mode(true);
|
||||
handle::Handle::notify_startup_completed();
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
|
||||
if is_show {
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let creating_lock = get_window_creating_lock();
|
||||
let mut creating = creating_lock.lock();
|
||||
|
||||
let (is_creating, last_time) = *creating;
|
||||
let elapsed = last_time.elapsed();
|
||||
|
||||
if is_creating && elapsed < Duration::from_secs(2) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
|
||||
elapsed.as_millis()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
*creating = (true, Instant::now());
|
||||
|
||||
// ScopeGuard 确保创建状态重置,防止 webview 卡死
|
||||
let _guard = scopeguard::guard(creating, |mut creating_guard| {
|
||||
*creating_guard = (false, Instant::now());
|
||||
logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置");
|
||||
});
|
||||
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Window,
|
||||
true,
|
||||
"无法获取app_handle,窗口创建失败"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"main", /* the unique window label */
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.center()
|
||||
.decorations(true)
|
||||
.fullscreen(false)
|
||||
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
|
||||
.min_inner_size(520.0, 520.0)
|
||||
.visible(true) // 立即显示窗口,避免用户等待
|
||||
.initialization_script(
|
||||
r#"
|
||||
console.log('[Tauri] 窗口初始化脚本开始执行');
|
||||
|
||||
function createLoadingOverlay() {
|
||||
|
||||
if (document.getElementById('initial-loading-overlay')) {
|
||||
console.log('[Tauri] 加载指示器已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Tauri] 创建加载指示器');
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.id = 'initial-loading-overlay';
|
||||
loadingDiv.innerHTML = `
|
||||
<div style="
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; z-index: 9999;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transition: opacity 0.3s ease;
|
||||
">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<div style="
|
||||
width: 40px; height: 40px; border: 3px solid #e3e3e3;
|
||||
border-top: 3px solid #3498db; border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
"></div>
|
||||
</div>
|
||||
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --bg-color: #1a1a1a; --text-color: #ffffff; }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
if (document.body) {
|
||||
document.body.appendChild(loadingDiv);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.body && !document.getElementById('initial-loading-overlay')) {
|
||||
document.body.appendChild(loadingDiv);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createLoadingOverlay();
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', createLoadingOverlay);
|
||||
} else {
|
||||
createLoadingOverlay();
|
||||
}
|
||||
|
||||
console.log('[Tauri] 窗口初始化脚本执行完成');
|
||||
"#,
|
||||
)
|
||||
.build()
|
||||
{
|
||||
Ok(newly_created_window) => {
|
||||
logging!(debug, Type::Window, true, "主窗口实例创建成功");
|
||||
|
||||
update_ui_ready_stage(UiReadyStage::NotStarted);
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
handle::Handle::global().mark_startup_completed();
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"异步窗口任务开始 (启动已标记完成)"
|
||||
);
|
||||
|
||||
// 先运行轻量模式检测
|
||||
lightweight::run_once_auto_lightweight();
|
||||
|
||||
// 发送启动完成事件,触发前端开始加载
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"发送 verge://startup-completed 事件"
|
||||
);
|
||||
handle::Handle::notify_startup_completed();
|
||||
|
||||
if is_show {
|
||||
let window_clone = newly_created_window.clone();
|
||||
|
||||
// 立即显示窗口
|
||||
let _ = window_clone.show();
|
||||
let _ = window_clone.set_focus();
|
||||
logging!(info, Type::Window, true, "窗口已立即显示");
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
|
||||
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
3
|
||||
} else {
|
||||
8
|
||||
};
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"开始监控UI加载状态 (最多{}秒)...",
|
||||
timeout_seconds
|
||||
);
|
||||
|
||||
// 异步监控UI状态,使用try_read避免死锁
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"启动UI状态监控线程,超时{}秒",
|
||||
timeout_seconds
|
||||
);
|
||||
|
||||
let ui_ready_checker = || async {
|
||||
let (mut check_count, mut consecutive_failures) = (0, 0);
|
||||
|
||||
loop {
|
||||
let is_ready = get_ui_ready()
|
||||
.try_read()
|
||||
.map(|guard| *guard)
|
||||
.unwrap_or_else(|| {
|
||||
consecutive_failures += 1;
|
||||
if consecutive_failures > 50 {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI状态监控连续{}次无法获取读锁,可能存在死锁",
|
||||
consecutive_failures
|
||||
);
|
||||
consecutive_failures = 0;
|
||||
}
|
||||
false
|
||||
});
|
||||
|
||||
if is_ready {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI状态监控检测到就绪信号,退出监控"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
consecutive_failures = 0;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
check_count += 1;
|
||||
|
||||
if check_count % 20 == 0 {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI加载状态检查... ({}秒)",
|
||||
check_count / 10
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let wait_result = tokio::time::timeout(
|
||||
Duration::from_secs(timeout_seconds),
|
||||
ui_ready_checker(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match wait_result {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Window, true, "UI已完全加载就绪");
|
||||
handle::Handle::global()
|
||||
.get_window()
|
||||
.map(|window| window.eval(r"
|
||||
const overlay = document.getElementById('initial-loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.style.opacity = '0';
|
||||
setTimeout(() => overlay.remove(), 300);
|
||||
}
|
||||
"));
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI加载监控超时({}秒),但窗口已正常显示",
|
||||
timeout_seconds
|
||||
);
|
||||
|
||||
get_ui_ready()
|
||||
.try_write()
|
||||
.map(|mut guard| {
|
||||
*guard = true;
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"超时后成功设置UI就绪状态"
|
||||
);
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Window,
|
||||
true,
|
||||
"超时后无法获取UI状态写锁,可能存在严重死锁"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logging!(info, Type::Window, true, "窗口显示流程完成");
|
||||
} else {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"is_show为false,窗口保持隐藏状态"
|
||||
);
|
||||
}
|
||||
});
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
log::info!(target:"app", "received deep link: {param}");
|
||||
|
||||
let param_str = if param.starts_with("[") && param.len() > 4 {
|
||||
param
|
||||
.get(2..param.len() - 2)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))?
|
||||
} else {
|
||||
param.as_str()
|
||||
};
|
||||
|
||||
// 解析 URL
|
||||
let link_parsed = match Url::parse(param_str) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
bail!("failed to parse deep link: {:?}, param: {:?}", e, param);
|
||||
}
|
||||
};
|
||||
|
||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
||||
let name = link_parsed
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.into_owned());
|
||||
|
||||
let url_param = if let Some(query) = link_parsed.query() {
|
||||
let prefix = "url=";
|
||||
if let Some(pos) = query.find(prefix) {
|
||||
let raw_url = &query[pos + prefix.len()..];
|
||||
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match url_param {
|
||||
Some(url) => {
|
||||
log::info!(target:"app", "decoded subscription url: {url}");
|
||||
|
||||
create_window(false);
|
||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
||||
Ok(item) => {
|
||||
let uid = match item.uid.clone() {
|
||||
Some(uid) => uid,
|
||||
None => {
|
||||
logging!(error, Type::Config, true, "Profile item missing UID");
|
||||
handle::Handle::notice_message(
|
||||
"import_sub_url::error",
|
||||
"Profile item missing UID".to_string(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let _ = wrap_err!(Config::profiles().data_mut().append_item(item));
|
||||
handle::Handle::notice_message("import_sub_url::ok", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None => bail!("failed to get profile url"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn set_public_dns(dns_server: String) {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::error!(target: "app", "app_handle not available for DNS configuration");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(target: "app", "try to set system dns");
|
||||
let resource_dir = match dirs::app_resources_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get resource directory: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let script = resource_dir.join("set_dns.sh");
|
||||
if !script.exists() {
|
||||
log::error!(target: "app", "set_dns.sh not found");
|
||||
return;
|
||||
}
|
||||
let script = script.to_string_lossy().into_owned();
|
||||
match app_handle
|
||||
.shell()
|
||||
.command("bash")
|
||||
.args([script, dns_server])
|
||||
.current_dir(resource_dir)
|
||||
.status()
|
||||
.await
|
||||
{
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
log::info!(target: "app", "set system dns successfully");
|
||||
} else {
|
||||
let code = status.code().unwrap_or(-1);
|
||||
log::error!(target: "app", "set system dns failed: {code}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "set system dns failed: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn restore_public_dns() {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::error!(target: "app", "app_handle not available for DNS restoration");
|
||||
return;
|
||||
}
|
||||
};
|
||||
log::info!(target: "app", "try to unset system dns");
|
||||
let resource_dir = match dirs::app_resources_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get resource directory: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let script = resource_dir.join("unset_dns.sh");
|
||||
if !script.exists() {
|
||||
log::error!(target: "app", "unset_dns.sh not found");
|
||||
return;
|
||||
}
|
||||
let script = script.to_string_lossy().into_owned();
|
||||
match app_handle
|
||||
.shell()
|
||||
.command("bash")
|
||||
.args([script])
|
||||
.current_dir(resource_dir)
|
||||
.status()
|
||||
.await
|
||||
{
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
log::info!(target: "app", "unset system dns successfully");
|
||||
} else {
|
||||
let code = status.code().unwrap_or(-1);
|
||||
log::error!(target: "app", "unset system dns failed: {code}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "unset system dns failed: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn set_public_dns(dns_server: String) {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::error!(target: "app", "app_handle not available for DNS configuration");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(target: "app", "try to set system dns");
|
||||
let resource_dir = match dirs::app_resources_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get resource directory: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let script = resource_dir.join("set_dns.sh");
|
||||
if !script.exists() {
|
||||
log::error!(target: "app", "set_dns.sh not found");
|
||||
return;
|
||||
}
|
||||
let script = script.to_string_lossy().into_owned();
|
||||
match app_handle
|
||||
.shell()
|
||||
.command("bash")
|
||||
.args([script, dns_server])
|
||||
.current_dir(resource_dir)
|
||||
.status()
|
||||
.await
|
||||
{
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
log::info!(target: "app", "set system dns successfully");
|
||||
} else {
|
||||
let code = status.code().unwrap_or(-1);
|
||||
log::error!(target: "app", "set system dns failed: {code}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "set system dns failed: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn restore_public_dns() {
|
||||
use crate::{core::handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::error!(target: "app", "app_handle not available for DNS restoration");
|
||||
return;
|
||||
}
|
||||
};
|
||||
log::info!(target: "app", "try to unset system dns");
|
||||
let resource_dir = match dirs::app_resources_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get resource directory: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let script = resource_dir.join("unset_dns.sh");
|
||||
if !script.exists() {
|
||||
log::error!(target: "app", "unset_dns.sh not found");
|
||||
return;
|
||||
}
|
||||
let script = script.to_string_lossy().into_owned();
|
||||
match app_handle
|
||||
.shell()
|
||||
.command("bash")
|
||||
.args([script])
|
||||
.current_dir(resource_dir)
|
||||
.status()
|
||||
.await
|
||||
{
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
log::info!(target: "app", "unset system dns successfully");
|
||||
} else {
|
||||
let code = status.code().unwrap_or(-1);
|
||||
log::error!(target: "app", "unset system dns failed: {code}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "unset system dns failed: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
use anyhow::Result;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, hotkey::Hotkey, sysopt, tray::Tray, CoreManager, Timer},
|
||||
logging, logging_error,
|
||||
module::lightweight::auto_lightweight_mode_init,
|
||||
process::AsyncHandler,
|
||||
utils::{init, logging::Type, network::NetworkManager, resolve::window::create_window, server},
|
||||
};
|
||||
|
||||
pub mod dns;
|
||||
pub mod scheme;
|
||||
pub mod ui;
|
||||
pub mod window;
|
||||
pub mod window_script;
|
||||
|
||||
pub fn resolve_setup_sync(app_handle: AppHandle) {
|
||||
init_handle(app_handle);
|
||||
init_scheme();
|
||||
init_embed_server();
|
||||
NetworkManager::new().init();
|
||||
}
|
||||
|
||||
pub fn resolve_setup_async() {
|
||||
let start_time = std::time::Instant::now();
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"开始执行异步设置任务... 线程ID: {:?}",
|
||||
std::thread::current().id()
|
||||
);
|
||||
|
||||
// AsyncHandler::spawn_blocking(|| AsyncHandler::block_on(init_work_config()));
|
||||
|
||||
AsyncHandler::spawn(|| async {
|
||||
init_work_config().await;
|
||||
init_resources().await;
|
||||
init_startup_script().await;
|
||||
|
||||
init_timer().await;
|
||||
init_hotkey().await;
|
||||
init_auto_lightweight_mode().await;
|
||||
|
||||
init_verge_config().await;
|
||||
init_core_manager().await;
|
||||
init_system_proxy().await;
|
||||
|
||||
AsyncHandler::spawn_blocking(|| {
|
||||
init_system_proxy_guard();
|
||||
});
|
||||
|
||||
init_window().await;
|
||||
init_tray().await;
|
||||
refresh_tray_menu().await
|
||||
});
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"异步设置任务完成,耗时: {:?}",
|
||||
elapsed
|
||||
);
|
||||
|
||||
if elapsed.as_secs() > 10 {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"异步设置任务耗时较长({:?})",
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 其它辅助函数不变
|
||||
pub async fn resolve_reset_async() {
|
||||
logging!(info, Type::Tray, true, "Resetting system proxy");
|
||||
logging_error!(
|
||||
Type::System,
|
||||
true,
|
||||
sysopt::Sysopt::global().reset_sysproxy().await
|
||||
);
|
||||
|
||||
logging!(info, Type::Core, true, "Stopping core service");
|
||||
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use dns::restore_public_dns;
|
||||
|
||||
logging!(info, Type::System, true, "Restoring system DNS settings");
|
||||
restore_public_dns().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_handle(app_handle: AppHandle) {
|
||||
logging!(info, Type::Setup, true, "Initializing app handle...");
|
||||
handle::Handle::global().init(app_handle);
|
||||
}
|
||||
|
||||
pub(super) fn init_scheme() {
|
||||
logging!(info, Type::Setup, true, "Initializing custom URL scheme");
|
||||
logging_error!(Type::Setup, true, init::init_scheme());
|
||||
}
|
||||
|
||||
pub async fn resolve_scheme(param: String) -> Result<()> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Resolving scheme for param: {}",
|
||||
param
|
||||
);
|
||||
logging_error!(Type::Setup, true, scheme::resolve_scheme(param).await);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn init_embed_server() {
|
||||
logging!(info, Type::Setup, true, "Initializing embedded server...");
|
||||
server::embed_server();
|
||||
}
|
||||
pub(super) async fn init_resources() {
|
||||
logging!(info, Type::Setup, true, "Initializing resources...");
|
||||
logging_error!(Type::Setup, true, init::init_resources().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_startup_script() {
|
||||
logging!(info, Type::Setup, true, "Initializing startup script");
|
||||
logging_error!(Type::Setup, true, init::startup_script().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_timer() {
|
||||
logging!(info, Type::Setup, true, "Initializing timer...");
|
||||
logging_error!(Type::Setup, true, Timer::global().init().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_hotkey() {
|
||||
logging!(info, Type::Setup, true, "Initializing hotkey...");
|
||||
logging_error!(Type::Setup, true, Hotkey::global().init().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_auto_lightweight_mode() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Initializing auto lightweight mode..."
|
||||
);
|
||||
logging_error!(Type::Setup, true, auto_lightweight_mode_init().await);
|
||||
}
|
||||
|
||||
pub async fn init_work_config() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Initializing work configuration..."
|
||||
);
|
||||
logging_error!(Type::Setup, true, init::init_config().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_tray() {
|
||||
logging!(info, Type::Setup, true, "Initializing system tray...");
|
||||
logging_error!(Type::Setup, true, Tray::global().init().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_verge_config() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Initializing verge configuration..."
|
||||
);
|
||||
logging_error!(Type::Setup, true, Config::init_config().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_core_manager() {
|
||||
logging!(info, Type::Setup, true, "Initializing core manager...");
|
||||
logging_error!(Type::Setup, true, CoreManager::global().init().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_system_proxy() {
|
||||
logging!(info, Type::Setup, true, "Initializing system proxy...");
|
||||
logging_error!(
|
||||
Type::Setup,
|
||||
true,
|
||||
sysopt::Sysopt::global().update_sysproxy().await
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn init_system_proxy_guard() {
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Initializing system proxy guard..."
|
||||
);
|
||||
logging_error!(
|
||||
Type::Setup,
|
||||
true,
|
||||
sysopt::Sysopt::global().init_guard_sysproxy()
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) async fn refresh_tray_menu() {
|
||||
logging!(info, Type::Setup, true, "Refreshing tray menu...");
|
||||
logging_error!(Type::Setup, true, Tray::global().update_part().await);
|
||||
}
|
||||
|
||||
pub(super) async fn init_window() {
|
||||
let is_silent_start =
|
||||
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if is_silent_start {
|
||||
use crate::core::handle::Handle;
|
||||
|
||||
Handle::global().set_activation_policy_accessory();
|
||||
}
|
||||
}
|
||||
create_window(!is_silent_start).await;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use anyhow::{bail, Result};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use tauri::Url;
|
||||
|
||||
use crate::{config::PrfItem, core::handle, logging, utils::logging::Type, wrap_err};
|
||||
|
||||
pub(super) async fn resolve_scheme(param: String) -> Result<()> {
|
||||
log::info!(target:"app", "received deep link: {param}");
|
||||
|
||||
let param_str = if param.starts_with("[") && param.len() > 4 {
|
||||
param
|
||||
.get(2..param.len() - 2)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))?
|
||||
} else {
|
||||
param.as_str()
|
||||
};
|
||||
|
||||
// 解析 URL
|
||||
let link_parsed = match Url::parse(param_str) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
bail!("failed to parse deep link: {:?}, param: {:?}", e, param);
|
||||
}
|
||||
};
|
||||
|
||||
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
|
||||
let name = link_parsed
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "name")
|
||||
.map(|(_, value)| value.into_owned());
|
||||
|
||||
let url_param = if let Some(query) = link_parsed.query() {
|
||||
let prefix = "url=";
|
||||
if let Some(pos) = query.find(prefix) {
|
||||
let raw_url = &query[pos + prefix.len()..];
|
||||
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match url_param {
|
||||
Some(url) => {
|
||||
log::info!(target:"app", "decoded subscription url: {url}");
|
||||
// create_window(false).await;
|
||||
match PrfItem::from_url(url.as_ref(), name, None, None).await {
|
||||
Ok(item) => {
|
||||
let uid = match item.uid.clone() {
|
||||
Some(uid) => uid,
|
||||
None => {
|
||||
logging!(error, Type::Config, true, "Profile item missing UID");
|
||||
handle::Handle::notice_message(
|
||||
"import_sub_url::error",
|
||||
"Profile item missing UID".to_string(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let result = crate::config::profiles::profiles_append_item_safe(item).await;
|
||||
let _ = wrap_err!(result);
|
||||
handle::Handle::notice_message("import_sub_url::ok", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None => bail!("failed to get profile url"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
// 使用 AtomicBool 替代 RwLock<bool>,性能更好且无锁
|
||||
static UI_READY: OnceCell<AtomicBool> = OnceCell::new();
|
||||
// 获取UI就绪状态细节
|
||||
static UI_READY_STATE: OnceCell<UiReadyState> = OnceCell::new();
|
||||
// 添加通知机制,用于事件驱动的 UI 就绪检测
|
||||
static UI_READY_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
|
||||
|
||||
// UI就绪阶段状态枚举
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum UiReadyStage {
|
||||
NotStarted,
|
||||
Loading,
|
||||
DomReady,
|
||||
ResourcesLoaded,
|
||||
Ready,
|
||||
}
|
||||
|
||||
// UI就绪详细状态
|
||||
#[derive(Debug)]
|
||||
struct UiReadyState {
|
||||
stage: RwLock<UiReadyStage>,
|
||||
}
|
||||
|
||||
impl Default for UiReadyState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stage: RwLock::new(UiReadyStage::NotStarted),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_ui_ready() -> &'static AtomicBool {
|
||||
UI_READY.get_or_init(|| AtomicBool::new(false))
|
||||
}
|
||||
|
||||
fn get_ui_ready_state() -> &'static UiReadyState {
|
||||
UI_READY_STATE.get_or_init(UiReadyState::default)
|
||||
}
|
||||
|
||||
fn get_ui_ready_notify() -> &'static Arc<Notify> {
|
||||
UI_READY_NOTIFY.get_or_init(|| Arc::new(Notify::new()))
|
||||
}
|
||||
|
||||
/// 等待 UI 就绪的异步函数,使用事件驱动而非轮询
|
||||
pub async fn wait_for_ui_ready(timeout_seconds: u64) -> bool {
|
||||
// 首先检查是否已经就绪
|
||||
if get_ui_ready().load(Ordering::Acquire) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 使用 tokio::select! 同时等待通知和超时
|
||||
tokio::select! {
|
||||
_ = get_ui_ready_notify().notified() => {
|
||||
// 收到通知后再次确认状态(防止虚假通知)
|
||||
get_ui_ready().load(Ordering::Acquire)
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(timeout_seconds)) => {
|
||||
// 超时,返回当前状态
|
||||
get_ui_ready().load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新UI准备阶段
|
||||
pub fn update_ui_ready_stage(stage: UiReadyStage) {
|
||||
let state = get_ui_ready_state();
|
||||
let mut stage_lock = state.stage.write();
|
||||
|
||||
*stage_lock = stage;
|
||||
// 如果是最终阶段,标记UI完全就绪
|
||||
if stage == UiReadyStage::Ready {
|
||||
mark_ui_ready();
|
||||
}
|
||||
}
|
||||
|
||||
// 标记UI已准备就绪
|
||||
pub fn mark_ui_ready() {
|
||||
get_ui_ready().store(true, Ordering::Release);
|
||||
logging!(info, Type::Window, true, "UI已标记为完全就绪");
|
||||
|
||||
// 通知所有等待的任务
|
||||
get_ui_ready_notify().notify_waiters();
|
||||
}
|
||||
|
||||
// 重置UI就绪状态
|
||||
pub fn reset_ui_ready() {
|
||||
get_ui_ready().store(false, Ordering::Release);
|
||||
{
|
||||
let state = get_ui_ready_state();
|
||||
let mut stage = state.stage.write();
|
||||
*stage = UiReadyStage::NotStarted;
|
||||
}
|
||||
logging!(info, Type::Window, true, "UI就绪状态已重置");
|
||||
|
||||
// 注意:这里不需要通知,因为重置后状态变为 false
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use tauri::{Manager, WebviewWindow};
|
||||
|
||||
use crate::{
|
||||
core::handle,
|
||||
logging,
|
||||
module::lightweight,
|
||||
process::AsyncHandler,
|
||||
utils::{
|
||||
logging::Type,
|
||||
resolve::{
|
||||
ui::{get_ui_ready, update_ui_ready_stage, wait_for_ui_ready, UiReadyStage},
|
||||
window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 定义默认窗口尺寸常量
|
||||
const DEFAULT_WIDTH: f64 = 940.0;
|
||||
const DEFAULT_HEIGHT: f64 = 700.0;
|
||||
|
||||
const MINIMAL_WIDTH: f64 = 520.0;
|
||||
const MINIMAL_HEIGHT: f64 = 520.0;
|
||||
|
||||
// 窗口创建锁,防止并发创建窗口
|
||||
static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
|
||||
|
||||
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
|
||||
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
|
||||
}
|
||||
|
||||
/// 检查是否已存在窗口,如果存在则显示并返回 true
|
||||
fn check_existing_window(is_show: bool) -> Option<bool> {
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
|
||||
if is_show {
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
handle::Handle::global().set_activation_policy_regular();
|
||||
}
|
||||
return Some(true);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 获取窗口创建锁,防止并发创建
|
||||
fn acquire_window_creation_lock() -> Result<(), bool> {
|
||||
let creating_lock = get_window_creating_lock();
|
||||
let mut creating = creating_lock.lock();
|
||||
|
||||
let (is_creating, last_time) = *creating;
|
||||
let elapsed = last_time.elapsed();
|
||||
|
||||
if is_creating && elapsed < Duration::from_secs(2) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
|
||||
elapsed.as_millis()
|
||||
);
|
||||
return Err(false);
|
||||
}
|
||||
|
||||
*creating = (true, Instant::now());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重置窗口创建锁
|
||||
fn reset_window_creation_lock() {
|
||||
let creating_lock = get_window_creating_lock();
|
||||
let mut creating = creating_lock.lock();
|
||||
*creating = (false, Instant::now());
|
||||
logging!(debug, Type::Window, true, "窗口创建状态已重置");
|
||||
}
|
||||
|
||||
/// 构建新的 WebView 窗口
|
||||
fn build_new_window() -> Result<WebviewWindow, String> {
|
||||
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Window,
|
||||
true,
|
||||
"无法获取app_handle,窗口创建失败"
|
||||
);
|
||||
"无法获取app_handle".to_string()
|
||||
})?;
|
||||
|
||||
tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"main", /* the unique window label */
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.center()
|
||||
.decorations(true)
|
||||
.fullscreen(false)
|
||||
.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
|
||||
.min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT)
|
||||
.visible(true) // 立即显示窗口,避免用户等待
|
||||
.initialization_script(WINDOW_INITIAL_SCRIPT)
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
|
||||
e.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
/// 窗口创建后的初始设置
|
||||
async fn setup_window_post_creation() {
|
||||
update_ui_ready_stage(UiReadyStage::NotStarted);
|
||||
handle::Handle::global().mark_startup_completed();
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"异步窗口任务开始 (启动已标记完成)"
|
||||
);
|
||||
|
||||
// 先运行轻量模式检测
|
||||
lightweight::run_once_auto_lightweight().await;
|
||||
|
||||
// 发送启动完成事件,触发前端开始加载
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"发送 verge://startup-completed 事件"
|
||||
);
|
||||
handle::Handle::notify_startup_completed();
|
||||
}
|
||||
|
||||
/// 通过窗口标签处理窗口显示逻辑(减少任务大小的优化版本)
|
||||
fn handle_window_display_by_label(window_label: String, is_show: bool) {
|
||||
if !is_show {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"is_show为false,窗口保持隐藏状态"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 通过标签重新获取窗口实例
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
logging!(error, Type::Window, true, "无法获取app handle");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let window = match app_handle.get_webview_window(&window_label) {
|
||||
Some(window) => window,
|
||||
None => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Window,
|
||||
true,
|
||||
"无法通过标签获取窗口: {}",
|
||||
window_label
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 立即显示窗口
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
logging!(info, Type::Window, true, "窗口已立即显示");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
handle::Handle::global().set_activation_policy_regular();
|
||||
|
||||
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
3
|
||||
} else {
|
||||
8
|
||||
};
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"开始监控UI加载状态 (最多{}秒)...",
|
||||
timeout_seconds
|
||||
);
|
||||
|
||||
// 异步监控UI状态
|
||||
AsyncHandler::spawn(move || async move {
|
||||
monitor_ui_loading(timeout_seconds).await;
|
||||
});
|
||||
|
||||
logging!(info, Type::Window, true, "窗口显示流程完成");
|
||||
}
|
||||
|
||||
/// 监控 UI 加载状态 - 优雅的事件驱动版本
|
||||
async fn monitor_ui_loading(timeout_seconds: u64) {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Window,
|
||||
true,
|
||||
"启动UI状态监控,使用事件驱动方式,超时{}秒",
|
||||
timeout_seconds
|
||||
);
|
||||
|
||||
// 使用事件驱动的方式等待 UI 就绪,完全消除轮询
|
||||
let ui_ready = wait_for_ui_ready(timeout_seconds).await;
|
||||
|
||||
if ui_ready {
|
||||
logging!(info, Type::Window, true, "UI已完全加载就绪(事件驱动)");
|
||||
handle::Handle::global()
|
||||
.get_window()
|
||||
.map(|window| window.eval(INITIAL_LOADING_OVERLAY));
|
||||
} else {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Window,
|
||||
true,
|
||||
"UI加载监控超时({}秒),但窗口已正常显示",
|
||||
timeout_seconds
|
||||
);
|
||||
|
||||
// 超时后手动设置 UI 就绪状态(保持向后兼容性)
|
||||
get_ui_ready().store(true, std::sync::atomic::Ordering::Release);
|
||||
logging!(info, Type::Window, true, "超时后成功设置UI就绪状态");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_window(is_show: bool) -> bool {
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
true,
|
||||
"开始创建/显示主窗口, is_show={}",
|
||||
is_show
|
||||
);
|
||||
|
||||
if !is_show {
|
||||
lightweight::set_lightweight_mode(true).await;
|
||||
handle::Handle::notify_startup_completed();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否已存在窗口
|
||||
if let Some(result) = check_existing_window(is_show) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 检查 app_handle 是否存在
|
||||
if handle::Handle::global().app_handle().is_none() {
|
||||
logging!(error, Type::Window, true, "No window found in app_handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取窗口创建锁
|
||||
if let Err(should_return) = acquire_window_creation_lock() {
|
||||
return should_return;
|
||||
}
|
||||
|
||||
// 构建新窗口
|
||||
let newly_created_window = match build_new_window() {
|
||||
Ok(window) => {
|
||||
// 窗口创建成功,重置锁状态
|
||||
reset_window_creation_lock();
|
||||
window
|
||||
}
|
||||
Err(_) => {
|
||||
// 窗口创建失败,重置锁状态
|
||||
reset_window_creation_lock();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
logging!(debug, Type::Window, true, "主窗口实例创建成功");
|
||||
|
||||
// 获取窗口标签,减少闭包捕获的大小
|
||||
let window_label = newly_created_window.label().to_string();
|
||||
|
||||
// 异步处理窗口后续设置,只捕获必要的小数据
|
||||
setup_window_post_creation().await;
|
||||
handle_window_display_by_label(window_label, is_show);
|
||||
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
pub const WINDOW_INITIAL_SCRIPT: &str = r#"
|
||||
console.log('[Tauri] 窗口初始化脚本开始执行');
|
||||
|
||||
function createLoadingOverlay() {
|
||||
|
||||
if (document.getElementById('initial-loading-overlay')) {
|
||||
console.log('[Tauri] 加载指示器已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Tauri] 创建加载指示器');
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.id = 'initial-loading-overlay';
|
||||
loadingDiv.innerHTML = `
|
||||
<div style="
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; z-index: 9999;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transition: opacity 0.3s ease;
|
||||
">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<div style="
|
||||
width: 40px; height: 40px; border: 3px solid #e3e3e3;
|
||||
border-top: 3px solid #3498db; border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
"></div>
|
||||
</div>
|
||||
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --bg-color: #1a1a1a; --text-color: #ffffff; }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
if (document.body) {
|
||||
document.body.appendChild(loadingDiv);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.body && !document.getElementById('initial-loading-overlay')) {
|
||||
document.body.appendChild(loadingDiv);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createLoadingOverlay();
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', createLoadingOverlay);
|
||||
} else {
|
||||
createLoadingOverlay();
|
||||
}
|
||||
|
||||
console.log('[Tauri] 窗口初始化脚本执行完成');
|
||||
"#;
|
||||
|
||||
pub const INITIAL_LOADING_OVERLAY: &str = r"
|
||||
const overlay = document.getElementById('initial-loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.style.opacity = '0';
|
||||
setTimeout(() => overlay.remove(), 300);
|
||||
}
|
||||
";
|
||||
@@ -1,5 +1,3 @@
|
||||
extern crate warp;
|
||||
|
||||
use super::resolve;
|
||||
use crate::{
|
||||
config::{Config, IVerge, DEFAULT_PAC},
|
||||
@@ -9,7 +7,6 @@ use crate::{
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use port_scanner::local_port_available;
|
||||
use std::convert::Infallible;
|
||||
use warp::Filter;
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
@@ -47,40 +44,51 @@ pub async fn check_singleton() -> Result<()> {
|
||||
pub fn embed_server() {
|
||||
let port = IVerge::get_singleton_port();
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let visible = warp::path!("commands" / "visible").map(|| {
|
||||
resolve::create_window(false);
|
||||
warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK)
|
||||
AsyncHandler::spawn_blocking(move || async move {
|
||||
let visible = warp::path!("commands" / "visible").and_then(|| async {
|
||||
Ok::<_, warp::Rejection>(warp::reply::with_status(
|
||||
"ok".to_string(),
|
||||
warp::http::StatusCode::OK,
|
||||
))
|
||||
});
|
||||
|
||||
let pac = warp::path!("commands" / "pac").map(|| {
|
||||
let content = Config::verge()
|
||||
.latest_ref()
|
||||
.pac_file_content
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_PAC.to_string());
|
||||
let port = Config::verge()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
let content = content.replace("%mixed-port%", &format!("{port}"));
|
||||
let verge_config = Config::verge().await;
|
||||
let clash_config = Config::clash().await;
|
||||
|
||||
let content = verge_config
|
||||
.latest_ref()
|
||||
.pac_file_content
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_PAC.to_string());
|
||||
|
||||
let mixed_port = verge_config
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(clash_config.latest_ref().get_mixed_port());
|
||||
|
||||
// Clone the content and port for the closure to avoid borrowing issues
|
||||
let pac_content = content.clone();
|
||||
let pac_port = mixed_port;
|
||||
let pac = warp::path!("commands" / "pac").map(move || {
|
||||
let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}"));
|
||||
warp::http::Response::builder()
|
||||
.header("Content-Type", "application/x-ns-proxy-autoconfig")
|
||||
.body(content)
|
||||
.body(processed_content)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
async fn scheme_handler(query: QueryParam) -> Result<String, Infallible> {
|
||||
logging_error!(
|
||||
Type::Setup,
|
||||
true,
|
||||
resolve::resolve_scheme(query.param).await
|
||||
);
|
||||
Ok("ok".to_string())
|
||||
}
|
||||
|
||||
// Use map instead of and_then to avoid Send issues
|
||||
let scheme = warp::path!("commands" / "scheme")
|
||||
.and(warp::query::<QueryParam>())
|
||||
.and_then(scheme_handler);
|
||||
.map(|query: QueryParam| {
|
||||
// Spawn async work in a fire-and-forget manner
|
||||
let param = query.param.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
logging_error!(Type::Setup, true, resolve::resolve_scheme(param).await);
|
||||
});
|
||||
warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK)
|
||||
});
|
||||
|
||||
let commands = visible.or(scheme).or(pac);
|
||||
warp::serve(commands).run(([127, 0, 0, 1], port)).await;
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::{core::handle, logging, utils::logging::Type};
|
||||
use tauri::{Manager, WebviewWindow, Wry};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use scopeguard;
|
||||
@@ -118,7 +115,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
/// 智能显示主窗口
|
||||
pub fn show_main_window() -> WindowOperationResult {
|
||||
pub async fn show_main_window() -> WindowOperationResult {
|
||||
// 防抖检查
|
||||
if !should_handle_window_operation() {
|
||||
return WindowOperationResult::NoAction;
|
||||
@@ -141,7 +138,7 @@ impl WindowManager {
|
||||
match current_state {
|
||||
WindowState::NotExist => {
|
||||
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
|
||||
if Self::create_new_window() {
|
||||
if Self::create_new_window().await {
|
||||
logging!(info, Type::Window, true, "窗口创建成功");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
WindowOperationResult::Created
|
||||
@@ -175,7 +172,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
/// 切换主窗口显示状态(显示/隐藏)
|
||||
pub fn toggle_main_window() -> WindowOperationResult {
|
||||
pub async fn toggle_main_window() -> WindowOperationResult {
|
||||
// 防抖检查
|
||||
if !should_handle_window_operation() {
|
||||
return WindowOperationResult::NoAction;
|
||||
@@ -201,7 +198,7 @@ impl WindowManager {
|
||||
// 窗口不存在,创建新窗口
|
||||
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
|
||||
// 由于已经有防抖保护,直接调用内部方法
|
||||
if Self::create_new_window() {
|
||||
if Self::create_new_window().await {
|
||||
WindowOperationResult::Created
|
||||
} else {
|
||||
WindowOperationResult::Failed
|
||||
@@ -283,7 +280,7 @@ impl WindowManager {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
handle::Handle::global().set_activation_policy_regular();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -363,9 +360,10 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
/// 创建新窗口,防抖避免重复调用
|
||||
fn create_new_window() -> bool {
|
||||
async fn create_new_window() -> bool {
|
||||
use crate::utils::resolve;
|
||||
resolve::create_window(true)
|
||||
|
||||
resolve::window::create_window(true).await
|
||||
}
|
||||
|
||||
/// 获取详细的窗口状态信息
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
||||
@@ -62,6 +62,7 @@ export const BaseSearchBox = (props: SearchProps) => {
|
||||
new RegExp(pattern);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn("[BaseSearchBox] validateRegex error:", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
|
||||
import { useThemeMode } from "@/services/states";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { t } from "i18next";
|
||||
@@ -14,9 +13,6 @@ interface Props {
|
||||
|
||||
export const ConnectionTable = (props: Props) => {
|
||||
const { connections, onShowDetail } = props;
|
||||
const mode = useThemeMode();
|
||||
const isDark = mode === "light" ? false : true;
|
||||
const backgroundColor = isDark ? "#282A36" : "#ffffff";
|
||||
|
||||
const [columnVisible, setColumnVisible] = useState<
|
||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useRef,
|
||||
memo,
|
||||
} from "react";
|
||||
import { Box, useTheme, Tooltip, Paper, Typography } from "@mui/material";
|
||||
import { Box, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import {
|
||||
@@ -126,13 +126,6 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
[theme],
|
||||
);
|
||||
|
||||
// 根据时间范围获取数据点数量
|
||||
const getPointsForTimeRange = useCallback(
|
||||
(minutes: TimeRange): number =>
|
||||
Math.min(minutes * 60, GRAPH_CONFIG.maxPoints),
|
||||
[],
|
||||
);
|
||||
|
||||
// 更新显示数据(防抖处理)
|
||||
const updateDisplayDataDebounced = useMemo(() => {
|
||||
let timeoutId: number;
|
||||
@@ -283,7 +276,6 @@ export const EnhancedCanvasTrafficGraph = memo(
|
||||
};
|
||||
|
||||
const padding = GRAPH_CONFIG.padding;
|
||||
const effectiveHeight = height - padding.top - padding.bottom;
|
||||
|
||||
// 强制显示三个刻度:底部、中间、顶部
|
||||
const topY = padding.top + 10; // 避免与顶部时间范围按钮重叠
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useCallback, memo, useMemo } from "react";
|
||||
import { useRef, useCallback, memo, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Typography,
|
||||
@@ -20,10 +20,8 @@ import {
|
||||
import {
|
||||
EnhancedCanvasTrafficGraph,
|
||||
type EnhancedCanvasTrafficGraphRef,
|
||||
type ITrafficItem,
|
||||
} from "./enhanced-canvas-traffic-graph";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { isDebugEnabled, gc } from "@/services/cmds";
|
||||
@@ -33,17 +31,6 @@ import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
|
||||
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface MemoryUsage {
|
||||
inuse: number;
|
||||
oslimit?: number;
|
||||
}
|
||||
|
||||
interface TrafficStatData {
|
||||
uploadTotal: number;
|
||||
downloadTotal: number;
|
||||
activeConnections: number;
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
@@ -64,9 +51,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// 控制更新频率
|
||||
const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
|
||||
|
||||
// 统计卡片组件 - 使用memo优化
|
||||
const CompactStatCard = memo(
|
||||
({ icon, title, value, unit, color, onClick }: StatCardProps) => {
|
||||
@@ -159,13 +143,12 @@ CompactStatCard.displayName = "CompactStatCard";
|
||||
export const EnhancedTrafficStats = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { clashInfo } = useClashInfo();
|
||||
const { verge } = useVerge();
|
||||
const trafficRef = useRef<EnhancedCanvasTrafficGraphRef>(null);
|
||||
const pageVisible = useVisibility();
|
||||
|
||||
// 使用AppDataProvider
|
||||
const { connections, uptime } = useAppData();
|
||||
const { connections } = useAppData();
|
||||
|
||||
// 使用增强版的统一流量数据Hook
|
||||
const { traffic, memory, isLoading, isDataFresh, hasValidData } =
|
||||
@@ -258,7 +241,7 @@ export const EnhancedTrafficStats = () => {
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
DEBUG: {!!trafficRef.current ? "图表已初始化" : "图表未初始化"}
|
||||
DEBUG: {trafficRef.current ? "图表已初始化" : "图表未初始化"}
|
||||
<br />
|
||||
状态: {isDataFresh ? "active" : "inactive"}
|
||||
<br />
|
||||
|
||||
@@ -73,14 +73,6 @@ export interface HomeProfileCardProps {
|
||||
onProfileUpdated?: () => void;
|
||||
}
|
||||
|
||||
// 添加一个通用的截断样式
|
||||
const truncateStyle = {
|
||||
maxWidth: "calc(100% - 28px)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
// 提取独立组件减少主组件复杂度
|
||||
const ProfileDetails = ({
|
||||
current,
|
||||
|
||||
@@ -232,6 +232,7 @@ export const ProxyTunCard: FC = () => {
|
||||
<ProxyControlSwitches
|
||||
onError={handleError}
|
||||
label={activeTab === "system" ? t("System Proxy") : t("Tun Mode")}
|
||||
noRightPadding={true}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -32,7 +32,7 @@ export const SystemInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { verge, patchVerge } = useVerge();
|
||||
const navigate = useNavigate();
|
||||
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
|
||||
const { isAdminMode, isSidecarMode } = useSystemState();
|
||||
const { installServiceAndRestartCore } = useServiceInstaller();
|
||||
|
||||
// 系统信息状态
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import {
|
||||
ArrowDownwardRounded,
|
||||
@@ -16,11 +16,6 @@ import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
|
||||
import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface MemoryUsage {
|
||||
inuse: number;
|
||||
oslimit?: number;
|
||||
}
|
||||
|
||||
// setup the traffic
|
||||
export const LayoutTraffic = () => {
|
||||
const { data: isDebug } = useSWR(
|
||||
@@ -46,8 +41,7 @@ export const LayoutTraffic = () => {
|
||||
const pageVisible = useVisibility();
|
||||
|
||||
// 使用增强版的统一流量数据Hook
|
||||
const { traffic, memory, isLoading, isDataFresh, hasValidData } =
|
||||
useTrafficDataEnhanced();
|
||||
const { traffic, memory } = useTrafficDataEnhanced();
|
||||
|
||||
// 启动流量服务
|
||||
useEffect(() => {
|
||||
|
||||
@@ -75,7 +75,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const [visualization, setVisualization] = useState(true);
|
||||
const [match, setMatch] = useState(() => (_: string) => true);
|
||||
const [interfaceNameList, setInterfaceNameList] = useState<string[]>([]);
|
||||
const { control, watch, register, ...formIns } = useForm<IProxyGroupConfig>({
|
||||
const { control, ...formIns } = useForm<IProxyGroupConfig>({
|
||||
defaultValues: {
|
||||
type: "select",
|
||||
name: "",
|
||||
@@ -196,6 +196,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("[GroupsEditorViewer] yaml.dump failed:", e);
|
||||
// 防止异常导致UI卡死
|
||||
}
|
||||
};
|
||||
|
||||
@@ -315,7 +315,7 @@ export const ProfileItem = (props: Props) => {
|
||||
// 更新成功,刷新列表
|
||||
showNotice("success", t("Update subscription successfully"));
|
||||
mutate("getProfiles");
|
||||
} catch (err: any) {
|
||||
} catch {
|
||||
// 更新完全失败(包括后端的回退尝试)
|
||||
// 不需要做处理,后端会通过事件通知系统发送错误
|
||||
} finally {
|
||||
|
||||
@@ -47,11 +47,6 @@ export const ProfileMore = (props: Props) => {
|
||||
}
|
||||
});
|
||||
|
||||
const fnWrapper = (fn: () => void) => () => {
|
||||
setAnchorEl(null);
|
||||
return fn();
|
||||
};
|
||||
|
||||
const hasError = !!logInfo.find((e) => e[0] === "exception");
|
||||
|
||||
const itemMenu = [
|
||||
|
||||
@@ -47,7 +47,12 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
||||
// file input
|
||||
const fileDataRef = useRef<string | null>(null);
|
||||
|
||||
const { control, watch, register, ...formIns } = useForm<IProfileItem>({
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
register: _register,
|
||||
...formIns
|
||||
} = useForm<IProfileItem>({
|
||||
defaultValues: {
|
||||
type: "remote",
|
||||
name: "",
|
||||
@@ -144,7 +149,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
await patchProfile(form.uid, item);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// 首次创建/更新失败,尝试使用自身代理
|
||||
showNotice(
|
||||
"info",
|
||||
@@ -201,7 +206,9 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
||||
setOpen(false);
|
||||
fileDataRef.current = null;
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
console.warn("[ProfileViewer] handleClose error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const text = {
|
||||
|
||||
@@ -154,6 +154,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
names.push(proxy.name);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn(
|
||||
"[ProxiesEditorViewer] parseUri failed for line:",
|
||||
uri,
|
||||
err?.message || err,
|
||||
);
|
||||
// 不阻塞主流程
|
||||
}
|
||||
}
|
||||
@@ -212,6 +217,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("[ProxiesEditorViewer] yaml.dump failed:", e);
|
||||
// 防止异常导致UI卡死
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
LinearProgress,
|
||||
alpha,
|
||||
styled,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
@@ -61,7 +60,6 @@ const parseExpire = (expire?: number) => {
|
||||
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData();
|
||||
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||
@@ -312,7 +310,7 @@ export const ProviderButton = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={(e) => {
|
||||
onClick={() => {
|
||||
updateProvider(key);
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
|
||||
@@ -594,15 +594,3 @@ function throttle<T extends (...args: any[]) => any>(
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 保留防抖函数以兼容其他地方可能的使用
|
||||
function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ const Widget = styled(Box)(({ theme: { typography } }) => ({
|
||||
|
||||
const TypeBox = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== "component",
|
||||
})<{ component?: React.ElementType }>(({ theme: { palette, typography } }) => ({
|
||||
})<{ component?: React.ElementType }>(({ theme: { typography } }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: "text.secondary",
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Divider,
|
||||
alpha,
|
||||
styled,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
@@ -47,7 +46,6 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||
|
||||
export const ProviderButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData();
|
||||
const [updating, setUpdating] = useState<Record<string, boolean>>({});
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { BaseLoadingOverlay } from "@/components/base";
|
||||
import dayjs from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
@@ -33,8 +32,6 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setOpen(true);
|
||||
|
||||
@@ -150,22 +150,6 @@ export const ClashPortViewer = forwardRef<
|
||||
await saveSettings({ clashConfig, vergeConfig });
|
||||
});
|
||||
|
||||
// 优化的数字输入处理
|
||||
const handleNumericChange =
|
||||
(setter: (value: number) => void) =>
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/\D+/, "");
|
||||
if (value === "") {
|
||||
setter(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const num = parseInt(value, 10);
|
||||
if (!isNaN(num) && num >= 0 && num <= 65535) {
|
||||
setter(num);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
|
||||
@@ -91,6 +91,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
setCopySuccess(type);
|
||||
setTimeout(() => setCopySuccess(null));
|
||||
} catch (err) {
|
||||
console.warn("[ControllerViewer] copy to clipboard failed:", err);
|
||||
showNotice("error", t("Failed to copy"));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ import getSystem from "@/utils/get-system";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
const Item = styled(ListItem)(({ theme }) => ({
|
||||
const Item = styled(ListItem)(() => ({
|
||||
padding: "5px 2px",
|
||||
"& textarea": {
|
||||
lineHeight: 1.5,
|
||||
@@ -88,7 +88,7 @@ const DEFAULT_DNS_CONFIG = {
|
||||
|
||||
export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { clash, mutateClash, patchClash } = useClash();
|
||||
const { clash, mutateClash } = useClash();
|
||||
const themeMode = useThemeMode();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -325,7 +325,7 @@ export const DnsViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
if (!parsedYaml) return;
|
||||
|
||||
updateValuesFromConfig(parsedYaml);
|
||||
} catch (err: any) {
|
||||
} catch {
|
||||
showNotice("error", t("Invalid YAML format"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,15 +2,7 @@ import { BaseDialog, Switch } from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { Delete as DeleteIcon } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
styled,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { Box, Button, Divider, List, ListItem, TextField } from "@mui/material";
|
||||
import { useLockFn, useRequest } from "ahooks";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
import { getNetworkInterfacesInfo } from "@/services/cmds";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user