Compare commits

..

2 Commits

360 changed files with 12623 additions and 28033 deletions

View File

@@ -90,7 +90,7 @@ jobs:
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
@@ -103,7 +103,7 @@ jobs:
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -169,8 +169,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
shared-key: autobuild-shared
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -198,14 +197,6 @@ jobs:
node-version: "22"
cache: "pnpm"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Pnpm install and check
run: |
pnpm i
@@ -268,8 +259,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
shared-key: autobuild-shared
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -282,14 +272,6 @@ jobs:
node-version: "22"
cache: "pnpm"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Pnpm install and check
run: |
pnpm i
@@ -409,8 +391,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
shared-key: autobuild-shared
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -423,14 +404,6 @@ jobs:
node-version: "22"
cache: "pnpm"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Pnpm install and check
run: |
pnpm i
@@ -565,7 +538,7 @@ jobs:
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
@@ -578,7 +551,7 @@ jobs:
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)

View File

@@ -59,10 +59,9 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
save-if: false
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
cache-all-crates: false
shared-key: autobuild-shared
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -73,10 +72,3 @@ jobs:
- name: Run Clippy
working-directory: ./src-tauri
run: cargo clippy-all
- name: Run Logging Check
working-directory: ./src-tauri
shell: bash
run: |
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
clash-verge-logging-check

View File

@@ -9,19 +9,6 @@ if ! command -v pnpm >/dev/null 2>&1; then
exit 1
fi
LOCALE_DIFF="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src/locales/' || true)"
if [ -n "$LOCALE_DIFF" ]; then
echo "[pre-commit] Locale changes detected. Regenerating i18n types..."
pnpm i18n:types
if [ -d src/types/generated ]; then
echo "[pre-commit] Staging regenerated i18n type artifacts..."
git add src/types/generated
fi
fi
echo "[pre-commit] Running pnpm format before lint..."
pnpm format
echo "[pre-commit] Running lint-staged for JS/TS files..."
pnpm exec lint-staged
@@ -40,11 +27,6 @@ if [ -n "$RUST_FILES" ]; then
(
cd src-tauri
cargo clippy-all
if ! command -v clash-verge-logging-check >/dev/null 2>&1; then
echo "[pre-commit] Installing clash-verge-logging-check..."
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
fi
clash-verge-logging-check
)
fi

View File

@@ -2,10 +2,6 @@
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
## Internationalization (i18n)
We welcome translations and improvements to existing locales. Please follow the detailed guidelines in [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md) for instructions on extracting strings, file naming conventions, testing translations, and submitting translation PRs.
## Development Setup
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:

View File

@@ -1,45 +1,6 @@
## v2.4.3
感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复无法导入订阅
- 修复实际导入成功但显示导入失败的问题
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
- 修复首页自定义卡片在切换轻量模式时失效
- 修复悬浮跳转导航失效
- 修复小键盘热键映射错误
- 修复前端无法及时刷新操作状态
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
- 修复 Linux 系统主题切换不生效
- 修复 `允许自动更新` 字段使手动订阅刷新失效
- 修复轻量模式托盘状态不同步
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.15**
- 支持前端修改日志(最大文件大小、最大保留数量)
@@ -54,11 +15,8 @@
- 允许独立控制订阅自动更新
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
- 托盘 `打开目录` 中新增 `应用日志``内核日志`
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
### 🚀 优化改进
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
@@ -85,11 +43,36 @@
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
- 添加热键绑定错误的提示信息
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122以解决 Intel 架构 Mac 无法运行内核的问题
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
</details>
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复无法导入订阅
- 修复实际导入成功但显示导入失败的问题
- 修复删除订阅时未能实际删除相关文件
- 修复 macOS 连接界面显示异常
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
- 修复自动更新使版本回退的问题
- 修复首页自定义卡片在切换轻量模式时失效
- 修复悬浮跳转导航失效
- 修复小键盘热键映射错误
## v2.4.2

4
crowdin.yml Normal file
View File

@@ -0,0 +1,4 @@
files:
- source: /src/locales/en.json
translation: /src/locales
multilingual: 1

View File

@@ -1,79 +0,0 @@
# CONTRIBUTING — i18n
Thanks for helping localize Clash Verge Rev. This guide reflects the current architecture, where the React frontend and the Tauri backend keep their translation bundles separate. Follow the steps below to keep both sides in sync without stepping on each other.
## Quick workflow
- Update the language folder under `src/locales/<lang>/`; use `src/locales/en/` as the canonical reference for keys and intent.
- Run `pnpm format:i18n` to align structure and `pnpm i18n:types` to refresh generated typings.
- If you touch backend copy, edit the matching YAML file in `src-tauri/locales/<lang>.yml`.
- Preview UI changes with `pnpm dev` (desktop shell) or `pnpm web:dev` (web only).
- Keep PRs focused and add screenshots whenever layout could be affected by text length.
## Frontend locale structure
Each locale folder mirrors the namespaces under `src/locales/en/`:
```
src/locales/
en/
connections.json
home.json
shared.json
...
index.ts
zh/
...
```
- JSON files map to namespaces (for example `home.json``home.*`). Keep keys scoped to the file they belong to.
- `shared.json` stores reusable vocabulary (buttons, validations, etc.); feature-specific wording should live in the relevant namespace.
- `index.ts` re-exports a `resources` object that aggregates the namespace JSON files. When adding or removing namespaces, mirror the pattern from `src/locales/en/index.ts`.
- Frontend bundles are lazy-loaded by `src/services/i18n.ts`. Only languages listed in `supportedLanguages` are fetched at runtime, so append new codes there when you add a locale.
Because backend translations now live in their own directory, you no longer need to run `pnpm prebuild` just to sync locales—the frontend folder is the sole source of truth for web bundles.
## Tooling for frontend contributors
- `pnpm format:i18n``node scripts/cleanup-unused-i18n.mjs --align --apply`. It aligns key ordering, removes unused entries, and keeps all locales in lock-step with English.
- `pnpm node scripts/cleanup-unused-i18n.mjs` (without flags) performs a dry-run audit. Use it to inspect missing or extra keys before committing.
- `pnpm i18n:types` regenerates `src/types/generated/i18n-keys.ts` and `src/types/generated/i18n-resources.ts`, ensuring TypeScript catches invalid key usage.
- For dynamic keys that the analyzer cannot statically detect, add explicit references in code or update the script whitelist to avoid false positives.
## Backend (Tauri) locale bundles
Native UI strings (tray menu, notifications, dialogs) use `rust-i18n` with YAML bundles stored in `src-tauri/locales/<lang>.yml`. These files are completely independent from the frontend JSON modules.
- Keep `en.yml` semantically aligned with the Simplified Chinese baseline (`zh.yml`). Other locales may temporarily copy English if no translation is available yet.
- When a backend feature introduces new strings, update every YAML file to keep the key set consistent. Missing keys fall back to the default language (`zh`), so catching gaps early avoids mixed-language output.
- Rust code resolves the active language through `src-tauri/src/utils/i18n.rs`. No additional build step is required after editing YAML files; `tauri dev` and `tauri build` pick them up automatically.
## Adding a new language
1. Duplicate `src/locales/en/` into `src/locales/<new-lang>/` and translate the JSON files while preserving key structure.
2. Update the locales `index.ts` to import every namespace. Matching the English file is the easiest way to avoid missing exports.
3. Append the language code to `supportedLanguages` in `src/services/i18n.ts`.
4. If the backend should expose the language, create `src-tauri/locales/<new-lang>.yml` and translate the keys used in existing YAML files.
5. Adjust `crowdin.yml` if the locale requires a special mapping for Crowdin.
6. Run `pnpm format:i18n`, `pnpm i18n:types`, and (optionally) `pnpm node scripts/cleanup-unused-i18n.mjs` in dry-run mode to confirm structure.
## Authoring guidelines
- **Reuse shared vocabulary** before introducing new phrases—check `shared.json` for common actions, statuses, and labels.
- **Prefer semantic keys** (`systemProxy`, `updateInterval`, `autoRefresh`) over positional ones (`item1`, `dialogTitle2`).
- **Document placeholders** using `{{placeholder}}` and ensure components supply the required values.
- **Group keys by UI responsibility** inside each namespace (`page`, `sections`, `forms`, `actions`, `tooltips`, `notifications`, `errors`, `tables`, `statuses`, etc.).
- **Keep strings concise** to avoid layout issues. If a translation needs more context, leave a PR note so reviewers can verify the UI.
## Testing & QA
- Launch the desktop shell with `pnpm dev` (or `pnpm web:dev`) and navigate through the affected views to confirm translations load and layouts behave.
- Run `pnpm test` if you touched code that consumes translations or adjusts formatting logic.
- For backend changes, trigger the relevant tray actions or notifications to verify the updated copy.
- Note any remaining untranslated sections or layout concerns in your PR description so maintainers can follow up.
## Feedback & support
- File an issue for missing context, tooling bugs, or localization gaps so we can track them.
- PRs that touch UI should include screenshots or GIFs whenever text length may affect layout.
- Mention the commands you ran (formatting, type generation, tests) in the PR checklist. If you need extra context or review help, request it via a PR comment.

View File

@@ -17,7 +17,6 @@ export default defineConfig([
plugins: {
js: eslintJS,
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
"react-hooks": pluginReactHooks,
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
"import-x": pluginImportX,
@@ -133,14 +132,4 @@ export default defineConfig([
"prettier/prettier": "warn",
},
},
{
files: ["scripts/**/*.{js,mjs,cjs}", "scripts-workflow/**/*.{js,mjs,cjs}"],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
]);

View File

@@ -26,12 +26,10 @@
"publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src",
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
"lint": "eslint -c eslint.config.ts --cache --cache-location .eslintcache src",
"lint:fix": "eslint -c eslint.config.ts --cache --cache-location .eslintcache --fix src",
"format": "prettier --write .",
"format:check": "prettier --check .",
"format:i18n": "node scripts/cleanup-unused-i18n.mjs --align --apply",
"i18n:types": "node scripts/generate-i18n-keys.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
@@ -42,10 +40,10 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.3.5",
"@mui/icons-material": "^7.3.4",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.5",
"@mui/x-data-grid": "^8.17.0",
"@mui/material": "^7.3.4",
"@mui/x-data-grid": "^8.15.0",
"@tauri-apps/api": "2.9.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2",
@@ -56,8 +54,8 @@
"@tauri-apps/plugin-updater": "2.9.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.9.6",
"axios": "^1.13.2",
"dayjs": "1.11.19",
"axios": "^1.13.1",
"dayjs": "1.11.18",
"foxact": "^0.2.49",
"i18next": "^25.6.0",
"js-yaml": "^4.1.0",
@@ -69,43 +67,43 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.66.0",
"react-i18next": "16.2.4",
"react-hook-form": "^7.65.0",
"react-i18next": "16.2.1",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router": "^7.9.5",
"react-router": "^7.9.4",
"react-virtuoso": "^4.14.1",
"swr": "^2.3.6",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo",
"types-pac": "^1.0.3"
"types-pac": "^1.0.3",
"zustand": "^5.0.8"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@eslint-react/eslint-plugin": "^2.3.1",
"@eslint/js": "^9.39.1",
"@tauri-apps/cli": "2.9.3",
"@eslint-react/eslint-plugin": "^2.2.4",
"@eslint/js": "^9.38.0",
"@tauri-apps/cli": "2.9.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.0",
"@types/node": "^24.9.2",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react-swc": "^4.2.1",
"@vitejs/plugin-react": "5.1.0",
"adm-zip": "^0.5.16",
"cli-color": "^2.0.4",
"commander": "^14.0.2",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-i18next": "^6.1.3",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-unused-imports": "^4.3.0",
"glob": "^11.0.3",
"globals": "^16.5.0",
"globals": "^16.4.0",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"jiti": "^2.6.1",
@@ -113,19 +111,19 @@
"meta-json-schema": "^1.19.14",
"node-fetch": "^3.3.2",
"prettier": "^3.6.2",
"sass": "^1.93.3",
"tar": "^7.5.2",
"terser": "^5.44.1",
"sass": "^1.93.2",
"tar": "^7.5.1",
"terser": "^5.44.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "^7.2.1",
"vite-plugin-monaco-editor-esm": "^2.0.2",
"typescript-eslint": "^8.46.2",
"vite": "^7.1.12",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.7"
"vitest": "^4.0.4"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix --max-warnings=0",
"eslint --fix",
"prettier --write",
"git add"
],

1241
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,6 +42,6 @@
"groupName": "github actions"
}
],
"postUpdateOptions": ["pnpmDedupe", "updateCargoLock"],
"postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["criterion"]
}

View File

@@ -0,0 +1,102 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const SRC_DIRS = [
path.resolve(__dirname, "../src"),
path.resolve(__dirname, "../src-tauri"),
];
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
// 递归获取所有文件
function getAllFiles(dir, exts) {
let files = [];
fs.readdirSync(dir).forEach((file) => {
const full = path.join(dir, file);
if (fs.statSync(full).isDirectory()) {
files = files.concat(getAllFiles(full, exts));
} else if (exts.includes(path.extname(full))) {
files.push(full);
}
});
return files;
}
// 读取所有源码内容为一个大字符串
function getAllSourceContent() {
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
}
// 白名单 key不检查这些 key 是否被使用
const WHITELIST_KEYS = [
"theme.light",
"theme.dark",
"theme.system",
"Already Using Latest Core Version",
];
// 主流程
function processI18nFile(i18nPath, lang, allSource) {
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
const keys = Object.keys(i18n);
const used = {};
const unused = [];
let checked = 0;
const total = keys.length;
keys.forEach((key) => {
if (WHITELIST_KEYS.includes(key)) {
used[key] = i18n[key];
} else {
// 只查找一次
const regex = new RegExp(`["'\`]${key}["'\`]`);
if (regex.test(allSource)) {
used[key] = i18n[key];
} else {
unused.push(key);
}
}
checked++;
if (checked % 20 === 0 || checked === total) {
const percent = ((checked / total) * 100).toFixed(1);
process.stdout.write(
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
);
if (checked === total) process.stdout.write("\n");
}
});
// 输出未使用的 key
console.log(`\n[${lang}] Unused keys:`, unused);
// 备份原文件
const oldPath = i18nPath + ".old";
fs.renameSync(i18nPath, oldPath);
// 写入精简后的 i18n 文件(保留原文件名)
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
console.log(
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
);
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
}
function main() {
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
const files = fs
.readdirSync(LOCALES_DIR)
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
const allSource = getAllSourceContent();
files.forEach((file) => {
const lang = path.basename(file, ".json");
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
});
}
main();

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { exec } from "child_process";
import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import { promisify } from "util";
/**
* 为Alpha版本重命名版本号

View File

@@ -1,98 +0,0 @@
#!/usr/bin/env node
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, "..");
const LOCALE_DIR = path.resolve(ROOT_DIR, "src/locales/en");
const KEY_OUTPUT = path.resolve(ROOT_DIR, "src/types/generated/i18n-keys.ts");
const RESOURCE_OUTPUT = path.resolve(
ROOT_DIR,
"src/types/generated/i18n-resources.ts",
);
const isPlainObject = (value) =>
typeof value === "object" && value !== null && !Array.isArray(value);
const flattenKeys = (data, prefix = "") => {
const keys = [];
for (const [key, value] of Object.entries(data)) {
const nextPrefix = prefix ? `${prefix}.${key}` : key;
if (isPlainObject(value)) {
keys.push(...flattenKeys(value, nextPrefix));
} else {
keys.push(nextPrefix);
}
}
return keys;
};
const buildType = (data, indent = 0) => {
if (!isPlainObject(data)) {
return "string";
}
const entries = Object.entries(data).sort(([a], [b]) => a.localeCompare(b));
const pad = " ".repeat(indent);
const inner = entries
.map(([key, value]) => {
const typeStr = buildType(value, indent + 2);
return `${" ".repeat(indent + 2)}${JSON.stringify(key)}: ${typeStr};`;
})
.join("\n");
return entries.length
? `{
${inner}
${pad}}`
: "{}";
};
const loadNamespaceJson = async () => {
const dirents = await fs.readdir(LOCALE_DIR, { withFileTypes: true });
const namespaces = [];
for (const dirent of dirents) {
if (!dirent.isFile() || !dirent.name.endsWith(".json")) continue;
const name = dirent.name.replace(/\.json$/, "");
const filePath = path.join(LOCALE_DIR, dirent.name);
const raw = await fs.readFile(filePath, "utf8");
const json = JSON.parse(raw);
namespaces.push({ name, json });
}
namespaces.sort((a, b) => a.name.localeCompare(b.name));
return namespaces;
};
const buildKeysFile = (keys) => {
const arrayLiteral = keys.map((key) => ` "${key}"`).join(",\n");
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport const translationKeys = [\n${arrayLiteral}\n] as const;\n\nexport type TranslationKey = typeof translationKeys[number];\n`;
};
const buildResourcesFile = (namespaces) => {
const namespaceEntries = namespaces
.map(({ name, json }) => {
const typeStr = buildType(json, 4);
return ` ${JSON.stringify(name)}: ${typeStr};`;
})
.join("\n");
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport interface TranslationResources {\n translation: {\n${namespaceEntries}\n };\n}\n`;
};
const main = async () => {
const namespaces = await loadNamespaceJson();
const keys = namespaces.flatMap(({ name, json }) => flattenKeys(json, name));
const keysContent = buildKeysFile(keys);
const resourcesContent = buildResourcesFile(namespaces);
await fs.mkdir(path.dirname(KEY_OUTPUT), { recursive: true });
await fs.writeFile(KEY_OUTPUT, keysContent, "utf8");
await fs.writeFile(RESOURCE_OUTPUT, resourcesContent, "utf8");
console.log(`Generated ${keys.length} translation keys.`);
};
main().catch((error) => {
console.error("Failed to generate i18n metadata:", error);
process.exitCode = 1;
});

View File

@@ -1,10 +1,9 @@
import fs from "fs";
import fsp from "fs/promises";
import { createRequire } from "module";
import path from "path";
import { getOctokit, context } from "@actions/github";
import AdmZip from "adm-zip";
import { createRequire } from "module";
import { getOctokit, context } from "@actions/github";
const target = process.argv.slice(2)[0];
const alpha = process.argv.slice(2)[1];
@@ -80,11 +79,11 @@ async function resolvePortable() {
tag,
});
const assets = release.assets.filter((x) => {
let assets = release.assets.filter((x) => {
return x.name === zipFile;
});
if (assets.length > 0) {
const id = assets[0].id;
let id = assets[0].id;
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: id,

View File

@@ -1,9 +1,8 @@
import fs from "fs";
import fsp from "fs/promises";
import { createRequire } from "module";
import path from "path";
import AdmZip from "adm-zip";
import { createRequire } from "module";
import fsp from "fs/promises";
const target = process.argv.slice(2)[0];
const ARCH_MAP = {

View File

@@ -1,16 +1,14 @@
import AdmZip from "adm-zip";
import { execSync } from "child_process";
import { createHash } from "crypto";
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
import zlib from "zlib";
import AdmZip from "adm-zip";
import { glob } from "glob";
import { HttpsProxyAgent } from "https-proxy-agent";
import fetch from "node-fetch";
import path from "path";
import { extract } from "tar";
import zlib from "zlib";
import { log_debug, log_error, log_info, log_success } from "./utils.mjs";
/**
@@ -57,7 +55,7 @@ const ARCH_MAP = {
const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1];
const target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
let target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process;
@@ -115,7 +113,7 @@ async function calculateFileHash(filePath) {
const hashSum = createHash("sha256");
hashSum.update(fileBuffer);
return hashSum.digest("hex");
} catch (ignoreErr) {
} catch (err) {
return null;
}
}
@@ -549,9 +547,9 @@ const resolveServicePermission = async () => {
const hashCache = await loadHashCache();
let hasChanges = false;
for (const f of serviceExecutables) {
for (let f of serviceExecutables) {
const files = glob.sync(path.join(resDir, f));
for (const filePath of files) {
for (let filePath of files) {
if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath);
const cacheKey = `${filePath}_chmod`;
@@ -575,29 +573,52 @@ const resolveServicePermission = async () => {
}
};
// resolve locales (从 src/locales 复制到 resources/locales并使用 hash 检查)
async function resolveLocales() {
const srcLocalesDir = path.join(cwd, "src/locales");
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
try {
await fsp.mkdir(targetLocalesDir, { recursive: true });
const files = await fsp.readdir(srcLocalesDir);
for (const file of files) {
const srcPath = path.join(srcLocalesDir, file);
const targetPath = path.join(targetLocalesDir, file);
if (!(await hasFileChanged(srcPath, targetPath))) continue;
await fsp.copyFile(srcPath, targetPath);
await updateHashCache(targetPath);
log_success(`Copied locale file: ${file}`);
}
log_success("All locale files processed successfully");
} catch (err) {
log_error("Error copying locale files:", err.message);
throw err;
}
}
// =======================
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
// =======================
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
const ext = platform === "win32" ? ".exe" : "";
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
};
const resolveInstall = () => {
const ext = platform === "win32" ? ".exe" : "";
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service-install" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
});
};
const resolveUninstall = () => {
const ext = platform === "win32" ? ".exe" : "";
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service-uninstall" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
@@ -694,6 +715,7 @@ const tasks = [
retry: 5,
macosOnly: true,
},
{ name: "locales", func: resolveLocales, retry: 2 },
];
async function runTask() {

View File

@@ -30,11 +30,10 @@
*/
import { execSync } from "child_process";
import { program } from "commander";
import fs from "fs/promises";
import path from "path";
import { program } from "commander";
/**
* 获取当前 git 短 commit hash
* @returns {string}

View File

@@ -1,8 +1,6 @@
import { readFileSync } from "fs";
import axios from "axios";
import { log_error, log_info, log_success } from "./utils.mjs";
import { readFileSync } from "fs";
import { log_success, log_error, log_info } from "./utils.mjs";
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
const CHAT_ID_TEST = "@vergetest"; // 测试频道
@@ -73,19 +71,6 @@ async function sendTelegramNotification() {
.join("\n");
}
function normalizeDetailsTags(content) {
return content
.replace(
/<summary>\s*<strong>\s*(.*?)\s*<\/strong>\s*<\/summary>/g,
"\n<b>$1</b>\n",
)
.replace(/<summary>\s*(.*?)\s*<\/summary>/g, "\n<b>$1</b>\n")
.replace(/<\/?details>/g, "")
.replace(/<\/?strong>/g, (m) => (m === "</strong>" ? "</b>" : "<b>"))
.replace(/<br\s*\/?>/g, "\n");
}
releaseContent = normalizeDetailsTags(releaseContent);
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";

View File

@@ -58,7 +58,7 @@ export async function resolveUpdateLogDefault() {
const reEnd = /^---/;
let isCapturing = false;
const content = [];
let content = [];
let firstTag = "";
for (const line of data.split("\n")) {

View File

@@ -1,6 +1,5 @@
import { getOctokit, context } from "@actions/github";
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import { resolveUpdateLog } from "./updatelog.mjs";
const UPDATE_TAG_NAME = "updater";
@@ -114,7 +113,7 @@ async function resolveUpdater() {
});
// delete the old assets
for (const asset of updateRelease.assets) {
for (let asset of updateRelease.assets) {
if (asset.name === UPDATE_JSON_FILE) {
await github.rest.repos.deleteReleaseAsset({
...options,

View File

@@ -1,6 +1,5 @@
import { getOctokit, context } from "@actions/github";
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
// Add stable update JSON filenames
@@ -260,7 +259,7 @@ async function processRelease(github, options, tag, isAlpha) {
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
// Delete existing assets with these names
for (const asset of updateRelease.assets) {
for (let asset of updateRelease.assets) {
if (asset.name === jsonFile) {
await github.rest.repos.deleteReleaseAsset({
...options,

607
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ tauri-build = { version = "2.5.1", features = [] }
[dependencies]
warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.100"
dirs = "6.0"
open = "5.3.2"
log = "0.4.28"
dunce = "1.0.5"
@@ -66,7 +67,11 @@ futures = "0.3.31"
sys-locale = "0.3.2"
libc = "0.2.177"
gethostname = "1.1.0"
hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.3"
tokio-stream = "0.1.17"
isahc = { version = "1.7.2", default-features = false, features = [
@@ -77,17 +82,15 @@ backoff = { version = "0.4.0", features = ["tokio"] }
compact_str = { version = "0.9.0", features = ["serde"] }
tauri-plugin-http = "2.5.4"
flexi_logger = "0.31.7"
console-subscriber = { version = "0.5.0", optional = true }
console-subscriber = { version = "0.4.1", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
async-trait = "0.1.89"
smartstring = { version = "1.0.1", features = ["serde"] }
clash_verge_service_ipc = { version = "2.0.21", features = [
clash_verge_service_ipc = { version = "2.0.20", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.7.1"
rust-i18n = "3.1.5"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
@@ -227,23 +230,3 @@ needless_raw_string_hashes = "deny" # Too many in existing code
or_fun_call = "deny"
cognitive_complexity = "deny"
useless_let_if_seq = "deny"
use_self = "deny"
tuple_array_conversions = "deny"
trait_duplication_in_bounds = "deny"
suspicious_operation_groupings = "deny"
string_lit_as_bytes = "deny"
significant_drop_tightening = "deny"
significant_drop_in_scrutinee = "deny"
redundant_clone = "deny"
# option_if_let_else = "deny" // 过于激进,暂时不开启
needless_pass_by_ref_mut = "deny"
needless_collect = "deny"
missing_const_for_fn = "deny"
iter_with_drain = "deny"
iter_on_single_items = "deny"
iter_on_empty_collections = "deny"
# fallible_impl_from = "deny" // 过于激进,暂时不开启
equatable_if_let = "deny"
collection_is_never_read = "deny"
branches_sharing_code = "deny"

View File

@@ -3,104 +3,122 @@ use std::hint::black_box;
use std::process;
use tokio::runtime::Runtime;
// 引入业务模型 & Draft 实现
use app_lib::config::IVerge;
use app_lib::utils::Draft as DraftNew;
/// 创建测试数据
fn make_draft() -> DraftNew<IVerge> {
let verge = IVerge {
fn make_draft() -> DraftNew<Box<IVerge>> {
let verge = Box::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..Default::default()
};
DraftNew::new(verge)
});
DraftNew::from(verge)
}
pub fn bench_draft(c: &mut Criterion) {
let rt = Runtime::new().unwrap_or_else(|e| {
eprintln!("Tokio runtime init failed: {e}");
/// 基准:只读 data_ref正式数据
fn bench_data_ref(c: &mut Criterion) {
c.bench_function("draft_data_ref", |b| {
b.iter(|| {
let draft = make_draft();
let data = draft.data_ref();
black_box(data.enable_auto_launch);
});
});
}
/// 基准:可写 data_mut正式数据
fn bench_data_mut(c: &mut Criterion) {
c.bench_function("draft_data_mut", |b| {
b.iter(|| {
let draft = make_draft();
let mut data = draft.data_mut();
data.enable_tun_mode = Some(true);
black_box(data.enable_tun_mode);
});
});
}
/// 基准:首次创建草稿(会触发 clone
fn bench_draft_mut_first(c: &mut Criterion) {
c.bench_function("draft_draft_mut_first", |b| {
b.iter(|| {
let draft = make_draft();
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
black_box(d.enable_auto_launch);
});
});
}
/// 基准:重复 draft_mut已存在草稿不再 clone
fn bench_draft_mut_existing(c: &mut Criterion) {
c.bench_function("draft_draft_mut_existing", |b| {
b.iter(|| {
let draft = make_draft();
{
let mut first = draft.draft_mut();
first.enable_tun_mode = Some(true);
}
let mut second = draft.draft_mut();
second.enable_tun_mode = Some(false);
black_box(second.enable_tun_mode);
});
});
}
/// 基准零拷贝读取最新视图latest_ref
fn bench_latest_ref(c: &mut Criterion) {
c.bench_function("draft_latest_ref", |b| {
b.iter(|| {
let draft = make_draft();
let latest = draft.latest_ref();
black_box(latest.enable_auto_launch);
});
});
}
/// 基准apply提交草稿
fn bench_apply(c: &mut Criterion) {
c.bench_function("draft_apply", |b| {
b.iter(|| {
let draft = make_draft();
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.apply();
});
});
}
/// 基准discard丢弃草稿
fn bench_discard(c: &mut Criterion) {
c.bench_function("draft_discard", |b| {
b.iter(|| {
let draft = make_draft();
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.discard();
});
});
}
/// 基准:异步 with_data_modify
fn bench_with_data_modify(c: &mut Criterion) {
let rt = Runtime::new().unwrap_or_else(|error| {
eprintln!("draft benchmarks require a Tokio runtime: {error}");
process::exit(1);
});
let mut group = c.benchmark_group("draft");
group.sample_size(100);
group.warm_up_time(std::time::Duration::from_millis(300));
group.measurement_time(std::time::Duration::from_secs(1));
group.bench_function("data_mut", |b| {
b.iter(|| {
let draft = black_box(make_draft());
draft.edit_draft(|d| d.enable_tun_mode = Some(true));
black_box(&draft.latest_arc().enable_tun_mode);
});
});
group.bench_function("draft_mut_first", |b| {
b.iter(|| {
let draft = black_box(make_draft());
draft.edit_draft(|d| d.enable_auto_launch = Some(false));
let latest = draft.latest_arc();
black_box(&latest.enable_auto_launch);
});
});
group.bench_function("draft_mut_existing", |b| {
b.iter(|| {
let draft = black_box(make_draft());
{
draft.edit_draft(|d| {
d.enable_tun_mode = Some(true);
});
let latest1 = draft.latest_arc();
black_box(&latest1.enable_tun_mode);
}
draft.edit_draft(|d| {
d.enable_tun_mode = Some(false);
});
let latest2 = draft.latest_arc();
black_box(&latest2.enable_tun_mode);
});
});
group.bench_function("latest_arc", |b| {
b.iter(|| {
let draft = black_box(make_draft());
let latest = draft.latest_arc();
black_box(&latest.enable_auto_launch);
});
});
group.bench_function("apply", |b| {
b.iter(|| {
let draft = black_box(make_draft());
{
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
});
}
draft.apply();
black_box(&draft);
});
});
group.bench_function("discard", |b| {
b.iter(|| {
let draft = black_box(make_draft());
{
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
});
}
draft.discard();
black_box(&draft);
});
});
group.bench_function("with_data_modify_async", |b| {
c.bench_function("draft_with_data_modify", |b| {
b.to_async(&rt).iter(|| async {
let draft = black_box(make_draft());
let _: Result<(), anyhow::Error> = draft
.with_data_modify::<_, _, _>(|mut box_data| async move {
let draft = make_draft();
let _res: Result<(), anyhow::Error> = draft
.with_data_modify(|mut box_data| async move {
box_data.enable_auto_launch =
Some(!box_data.enable_auto_launch.unwrap_or(false));
Ok((box_data, ()))
@@ -108,9 +126,17 @@ pub fn bench_draft(c: &mut Criterion) {
.await;
});
});
group.finish();
}
criterion_group!(benches, bench_draft);
criterion_group!(
benches,
bench_data_ref,
bench_data_mut,
bench_draft_mut_first,
bench_draft_mut_existing,
bench_latest_ref,
bench_apply,
bench_discard,
bench_with_data_modify
);
criterion_main!(benches);

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: 仪表板
body: 仪表板显示状态已更新。
clashModeChanged:
title: 模式切换
body: 已切换至 {mode}。
systemProxyToggled:
title: 系统代理
body: 系统代理状态已更新。
tunModeToggled:
title: TUN 模式
body: TUN 模式状态已更新。
lightweightModeEntered:
title: 轻量模式
body: 已进入轻量模式。
appQuit:
title: 即将退出
body: Clash Verge 即将退出。
appHidden:
title: 应用已隐藏
body: Clash Verge 正在后台运行。
service:
adminPrompt: 安装服务需要管理员权限
tray:
dashboard: 仪表板
ruleMode: 规则模式
globalMode: 全局模式
directMode: 直连模式
profiles: 订阅
proxies: 代理
systemProxy: 系统代理
tunMode: TUN 模式
closeAllConnections: 关闭所有连接
lightweightMode: 轻量模式
copyEnv: 复制环境变量
confDir: 配置目录
coreDir: 内核目录
logsDir: 日志目录
openDir: 打开目录
appLog: 应用日志
coreLog: 内核日志
restartClash: 重启 Clash 内核
restartApp: 重启应用
vergeVersion: Verge 版本
more: 更多
exit: 退出
tooltip:
systemProxy: 系统代理
tun: TUN
profile: 订阅

View File

@@ -1,52 +0,0 @@
_version: 1
notifications:
dashboardToggled:
title: 儀表板
body: 儀表板顯示狀態已更新。
clashModeChanged:
title: 模式切換
body: 已切換至 {mode}。
systemProxyToggled:
title: 系統代理
body: 系統代理狀態已更新。
tunModeToggled:
title: TUN 模式
body: TUN 模式狀態已更新。
lightweightModeEntered:
title: 輕量模式
body: 已進入輕量模式。
appQuit:
title: 即將退出
body: Clash Verge 即將退出。
appHidden:
title: 應用已隱藏
body: Clash Verge 正在背景執行。
service:
adminPrompt: 安裝服務需要管理員權限
tray:
dashboard: 儀表板
ruleMode: 規則模式
globalMode: 全域模式
directMode: 直連模式
profiles: 訂閱
proxies: 代理
systemProxy: 系統代理
tunMode: TUN 模式
closeAllConnections: 關閉所有連線
lightweightMode: 輕量模式
copyEnv: 複製環境變數
confDir: 設定目錄
coreDir: 核心目錄
logsDir: 日誌目錄
openDir: 開啟目錄
appLog: 應用程式日誌
coreLog: 核心日誌
restartClash: 重新啟動 Clash 核心
restartApp: 重新啟動應用程式
vergeVersion: Verge 版本
more: 更多
exit: 離開
tooltip:
systemProxy: 系統代理
tun: TUN
profile: 訂閱

View File

@@ -12,7 +12,6 @@ use smartstring::alias::String;
use std::path::Path;
use tauri::{AppHandle, Manager};
use tokio::fs;
use tokio::io::AsyncWriteExt;
/// 打开应用程序所在目录
#[tauri::command]
@@ -42,20 +41,6 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
open::that(url.as_str()).stringify_err()
}
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
/// 打开 Verge 最新日志
#[tauri::command]
pub async fn open_app_log() -> CmdResult<()> {
open::that(dirs::app_latest_log().stringify_err()?).stringify_err()
}
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
/// 打开 Clash 最新日志
#[tauri::command]
pub async fn open_core_log() -> CmdResult<()> {
open::that(dirs::clash_latest_log().stringify_err()?).stringify_err()
}
/// 打开/关闭开发者工具
#[tauri::command]
pub fn open_devtools(app_handle: AppHandle) {
@@ -117,7 +102,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
}
if !icon_cache_dir.exists() {
let _ = fs::create_dir_all(&icon_cache_dir).await;
let _ = std::fs::create_dir_all(&icon_cache_dir);
}
let temp_path = icon_cache_dir.join(format!("{}.downloading", name.as_str()));
@@ -141,7 +126,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
if is_image && !is_html {
{
let mut file = match fs::File::create(&temp_path).await {
let mut file = match std::fs::File::create(&temp_path) {
Ok(file) => file,
Err(_) => {
if icon_path.exists() {
@@ -150,12 +135,12 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
return Err("Failed to create temporary file".into());
}
};
file.write_all(content.as_ref()).await.stringify_err()?;
file.flush().await.stringify_err()?;
std::io::copy(&mut content.as_ref(), &mut file).stringify_err()?;
}
if !icon_path.exists() {
match fs::rename(&temp_path, &icon_path).await {
match std::fs::rename(&temp_path, &icon_path) {
Ok(_) => {}
Err(_) => {
let _ = temp_path.remove_if_exists().await;
@@ -241,7 +226,7 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<Stri
/// 通知UI已准备就绪
#[tauri::command]
pub fn notify_ui_ready() -> CmdResult<()> {
logging!(info, Type::Cmd, "前端UI已准备就绪");
log::info!(target: "app", "前端UI已准备就绪");
crate::utils::resolve::ui::mark_ui_ready();
Ok(())
}
@@ -249,7 +234,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
/// UI加载阶段
#[tauri::command]
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
logging!(info, Type::Cmd, "UI加载阶段更新: {}", stage.as_str());
log::info!(target: "app", "UI加载阶段更新: {}", stage.as_str());
use crate::utils::resolve::ui::UiReadyStage;
@@ -260,12 +245,7 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
"Ready" => UiReadyStage::Ready,
_ => {
logging!(
warn,
Type::Cmd,
"Warning: 未知的UI加载阶段: {}",
stage.as_str()
);
log::warn!(target: "app", "未知的UI加载阶段: {}", stage.as_str());
return Err(format!("未知的UI加载阶段: {}", stage.as_str()).into());
}
};

View File

@@ -11,8 +11,8 @@ pub async fn create_local_backup() -> CmdResult<()> {
/// List local backups
#[tauri::command]
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
feat::list_local_backup().await.stringify_err()
pub fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
feat::list_local_backup().stringify_err()
}
/// Delete local backup
@@ -29,8 +29,6 @@ pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
/// Export local backup to a user selected destination
#[tauri::command]
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
feat::export_local_backup(filename, destination)
.await
.stringify_err()
pub fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
feat::export_local_backup(filename, destination).stringify_err()
}

View File

@@ -1,16 +1,13 @@
use super::CmdResult;
use crate::utils::dirs;
use crate::{
cmd::StringifyErr,
config::{ClashInfo, Config},
constants,
config::Config,
core::{CoreManager, handle, validate::CoreConfigValidator},
};
use crate::{feat, logging, utils::logging::Type};
use crate::{config::*, feat, logging, utils::logging::Type};
use compact_str::CompactString;
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
/// 复制Clash环境变量
#[tauri::command]
@@ -22,7 +19,7 @@ pub async fn copy_clash_env() -> CmdResult {
/// 获取Clash信息
#[tauri::command]
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().await.latest_arc().get_client_info())
Ok(Config::clash().await.latest_ref().get_client_info())
}
/// 修改Clash配置
@@ -43,7 +40,10 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global().change_core(&clash_core).await {
match CoreManager::global()
.change_core(Some(clash_core.clone()))
.await
{
Ok(_) => {
// 切换内核后重启内核
match CoreManager::global().restart_core().await {
@@ -111,7 +111,7 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
logging!(error, Type::Cmd, "{}", e);
log::error!(target: "app", "{}", e);
10000u32
}
};
@@ -128,7 +128,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
// 获取DNS配置文件路径
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join(constants::files::DNS_CONFIG);
.join("dns_config.yaml");
// 保存DNS配置到文件
let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?;
@@ -141,20 +141,28 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
/// 应用或撤销DNS配置
#[tauri::command]
pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{
config::Config,
core::{CoreManager, handle},
utils::dirs,
};
if apply {
// 读取DNS配置文件
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join(constants::files::DNS_CONFIG);
.join("dns_config.yaml");
if !dns_path.exists() {
logging!(warn, Type::Config, "DNS config file not found");
return Err("DNS config file not found".into());
}
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
logging!(error, Type::Config, "Failed to read DNS config: {e}");
})?;
let dns_yaml = tokio::fs::read_to_string(&dns_path)
.await
.stringify_err_log(|e| {
logging!(error, Type::Config, "Failed to read DNS config: {e}");
})?;
// 解析DNS配置
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
@@ -169,9 +177,7 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
patch.insert("dns".into(), patch_config.into());
// 应用DNS配置到运行时配置
Config::runtime().await.edit_draft(|d| {
d.patch_config(patch);
});
Config::runtime().await.draft_mut().patch_config(patch);
// 重新生成配置
Config::generate().await.stringify_err_log(|err| {
@@ -189,6 +195,7 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
})?;
logging!(info, Type::Config, "DNS config successfully applied");
handle::Handle::refresh_clash();
} else {
// 当关闭DNS设置时重新生成配置不加载DNS配置文件
logging!(
@@ -211,9 +218,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
})?;
logging!(info, Type::Config, "Config regenerated successfully");
handle::Handle::refresh_clash();
}
handle::Handle::refresh_clash();
Ok(())
}
@@ -224,7 +231,7 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join(constants::files::DNS_CONFIG);
.join("dns_config.yaml");
Ok(dns_path.exists())
}
@@ -237,7 +244,7 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
let dns_path = dirs::app_home_dir()
.stringify_err()?
.join(constants::files::DNS_CONFIG);
.join("dns_config.yaml");
if !fs::try_exists(&dns_path).await.stringify_err()? {
return Err("DNS config file not found".into());
@@ -250,8 +257,10 @@ pub async fn get_dns_config_content() -> CmdResult<String> {
/// 验证DNS配置文件
#[tauri::command]
pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
use crate::utils::dirs;
let app_dir = dirs::app_home_dir().stringify_err()?;
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
let dns_path = app_dir.join("dns_config.yaml");
let dns_path_str = dns_path.to_str().unwrap_or_default();
if !dns_path.exists() {

View File

@@ -13,23 +13,19 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
Ok(response) => {
if let Ok(body) = response.text().await {
let body_lower = body.to_lowercase();
let mut status = "Failed";
let mut region = None;
if body_lower.contains("youtube premium is not available in your country") {
status = "No";
} else if body_lower.contains("ad-free") {
match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
Ok(re) => {
if let Some(caps) = re.captures(&body)
&& let Some(m) = caps.get(1)
{
let country_code = m.as_str().trim();
let emoji = country_code_to_emoji(country_code);
region = Some(format!("{emoji}{country_code}"));
status = "Yes";
}
}
return UnlockItem {
name: "Youtube Premium".to_string(),
status: "No".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
if body_lower.contains("ad-free") {
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
@@ -37,14 +33,34 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
"Failed to compile YouTube Premium regex: {}",
e
);
return UnlockItem {
name: "Youtube Premium".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
}
};
let region = re.captures(&body).and_then(|caps| {
caps.get(1).map(|m| {
let country_code = m.as_str().trim();
let emoji = country_code_to_emoji(country_code);
format!("{emoji}{country_code}")
})
});
return UnlockItem {
name: "Youtube Premium".to_string(),
status: "Yes".to_string(),
region,
check_time: Some(get_local_date_string()),
};
}
UnlockItem {
name: "Youtube Premium".to_string(),
status: status.to_string(),
region,
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
}
} else {

View File

@@ -2,14 +2,13 @@ use super::CmdResult;
use crate::cmd::StringifyErr;
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use network_interface::NetworkInterface;
use serde_yaml_ng::Mapping;
/// get the system proxy
#[tauri::command]
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
logging!(debug, Type::Network, "异步获取系统代理配置");
log::debug!(target: "app", "异步获取系统代理配置");
let current = AsyncProxyQuery::get_system_proxy().await;
@@ -21,21 +20,14 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
);
map.insert("bypass".into(), current.bypass.into());
logging!(
debug,
Type::Network,
"返回系统代理配置: enable={}, {}:{}",
current.enable,
current.host,
current.port
);
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
Ok(map)
}
/// 获取自动代理配置
#[tauri::command]
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)");
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
let proxy_manager = EventDrivenProxyManager::global();
@@ -49,13 +41,7 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
map.insert("enable".into(), current.enable.into());
map.insert("url".into(), current.url.clone().into());
logging!(
debug,
Type::Network,
"返回自动代理配置(缓存): enable={}, url={}",
current.enable,
current.url
);
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
Ok(map)
}

View File

@@ -1,6 +1,5 @@
use super::CmdResult;
use super::StringifyErr;
use crate::utils::draft::SharedBox;
use crate::{
config::{
Config, IProfiles, PrfItem, PrfOption,
@@ -16,19 +15,68 @@ use crate::{
ret_err,
utils::{dirs, help, logging::Type},
};
use scopeguard::defer;
use smartstring::alias::String;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
// 全局请求序列号跟踪,用于避免队列化执行
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
#[tauri::command]
pub async fn get_profiles() -> CmdResult<SharedBox<IProfiles>> {
logging!(debug, Type::Cmd, "获取配置文件列表");
let draft = Config::profiles().await;
let data = draft.data_arc();
Ok(data)
pub async fn get_profiles() -> CmdResult<IProfiles> {
// 策略1: 尝试快速获取latest数据
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(profiles) => {
logging!(info, Type::Cmd, "快速获取配置列表成功");
return Ok(profiles);
}
Err(_) => {
logging!(warn, Type::Cmd, "快速获取配置超时(500ms)");
}
}
// 策略2: 如果快速获取失败尝试获取data()
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(profiles) => {
logging!(info, Type::Cmd, "获取draft配置列表成功");
return Ok(profiles);
}
Err(join_err) => {
logging!(
error,
Type::Cmd,
"获取draft配置任务失败或超时: {}",
join_err
);
}
}
// 策略3: fallback尝试重新创建配置
logging!(warn, Type::Cmd, "所有获取配置策略都失败尝试fallback");
Ok(IProfiles::new().await)
}
/// 增强配置文件
@@ -37,7 +85,7 @@ pub async fn enhance_profiles() -> CmdResult {
match feat::enhance_profiles().await {
Ok(_) => {}
Err(e) => {
logging!(error, Type::Cmd, "{}", e);
log::error!(target: "app", "{}", e);
return Err(e.to_string().into());
}
}
@@ -51,7 +99,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url);
// 直接依赖 PrfItem::from_url 自身的超时/重试逻辑,不再使用 tokio::time::timeout 包裹
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
let item = match PrfItem::from_url(&url, None, None, option).await {
Ok(it) => {
logging!(info, Type::Cmd, "[导入订阅] 下载完成,开始保存配置");
it
@@ -62,7 +110,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
}
};
match profiles_append_item_safe(item).await {
match profiles_append_item_safe(item.clone()).await {
Ok(_) => match profiles_save_file_safe().await {
Ok(_) => {
logging!(info, Type::Cmd, "[导入订阅] 配置文件保存成功");
@@ -97,15 +145,13 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
/// 调整profile的顺序
#[tauri::command]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
match profiles_reorder_safe(&active_id, &over_id).await {
match profiles_reorder_safe(active_id, over_id).await {
Ok(_) => {
logging!(info, Type::Cmd, "重新排序配置文件");
Config::profiles().await.apply();
log::info!(target: "app", "重新排序配置文件");
Ok(())
}
Err(err) => {
Config::profiles().await.discard();
logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err);
log::error!(target: "app", "重新排序配置文件失败: {}", err);
Err(format!("重新排序配置文件失败: {}", err).into())
}
}
@@ -115,37 +161,29 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
/// 创建一个新的配置文件
#[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
match profiles_append_item_with_filedata_safe(&item, file_data).await {
match profiles_append_item_with_filedata_safe(item.clone(), file_data).await {
Ok(_) => {
// 发送配置变更通知
if let Some(uid) = &item.uid {
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid.clone());
}
Config::profiles().await.apply();
Ok(())
}
Err(err) => {
Config::profiles().await.discard();
match err.to_string().as_str() {
"the file already exists" => Err("the file already exists".into()),
_ => Err(format!("add profile error: {err}").into()),
}
}
Err(err) => match err.to_string().as_str() {
"the file already exists" => Err("the file already exists".into()),
_ => Err(format!("add profile error: {err}").into()),
},
}
}
/// 更新配置文件
#[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
match feat::update_profile(&index, option.as_ref(), true, true).await {
Ok(_) => {
let _: () = Config::profiles().await.apply();
Ok(())
}
match feat::update_profile(index, option, Some(true)).await {
Ok(_) => Ok(()),
Err(e) => {
Config::profiles().await.discard();
logging!(error, Type::Cmd, "{}", e);
log::error!(target: "app", "{}", e);
Err(e.to_string().into())
}
}
@@ -154,11 +192,14 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
println!("delete_profile: {}", index);
// 使用Send-safe helper函数
let should_update = profiles_delete_item_safe(&index).await.stringify_err()?;
let should_update = profiles_delete_item_safe(index.clone())
.await
.stringify_err()?;
profiles_save_file_safe().await.stringify_err()?;
if should_update {
Config::profiles().await.apply();
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
@@ -167,7 +208,7 @@ pub async fn delete_profile(index: String) -> CmdResult {
handle::Handle::notify_profile_changed(index);
}
Err(e) => {
logging!(error, Type::Cmd, "{}", e);
log::error!(target: "app", "{}", e);
return Err(e.to_string().into());
}
}
@@ -182,7 +223,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
// 获取目标配置文件路径
let config_file_result = {
let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_arc();
let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) {
Ok(item) => {
if let Some(file) = &item.file {
@@ -244,7 +285,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
);
handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
error_msg,
error_msg.clone(),
);
Err(())
}
@@ -253,7 +294,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
error_msg,
error_msg.clone(),
);
Err(())
}
@@ -262,13 +303,19 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message("config_validate::file_read_error", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
error_msg.clone(),
);
Err(())
}
Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message("config_validate::file_read_timeout", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
error_msg.clone(),
);
Err(())
}
}
@@ -278,52 +325,80 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
}
/// 执行配置更新并处理结果
async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> {
async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
let restore_profiles = IProfiles {
current: Some(prev_profile.to_owned()),
current: Some(prev_profile),
items: None,
};
Config::profiles()
.await
.edit_draft(|d| d.patch_config(&restore_profiles));
.draft_mut()
.patch_config(restore_profiles)
.stringify_err()?;
Config::profiles().await.apply();
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = profiles_save_file_safe().await {
logging!(warn, Type::Cmd, "Warning: 异步保存恢复配置文件失败: {e}");
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
}
});
logging!(info, Type::Cmd, "成功恢复到之前的配置");
Ok(())
}
async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
async fn handle_success(current_sequence: u64, current_value: Option<String>) -> CmdResult<bool> {
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
current_sequence,
latest_sequence
);
Config::profiles().await.discard();
return Ok(false);
}
logging!(
info,
Type::Cmd,
"配置更新成功,序列号: {}",
current_sequence
);
Config::profiles().await.apply();
handle::Handle::refresh_clash();
if let Err(e) = Tray::global().update_tooltip().await {
logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}");
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
}
if let Err(e) = Tray::global().update_menu().await {
logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}");
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
}
if let Err(e) = profiles_save_file_safe().await {
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
log::warn!(target: "app", "异步保存配置文件失败: {e}");
}
if let Some(current) = current_value {
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current);
handle::Handle::notify_profile_changed(current.to_owned());
if let Some(current) = &current_value {
logging!(
info,
Type::Cmd,
"向前端发送配置变更事件: {}, 序列号: {}",
current,
current_sequence
);
handle::Handle::notify_profile_changed(current.clone());
}
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(true)
}
async fn handle_validation_failure(
error_msg: String,
current_profile: Option<&String>,
current_profile: Option<String>,
) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
Config::profiles().await.discard();
@@ -331,34 +406,53 @@ async fn handle_validation_failure(
restore_previous_profile(prev_profile).await?;
}
handle::Handle::notice_message("config_validate::error", error_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,);
async fn handle_update_error<E: std::fmt::Display>(e: E, current_sequence: u64) -> CmdResult<bool> {
logging!(
warn,
Type::Cmd,
"更新过程发生错误: {}, 序列号: {}",
e,
current_sequence
);
Config::profiles().await.discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn handle_timeout(current_profile: Option<&String>) -> CmdResult<bool> {
async fn handle_timeout(current_profile: Option<String>, current_sequence: u64) -> CmdResult<bool> {
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(error, Type::Cmd, "{}", timeout_msg);
logging!(
error,
Type::Cmd,
"{}, 序列号: {}",
timeout_msg,
current_sequence
);
Config::profiles().await.discard();
if let Some(prev_profile) = current_profile {
restore_previous_profile(prev_profile).await?;
}
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn perform_config_update(
current_value: Option<&String>,
current_profile: Option<&String>,
current_sequence: u64,
current_value: Option<String>,
current_profile: Option<String>,
) -> CmdResult<bool> {
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
logging!(
info,
Type::Cmd,
"开始内核配置更新,序列号: {}",
current_sequence
);
let update_result = tokio::time::timeout(
Duration::from_secs(30),
CoreManager::global().update_config(),
@@ -366,50 +460,99 @@ async fn perform_config_update(
.await;
match update_result {
Ok(Ok((true, _))) => handle_success(current_value).await,
Ok(Ok((true, _))) => handle_success(current_sequence, current_value).await,
Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await,
Ok(Err(e)) => handle_update_error(e).await,
Err(_) => handle_timeout(current_profile).await,
Ok(Err(e)) => handle_update_error(e, current_sequence).await,
Err(_) => handle_timeout(current_profile, current_sequence).await,
}
}
/// 修改profiles的配置
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
if CURRENT_SWITCHING_PROFILE
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Ok(false);
}
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
let target_profile = profiles.current.as_ref();
// 为当前请求分配序列号
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
let target_profile = profiles.current.clone();
logging!(
info,
Type::Cmd,
"开始修改配置文件目标profile: {:?}",
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
current_sequence,
target_profile
);
// 保存当前配置,以便在验证失败时恢复
let previous_profile = Config::profiles().await.data_arc().current.clone();
logging!(info, Type::Cmd, "当前配置: {:?}", previous_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(switch_to_profile) = target_profile
&& previous_profile.as_ref() != Some(switch_to_profile)
&& validate_new_profile(switch_to_profile).await.is_err()
{
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
Config::profiles()
.await
.edit_draft(|d| d.patch_config(&profiles));
perform_config_update(target_profile, previous_profile.as_ref()).await
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().await.latest_ref().current.clone();
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref()
&& current_profile.as_ref() != Some(new_profile)
&& validate_new_profile(new_profile).await.is_err()
{
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
return Ok(false);
}
// 检查请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
// 更新profiles配置
logging!(
info,
Type::Cmd,
"正在更新配置草稿,序列号: {}",
current_sequence
);
let current_value = profiles.current.clone();
let _ = Config::profiles().await.draft_mut().patch_config(profiles);
// 在调用内核前再次验证请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
Config::profiles().await.discard();
return Ok(false);
}
perform_config_update(current_sequence, current_value, current_profile).await
}
/// 根据profile name修改profiles
@@ -429,34 +572,33 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
// 保存修改前检查是否有更新 update_interval
let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
&& let Some(new_option) = profile.option.as_ref()
{
let should_refresh_timer = 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 = new_option.update_interval;
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
let old_allow_auto_update = old_profile
.option
.as_ref()
.and_then(|o| o.allow_auto_update);
let new_allow_auto_update = new_option.allow_auto_update;
let new_allow_auto_update = profile.option.as_ref().and_then(|o| o.allow_auto_update);
(old_interval != new_interval) || (old_allow_auto_update != new_allow_auto_update)
} else {
false
};
profiles_patch_item_safe(&index, &profile)
profiles_patch_item_safe(index.clone(), profile)
.await
.stringify_err()?;
// 如果更新间隔或允许自动更新变更,异步刷新定时器
if should_refresh_timer {
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().await {
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
} else {
// 刷新成功后发送自定义事件,不触发配置重载
crate::core::handle::Handle::notify_timer_updated(index);
crate::core::handle::Handle::notify_timer_updated(index_clone);
}
});
}
@@ -468,7 +610,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
#[tauri::command]
pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let profiles_ref = profiles.latest_ref();
let file = profiles_ref
.get_item(&index)
.stringify_err()?
@@ -489,15 +631,10 @@ pub async fn view_profile(index: String) -> CmdResult {
/// 读取配置文件内容
#[tauri::command]
pub async fn read_profile_file(index: String) -> CmdResult<String> {
let item = {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
PrfItem {
file: profiles_ref.get_item(&index).stringify_err()?.file.clone(),
..Default::default()
}
};
let data = item.read_file().await.stringify_err()?;
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let item = profiles_ref.get_item(&index).stringify_err()?;
let data = item.read_file().stringify_err()?;
Ok(data)
}

View File

@@ -8,14 +8,14 @@ use std::collections::HashMap;
/// 获取运行时配置
#[tauri::command]
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().await.latest_arc().config.clone())
Ok(Config::runtime().await.latest_ref().config.clone())
}
/// 获取运行时YAML配置
#[tauri::command]
pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
let runtime = runtime.latest_ref();
let config = runtime.config.as_ref();
config
@@ -31,19 +31,19 @@ pub async fn get_runtime_yaml() -> CmdResult<String> {
/// 获取运行时存在的键
#[tauri::command]
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().await.latest_arc().exists_keys.clone())
Ok(Config::runtime().await.latest_ref().exists_keys.clone())
}
/// 获取运行时日志
#[tauri::command]
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().await.latest_arc().chain_logs.clone())
Ok(Config::runtime().await.latest_ref().chain_logs.clone())
}
#[tauri::command]
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
let runtime = runtime.latest_ref();
let config = runtime
.config
@@ -98,7 +98,9 @@ pub async fn update_proxy_chain_config_in_runtime(
) -> CmdResult<()> {
{
let runtime = Config::runtime().await;
runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
let mut draft = runtime.draft_mut();
draft.update_proxy_chain_config(proxy_chain_config);
drop(draft);
runtime.apply();
}

View File

@@ -12,37 +12,28 @@ use tokio::fs;
/// 保存profiles的配置
#[tauri::command]
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
let file_data = match file_data {
Some(d) => d,
None => return Ok(()),
};
// 在异步操作前获取必要元数据并释放锁
let (rel_path, is_merge_file) = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_arc();
let item = profiles_guard.get_item(&index).stringify_err()?;
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
let path = item.file.clone().ok_or("file field is null")?;
(path, is_merge)
};
// 读取原始内容在释放profiles_guard后进行
let original_content = PrfItem {
file: Some(rel_path.clone()),
..Default::default()
if file_data.is_none() {
return Ok(());
}
.read_file()
.await
.stringify_err()?;
let profiles_dir = dirs::app_profiles_dir().stringify_err()?;
let file_path = profiles_dir.join(rel_path.as_str());
let file_path_str = file_path.to_string_lossy().to_string();
// 在异步操作前完成所有文件操作
let (file_path, original_content, is_merge_file) = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_ref();
let item = profiles_guard.get_item(&index).stringify_err()?;
// 确定是否为merge类型文件
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
let content = item.read_file().stringify_err()?;
let path = item.file.clone().ok_or("file field is null")?;
let profiles_dir = dirs::app_profiles_dir().stringify_err()?;
(profiles_dir.join(path.as_str()), content, is_merge)
};
// 保存新的配置文件
let file_data = file_data.ok_or("file_data is None")?;
fs::write(&file_path, &file_data).await.stringify_err()?;
let file_path_str = file_path.to_string_lossy().to_string();
logging!(
info,
Type::Config,
@@ -51,107 +42,102 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
is_merge_file
);
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
if is_merge_file {
return handle_merge_file(&file_path_str, &file_path, &original_content).await;
}
handle_full_validation(&file_path_str, &file_path, &original_content).await
}
async fn restore_original(
file_path: &std::path::Path,
original_content: &str,
) -> Result<(), String> {
fs::write(file_path, original_content).await.stringify_err()
}
fn is_script_error(err: &str, file_path_str: &str) -> bool {
file_path_str.ends_with(".js")
|| err.contains("Script syntax error")
|| err.contains("Script must contain a main function")
|| err.contains("Failed to read script file")
}
async fn handle_merge_file(
file_path_str: &str,
file_path: &std::path::Path,
original_content: &str,
) -> CmdResult {
logging!(
info,
Type::Config,
"[cmd配置save] 检测到merge文件只进行语法验证"
);
match CoreConfigValidator::validate_config_file(file_path_str, Some(true)).await {
Ok((true, _)) => {
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
if let Err(e) = CoreManager::global().update_config().await {
logging!(
info,
Type::Config,
"[cmd配置save] 检测到merge文件只进行语法验证"
);
match CoreConfigValidator::validate_config_file(&file_path_str, Some(true)).await {
Ok((true, _)) => {
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
// 成功后尝试更新整体配置
match CoreManager::global().update_config().await {
Ok(_) => {
// 配置更新成功,刷新前端
handle::Handle::refresh_clash();
}
Err(e) => {
logging!(
warn,
Type::Config,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
}
}
return Ok(());
}
Ok((false, error_msg)) => {
logging!(
warn,
Type::Config,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
"[cmd配置save] merge文件语法验证失败: {}",
error_msg
);
} else {
handle::Handle::refresh_clash();
// 恢复原始配置文件
fs::write(&file_path, original_content)
.await
.stringify_err()?;
// 发送合并文件专用错误通知
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
return Ok(());
}
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
fs::write(&file_path, original_content)
.await
.stringify_err()?;
return Err(e.to_string().into());
}
Ok(())
}
Ok((false, error_msg)) => {
logging!(
warn,
Type::Config,
"[cmd配置save] merge文件语法验证失败: {}",
error_msg
);
restore_original(file_path, original_content).await?;
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
Ok(())
}
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
restore_original(file_path, original_content).await?;
Err(e.to_string().into())
}
}
}
async fn handle_full_validation(
file_path_str: &str,
file_path: &std::path::Path,
original_content: &str,
) -> CmdResult {
match CoreConfigValidator::validate_config_file(file_path_str, None).await {
// 非merge文件使用完整验证流程
match CoreConfigValidator::validate_config_file(&file_path_str, None).await {
Ok((true, _)) => {
logging!(info, Type::Config, "[cmd配置save] 验证成功");
Ok(())
}
Ok((false, error_msg)) => {
logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg);
restore_original(file_path, original_content).await?;
// 恢复原始配置文件
fs::write(&file_path, original_content)
.await
.stringify_err()?;
// 智能判断错误类型
let is_script_error = file_path_str.ends_with(".js")
|| error_msg.contains("Script syntax error")
|| error_msg.contains("Script must contain a main function")
|| error_msg.contains("Failed to read script file");
if error_msg.contains("YAML syntax error")
|| error_msg.contains("Failed to read file:")
|| (!file_path_str.ends_with(".js") && !is_script_error(&error_msg, file_path_str))
|| (!file_path_str.ends_with(".js") && !is_script_error)
{
// 普通YAML错误使用YAML通知处理
logging!(
info,
Type::Config,
"[cmd配置save] YAML配置文件验证失败发送通知"
);
let result = (false, error_msg.to_owned());
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error(&error_msg, file_path_str) {
} else if is_script_error {
// 脚本错误使用专门的通知处理
logging!(
info,
Type::Config,
"[cmd配置save] 脚本文件验证失败,发送通知"
);
let result = (false, error_msg.to_owned());
let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
logging!(
info,
Type::Config,
@@ -164,7 +150,10 @@ async fn handle_full_validation(
}
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
restore_original(file_path, original_content).await?;
// 恢复原始配置文件
fs::write(&file_path, original_content)
.await
.stringify_err()?;
Err(e.to_string().into())
}
}

View File

@@ -1,5 +1,8 @@
use super::{CmdResult, StringifyErr};
use crate::core::service::{self, SERVICE_MANAGER, ServiceStatus};
use crate::{
core::service::{self, SERVICE_MANAGER, ServiceStatus},
utils::i18n::t,
};
use smartstring::SmartString;
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
@@ -10,7 +13,7 @@ async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) ->
.await
{
let emsg = format!("{} Service failed: {}", op_type, e);
return Err(SmartString::from(emsg));
return Err(SmartString::from(&*t(emsg.as_str()).await));
}
Ok(())
}

View File

@@ -1,28 +1,26 @@
use std::sync::Arc;
use super::CmdResult;
use crate::{
core::{CoreManager, handle, manager::RunningMode},
core::{CoreManager, handle},
logging,
module::sysinfo::PlatformSpecification,
utils::logging::Type,
};
#[cfg(target_os = "windows")]
use deelevate::{PrivilegeLevel, Token};
use once_cell::sync::Lazy;
use std::{
sync::atomic::{AtomicI64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tokio::time::Instant;
// 存储应用启动时间的全局变量
static APP_START_TIME: Lazy<Instant> = Lazy::new(Instant::now);
#[cfg(not(target_os = "windows"))]
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| unsafe { libc::geteuid() } == 0);
#[cfg(target_os = "windows")]
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| {
Token::with_current_process()
.and_then(|token| token.privilege_level())
.map(|level| level != PrivilegeLevel::NotPrivileged)
.unwrap_or(false)
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
// 获取当前系统时间,转换为毫秒级时间戳
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
AtomicI64::new(now)
});
#[tauri::command]
@@ -47,18 +45,52 @@ pub async fn get_system_info() -> CmdResult<String> {
/// 获取当前内核运行模式
#[tauri::command]
pub async fn get_running_mode() -> Result<Arc<RunningMode>, String> {
Ok(CoreManager::global().get_running_mode())
pub async fn get_running_mode() -> Result<String, String> {
Ok(CoreManager::global().get_running_mode().to_string())
}
/// 获取应用的运行时间(毫秒)
#[tauri::command]
pub fn get_app_uptime() -> CmdResult<u128> {
Ok(APP_START_TIME.elapsed().as_millis())
pub fn get_app_uptime() -> CmdResult<i64> {
let start_time = APP_START_TIME.load(Ordering::Relaxed);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
Ok(now - start_time)
}
/// 检查应用是否以管理员身份运行
#[tauri::command]
#[cfg(target_os = "windows")]
pub fn is_admin() -> CmdResult<bool> {
Ok(*APPS_RUN_AS_ADMIN)
use deelevate::{PrivilegeLevel, Token};
let result = Token::with_current_process()
.and_then(|token| token.privilege_level())
.map(|level| level != PrivilegeLevel::NotPrivileged)
.unwrap_or(false);
Ok(result)
}
/// 非Windows平台检测是否以管理员身份运行
#[tauri::command]
#[cfg(not(target_os = "windows"))]
pub fn is_admin() -> CmdResult<bool> {
#[cfg(target_os = "macos")]
{
Ok(unsafe { libc::geteuid() } == 0)
}
#[cfg(target_os = "linux")]
{
Ok(unsafe { libc::geteuid() } == 0)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Ok(false)
}
}

View File

@@ -17,7 +17,7 @@ mod platform {
mod platform {
use super::CmdResult;
pub const fn invoke_uwp_tool() -> CmdResult {
pub fn invoke_uwp_tool() -> CmdResult {
Ok(())
}
}

View File

@@ -1,14 +1,20 @@
use super::CmdResult;
use crate::{cmd::StringifyErr, config::IVerge, feat, utils::draft::SharedBox};
use crate::{cmd::StringifyErr, config::*, feat};
/// 获取Verge配置
#[tauri::command]
pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
feat::fetch_verge_config().await.stringify_err()
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配置
#[tauri::command]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
feat::patch_verge(&payload, false).await.stringify_err()
feat::patch_verge(payload, false).await.stringify_err()
}

View File

@@ -1,9 +1,5 @@
use super::CmdResult;
use crate::{
cmd::StringifyErr,
config::{Config, IVerge},
core, feat,
};
use crate::{cmd::StringifyErr, config::*, core, feat};
use reqwest_dav::list_cmd::ListFile;
use smartstring::alias::String;
@@ -16,11 +12,18 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
webdav_password: Some(password),
..IVerge::default()
};
Config::verge().await.edit_draft(|e| e.patch_config(&patch));
Config::verge()
.await
.draft_mut()
.patch_config(patch.clone());
Config::verge().await.apply();
let verge_data = Config::verge().await.latest_arc();
verge_data.save_file().await.stringify_err()?;
// 分离数据获取和异步调用
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(())
}

View File

@@ -1,8 +1,6 @@
use crate::config::Config;
use crate::constants::{network, tun as tun_const};
use crate::utils::dirs::{ipc_path, path_to_str};
use crate::utils::{dirs, help};
use crate::{logging, utils::logging::Type};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::{Mapping, Value};
@@ -42,13 +40,15 @@ impl IClashTemp {
Self(Self::guard(map))
}
Err(err) => {
logging!(error, Type::Config, "{err}");
log::error!(target: "app", "{err}");
template
}
}
}
pub fn template() -> Self {
use crate::constants::{network, tun as tun_const};
let mut map = Mapping::new();
let mut tun_config = Mapping::new();
let mut cors_map = Mapping::new();
@@ -214,9 +214,9 @@ impl IClashTemp {
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(network::ports::DEFAULT_TPROXY);
.unwrap_or(7896);
if port == 0 {
port = network::ports::DEFAULT_TPROXY;
port = 7896;
}
port
}
@@ -300,7 +300,7 @@ impl IClashTemp {
// 检查 enable_external_controller 设置,用于运行时配置生成
let enable_external_controller = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_external_controller
.unwrap_or(false);
@@ -330,7 +330,7 @@ impl IClashTemp {
.ok()
.and_then(|path| path_to_str(&path).ok().map(|s| s.into()))
.unwrap_or_else(|| {
logging!(error, Type::Config, "Failed to get IPC path");
log::error!(target: "app", "Failed to get IPC path");
crate::constants::network::DEFAULT_EXTERNAL_CONTROLLER.into()
})
}

View File

@@ -1,10 +1,9 @@
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{
cmd,
config::{PrfItem, profiles_append_item_safe},
constants::{files, timing},
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
enhance, logging, logging_error,
core::{CoreManager, handle, validate::CoreConfigValidator},
enhance, logging,
utils::{Draft, dirs, help, logging::Type},
};
use anyhow::{Result, anyhow};
@@ -15,40 +14,40 @@ use tokio::sync::OnceCell;
use tokio::time::sleep;
pub struct Config {
clash_config: Draft<IClashTemp>,
verge_config: Draft<IVerge>,
profiles_config: Draft<IProfiles>,
runtime_config: Draft<IRuntime>,
clash_config: Draft<Box<IClashTemp>>,
verge_config: Draft<Box<IVerge>>,
profiles_config: Draft<Box<IProfiles>>,
runtime_config: Draft<Box<IRuntime>>,
}
impl Config {
pub async fn global() -> &'static Self {
pub async fn global() -> &'static Config {
static CONFIG: OnceCell<Config> = OnceCell::const_new();
CONFIG
.get_or_init(|| async {
Self {
clash_config: Draft::new(IClashTemp::new().await),
verge_config: Draft::new(IVerge::new().await),
profiles_config: Draft::new(IProfiles::new().await),
runtime_config: Draft::new(IRuntime::new()),
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 async fn clash() -> Draft<IClashTemp> {
pub async fn clash() -> Draft<Box<IClashTemp>> {
Self::global().await.clash_config.clone()
}
pub async fn verge() -> Draft<IVerge> {
pub async fn verge() -> Draft<Box<IVerge>> {
Self::global().await.verge_config.clone()
}
pub async fn profiles() -> Draft<IProfiles> {
pub async fn profiles() -> Draft<Box<IProfiles>> {
Self::global().await.profiles_config.clone()
}
pub async fn runtime() -> Draft<IRuntime> {
pub async fn runtime() -> Draft<Box<IRuntime>> {
Self::global().await.runtime_config.clone()
}
@@ -56,22 +55,6 @@ impl Config {
pub async fn init_config() -> Result<()> {
Self::ensure_default_profile_items().await?;
// init Tun mode
if !cmd::system::is_admin().unwrap_or_default()
&& service::is_service_available().await.is_err()
{
let verge = Self::verge().await;
verge.edit_draft(|d| {
d.enable_tun_mode = Some(false);
});
verge.apply();
let _ = tray::Tray::global().update_menu().await;
// 分离数据获取和异步调用避免Send问题
let verge_data = Self::verge().await.latest_arc();
logging_error!(Type::Core, verge_data.save_file().await);
}
let validation_result = Self::generate_and_validate().await?;
if let Some((msg_type, msg_content)) = validation_result {
@@ -85,13 +68,13 @@ impl Config {
// Ensure "Merge" and "Script" profile items exist, adding them if missing.
async fn ensure_default_profile_items() -> Result<()> {
let profiles = Self::profiles().await;
if profiles.latest_arc().get_item("Merge").is_err() {
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
profiles_append_item_safe(merge_item).await?;
if profiles.latest_ref().get_item(&"Merge".into()).is_err() {
let merge_item = PrfItem::from_merge(Some("Merge".into()))?;
profiles_append_item_safe(merge_item.clone()).await?;
}
if profiles.latest_arc().get_item("Script").is_err() {
let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
profiles_append_item_safe(script_item).await?;
if profiles.latest_ref().get_item(&"Script".into()).is_err() {
let script_item = PrfItem::from_script(Some("Script".into()))?;
profiles_append_item_safe(script_item.clone()).await?;
}
Ok(())
}
@@ -154,26 +137,26 @@ impl Config {
ConfigType::Check => dirs::app_home_dir()?.join(files::CHECK_CONFIG),
};
let runtime = Self::runtime().await;
let runtime_arc = runtime.latest_arc();
let config = runtime_arc
let runtime = Config::runtime().await;
let config = runtime
.latest_ref()
.config
.as_ref()
.ok_or_else(|| anyhow!("failed to get runtime config"))?;
.ok_or_else(|| anyhow!("failed to get runtime config"))?
.clone();
drop(runtime); // 显式释放锁
help::save_yaml(&path, config, Some("# Generated by Clash Verge")).await?;
help::save_yaml(&path, &config, Some("# Generated by Clash Verge")).await?;
Ok(path)
}
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
Self::runtime().await.edit_draft(|d| {
*d = IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
}
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
});
Ok(())
@@ -189,11 +172,11 @@ impl Config {
};
let operation = || async {
if Self::runtime().await.latest_arc().config.is_some() {
if Config::runtime().await.latest_ref().config.is_some() {
return Ok::<(), BackoffError<anyhow::Error>>(());
}
Self::generate().await.map_err(BackoffError::transient)
Config::generate().await.map_err(BackoffError::transient)
};
if let Err(e) = backoff::future::retry(backoff_strategy, operation).await {
@@ -230,7 +213,7 @@ mod tests {
#[test]
#[allow(unused_variables)]
fn test_draft_size_non_boxed() {
let draft = Draft::new(IRuntime::new());
let draft = Draft::from(IRuntime::new());
let iruntime_size = std::mem::size_of_val(&draft);
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
}
@@ -238,7 +221,7 @@ mod tests {
#[test]
#[allow(unused_variables)]
fn test_draft_size_boxed() {
let draft = Draft::new(Box::new(IRuntime::new()));
let draft = Draft::from(Box::new(IRuntime::new()));
let box_iruntime_size = std::mem::size_of_val(&draft);
assert_eq!(
box_iruntime_size,

View File

@@ -5,16 +5,9 @@ use aes_gcm::{
};
use base64::{Engine, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::Cell;
use std::future::Future;
const NONCE_LENGTH: usize = 12;
// Use task-local context so the flag follows the async task across threads
tokio::task_local! {
static ENCRYPTION_ACTIVE: Cell<bool>;
}
/// Encrypt data
#[allow(deprecated)]
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
@@ -66,45 +59,39 @@ where
T: Serialize,
S: Serializer,
{
if is_encryption_active() {
let json = serde_json::to_string(value).map_err(serde::ser::Error::custom)?;
let encrypted = encrypt_data(&json).map_err(serde::ser::Error::custom)?;
serializer.serialize_str(&encrypted)
} else {
value.serialize(serializer)
// 如果序列化失败,返回 None
let json = match serde_json::to_string(value) {
Ok(j) => j,
Err(_) => return serializer.serialize_none(),
};
// 如果加密失败,返回 None
match encrypt_data(&json) {
Ok(encrypted) => serializer.serialize_str(&encrypted),
Err(_) => serializer.serialize_none(),
}
}
/// Deserialize decrypted function
pub fn deserialize_encrypted<'a, D, T>(deserializer: D) -> Result<T, D::Error>
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: for<'de> Deserialize<'de> + Default,
D: Deserializer<'a>,
{
if is_encryption_active() {
let encrypted_opt: Option<String> = Option::deserialize(deserializer)?;
// 如果反序列化字符串失败,返回默认值
let encrypted = match String::deserialize(deserializer) {
Ok(s) => s,
Err(_) => return Ok(T::default()),
};
match encrypted_opt {
Some(encrypted) if !encrypted.is_empty() => {
let decrypted_string =
decrypt_data(&encrypted).map_err(serde::de::Error::custom)?;
serde_json::from_str(&decrypted_string).map_err(serde::de::Error::custom)
}
_ => Ok(T::default()),
}
} else {
T::deserialize(deserializer)
// 如果解密失败,返回默认值
let decrypted_string = match decrypt_data(&encrypted) {
Ok(data) => data,
Err(_) => return Ok(T::default()),
};
// 如果 JSON 解析失败,返回默认值
match serde_json::from_str(&decrypted_string) {
Ok(value) => Ok(value),
Err(_) => Ok(T::default()),
}
}
pub async fn with_encryption<F, Fut, R>(f: F) -> R
where
F: FnOnce() -> Fut,
Fut: Future<Output = R>,
{
ENCRYPTION_ACTIVE.scope(Cell::new(true), f()).await
}
fn is_encryption_active() -> bool {
ENCRYPTION_ACTIVE.try_with(|c| c.get()).unwrap_or(false)
}

View File

@@ -1,17 +1,13 @@
use crate::{
config::profiles,
utils::{
dirs, help,
network::{NetworkManager, ProxyType},
tmpl,
},
use crate::utils::{
dirs, help,
network::{NetworkManager, ProxyType},
tmpl,
};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::time::Duration;
use tokio::fs;
use std::{fs, time::Duration};
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PrfItem {
@@ -122,29 +118,26 @@ pub struct PrfOption {
}
impl PrfOption {
pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option<Self> {
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
match (one, other) {
(Some(a_ref), Some(b_ref)) => {
let mut result = a_ref.clone();
result.user_agent = b_ref.user_agent.clone().or(result.user_agent);
result.with_proxy = b_ref.with_proxy.or(result.with_proxy);
result.self_proxy = b_ref.self_proxy.or(result.self_proxy);
result.danger_accept_invalid_certs = b_ref
(Some(mut a), Some(b)) => {
a.user_agent = b.user_agent.or(a.user_agent);
a.with_proxy = b.with_proxy.or(a.with_proxy);
a.self_proxy = b.self_proxy.or(a.self_proxy);
a.danger_accept_invalid_certs = b
.danger_accept_invalid_certs
.or(result.danger_accept_invalid_certs);
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update);
result.update_interval = b_ref.update_interval.or(result.update_interval);
result.merge = b_ref.merge.clone().or(result.merge);
result.script = b_ref.script.clone().or(result.script);
result.rules = b_ref.rules.clone().or(result.rules);
result.proxies = b_ref.proxies.clone().or(result.proxies);
result.groups = b_ref.groups.clone().or(result.groups);
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
Some(result)
.or(a.danger_accept_invalid_certs);
a.allow_auto_update = b.allow_auto_update.or(a.allow_auto_update);
a.update_interval = b.update_interval.or(a.update_interval);
a.merge = b.merge.or(a.merge);
a.script = b.script.or(a.script);
a.rules = b.rules.or(a.rules);
a.proxies = b.proxies.or(a.proxies);
a.groups = b.groups.or(a.groups);
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
Some(a)
}
(Some(a_ref), None) => Some(a_ref.clone()),
(None, Some(b_ref)) => Some(b_ref.clone()),
(None, None) => None,
t => t.0.or(t.1),
}
}
}
@@ -152,14 +145,13 @@ impl PrfOption {
impl PrfItem {
/// From partial item
/// must contain `itype`
pub async fn from(item: &Self, file_data: Option<String>) -> Result<Self> {
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
if item.itype.is_none() {
bail!("type should not be null");
}
let itype = item
.itype
.as_ref()
.ok_or_else(|| anyhow::anyhow!("type should not be null"))?;
match itype.as_str() {
"remote" => {
@@ -167,16 +159,14 @@ impl PrfItem {
.url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("url should not be null"))?;
let name = item.name.as_ref();
let desc = item.desc.as_ref();
let option = item.option.as_ref();
Self::from_url(url, name, desc, option).await
let name = item.name;
let desc = item.desc;
PrfItem::from_url(url, name, desc, item.option).await
}
"local" => {
let name = item.name.clone().unwrap_or_else(|| "Local File".into());
let desc = item.desc.clone().unwrap_or_else(|| "".into());
let option = item.option.as_ref();
Self::from_local(name, desc, file_data, option).await
let name = item.name.unwrap_or_else(|| "Local File".into());
let desc = item.desc.unwrap_or_else(|| "".into());
PrfItem::from_local(name, desc, file_data, item.option).await
}
typ => bail!("invalid profile item type \"{typ}\""),
}
@@ -188,8 +178,8 @@ impl PrfItem {
name: String,
desc: String,
file_data: Option<String>,
option: Option<&PrfOption>,
) -> Result<Self> {
option: Option<PrfOption>,
) -> Result<PrfItem> {
let uid = help::get_uid("L").into();
let file = format!("{uid}.yaml").into();
let opt_ref = option.as_ref();
@@ -201,31 +191,31 @@ impl PrfItem {
let mut groups = opt_ref.and_then(|o| o.groups.clone());
if merge.is_none() {
let merge_item = &mut Self::from_merge(None)?;
profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone();
let merge_item = PrfItem::from_merge(None)?;
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = &mut Self::from_script(None)?;
profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone();
let script_item = PrfItem::from_script(None)?;
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
script = script_item.uid;
}
if rules.is_none() {
let rules_item = &mut Self::from_rules()?;
profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone();
let rules_item = PrfItem::from_rules()?;
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
rules = rules_item.uid;
}
if proxies.is_none() {
let proxies_item = &mut Self::from_proxies()?;
profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone();
let proxies_item = PrfItem::from_proxies()?;
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
proxies = proxies_item.uid;
}
if groups.is_none() {
let groups_item = &mut Self::from_groups()?;
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
let groups_item = PrfItem::from_groups()?;
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
groups = groups_item.uid;
}
Ok(Self {
Ok(PrfItem {
uid: Some(uid),
itype: Some("local".into()),
name: Some(name),
@@ -253,23 +243,24 @@ impl PrfItem {
/// create a new item from url
pub async fn from_url(
url: &str,
name: Option<&String>,
desc: Option<&String>,
option: Option<&PrfOption>,
) -> Result<Self> {
let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false));
let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false));
name: Option<String>,
desc: Option<String>,
option: Option<PrfOption>,
) -> Result<PrfItem> {
let opt_ref = option.as_ref();
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
let accept_invalid_certs =
option.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
let allow_auto_update = option.map(|o| o.allow_auto_update.unwrap_or(true));
let user_agent = option.and_then(|o| o.user_agent.clone());
let update_interval = option.and_then(|o| o.update_interval);
let timeout = option.and_then(|o| o.timeout_seconds).unwrap_or(20);
let mut merge = option.and_then(|o| o.merge.clone());
let mut script = option.and_then(|o| o.script.clone());
let mut rules = option.and_then(|o| o.rules.clone());
let mut proxies = option.and_then(|o| o.proxies.clone());
let mut groups = option.and_then(|o| o.groups.clone());
opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
let allow_auto_update = opt_ref.map(|o| o.allow_auto_update.unwrap_or(true));
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
let update_interval = opt_ref.and_then(|o| o.update_interval);
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
let mut merge = opt_ref.and_then(|o| o.merge.clone());
let mut script = opt_ref.and_then(|o| o.script.clone());
let mut rules = opt_ref.and_then(|o| o.rules.clone());
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
let mut groups = opt_ref.and_then(|o| o.groups.clone());
// 选择代理类型
let proxy_type = if self_proxy {
@@ -306,27 +297,18 @@ impl PrfItem {
let header = resp.headers();
// parse the Subscription UserInfo
let extra;
'extra: {
for (k, v) in header.iter() {
let key_lower = k.as_str().to_ascii_lowercase();
// Accept standard custom-metadata prefixes (x-amz-meta-, x-obs-meta-, x-cos-meta-, etc.).
if key_lower
.strip_suffix("subscription-userinfo")
.is_some_and(|prefix| prefix.is_empty() || prefix.ends_with('-'))
{
let sub_info = v.to_str().unwrap_or("");
extra = Some(PrfExtra {
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
download: help::parse_str(sub_info, "download").unwrap_or(0),
total: help::parse_str(sub_info, "total").unwrap_or(0),
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
});
break 'extra;
}
let extra = match header.get("Subscription-Userinfo") {
Some(value) => {
let sub_info = value.to_str().unwrap_or("");
Some(PrfExtra {
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
download: help::parse_str(sub_info, "download").unwrap_or(0),
total: help::parse_str(sub_info, "total").unwrap_or(0),
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
})
}
extra = None;
}
None => None,
};
// parse the Content-Disposition
let filename = match header.get("Content-Disposition") {
@@ -374,11 +356,7 @@ impl PrfItem {
let uid = help::get_uid("R").into();
let file = format!("{uid}.yaml").into();
let name = name.map(|s| s.to_owned()).unwrap_or_else(|| {
filename
.map(|s| s.into())
.unwrap_or_else(|| "Remote File".into())
});
let name = name.unwrap_or_else(|| filename.unwrap_or_else(|| "Remote File".into()).into());
let data = resp.text_with_charset()?;
// process the charset "UTF-8 with BOM"
@@ -393,36 +371,36 @@ impl PrfItem {
}
if merge.is_none() {
let merge_item = &mut Self::from_merge(None)?;
profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone();
let merge_item = PrfItem::from_merge(None)?;
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = &mut Self::from_script(None)?;
profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone();
let script_item = PrfItem::from_script(None)?;
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
script = script_item.uid;
}
if rules.is_none() {
let rules_item = &mut Self::from_rules()?;
profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone();
let rules_item = PrfItem::from_rules()?;
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
rules = rules_item.uid;
}
if proxies.is_none() {
let proxies_item = &mut Self::from_proxies()?;
profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone();
let proxies_item = PrfItem::from_proxies()?;
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
proxies = proxies_item.uid;
}
if groups.is_none() {
let groups_item = &mut Self::from_groups()?;
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
let groups_item = PrfItem::from_groups()?;
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
groups = groups_item.uid;
}
Ok(Self {
Ok(PrfItem {
uid: Some(uid),
itype: Some("remote".into()),
name: Some(name),
desc: desc.cloned(),
desc,
file: Some(file),
url: Some(url.into()),
selected: None,
@@ -445,15 +423,16 @@ impl PrfItem {
/// ## Merge type (enhance)
/// create the enhanced item by using `merge` rule
pub fn from_merge(uid: Option<String>) -> Result<Self> {
let (id, template) = if let Some(uid) = uid {
(uid, tmpl::ITEM_MERGE.into())
} else {
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
};
pub fn from_merge(uid: Option<String>) -> Result<PrfItem> {
let mut id = help::get_uid("m").into();
let mut template = tmpl::ITEM_MERGE_EMPTY.into();
if let Some(uid) = uid {
id = uid;
template = tmpl::ITEM_MERGE.into();
}
let file = format!("{id}.yaml").into();
Ok(Self {
Ok(PrfItem {
uid: Some(id),
itype: Some("merge".into()),
name: None,
@@ -471,14 +450,14 @@ impl PrfItem {
/// ## Script type (enhance)
/// create the enhanced item by using javascript quick.js
pub fn from_script(uid: Option<String>) -> Result<Self> {
let id = if let Some(uid) = uid {
uid
} else {
help::get_uid("s").into()
};
pub fn from_script(uid: Option<String>) -> Result<PrfItem> {
let mut id = help::get_uid("s").into();
if let Some(uid) = uid {
id = uid;
}
let file = format!("{id}.js").into(); // js ext
Ok(Self {
Ok(PrfItem {
uid: Some(id),
itype: Some("script".into()),
name: None,
@@ -495,11 +474,11 @@ impl PrfItem {
}
/// ## Rules type (enhance)
pub fn from_rules() -> Result<Self> {
pub fn from_rules() -> Result<PrfItem> {
let uid = help::get_uid("r").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Ok(Self {
Ok(PrfItem {
uid: Some(uid),
itype: Some("rules".into()),
name: None,
@@ -516,11 +495,11 @@ impl PrfItem {
}
/// ## Proxies type (enhance)
pub fn from_proxies() -> Result<Self> {
pub fn from_proxies() -> Result<PrfItem> {
let uid = help::get_uid("p").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Ok(Self {
Ok(PrfItem {
uid: Some(uid),
itype: Some("proxies".into()),
name: None,
@@ -537,11 +516,11 @@ impl PrfItem {
}
/// ## Groups type (enhance)
pub fn from_groups() -> Result<Self> {
pub fn from_groups() -> Result<PrfItem> {
let uid = help::get_uid("g").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Ok(Self {
Ok(PrfItem {
uid: Some(uid),
itype: Some("groups".into()),
name: None,
@@ -558,59 +537,28 @@ impl PrfItem {
}
/// get the file data
pub async fn read_file(&self) -> Result<String> {
pub fn read_file(&self) -> Result<String> {
let file = self
.file
.as_ref()
.clone()
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
let path = dirs::app_profiles_dir()?.join(file.as_str());
let content = fs::read_to_string(path)
.await
.context("failed to read the file")?;
let content = fs::read_to_string(path).context("failed to read the file")?;
Ok(content.into())
}
/// save the file data
pub async fn save_file(&self, data: String) -> Result<()> {
pub fn save_file(&self, data: String) -> Result<()> {
let file = self
.file
.as_ref()
.clone()
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
let path = dirs::app_profiles_dir()?.join(file.as_str());
fs::write(path, data.as_bytes())
.await
.context("failed to save the file")
}
}
impl PrfItem {
/// 获取current指向的订阅的merge
pub fn current_merge(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.merge.clone())
}
/// 获取current指向的订阅的script
pub fn current_script(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.script.clone())
}
/// 获取current指向的订阅的rules
pub fn current_rules(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.rules.clone())
}
/// 获取current指向的订阅的proxies
pub fn current_proxies(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.proxies.clone())
}
/// 获取current指向的订阅的groups
pub fn current_groups(&self) -> Option<String> {
self.option.as_ref().and_then(|o| o.groups.clone())
fs::write(path, data.as_bytes()).context("failed to save the file")
}
}
// 向前兼容,默认为订阅启用自动更新
const fn default_allow_auto_update() -> Option<bool> {
fn default_allow_auto_update() -> Option<bool> {
Some(true)
}

View File

@@ -3,12 +3,11 @@ use crate::utils::{
dirs::{self, PathBufExec},
help,
};
use crate::{logging, utils::logging::Type};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::{collections::HashSet, sync::Arc};
use std::collections::HashSet;
use tokio::fs;
/// Define the `profiles.yaml` schema
@@ -32,7 +31,7 @@ pub struct CleanupResult {
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key.to_owned();
$lv.$key = $rv.$key;
}
};
}
@@ -50,33 +49,42 @@ impl IProfiles {
}
None
}
pub async fn new() -> Self {
let path = match dirs::profiles_path() {
Ok(p) => p,
Err(err) => {
logging!(error, Type::Config, "{err}");
return Self::default();
}
};
match help::read_yaml::<Self>(&path).await {
Ok(mut profiles) => {
let items = profiles.items.get_or_insert_with(Vec::new);
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d").into());
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").into());
}
}
}
profiles
}
profiles
}
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
}
},
Err(err) => {
logging!(error, Type::Config, "{err}");
Self::default()
log::error!(target: "app", "{err}");
Self::template()
}
}
}
pub fn template() -> Self {
Self {
items: Some(vec![]),
..Self::default()
}
}
pub async fn save_file(&self) -> Result<()> {
help::save_yaml(
&dirs::profiles_path()?,
@@ -87,64 +95,55 @@ impl IProfiles {
}
/// 只修改currentvalid和chain
pub fn patch_config(&mut self, patch: &Self) {
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
if let Some(current) = &patch.current
if let Some(current) = patch.current
&& let Some(items) = self.items.as_ref()
{
let some_uid = Some(current);
if items.iter().any(|e| e.uid.as_ref() == some_uid) {
self.current = some_uid.cloned();
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
}
Ok(())
}
pub const fn get_current(&self) -> Option<&String> {
self.current.as_ref()
pub fn get_current(&self) -> Option<String> {
self.current.clone()
}
/// get items ref
pub const fn get_items(&self) -> Option<&Vec<PrfItem>> {
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
self.items.as_ref()
}
/// find the item by the uid
pub fn get_item(&self, uid: impl AsRef<str>) -> Result<&PrfItem> {
let uid_str = uid.as_ref();
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
if let Some(items) = self.items.as_ref() {
let some_uid = Some(uid.clone());
for each in items.iter() {
if let Some(uid_val) = &each.uid
&& uid_val.as_str() == uid_str
{
if each.uid == some_uid {
return Ok(each);
}
}
}
bail!("failed to get the profile item \"uid:{}\"", uid_str);
}
pub fn get_item_arc(&self, uid: &str) -> Option<Arc<PrfItem>> {
self.items.as_ref().and_then(|items| {
items
.iter()
.find(|it| it.uid.as_deref() == Some(uid))
.map(|it| Arc::new(it.clone()))
})
bail!("failed to get the profile item \"uid:{uid}\"");
}
/// append new item
/// if the file_data is some
/// then should save the data to file
pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> {
let uid = &item.uid;
if uid.is_none() {
pub async fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
if item.uid.is_none() {
bail!("the uid should not be null");
}
let uid = item.uid.clone();
// save the file data
// move the field value after save
@@ -166,7 +165,7 @@ impl IProfiles {
if self.current.is_none()
&& (item.itype == Some("remote".into()) || item.itype == Some("local".into()))
{
self.current = uid.to_owned();
self.current = uid;
}
if self.items.is_none() {
@@ -174,23 +173,24 @@ impl IProfiles {
}
if let Some(items) = self.items.as_mut() {
items.push(item.to_owned());
items.push(item)
}
// self.save_file().await
Ok(())
}
/// reorder items
pub async 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;
for (i, _) in items.iter().enumerate() {
if items[i].uid.as_ref() == Some(active_id) {
if items[i].uid == Some(active_id.clone()) {
old_index = Some(i);
}
if items[i].uid.as_ref() == Some(over_id) {
if items[i].uid == Some(over_id.clone()) {
new_index = Some(i);
}
}
@@ -206,11 +206,11 @@ impl IProfiles {
}
/// update the item value
pub async 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() {
if each.uid.as_ref() == Some(uid) {
if each.uid == Some(uid.clone()) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
@@ -232,13 +232,13 @@ impl IProfiles {
/// be used to update the remote item
/// only patch `updated` `extra` `file_data`
pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> {
pub async fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
// find the item
let _ = self.get_item(uid)?;
let _ = self.get_item(&uid)?;
if let Some(items) = self.items.as_mut() {
let some_uid = Some(uid.clone());
@@ -247,8 +247,8 @@ impl IProfiles {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
each.home = item.home.to_owned();
each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref());
each.home = item.home;
each.option = PrfOption::merge(each.option.clone(), item.option);
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
@@ -279,10 +279,10 @@ impl IProfiles {
/// delete item
/// if delete the current then return true
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(uid);
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)?;
let item = self.get_item(&uid)?;
let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone());
let script_uid = item.option.as_ref().and_then(|e| e.script.clone());
let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone());
@@ -330,7 +330,7 @@ impl IProfiles {
.await;
}
// delete the original uid
if current == *uid {
if current == uid {
self.current = None;
for item in items.iter() {
if item.itype == Some("remote".into()) || item.itype == Some("local".into()) {
@@ -342,7 +342,7 @@ impl IProfiles {
self.items = Some(items);
self.save_file().await?;
Ok(current == *uid)
Ok(current == uid)
}
/// 获取current指向的订阅内容
@@ -362,18 +362,88 @@ impl IProfiles {
}
}
/// 获取current指向的订阅的merge
pub fn current_merge(&self) -> Option<String> {
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)) {
let merge = item.option.as_ref().and_then(|e| e.merge.clone());
return merge;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的script
pub fn current_script(&self) -> Option<String> {
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)) {
let script = item.option.as_ref().and_then(|e| e.script.clone());
return script;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的rules
pub fn current_rules(&self) -> Option<String> {
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)) {
let rules = item.option.as_ref().and_then(|e| e.rules.clone());
return rules;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的proxies
pub fn current_proxies(&self) -> Option<String> {
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)) {
let proxies = item.option.as_ref().and_then(|e| e.proxies.clone());
return proxies;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的groups
pub fn current_groups(&self) -> Option<String> {
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)) {
let groups = item.option.as_ref().and_then(|e| e.groups.clone());
return groups;
}
None
}
_ => None,
}
}
/// 判断profile是否是current指向的
pub fn is_current_profile_index(&self, index: &String) -> bool {
self.current.as_ref() == Some(index)
pub fn is_current_profile_index(&self, index: String) -> bool {
self.current == Some(index)
}
/// 获取所有的profiles(uid名称)
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(&String, &String)>> {
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
self.items.as_ref().map(|items| {
items
.iter()
.filter_map(|e| {
if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) {
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
Some((uid, name))
} else {
None
@@ -383,18 +453,6 @@ impl IProfiles {
})
}
/// 通过 uid 获取名称
pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> {
if let Some(items) = &self.items {
for item in items {
if item.uid.as_ref() == Some(uid) {
return item.name.as_ref();
}
}
}
None
}
/// 以 app 中的 profile 列表为准,删除不再需要的文件
pub async fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
let profiles_dir = dirs::app_profiles_dir()?;
@@ -433,7 +491,7 @@ impl IProfiles {
{
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
logging!(debug, Type::Config, "保护全局扩展配置文件: {file_name}");
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
continue;
}
@@ -442,15 +500,11 @@ impl IProfiles {
match path.to_path_buf().remove_if_exists().await {
Ok(_) => {
deleted_files.push(file_name.into());
logging!(info, Type::Config, "已清理冗余文件: {file_name}");
log::info!(target: "app", "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}").into());
logging!(
warn,
Type::Config,
"Warning: 清理文件失败: {file_name} - {e}"
);
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
}
}
@@ -463,9 +517,8 @@ impl IProfiles {
failed_deletions,
};
logging!(
info,
Type::Config,
log::info!(
target: "app",
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
result.total_files,
result.deleted_files.len(),
@@ -486,14 +539,14 @@ impl IProfiles {
}
/// 获取所有 active profile 关联的文件名
fn get_all_active_files(&self) -> HashSet<&str> {
let mut active_files: HashSet<&str> = HashSet::new();
fn get_all_active_files(&self) -> HashSet<String> {
let mut active_files = HashSet::new();
if let Some(items) = &self.items {
for item in items {
// 收集所有类型 profile 的文件
if let Some(file) = &item.file {
active_files.insert(file);
active_files.insert(file.clone());
}
// 对于主 profile 类型remote/local还需要收集其关联的扩展文件
@@ -506,35 +559,35 @@ impl IProfiles {
&& let Ok(merge_item) = self.get_item(merge_uid)
&& let Some(file) = &merge_item.file
{
active_files.insert(file);
active_files.insert(file.clone());
}
if let Some(script_uid) = &option.script
&& let Ok(script_item) = self.get_item(script_uid)
&& let Some(file) = &script_item.file
{
active_files.insert(file);
active_files.insert(file.clone());
}
if let Some(rules_uid) = &option.rules
&& let Ok(rules_item) = self.get_item(rules_uid)
&& let Some(file) = &rules_item.file
{
active_files.insert(file);
active_files.insert(file.clone());
}
if let Some(proxies_uid) = &option.proxies
&& let Ok(proxies_item) = self.get_item(proxies_uid)
&& let Some(file) = &proxies_item.file
{
active_files.insert(file);
active_files.insert(file.clone());
}
if let Some(groups_uid) = &option.groups
&& let Ok(groups_item) = self.get_item(groups_uid)
&& let Some(file) = &groups_item.file
{
active_files.insert(file);
active_files.insert(file.clone());
}
}
}
@@ -573,14 +626,14 @@ impl IProfiles {
use crate::config::Config;
pub async fn profiles_append_item_with_filedata_safe(
item: &PrfItem,
item: PrfItem,
file_data: Option<String>,
) -> Result<()> {
let item = &mut PrfItem::from(item, file_data).await?;
let item = PrfItem::from(item, file_data).await?;
profiles_append_item_safe(item).await
}
pub async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -590,7 +643,7 @@ pub async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
.await
}
pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<()> {
pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -600,7 +653,7 @@ pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<
.await
}
pub async fn profiles_delete_item_safe(index: &String) -> Result<bool> {
pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -610,7 +663,7 @@ pub async fn profiles_delete_item_safe(index: &String) -> Result<bool> {
.await
}
pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> {
pub async fn profiles_reorder_safe(active_id: String, over_id: String) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
@@ -630,7 +683,7 @@ pub async fn profiles_save_file_safe() -> Result<()> {
.await
}
pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem) -> Result<()> {
pub async fn profiles_draft_update_item_safe(index: String, item: PrfItem) -> Result<()> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {

View File

@@ -1,4 +1,3 @@
use crate::config::Config;
use crate::{
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
logging,
@@ -46,24 +45,19 @@ pub struct IVerge {
pub enable_memory_usage: Option<bool>,
/// enable group icon
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_group_icon: Option<bool>,
/// common tray icon
#[serde(skip_serializing_if = "Option::is_none")]
pub common_tray_icon: Option<bool>,
/// tray icon
#[cfg(target_os = "macos")]
#[serde(skip_serializing_if = "Option::is_none")]
pub tray_icon: Option<String>,
/// menu icon
#[serde(skip_serializing_if = "Option::is_none")]
pub menu_icon: Option<String>,
/// menu order
#[serde(skip_serializing_if = "Option::is_none")]
pub menu_order: Option<Vec<String>>,
/// sysproxy tray icon
@@ -120,7 +114,6 @@ pub struct IVerge {
/// hotkey map
/// format: {func},{key}
#[serde(skip_serializing_if = "Option::is_none")]
pub hotkeys: Option<Vec<String>>,
/// enable global hotkey
@@ -140,7 +133,7 @@ pub struct IVerge {
pub default_latency_test: Option<String>,
/// 默认的延迟测试超时时间
pub default_latency_timeout: Option<i16>,
pub default_latency_timeout: Option<i32>,
/// 是否自动检测当前节点延迟
pub enable_auto_delay_detection: Option<bool>,
@@ -149,7 +142,7 @@ pub struct IVerge {
pub enable_builtin_enhanced: Option<bool>,
/// proxy 页面布局 列数
pub proxy_layout_column: Option<u8>,
pub proxy_layout_column: Option<i32>,
/// 测试站列表
pub test_list: Option<Vec<IVergeTestItem>>,
@@ -208,7 +201,6 @@ pub struct IVerge {
)]
pub webdav_password: Option<String>,
#[serde(skip)]
pub enable_tray_speed: Option<bool>,
// pub enable_tray_icon: Option<bool>,
@@ -262,7 +254,7 @@ impl IVerge {
/// 验证并修正配置文件中的clash_core值
pub async fn validate_and_fix_config() -> Result<()> {
let config_path = dirs::verge_path()?;
let mut config = match help::read_yaml::<Self>(&config_path).await {
let mut config = match help::read_yaml::<IVerge>(&config_path).await {
Ok(config) => config,
Err(_) => Self::template(),
};
@@ -311,20 +303,20 @@ impl IVerge {
}
/// 配置修正后重新加载配置
async fn reload_config_after_fix(updated_config: Self) -> Result<()> {
async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
use crate::config::Config;
let config_draft = Config::verge().await;
*config_draft.draft_mut() = Box::new(updated_config.clone());
config_draft.apply();
logging!(
info,
Type::Config,
"内存配置已强制更新新的clash_core: {:?}",
&updated_config.clash_core
updated_config.clash_core
);
let config_draft = Config::verge().await;
config_draft.edit_draft(|d| {
*d = updated_config;
});
config_draft.apply();
Ok(())
}
@@ -351,7 +343,7 @@ impl IVerge {
pub async fn new() -> Self {
match dirs::verge_path() {
Ok(path) => match help::read_yaml::<Self>(&path).await {
Ok(path) => match help::read_yaml::<IVerge>(&path).await {
Ok(mut config) => {
// compatibility
if let Some(start_page) = config.start_page.clone()
@@ -362,12 +354,12 @@ impl IVerge {
config
}
Err(err) => {
logging!(error, Type::Config, "{err}");
log::error!(target: "app", "{err}");
Self::template()
}
},
Err(err) => {
logging!(error, Type::Config, "{err}");
log::error!(target: "app", "{err}");
Self::template()
}
}
@@ -446,11 +438,11 @@ impl IVerge {
/// patch verge config
/// only save to file
#[allow(clippy::cognitive_complexity)]
pub fn patch_config(&mut self, patch: &Self) {
pub fn patch_config(&mut self, patch: IVerge) {
macro_rules! patch {
($key: tt) => {
if patch.$key.is_some() {
self.$key = patch.$key.clone();
self.$key = patch.$key;
}
};
}
@@ -531,7 +523,7 @@ impl IVerge {
patch!(enable_external_controller);
}
pub const fn get_singleton_port() -> u16 {
pub fn get_singleton_port() -> u16 {
crate::constants::network::ports::SINGLETON_SERVER
}
@@ -552,3 +544,156 @@ impl IVerge {
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct IVergeResponse {
pub app_log_level: Option<String>,
pub app_log_max_size: Option<u64>,
pub app_log_max_count: Option<usize>,
pub language: Option<String>,
pub theme_mode: Option<String>,
pub tray_event: Option<String>,
pub env_type: Option<String>,
pub start_page: Option<String>,
pub startup_script: Option<String>,
pub traffic_graph: Option<bool>,
pub enable_memory_usage: Option<bool>,
pub enable_group_icon: Option<bool>,
pub common_tray_icon: Option<bool>,
#[cfg(target_os = "macos")]
pub tray_icon: Option<String>,
pub menu_icon: Option<String>,
pub menu_order: Option<Vec<String>>,
pub sysproxy_tray_icon: Option<bool>,
pub tun_tray_icon: Option<bool>,
pub enable_tun_mode: Option<bool>,
pub enable_auto_launch: Option<bool>,
pub enable_silent_start: Option<bool>,
pub enable_system_proxy: Option<bool>,
pub enable_proxy_guard: Option<bool>,
pub enable_global_hotkey: Option<bool>,
pub use_default_bypass: Option<bool>,
pub system_proxy_bypass: Option<String>,
pub proxy_guard_duration: Option<u64>,
pub proxy_auto_config: Option<bool>,
pub pac_file_content: Option<String>,
pub proxy_host: Option<String>,
pub theme_setting: Option<IVergeTheme>,
pub web_ui_list: Option<Vec<String>>,
pub clash_core: Option<String>,
pub hotkeys: Option<Vec<String>>,
pub auto_close_connection: Option<bool>,
pub auto_check_update: Option<bool>,
pub default_latency_test: Option<String>,
pub default_latency_timeout: Option<i32>,
pub enable_auto_delay_detection: Option<bool>,
pub enable_builtin_enhanced: Option<bool>,
pub proxy_layout_column: Option<i32>,
pub test_list: Option<Vec<IVergeTestItem>>,
pub auto_log_clean: Option<i32>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_port: Option<u16>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_enabled: Option<bool>,
#[cfg(target_os = "linux")]
pub verge_tproxy_port: Option<u16>,
#[cfg(target_os = "linux")]
pub verge_tproxy_enabled: Option<bool>,
pub verge_mixed_port: Option<u16>,
pub verge_socks_port: Option<u16>,
pub verge_socks_enabled: Option<bool>,
pub verge_port: Option<u16>,
pub verge_http_enabled: Option<bool>,
pub webdav_url: Option<String>,
pub webdav_username: Option<String>,
pub webdav_password: Option<String>,
pub enable_tray_speed: Option<bool>,
// pub enable_tray_icon: Option<bool>,
pub tray_inline_proxy_groups: Option<bool>,
pub enable_auto_light_weight_mode: Option<bool>,
pub auto_light_weight_minutes: Option<u64>,
pub enable_dns_settings: Option<bool>,
pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub hover_jump_navigator_delay: Option<u64>,
pub enable_external_controller: Option<bool>,
}
impl From<IVerge> for IVergeResponse {
fn from(verge: IVerge) -> Self {
// 先获取验证后的clash_core值避免后续借用冲突
let valid_clash_core = verge.get_valid_clash_core();
Self {
app_log_level: verge.app_log_level,
app_log_max_size: verge.app_log_max_size,
app_log_max_count: verge.app_log_max_count,
language: verge.language,
theme_mode: verge.theme_mode,
tray_event: verge.tray_event,
env_type: verge.env_type,
start_page: verge.start_page,
startup_script: verge.startup_script,
traffic_graph: verge.traffic_graph,
enable_memory_usage: verge.enable_memory_usage,
enable_group_icon: verge.enable_group_icon,
common_tray_icon: verge.common_tray_icon,
#[cfg(target_os = "macos")]
tray_icon: verge.tray_icon,
menu_icon: verge.menu_icon,
menu_order: verge.menu_order,
sysproxy_tray_icon: verge.sysproxy_tray_icon,
tun_tray_icon: verge.tun_tray_icon,
enable_tun_mode: verge.enable_tun_mode,
enable_auto_launch: verge.enable_auto_launch,
enable_silent_start: verge.enable_silent_start,
enable_system_proxy: verge.enable_system_proxy,
enable_proxy_guard: verge.enable_proxy_guard,
enable_global_hotkey: verge.enable_global_hotkey,
use_default_bypass: verge.use_default_bypass,
system_proxy_bypass: verge.system_proxy_bypass,
proxy_guard_duration: verge.proxy_guard_duration,
proxy_auto_config: verge.proxy_auto_config,
pac_file_content: verge.pac_file_content,
proxy_host: verge.proxy_host,
theme_setting: verge.theme_setting,
web_ui_list: verge.web_ui_list,
clash_core: Some(valid_clash_core),
hotkeys: verge.hotkeys,
auto_close_connection: verge.auto_close_connection,
auto_check_update: verge.auto_check_update,
default_latency_test: verge.default_latency_test,
default_latency_timeout: verge.default_latency_timeout,
enable_auto_delay_detection: verge.enable_auto_delay_detection,
enable_builtin_enhanced: verge.enable_builtin_enhanced,
proxy_layout_column: verge.proxy_layout_column,
test_list: verge.test_list,
auto_log_clean: verge.auto_log_clean,
#[cfg(not(target_os = "windows"))]
verge_redir_port: verge.verge_redir_port,
#[cfg(not(target_os = "windows"))]
verge_redir_enabled: verge.verge_redir_enabled,
#[cfg(target_os = "linux")]
verge_tproxy_port: verge.verge_tproxy_port,
#[cfg(target_os = "linux")]
verge_tproxy_enabled: verge.verge_tproxy_enabled,
verge_mixed_port: verge.verge_mixed_port,
verge_socks_port: verge.verge_socks_port,
verge_socks_enabled: verge.verge_socks_enabled,
verge_port: verge.verge_port,
verge_http_enabled: verge.verge_http_enabled,
webdav_url: verge.webdav_url,
webdav_username: verge.webdav_username,
webdav_password: verge.webdav_password,
enable_tray_speed: verge.enable_tray_speed,
// enable_tray_icon: verge.enable_tray_icon,
tray_inline_proxy_groups: verge.tray_inline_proxy_groups,
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
auto_light_weight_minutes: verge.auto_light_weight_minutes,
enable_dns_settings: verge.enable_dns_settings,
home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
hover_jump_navigator_delay: verge.hover_jump_navigator_delay,
enable_external_controller: verge.enable_external_controller,
}
}
}

View File

@@ -5,13 +5,15 @@ pub mod network {
pub const DEFAULT_EXTERNAL_CONTROLLER: &str = "127.0.0.1:9097";
pub mod ports {
#[cfg(not(target_os = "windows"))]
#[allow(dead_code)]
pub const DEFAULT_REDIR: u16 = 7895;
#[cfg(target_os = "linux")]
#[allow(dead_code)]
pub const DEFAULT_TPROXY: u16 = 7896;
pub const DEFAULT_MIXED: u16 = 7897;
pub const DEFAULT_SOCKS: u16 = 7898;
pub const DEFAULT_HTTP: u16 = 7899;
#[allow(dead_code)]
pub const DEFAULT_EXTERNAL_CONTROLLER: u16 = 9097;
#[cfg(not(feature = "verge-dev"))]
pub const SINGLETON_SERVER: u16 = 33331;
@@ -37,8 +39,11 @@ pub mod timing {
pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(500);
pub const CONFIG_RELOAD_DELAY: Duration = Duration::from_millis(300);
pub const PROCESS_VERIFY_DELAY: Duration = Duration::from_millis(100);
#[allow(dead_code)]
pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20);
pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2);
#[allow(dead_code)]
pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300);
#[cfg(target_os = "windows")]
@@ -48,16 +53,40 @@ pub mod timing {
}
pub mod retry {
#[allow(dead_code)]
pub const EVENT_EMIT_THRESHOLD: u64 = 10;
#[allow(dead_code)]
pub const SWR_ERROR_RETRY: usize = 2;
}
pub mod files {
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
#[allow(dead_code)]
pub const DNS_CONFIG: &str = "dns_config.yaml";
#[allow(dead_code)]
pub const WINDOW_STATE: &str = "window_state.json";
}
pub mod process {
pub const VERGE_MIHOMO: &str = "verge-mihomo";
pub const VERGE_MIHOMO_ALPHA: &str = "verge-mihomo-alpha";
pub fn process_names() -> [&'static str; 2] {
[VERGE_MIHOMO, VERGE_MIHOMO_ALPHA]
}
#[cfg(windows)]
pub fn with_extension(name: &str) -> String {
format!("{}.exe", name)
}
#[cfg(not(windows))]
pub fn with_extension(name: &str) -> String {
name.to_string()
}
}
pub mod error_patterns {
pub const CONNECTION_ERRORS: &[&str] = &[
"Failed to create connection",

View File

@@ -1,6 +1,5 @@
#[cfg(target_os = "windows")]
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::time::{Duration, timeout};
@@ -42,21 +41,15 @@ impl AsyncProxyQuery {
pub async fn get_auto_proxy() -> AsyncAutoproxy {
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
Ok(Ok(proxy)) => {
logging!(
debug,
Type::Network,
"异步获取自动代理成功: enable={}, url={}",
proxy.enable,
proxy.url
);
log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url);
proxy
}
Ok(Err(e)) => {
logging!(warn, Type::Network, "Warning: 异步获取自动代理失败: {e}");
log::warn!(target: "app", "异步获取自动代理失败: {e}");
AsyncAutoproxy::default()
}
Err(_) => {
logging!(warn, Type::Network, "Warning: 异步获取自动代理超时");
log::warn!(target: "app", "异步获取自动代理超时");
AsyncAutoproxy::default()
}
}
@@ -66,22 +59,15 @@ impl AsyncProxyQuery {
pub async fn get_system_proxy() -> AsyncSysproxy {
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
Ok(Ok(proxy)) => {
logging!(
debug,
Type::Network,
"异步获取系统代理成功: enable={}, {}:{}",
proxy.enable,
proxy.host,
proxy.port
);
log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
proxy
}
Ok(Err(e)) => {
logging!(warn, Type::Network, "Warning: 异步获取系统代理失败: {e}");
log::warn!(target: "app", "异步获取系统代理失败: {e}");
AsyncSysproxy::default()
}
Err(_) => {
logging!(warn, Type::Network, "Warning: 异步获取系统代理超时");
log::warn!(target: "app", "异步获取系统代理超时");
AsyncSysproxy::default()
}
}
@@ -113,7 +99,7 @@ impl AsyncProxyQuery {
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
if result != 0 {
logging!(debug, Type::Network, "无法打开注册表项");
log::debug!(target: "app", "无法打开注册表项");
return Ok(AsyncAutoproxy::default());
}
@@ -139,7 +125,7 @@ impl AsyncProxyQuery {
.position(|&x| x == 0)
.unwrap_or(url_buffer.len());
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
logging!(debug, Type::Network, "从注册表读取到PAC URL: {pac_url}");
log::debug!(target: "app", "从注册表读取到PAC URL: {pac_url}");
}
// 2. 检查自动检测设置是否启用
@@ -164,11 +150,7 @@ impl AsyncProxyQuery {
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
if pac_enabled {
logging!(
debug,
Type::Network,
"PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}"
);
log::debug!(target: "app", "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}");
if pac_url.is_empty() && auto_detect != 0 {
pac_url = "auto-detect".into();
@@ -179,7 +161,7 @@ impl AsyncProxyQuery {
url: pac_url,
})
} else {
logging!(debug, Type::Network, "PAC配置未启用");
log::debug!(target: "app", "PAC配置未启用");
Ok(AsyncAutoproxy::default())
}
}
@@ -195,11 +177,7 @@ impl AsyncProxyQuery {
}
let stdout = String::from_utf8_lossy(&output.stdout);
crate::logging!(
debug,
crate::utils::logging::Type::Network,
"scutil output: {stdout}"
);
log::debug!(target: "app", "scutil output: {stdout}");
let mut pac_enabled = false;
let mut pac_url = String::new();
@@ -218,11 +196,7 @@ impl AsyncProxyQuery {
}
}
crate::logging!(
debug,
crate::utils::logging::Type::Network,
"解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}"
);
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
Ok(AsyncAutoproxy {
enable: pac_enabled && !pac_url.is_empty(),
@@ -347,12 +321,11 @@ impl AsyncProxyQuery {
&mut buffer_size,
);
let proxy_server = if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
let mut proxy_server = String::new();
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
String::from_utf16_lossy(&buffer[..end_pos])
} else {
String::new()
};
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
}
// 读取代理绕过列表
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
@@ -369,16 +342,14 @@ impl AsyncProxyQuery {
&mut bypass_buffer_size,
);
let bypass_list =
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
let end_pos = bypass_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(bypass_buffer.len());
String::from_utf16_lossy(&bypass_buffer[..end_pos])
} else {
String::new()
};
let mut bypass_list = String::new();
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
let end_pos = bypass_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(bypass_buffer.len());
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
}
RegCloseKey(hkey);
@@ -392,11 +363,7 @@ impl AsyncProxyQuery {
(proxy_server, 8080)
};
logging!(
debug,
Type::Network,
"从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}"
);
log::debug!(target: "app", "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}");
Ok(AsyncSysproxy {
enable: true,
@@ -419,7 +386,7 @@ impl AsyncProxyQuery {
}
let stdout = String::from_utf8_lossy(&output.stdout);
logging!(debug, Type::Network, "scutil proxy output: {stdout}");
log::debug!(target: "app", "scutil proxy output: {stdout}");
let mut http_enabled = false;
let mut http_host = String::new();

View File

@@ -1,24 +1,19 @@
use crate::constants::files::DNS_CONFIG;
use crate::{
config::Config,
logging,
process::AsyncHandler,
utils::{dirs, logging::Type},
};
use crate::{config::Config, utils::dirs};
use anyhow::Error;
use arc_swap::{ArcSwap, ArcSwapOption};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reqwest_dav::list_cmd::{ListEntity, ListFile};
use smartstring::alias::String;
use std::{
collections::HashMap,
env::{consts::OS, temp_dir},
fs,
io::Write,
path::PathBuf,
sync::Arc,
time::Duration,
};
use tokio::{fs, time::timeout};
use tokio::time::timeout;
use zip::write::SimpleFileOptions;
// 应用版本常量,来自 tauri.conf.json
@@ -45,35 +40,35 @@ enum Operation {
}
impl Operation {
const fn timeout(&self) -> u64 {
fn timeout(&self) -> u64 {
match self {
Self::Upload => TIMEOUT_UPLOAD,
Self::Download => TIMEOUT_DOWNLOAD,
Self::List => TIMEOUT_LIST,
Self::Delete => TIMEOUT_DELETE,
Operation::Upload => TIMEOUT_UPLOAD,
Operation::Download => TIMEOUT_DOWNLOAD,
Operation::List => TIMEOUT_LIST,
Operation::Delete => TIMEOUT_DELETE,
}
}
}
pub struct WebDavClient {
config: Arc<ArcSwapOption<WebDavConfig>>,
clients: Arc<ArcSwap<HashMap<Operation, reqwest_dav::Client>>>,
config: Arc<Mutex<Option<WebDavConfig>>>,
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
}
impl WebDavClient {
pub fn global() -> &'static Self {
pub fn global() -> &'static WebDavClient {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| Self {
config: Arc::new(ArcSwapOption::new(None)),
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
config: Arc::new(Mutex::new(None)),
clients: Arc::new(Mutex::new(HashMap::new())),
})
}
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
// 先尝试从缓存获取
{
let clients_map = self.clients.load();
if let Some(client) = clients_map.get(&op) {
let clients = self.clients.lock();
if let Some(client) = clients.get(&op) {
return Ok(client.clone());
}
}
@@ -81,13 +76,13 @@ impl WebDavClient {
// 获取或创建配置
let config = {
// 首先检查是否已有配置
let existing_config = self.config.load();
let existing_config = self.config.lock().as_ref().cloned();
if let Some(cfg_arc) = existing_config.clone() {
(*cfg_arc).clone()
if let Some(cfg) = existing_config {
cfg
} else {
// 释放锁后获取异步配置
let verge = Config::verge().await.data_arc();
let verge = Config::verge().await.latest_ref().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
@@ -99,17 +94,15 @@ impl WebDavClient {
let config = WebDavConfig {
url: verge
.webdav_url
.as_ref()
.cloned()
.unwrap_or_default()
.trim_end_matches('/')
.into(),
username: verge.webdav_username.as_ref().cloned().unwrap_or_default(),
password: verge.webdav_password.as_ref().cloned().unwrap_or_default(),
username: verge.webdav_username.unwrap_or_default(),
password: verge.webdav_password.unwrap_or_default(),
};
// 存储配置到 ArcSwapOption
self.config.store(Some(Arc::new(config.clone())));
// 重新获取锁并存储配置
*self.config.lock() = Some(config.clone());
config
}
};
@@ -145,14 +138,9 @@ impl WebDavClient {
.is_err()
{
match client.mkcol(dirs::BACKUP_DIR).await {
Ok(_) => logging!(info, Type::Backup, "Successfully created backup directory"),
Ok(_) => log::info!("Successfully created backup directory"),
Err(e) => {
logging!(
warn,
Type::Backup,
"Warning: Failed to create backup directory: {}",
e
);
log::warn!("Failed to create backup directory: {}", e);
// 清除缓存,强制下次重新尝试
self.reset();
return Err(anyhow::Error::msg(format!(
@@ -163,19 +151,18 @@ impl WebDavClient {
}
}
// 缓存客户端(替换 Arc<Mutex<HashMap<...>>> 的写法)
// 缓存客户端
{
let mut map = (**self.clients.load()).clone();
map.insert(op, client.clone());
self.clients.store(map.into());
let mut clients = self.clients.lock();
clients.insert(op, client.clone());
}
Ok(client)
}
pub fn reset(&self) {
self.config.store(None);
self.clients.store(Arc::new(HashMap::new()));
*self.config.lock() = None;
self.clients.lock().clear();
}
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
@@ -183,7 +170,7 @@ impl WebDavClient {
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name).into();
// 读取文件并上传,如果失败尝试一次重试
let file_content = fs::read(&file_path).await?;
let file_content = fs::read(&file_path)?;
// 添加超时保护
let upload_result = timeout(
@@ -194,11 +181,7 @@ impl WebDavClient {
match upload_result {
Err(_) => {
logging!(
warn,
Type::Backup,
"Warning: Upload timed out, retrying once"
);
log::warn!("Upload timed out, retrying once");
tokio::time::sleep(Duration::from_millis(500)).await;
timeout(
Duration::from_secs(TIMEOUT_UPLOAD),
@@ -209,11 +192,7 @@ impl WebDavClient {
}
Ok(Err(e)) => {
logging!(
warn,
Type::Backup,
"Warning: Upload failed, retrying once: {e}"
);
log::warn!("Upload failed, retrying once: {e}");
tokio::time::sleep(Duration::from_millis(500)).await;
timeout(
Duration::from_secs(TIMEOUT_UPLOAD),
@@ -233,7 +212,7 @@ impl WebDavClient {
let fut = async {
let response = client.get(path.as_str()).await?;
let content = response.bytes().await?;
fs::write(&storage_path, &content).await?;
fs::write(&storage_path, &content)?;
Ok::<(), Error>(())
};
@@ -271,19 +250,18 @@ impl WebDavClient {
}
}
pub async fn create_backup() -> Result<(String, PathBuf), Error> {
pub fn create_backup() -> Result<(String, PathBuf), Error> {
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
let zip_file_name: String = format!("{OS}-backup-{now}.zip").into();
let zip_path = temp_dir().join(zip_file_name.as_str());
let value = zip_path.clone();
let file = AsyncHandler::spawn_blocking(move || std::fs::File::create(&value)).await??;
let file = fs::File::create(&zip_path)?;
let mut zip = zip::ZipWriter::new(file);
zip.add_directory("profiles/", SimpleFileOptions::default())?;
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
if let Ok(mut entries) = fs::read_dir(dirs::app_profiles_dir()?).await {
while let Some(entry) = entries.next_entry().await? {
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let file_name_os = entry.file_name();
@@ -292,16 +270,16 @@ pub async fn create_backup() -> Result<(String, PathBuf), Error> {
.ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?;
let backup_path = format!("profiles/{}", file_name);
zip.start_file(backup_path, options)?;
let file_content = fs::read(&path).await?;
let file_content = fs::read(&path)?;
zip.write_all(&file_content)?;
}
}
}
zip.start_file(dirs::CLASH_CONFIG, options)?;
zip.write_all(fs::read(dirs::clash_path()?).await?.as_slice())?;
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
let verge_text = fs::read_to_string(dirs::verge_path()?).await?;
let mut verge_config: serde_json::Value = serde_yaml_ng::from_str(&verge_text)?;
let mut verge_config: serde_json::Value =
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");
@@ -310,14 +288,14 @@ pub async fn create_backup() -> Result<(String, PathBuf), Error> {
zip.start_file(dirs::VERGE_CONFIG, options)?;
zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?;
let dns_config_path = dirs::app_home_dir()?.join(DNS_CONFIG);
let dns_config_path = dirs::app_home_dir()?.join(dirs::DNS_CONFIG);
if dns_config_path.exists() {
zip.start_file(DNS_CONFIG, options)?;
zip.write_all(fs::read(&dns_config_path).await?.as_slice())?;
zip.start_file(dirs::DNS_CONFIG, options)?;
zip.write_all(fs::read(&dns_config_path)?.as_slice())?;
}
zip.start_file(dirs::PROFILE_YAML, options)?;
zip.write_all(fs::read(dirs::profiles_path()?).await?.as_slice())?;
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
zip.finish()?;
Ok((zip_file_name, zip_path))
}

View File

@@ -7,7 +7,6 @@ use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use crate::config::{Config, IVerge};
use crate::core::{async_proxy_query::AsyncProxyQuery, handle};
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use once_cell::sync::Lazy;
use smartstring::alias::String;
use sysproxy::{Autoproxy, Sysproxy};
@@ -75,7 +74,7 @@ struct ProxyConfig {
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
impl EventDrivenProxyManager {
pub fn global() -> &'static Self {
pub fn global() -> &'static EventDrivenProxyManager {
&PROXY_MANAGER
}
@@ -105,14 +104,14 @@ impl EventDrivenProxyManager {
let query = QueryRequest { response_tx: tx };
if self.query_sender.send(query).is_err() {
logging!(error, Type::Network, "发送查询请求失败,返回缓存数据");
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
return self.get_auto_proxy_cached().await;
}
match timeout(Duration::from_secs(5), rx).await {
Ok(Ok(result)) => result,
_ => {
logging!(warn, Type::Network, "Warning: 查询超时,返回缓存数据");
log::warn!(target: "app", "查询超时,返回缓存数据");
self.get_auto_proxy_cached().await
}
}
@@ -135,7 +134,7 @@ impl EventDrivenProxyManager {
fn send_event(&self, event: ProxyEvent) {
if let Err(e) = self.event_sender.send(event) {
logging!(error, Type::Network, "发送代理事件失败: {e}");
log::error!(target: "app", "发送代理事件失败: {e}");
}
}
@@ -144,7 +143,7 @@ impl EventDrivenProxyManager {
event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
) {
logging!(info, Type::Network, "事件驱动代理管理器启动");
log::info!(target: "app", "事件驱动代理管理器启动");
// 将 mpsc 接收器包装成 Stream避免每次循环创建 future
let mut event_stream = UnboundedReceiverStream::new(event_rx);
@@ -159,7 +158,7 @@ impl EventDrivenProxyManager {
loop {
tokio::select! {
Some(event) = event_stream.next() => {
logging!(debug, Type::Network, "处理代理事件: {event:?}");
log::debug!(target: "app", "处理代理事件: {event:?}");
let event_clone = event.clone(); // 保存一份副本用于后续检查
Self::handle_event(&state, event).await;
@@ -180,13 +179,13 @@ impl EventDrivenProxyManager {
// 定时检查代理设置
let config = Self::get_proxy_config().await;
if config.guard_enabled && config.sys_enabled {
logging!(debug, Type::Network, "定时检查代理设置");
log::debug!(target: "app", "定时检查代理设置");
Self::check_and_restore_proxy(&state).await;
}
}
else => {
// 两个通道都关闭时退出
logging!(info, Type::Network, "事件或查询通道关闭,代理管理器停止");
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
break;
}
}
@@ -202,7 +201,7 @@ impl EventDrivenProxyManager {
Self::initialize_proxy_state(state).await;
}
ProxyEvent::AppStopping => {
logging!(info, Type::Network, "清理代理状态");
log::info!(target: "app", "清理代理状态");
Self::update_state_timestamp(state, |s| {
s.sys_enabled = false;
s.pac_enabled = false;
@@ -225,7 +224,7 @@ impl EventDrivenProxyManager {
}
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
logging!(info, Type::Network, "初始化代理状态");
log::info!(target: "app", "初始化代理状态");
let config = Self::get_proxy_config().await;
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
@@ -240,17 +239,11 @@ impl EventDrivenProxyManager {
})
.await;
logging!(
info,
Type::Network,
"代理状态初始化完成: sys={}, pac={}",
config.sys_enabled,
config.pac_enabled
);
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
}
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
logging!(debug, Type::Network, "更新代理配置");
log::debug!(target: "app", "更新代理配置");
let config = Self::get_proxy_config().await;
@@ -267,7 +260,7 @@ impl EventDrivenProxyManager {
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Network, "应用正在退出,跳过系统代理守卫检查");
log::debug!(target: "app", "应用正在退出,跳过系统代理守卫检查");
return;
}
let (sys_enabled, pac_enabled) = {
@@ -279,7 +272,7 @@ impl EventDrivenProxyManager {
return;
}
logging!(debug, Type::Network, "检查代理状态");
log::debug!(target: "app", "检查代理状态");
if pac_enabled {
Self::check_and_restore_pac_proxy(state).await;
@@ -290,7 +283,7 @@ impl EventDrivenProxyManager {
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Network, "应用正在退出跳过PAC代理恢复检查");
log::debug!(target: "app", "应用正在退出跳过PAC代理恢复检查");
return;
}
@@ -303,9 +296,9 @@ impl EventDrivenProxyManager {
.await;
if !current.enable || current.url != expected.url {
logging!(info, Type::Network, "PAC代理设置异常正在恢复...");
log::info!(target: "app", "PAC代理设置异常正在恢复...");
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
logging!(error, Type::Network, "恢复PAC代理失败: {}", e);
log::error!(target: "app", "恢复PAC代理失败: {}", e);
}
sleep(Duration::from_millis(500)).await;
@@ -321,7 +314,7 @@ impl EventDrivenProxyManager {
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复检查");
log::debug!(target: "app", "应用正在退出,跳过系统代理恢复检查");
return;
}
@@ -334,9 +327,9 @@ impl EventDrivenProxyManager {
.await;
if !current.enable || current.host != expected.host || current.port != expected.port {
logging!(info, Type::Network, "系统代理设置异常,正在恢复...");
log::info!(target: "app", "系统代理设置异常,正在恢复...");
if let Err(e) = Self::restore_sys_proxy(&expected).await {
logging!(error, Type::Network, "恢复系统代理失败: {}", e);
log::error!(target: "app", "恢复系统代理失败: {}", e);
}
sleep(Duration::from_millis(500)).await;
@@ -387,7 +380,7 @@ impl EventDrivenProxyManager {
async fn get_proxy_config() -> ProxyConfig {
let (sys_enabled, pac_enabled, guard_enabled, guard_duration) = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_arc();
let verge = verge_config.latest_ref();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
@@ -406,7 +399,7 @@ impl EventDrivenProxyManager {
async fn get_expected_pac_config() -> Autoproxy {
let proxy_host = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_arc();
let verge = verge_config.latest_ref();
verge
.proxy_host
.clone()
@@ -424,13 +417,13 @@ impl EventDrivenProxyManager {
let (verge_mixed_port, proxy_host) = {
let verge_config = Config::verge().await;
let verge_ref = verge_config.latest_arc();
let verge_ref = verge_config.latest_ref();
(verge_ref.verge_mixed_port, verge_ref.proxy_host.clone())
};
let default_port = {
let clash_config = Config::clash().await;
clash_config.latest_arc().get_mixed_port()
clash_config.latest_ref().get_mixed_port()
};
let port = verge_mixed_port.unwrap_or(default_port);
@@ -450,7 +443,7 @@ impl EventDrivenProxyManager {
use crate::constants::bypass;
let verge_config = Config::verge().await;
let verge = verge_config.latest_arc();
let verge = verge_config.latest_ref();
let use_default = verge.use_default_bypass.unwrap_or(true);
let custom = verge.system_proxy_bypass.as_deref().unwrap_or("");
@@ -464,7 +457,7 @@ impl EventDrivenProxyManager {
#[cfg(target_os = "windows")]
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Network, "应用正在退出跳过PAC代理恢复");
log::debug!(target: "app", "应用正在退出跳过PAC代理恢复");
return Ok(());
}
Self::execute_sysproxy_command(&["pac", expected_url]).await
@@ -488,7 +481,7 @@ impl EventDrivenProxyManager {
#[cfg(target_os = "windows")]
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Network, "应用正在退出,跳过系统代理恢复");
log::debug!(target: "app", "应用正在退出,跳过系统代理恢复");
return Ok(());
}
let address = format!("{}:{}", expected.host, expected.port);
@@ -509,9 +502,8 @@ impl EventDrivenProxyManager {
#[cfg(target_os = "windows")]
async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> {
if handle::Handle::global().is_exiting() {
logging!(
debug,
Type::Network,
log::debug!(
target: "app",
"应用正在退出,取消调用 sysproxy.exe参数: {:?}",
args
);
@@ -526,14 +518,14 @@ impl EventDrivenProxyManager {
let binary_path = match dirs::service_path() {
Ok(path) => path,
Err(e) => {
logging!(error, Type::Network, "获取服务路径失败: {e}");
log::error!(target: "app", "获取服务路径失败: {e}");
return Err(e);
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
logging!(error, Type::Network, "sysproxy.exe 不存在");
log::error!(target: "app", "sysproxy.exe 不存在");
}
anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist");

View File

@@ -102,14 +102,14 @@ impl Handle {
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
}
// TODO 利用 &str 等缩短 Clone
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
let handle = Self::global();
let status_str = status.into();
let msg_str = msg.into();
if !*handle.startup_completed.read() {
handle.startup_errors.write().push(ErrorMessage {
let mut errors = handle.startup_errors.write();
errors.push(ErrorMessage {
status: status_str,
message: msg_str,
});
@@ -158,7 +158,7 @@ impl Handle {
.spawn(move || {
thread::sleep(timing::STARTUP_ERROR_DELAY);
let handle = Self::global();
let handle = Handle::global();
if handle.is_exiting() {
return;
}

View File

@@ -5,9 +5,10 @@ use crate::{
singleton_with_logging, utils::logging::Type,
};
use anyhow::{Result, bail};
use arc_swap::ArcSwap;
use parking_lot::Mutex;
use smartstring::alias::String;
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri::{AppHandle, Manager};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
/// Enum representing all available hotkey functions
@@ -28,16 +29,16 @@ pub enum HotkeyFunction {
impl fmt::Display for HotkeyFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::OpenOrCloseDashboard => "open_or_close_dashboard",
Self::ClashModeRule => "clash_mode_rule",
Self::ClashModeGlobal => "clash_mode_global",
Self::ClashModeDirect => "clash_mode_direct",
Self::ToggleSystemProxy => "toggle_system_proxy",
Self::ToggleTunMode => "toggle_tun_mode",
Self::EntryLightweightMode => "entry_lightweight_mode",
Self::Quit => "quit",
HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard",
HotkeyFunction::ClashModeRule => "clash_mode_rule",
HotkeyFunction::ClashModeGlobal => "clash_mode_global",
HotkeyFunction::ClashModeDirect => "clash_mode_direct",
HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy",
HotkeyFunction::ToggleTunMode => "toggle_tun_mode",
HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode",
HotkeyFunction::Quit => "quit",
#[cfg(target_os = "macos")]
Self::Hide => "hide",
HotkeyFunction::Hide => "hide",
};
write!(f, "{s}")
}
@@ -48,16 +49,16 @@ impl FromStr for HotkeyFunction {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"open_or_close_dashboard" => Ok(Self::OpenOrCloseDashboard),
"clash_mode_rule" => Ok(Self::ClashModeRule),
"clash_mode_global" => Ok(Self::ClashModeGlobal),
"clash_mode_direct" => Ok(Self::ClashModeDirect),
"toggle_system_proxy" => Ok(Self::ToggleSystemProxy),
"toggle_tun_mode" => Ok(Self::ToggleTunMode),
"entry_lightweight_mode" => Ok(Self::EntryLightweightMode),
"quit" => Ok(Self::Quit),
"open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard),
"clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule),
"clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal),
"clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect),
"toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy),
"toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode),
"entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode),
"quit" => Ok(HotkeyFunction::Quit),
#[cfg(target_os = "macos")]
"hide" => Ok(Self::Hide),
"hide" => Ok(HotkeyFunction::Hide),
_ => bail!("invalid hotkey function: {}", s),
}
}
@@ -75,8 +76,8 @@ pub enum SystemHotkey {
impl fmt::Display for SystemHotkey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::CmdQ => "CMD+Q",
Self::CmdW => "CMD+W",
SystemHotkey::CmdQ => "CMD+Q",
SystemHotkey::CmdW => "CMD+W",
};
write!(f, "{s}")
}
@@ -84,73 +85,86 @@ impl fmt::Display for SystemHotkey {
#[cfg(target_os = "macos")]
impl SystemHotkey {
pub const fn function(self) -> HotkeyFunction {
pub fn function(self) -> HotkeyFunction {
match self {
Self::CmdQ => HotkeyFunction::Quit,
Self::CmdW => HotkeyFunction::Hide,
SystemHotkey::CmdQ => HotkeyFunction::Quit,
SystemHotkey::CmdW => HotkeyFunction::Hide,
}
}
}
pub struct Hotkey {
current: ArcSwap<Vec<String>>,
current: Arc<Mutex<Vec<String>>>,
}
impl Hotkey {
fn new() -> Self {
Self {
current: ArcSwap::new(Arc::new(Vec::new())),
current: Arc::new(Mutex::new(Vec::new())),
}
}
/// Execute the function associated with a hotkey function enum
fn execute_function(function: HotkeyFunction) {
fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) {
let app_handle = app_handle.clone();
match function {
HotkeyFunction::OpenOrCloseDashboard => {
AsyncHandler::spawn(async move || {
crate::feat::open_or_close_dashboard().await;
notify_event(NotificationEvent::DashboardToggled).await;
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
});
}
HotkeyFunction::ClashModeRule => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("rule".into()).await;
notify_event(NotificationEvent::ClashModeChanged { mode: "Rule" }).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Rule" },
)
.await;
});
}
HotkeyFunction::ClashModeGlobal => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("global".into()).await;
notify_event(NotificationEvent::ClashModeChanged { mode: "Global" }).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Global" },
)
.await;
});
}
HotkeyFunction::ClashModeDirect => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("direct".into()).await;
notify_event(NotificationEvent::ClashModeChanged { mode: "Direct" }).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Direct" },
)
.await;
});
}
HotkeyFunction::ToggleSystemProxy => {
AsyncHandler::spawn(async move || {
feat::toggle_system_proxy().await;
notify_event(NotificationEvent::SystemProxyToggled).await;
notify_event(app_handle, NotificationEvent::SystemProxyToggled).await;
});
}
HotkeyFunction::ToggleTunMode => {
AsyncHandler::spawn(async move || {
feat::toggle_tun_mode(None).await;
notify_event(NotificationEvent::TunModeToggled).await;
notify_event(app_handle, NotificationEvent::TunModeToggled).await;
});
}
HotkeyFunction::EntryLightweightMode => {
AsyncHandler::spawn(async move || {
entry_lightweight_mode().await;
notify_event(NotificationEvent::LightweightModeEntered).await;
notify_event(app_handle, NotificationEvent::LightweightModeEntered).await;
});
}
HotkeyFunction::Quit => {
AsyncHandler::spawn(async move || {
notify_event(NotificationEvent::AppQuit).await;
notify_event(app_handle, NotificationEvent::AppQuit).await;
feat::quit().await;
});
}
@@ -158,7 +172,7 @@ impl Hotkey {
HotkeyFunction::Hide => {
AsyncHandler::spawn(async move || {
feat::hide().await;
notify_event(NotificationEvent::AppHidden).await;
notify_event(app_handle, NotificationEvent::AppHidden).await;
});
}
}
@@ -210,12 +224,14 @@ impl Hotkey {
let is_quit = matches!(function, HotkeyFunction::Quit);
manager.on_shortcut(hotkey, move |_app_handle, hotkey_event, event| {
manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
let hotkey_event_owned = *hotkey_event;
let event_owned = event;
let function_owned = function;
let is_quit_owned = is_quit;
let app_handle_cloned = app_handle.clone();
AsyncHandler::spawn(move || async move {
if event_owned.state == ShortcutState::Pressed {
logging!(
@@ -226,30 +242,30 @@ impl Hotkey {
);
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
if let Some(window) = handle::Handle::get_window()
if let Some(window) = app_handle_cloned.get_webview_window("main")
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned);
Self::execute_function(function_owned, &app_handle_cloned);
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly");
let is_enable_global_hotkey = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if is_enable_global_hotkey {
Self::execute_function(function_owned);
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);
Self::execute_function(function_owned, &app_handle_cloned);
}
}
}
@@ -272,9 +288,9 @@ impl Hotkey {
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
impl Hotkey {
pub async fn init(&self, skip: bool) -> Result<()> {
pub async fn init(&self) -> Result<()> {
let verge = Config::verge().await;
let enable_global_hotkey = !skip && verge.latest_arc().enable_global_hotkey.unwrap_or(true);
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
logging!(
debug,
@@ -283,8 +299,12 @@ impl Hotkey {
enable_global_hotkey
);
if !enable_global_hotkey {
return Ok(());
}
// Extract hotkeys data before async operations
let hotkeys = verge.latest_arc().hotkeys.as_ref().cloned();
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
if let Some(hotkeys) = hotkeys {
logging!(
@@ -340,7 +360,7 @@ impl Hotkey {
}
}
}
self.current.store(Arc::new(hotkeys));
self.current.lock().clone_from(&hotkeys);
} else {
logging!(debug, Type::Hotkey, "No hotkeys configured");
}
@@ -371,8 +391,8 @@ impl Hotkey {
pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
// Extract current hotkeys before async operations
let current_hotkeys = &*self.current.load();
let old_map = Self::get_map_from_vec(current_hotkeys);
let current_hotkeys = self.current.lock().clone();
let old_map = Self::get_map_from_vec(&current_hotkeys);
let new_map = Self::get_map_from_vec(&new_hotkeys);
let (del, add) = Self::get_diff(old_map, new_map);
@@ -386,7 +406,7 @@ impl Hotkey {
}
// Update the current hotkeys after all async operations
self.current.store(Arc::new(new_hotkeys));
*self.current.lock() = new_hotkeys;
Ok(())
}

View File

@@ -17,14 +17,12 @@ impl CoreManager {
use crate::constants::files::RUNTIME_CONFIG;
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
let clash_config = &Config::clash().await.latest_arc().0;
let clash_config = Config::clash().await.latest_ref().0.clone();
Config::runtime().await.edit_draft(|d| {
*d = IRuntime {
config: Some(clash_config.to_owned()),
exists_keys: vec![],
chain_logs: Default::default(),
}
*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, &clash_config, Some("# Clash Verge Runtime")).await?;
@@ -41,20 +39,25 @@ impl CoreManager {
return Ok((true, String::new()));
}
let _permit = self
.update_semaphore
.try_acquire()
.map_err(|_| anyhow!("Config update already in progress"))?;
self.perform_config_update().await
}
fn should_update_config(&self) -> Result<bool> {
let now = Instant::now();
let last = self.get_last_update();
let mut last = self.last_update.lock();
if let Some(last_time) = last
&& now.duration_since(*last_time) < timing::CONFIG_UPDATE_DEBOUNCE
if let Some(last_time) = *last
&& now.duration_since(last_time) < timing::CONFIG_UPDATE_DEBOUNCE
{
return Ok(false);
}
self.set_last_update(now);
*last = Some(now);
Ok(true)
}
@@ -137,7 +140,7 @@ impl CoreManager {
}
}
const fn is_connection_io_error(kind: std::io::ErrorKind) -> bool {
fn is_connection_io_error(kind: std::io::ErrorKind) -> bool {
matches!(
kind,
std::io::ErrorKind::ConnectionAborted

View File

@@ -1,5 +1,4 @@
use super::{CoreManager, RunningMode};
use crate::config::{Config, ConfigType, IVerge};
use crate::{
core::{
logger::CLASH_LOGGER,
@@ -42,17 +41,21 @@ impl CoreManager {
self.start_core().await
}
pub async fn change_core(&self, clash_core: &String) -> Result<(), String> {
if !IVerge::VALID_CLASH_CORES.contains(&clash_core.as_str()) {
return Err(format!("Invalid clash core: {}", clash_core).into());
pub async fn change_core(&self, clash_core: Option<String>) -> Result<(), String> {
use crate::config::{Config, ConfigType, IVerge};
let core = clash_core
.as_ref()
.ok_or_else(|| "Clash core cannot be None".to_string())?;
if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) {
return Err(format!("Invalid clash core: {}", core).into());
}
Config::verge().await.edit_draft(|d| {
d.clash_core = Some(clash_core.to_owned());
});
Config::verge().await.draft_mut().clash_core = clash_core;
Config::verge().await.apply();
let verge_data = Config::verge().await.latest_arc();
let verge_data = Config::verge().await.latest_ref().clone();
verge_data.save_file().await.map_err(|e| e.to_string())?;
let run_path = Config::generate_file(ConfigType::Run)
@@ -68,8 +71,7 @@ impl CoreManager {
#[cfg(target_os = "windows")]
self.wait_for_service_if_needed().await;
let value = SERVICE_MANAGER.lock().await.current();
let mode = match value {
let mode = match SERVICE_MANAGER.lock().await.current() {
ServiceStatus::Ready => RunningMode::Service,
_ => RunningMode::Sidecar,
};
@@ -85,7 +87,7 @@ impl CoreManager {
let needs_service = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_tun_mode
.unwrap_or(false);

View File

@@ -1,10 +1,12 @@
mod config;
mod lifecycle;
mod process;
mod state;
use anyhow::Result;
use arc_swap::{ArcSwap, ArcSwapOption};
use parking_lot::Mutex;
use std::{fmt, sync::Arc, time::Instant};
use tokio::sync::Semaphore;
use crate::process::CommandChildGuard;
use crate::singleton_lazy;
@@ -28,21 +30,22 @@ impl fmt::Display for RunningMode {
#[derive(Debug)]
pub struct CoreManager {
state: ArcSwap<State>,
last_update: ArcSwapOption<Instant>,
state: Arc<Mutex<State>>,
update_semaphore: Arc<Semaphore>,
last_update: Arc<Mutex<Option<Instant>>>,
}
#[derive(Debug)]
struct State {
running_mode: ArcSwap<RunningMode>,
child_sidecar: ArcSwapOption<CommandChildGuard>,
running_mode: Arc<RunningMode>,
child_sidecar: Option<CommandChildGuard>,
}
impl Default for State {
fn default() -> Self {
Self {
running_mode: ArcSwap::new(Arc::new(RunningMode::NotRunning)),
child_sidecar: ArcSwapOption::new(None),
running_mode: Arc::new(RunningMode::NotRunning),
child_sidecar: None,
}
}
}
@@ -50,44 +53,28 @@ impl Default for State {
impl Default for CoreManager {
fn default() -> Self {
Self {
state: ArcSwap::new(Arc::new(State::default())),
last_update: ArcSwapOption::new(None),
state: Arc::new(Mutex::new(State::default())),
update_semaphore: Arc::new(Semaphore::new(1)),
last_update: Arc::new(Mutex::new(None)),
}
}
}
impl CoreManager {
pub fn get_running_mode(&self) -> Arc<RunningMode> {
Arc::clone(&self.state.load().running_mode.load())
}
pub fn take_child_sidecar(&self) -> Option<CommandChildGuard> {
self.state
.load()
.child_sidecar
.swap(None)
.and_then(|arc| Arc::try_unwrap(arc).ok())
}
pub fn get_last_update(&self) -> Option<Arc<Instant>> {
self.last_update.load_full()
Arc::clone(&self.state.lock().running_mode)
}
pub fn set_running_mode(&self, mode: RunningMode) {
let state = self.state.load();
state.running_mode.store(Arc::new(mode));
self.state.lock().running_mode = Arc::new(mode);
}
pub fn set_running_child_sidecar(&self, child: CommandChildGuard) {
let state = self.state.load();
state.child_sidecar.store(Some(Arc::new(child)));
}
pub fn set_last_update(&self, time: Instant) {
self.last_update.store(Some(Arc::new(time)));
self.state.lock().child_sidecar = Some(child);
}
pub async fn init(&self) -> Result<()> {
self.cleanup_orphaned_processes().await?;
self.start_core().await?;
Ok(())
}

View File

@@ -0,0 +1,244 @@
use super::CoreManager;
#[cfg(windows)]
use crate::process::AsyncHandler;
use crate::{
constants::{process, timing},
logging,
utils::logging::Type,
};
use anyhow::Result;
#[cfg(windows)]
use anyhow::anyhow;
impl CoreManager {
pub async fn cleanup_orphaned_processes(&self) -> Result<()> {
logging!(info, Type::Core, "Cleaning orphaned mihomo processes");
let current_pid = self
.state
.lock()
.child_sidecar
.as_ref()
.and_then(|c| c.pid());
let target_processes = process::process_names();
let process_futures = target_processes.iter().map(|&name| {
let process_name = process::with_extension(name);
self.find_processes_by_name(process_name, name)
});
let process_results = futures::future::join_all(process_futures).await;
let pids_to_kill: Vec<_> = process_results
.into_iter()
.filter_map(Result::ok)
.flat_map(|(pids, name)| {
pids.into_iter()
.filter(move |&pid| Some(pid) != current_pid)
.map(move |pid| (pid, name.clone()))
})
.collect();
if pids_to_kill.is_empty() {
return Ok(());
}
let kill_futures = pids_to_kill
.iter()
.map(|(pid, name)| self.kill_process_verified(*pid, name.clone()));
let killed_count = futures::future::join_all(kill_futures)
.await
.into_iter()
.filter(|&success| success)
.count();
if killed_count > 0 {
logging!(
info,
Type::Core,
"Cleaned {} orphaned processes",
killed_count
);
}
Ok(())
}
async fn find_processes_by_name(
&self,
process_name: String,
_target: &str,
) -> Result<(Vec<u32>, String)> {
#[cfg(windows)]
{
use std::mem;
use winapi::um::{
handleapi::CloseHandle,
tlhelp32::{
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
},
};
let process_name_clone = process_name.clone();
let pids = AsyncHandler::spawn_blocking(move || -> Result<Vec<u32>> {
let mut pids = Vec::new();
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
return Err(anyhow!("Failed to create process snapshot"));
}
let mut pe32: PROCESSENTRY32W = mem::zeroed();
pe32.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
if Process32FirstW(snapshot, &mut pe32) != 0 {
loop {
let end_pos = pe32
.szExeFile
.iter()
.position(|&x| x == 0)
.unwrap_or(pe32.szExeFile.len());
let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]);
if exe_file.eq_ignore_ascii_case(&process_name_clone) {
pids.push(pe32.th32ProcessID);
}
if Process32NextW(snapshot, &mut pe32) == 0 {
break;
}
}
}
CloseHandle(snapshot);
}
Ok(pids)
})
.await??;
Ok((pids, process_name))
}
#[cfg(not(windows))]
{
let cmd = if cfg!(target_os = "macos") {
"pgrep"
} else {
"pidof"
};
let output = tokio::process::Command::new(cmd)
.arg(&process_name)
.output()
.await?;
if !output.status.success() {
return Ok((Vec::new(), process_name));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let pids: Vec<u32> = stdout
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
Ok((pids, process_name))
}
}
async fn kill_process_verified(&self, pid: u32, process_name: String) -> bool {
#[cfg(windows)]
let success = {
use winapi::um::{
handleapi::CloseHandle,
processthreadsapi::{OpenProcess, TerminateProcess},
winnt::{HANDLE, PROCESS_TERMINATE},
};
AsyncHandler::spawn_blocking(move || unsafe {
let handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
if handle.is_null() {
return false;
}
let result = TerminateProcess(handle, 1) != 0;
CloseHandle(handle);
result
})
.await
.unwrap_or(false)
};
#[cfg(not(windows))]
let success = tokio::process::Command::new("kill")
.args(["-9", &pid.to_string()])
.output()
.await
.map(|output| output.status.success())
.unwrap_or(false);
if !success {
return false;
}
tokio::time::sleep(timing::PROCESS_VERIFY_DELAY).await;
if self.is_process_running(pid).await.unwrap_or(false) {
logging!(
warn,
Type::Core,
"Process {} (PID: {}) still running after termination",
process_name,
pid
);
false
} else {
logging!(
info,
Type::Core,
"Terminated process {} (PID: {})",
process_name,
pid
);
true
}
}
async fn is_process_running(&self, pid: u32) -> Result<bool> {
#[cfg(windows)]
{
use winapi::{
shared::minwindef::DWORD,
um::{
handleapi::CloseHandle,
processthreadsapi::{GetExitCodeProcess, OpenProcess},
winnt::{HANDLE, PROCESS_QUERY_INFORMATION},
},
};
AsyncHandler::spawn_blocking(move || unsafe {
let handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if handle.is_null() {
return Ok(false);
}
let mut exit_code: DWORD = 0;
let result = GetExitCodeProcess(handle, &mut exit_code);
CloseHandle(handle);
Ok(result != 0 && exit_code == 259)
})
.await?
}
#[cfg(not(windows))]
{
let output = tokio::process::Command::new("ps")
.args(["-p", &pid.to_string()])
.output()
.await?;
Ok(output.status.success() && !output.stdout.is_empty())
}
}
}

View File

@@ -32,7 +32,7 @@ impl CoreManager {
let config_file = Config::generate_file(crate::config::ConfigType::Run).await?;
let app_handle = handle::Handle::app_handle();
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
let (mut rx, child) = app_handle
@@ -62,12 +62,8 @@ impl CoreManager {
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let mut now = DeferredNow::default();
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
write_sidecar_log(
shared_writer.lock().await,
&mut now,
Level::Error,
&message,
);
let w = shared_writer.lock().await;
write_sidecar_log(w, &mut now, Level::Error, &message);
CLASH_LOGGER.append_log(message).await;
}
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
@@ -79,12 +75,8 @@ impl CoreManager {
} else {
CompactString::from("Process terminated")
};
write_sidecar_log(
shared_writer.lock().await,
&mut now,
Level::Info,
&message,
);
let w = shared_writer.lock().await;
write_sidecar_log(w, &mut now, Level::Info, &message);
CLASH_LOGGER.clear_logs().await;
break;
}
@@ -101,7 +93,8 @@ impl CoreManager {
defer! {
self.set_running_mode(RunningMode::NotRunning);
}
if let Some(child) = self.take_child_sidecar() {
let mut state = self.state.lock();
if let Some(child) = state.child_sidecar.take() {
let pid = child.pid();
drop(child);
logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid);

View File

@@ -1,4 +1,3 @@
use super::handle::Handle;
use crate::{
constants::{retry, timing},
logging,
@@ -92,23 +91,30 @@ impl NotificationSystem {
}
fn worker_loop(rx: mpsc::Receiver<FrontendEvent>) {
use super::handle::Handle;
let handle = Handle::global();
while !handle.is_exiting() {
match rx.try_recv() {
match rx.recv() {
Ok(event) => Self::process_event(handle, event),
Err(mpsc::TryRecvError::Disconnected) => break,
Err(mpsc::TryRecvError::Empty) => break,
Err(e) => {
logging!(
error,
Type::System,
"receive event error, stop notification worker: {}",
e
);
break;
}
}
}
}
// Clippy 似乎对 parking lot 的 RwLock 有误报,这里禁用相关警告
#[allow(clippy::significant_drop_tightening)]
fn process_event(handle: &super::handle::Handle, event: FrontendEvent) {
let binding = handle.notification_system.read();
let system = match binding.as_ref() {
Some(s) => s,
None => return,
let system_guard = handle.notification_system.read();
let Some(system) = system_guard.as_ref() else {
return;
};
if system.should_skip_event(&event) {

View File

@@ -1,6 +1,5 @@
use crate::{
config::Config,
core::tray,
logging, logging_error,
utils::{dirs, init::service_writer_config, logging::Type},
};
@@ -136,7 +135,7 @@ async fn uninstall_service() -> Result<()> {
let elevator = crate::utils::help::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(uninstall_shell).status()?,
_ => StdCommand::new(elevator)
_ => StdCommand::new(elevator.clone())
.arg("sh")
.arg("-c")
.arg(uninstall_shell)
@@ -177,7 +176,7 @@ async fn install_service() -> Result<()> {
let elevator = crate::utils::help::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(install_shell).status()?,
_ => StdCommand::new(elevator)
_ => StdCommand::new(elevator.clone())
.arg("sh")
.arg("-c")
.arg(install_shell)
@@ -220,6 +219,8 @@ async fn reinstall_service() -> Result<()> {
#[cfg(target_os = "macos")]
async fn uninstall_service() -> Result<()> {
use crate::utils::i18n::t;
logging!(info, Type::Service, "uninstall service");
let binary_path = dirs::service_path()?;
@@ -231,9 +232,7 @@ async fn uninstall_service() -> Result<()> {
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
crate::utils::i18n::sync_locale().await;
let prompt = rust_i18n::t!("service.adminPrompt").to_string();
let prompt = t("Service Administrator Prompt").await;
let command = format!(
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
);
@@ -256,6 +255,8 @@ async fn uninstall_service() -> Result<()> {
#[cfg(target_os = "macos")]
async fn install_service() -> Result<()> {
use crate::utils::i18n::t;
logging!(info, Type::Service, "install service");
let binary_path = dirs::service_path()?;
@@ -267,9 +268,7 @@ async fn install_service() -> Result<()> {
let install_shell: String = install_path.to_string_lossy().into_owned();
crate::utils::i18n::sync_locale().await;
let prompt = rust_i18n::t!("service.adminPrompt").to_string();
let prompt = t("Service Administrator Prompt").await;
let command = format!(
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
);
@@ -353,7 +352,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
logging!(info, Type::Service, "尝试使用现有服务启动核心");
let verge_config = Config::verge().await;
let clash_core = verge_config.latest_arc().get_valid_clash_core();
let clash_core = verge_config.latest_ref().get_valid_clash_core();
drop(verge_config);
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
@@ -449,7 +448,7 @@ impl ServiceManager {
Self(ServiceStatus::Unavailable("Need Checks".into()))
}
pub const fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
pub fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
Some(clash_verge_service_ipc::IpcConfig {
default_timeout: Duration::from_millis(30),
retry_delay: Duration::from_millis(250),
@@ -532,7 +531,6 @@ impl ServiceManager {
return Err(anyhow::anyhow!("服务不可用: {}", reason));
}
}
let _ = tray::Tray::global().update_menu().await;
Ok(())
}
}

View File

@@ -15,7 +15,6 @@ use sysproxy::{Autoproxy, Sysproxy};
use tauri_plugin_autostart::ManagerExt;
pub struct Sysopt {
initialed: AtomicBool,
update_sysproxy: AtomicBool,
reset_sysproxy: AtomicBool,
}
@@ -31,12 +30,12 @@ static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12
async fn get_bypass() -> String {
let use_default = Config::verge()
.await
.latest_arc()
.latest_ref()
.use_default_bypass
.unwrap_or(true);
let res = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.system_proxy_bypass.clone()
};
let custom_bypass = match res {
@@ -84,8 +83,7 @@ async fn execute_sysproxy_command(args: Vec<std::string::String>) -> Result<()>
impl Default for Sysopt {
fn default() -> Self {
Self {
initialed: AtomicBool::new(false),
Sysopt {
update_sysproxy: AtomicBool::new(false),
reset_sysproxy: AtomicBool::new(false),
}
@@ -96,22 +94,17 @@ impl Default for Sysopt {
singleton_lazy!(Sysopt, SYSOPT, Sysopt::default);
impl Sysopt {
pub fn is_initialed(&self) -> bool {
self.initialed.load(Ordering::SeqCst)
}
pub fn init_guard_sysproxy(&self) -> Result<()> {
// 使用事件驱动代理管理器
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_app_started();
logging!(info, Type::Core, "已启用事件驱动代理守卫");
log::info!(target: "app", "已启用事件驱动代理守卫");
Ok(())
}
/// init the sysproxy
pub async fn update_sysproxy(&self) -> Result<()> {
self.initialed.store(true, Ordering::SeqCst);
if self
.update_sysproxy
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
@@ -124,17 +117,17 @@ impl Sysopt {
}
let port = {
let verge_port = Config::verge().await.latest_arc().verge_mixed_port;
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
match verge_port {
Some(port) => port,
None => Config::clash().await.latest_arc().get_mixed_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().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
@@ -231,22 +224,14 @@ impl Sysopt {
let mut sysproxy: Sysproxy = match Sysproxy::get_system_proxy() {
Ok(sp) => sp,
Err(e) => {
logging!(
warn,
Type::Core,
"Warning: 重置代理时获取系统代理配置失败: {e}, 使用默认配置"
);
log::warn!(target: "app", "重置代理时获取系统代理配置失败: {e}, 使用默认配置");
Sysproxy::default()
}
};
let mut autoproxy = match Autoproxy::get_auto_proxy() {
Ok(ap) => ap,
Err(e) => {
logging!(
warn,
Type::Core,
"Warning: 重置代理时获取自动代理配置失败: {e}, 使用默认配置"
);
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
Autoproxy::default()
}
};
@@ -266,7 +251,7 @@ impl Sysopt {
/// update the startup
pub async fn update_launch(&self) -> Result<()> {
let enable_auto_launch = { Config::verge().await.latest_arc().enable_auto_launch };
let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch };
let is_enable = enable_auto_launch.unwrap_or(false);
logging!(
info,
@@ -280,14 +265,14 @@ impl Sysopt {
{
if is_enable {
if let Err(e) = startup_shortcut::create_shortcut().await {
logging!(error, Type::Setup, "创建启动快捷方式失败: {e}");
log::error!(target: "app", "创建启动快捷方式失败: {e}");
// 如果快捷方式创建失败,回退到原来的方法
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
}
} else if let Err(e) = startup_shortcut::remove_shortcut().await {
logging!(error, Type::Setup, "删除启动快捷方式失败: {e}");
log::error!(target: "app", "删除启动快捷方式失败: {e}");
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
@@ -322,11 +307,11 @@ impl Sysopt {
{
match startup_shortcut::is_shortcut_enabled() {
Ok(enabled) => {
logging!(info, Type::System, "快捷方式自启动状态: {enabled}");
log::info!(target: "app", "快捷方式自启动状态: {enabled}");
return Ok(enabled);
}
Err(e) => {
logging!(error, Type::System, "检查快捷方式失败,尝试原来的方法: {e}");
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {e}");
}
}
}
@@ -337,11 +322,11 @@ impl Sysopt {
match autostart_manager.is_enabled() {
Ok(status) => {
logging!(info, Type::System, "Auto launch status: {status}");
log::info!(target: "app", "Auto launch status: {status}");
Ok(status)
}
Err(e) => {
logging!(error, Type::System, "Failed to get auto launch status: {e}");
log::error!(target: "app", "Failed to get auto launch status: {e}");
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
}
}

View File

@@ -1,7 +1,4 @@
use crate::{
config::Config, core::sysopt::Sysopt, feat, logging, logging_error, singleton,
utils::logging::Type,
};
use crate::{config::Config, feat, logging, logging_error, singleton, utils::logging::Type};
use anyhow::{Context, Result};
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use parking_lot::RwLock;
@@ -13,9 +10,7 @@ use std::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
time::Duration,
};
use tokio::time::{sleep, timeout};
type TaskID = u64;
@@ -46,7 +41,7 @@ singleton!(Timer, TIMER_INSTANCE);
impl Timer {
fn new() -> Self {
Self {
Timer {
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
timer_map: Arc::new(RwLock::new(HashMap::new())),
timer_count: AtomicU64::new(1),
@@ -100,16 +95,10 @@ impl Timer {
// Collect profiles that need immediate update
let profiles_to_update =
if let Some(items) = Config::profiles().await.latest_arc().get_items() {
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
items
.iter()
.filter_map(|item| {
let allow_auto_update =
item.option.as_ref()?.allow_auto_update.unwrap_or_default();
if !allow_auto_update {
return None;
}
let interval = item.option.as_ref()?.update_interval? as i64;
let updated = item.updated? as i64;
let uid = item.uid.as_ref()?;
@@ -154,17 +143,19 @@ impl Timer {
/// 每 3 秒更新系统托盘菜单,总共执行 3 次
pub fn add_update_tray_menu_task(&self) -> Result<()> {
let tid = self.timer_count.fetch_add(1, Ordering::SeqCst);
let delay_timer = self.delay_timer.write();
let task = TaskBuilder::default()
.set_task_id(tid)
.set_maximum_parallel_runnable_num(1)
.set_frequency_count_down_by_seconds(3, 3)
.spawn_async_routine(|| async move {
logging!(debug, Type::Timer, "Updating tray menu");
crate::core::tray::Tray::global().update_menu().await
logging!(info, Type::Timer, "Updating tray menu");
crate::core::tray::Tray::global()
.update_tray_display()
.await
})
.context("failed to create update tray menu timer task")?;
self.delay_timer
.write()
delay_timer
.add_task(task)
.context("failed to add update tray menu timer task")?;
Ok(())
@@ -193,12 +184,14 @@ impl Timer {
// 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) => {
self.timer_map.write().remove(&uid);
let value = self.delay_timer.write().remove_task(tid);
if let Err(e) = value {
timer_map.remove(&uid);
if let Err(e) = delay_timer.remove_task(tid) {
logging!(
warn,
Type::Timer,
@@ -218,13 +211,12 @@ impl Timer {
last_run: chrono::Local::now().timestamp(),
};
self.timer_map.write().insert(uid.clone(), task);
timer_map.insert(uid.clone(), task);
operations_to_add.push((uid, tid, interval));
}
DiffFlag::Mod(tid, interval) => {
// Remove old task first
let value = self.delay_timer.write().remove_task(tid);
if let Err(e) = value {
if let Err(e) = delay_timer.remove_task(tid) {
logging!(
warn,
Type::Timer,
@@ -242,7 +234,7 @@ impl Timer {
last_run: chrono::Local::now().timestamp(),
};
self.timer_map.write().insert(uid.clone(), task);
timer_map.insert(uid.clone(), task);
operations_to_add.push((uid, tid, interval));
}
}
@@ -252,8 +244,8 @@ impl Timer {
// Now perform async operations without holding locks
for (uid, tid, interval) in operations_to_add {
// Re-acquire locks for individual operations
let delay_timer = self.delay_timer.write();
if let Err(e) = self.add_task(&delay_timer, uid.clone(), tid, interval) {
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);
// Rollback on failure - remove from timer_map
@@ -270,7 +262,7 @@ impl Timer {
async fn gen_map(&self) -> HashMap<String, u64> {
let mut new_map = HashMap::new();
if let Some(items) = Config::profiles().await.latest_arc().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()
&& let Some(allow_auto_update) = option.allow_auto_update
@@ -370,7 +362,7 @@ impl Timer {
/// Add a timer task with better error handling
fn add_task(
&self,
delay_timer: &DelayTimer,
delay_timer: &mut DelayTimer,
uid: String,
tid: TaskID,
minutes: u64,
@@ -392,8 +384,7 @@ impl Timer {
.spawn_async_routine(move || {
let uid = uid.clone();
Box::pin(async move {
Self::wait_until_sysopt(Duration::from_millis(1000)).await;
Self::async_task(&uid).await;
Self::async_task(uid).await;
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
})
.context("failed to create timer task")?;
@@ -422,15 +413,13 @@ impl Timer {
};
// Get the profile updated timestamp - now safe to await
let items = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_arc();
match profiles_guard.get_items() {
Some(i) => i.clone(),
None => {
logging!(warn, Type::Timer, "获取配置列表失败");
return None;
}
let config_profiles = Config::profiles().await;
let profiles = config_profiles.data_ref().clone();
let items = match profiles.get_items() {
Some(i) => i,
None => {
logging!(warn, Type::Timer, "获取配置列表失败");
return None;
}
};
@@ -479,14 +468,14 @@ impl Timer {
}
/// Async task with better error handling and logging
async fn async_task(uid: &String) {
async fn async_task(uid: String) {
let task_start = std::time::Instant::now();
logging!(info, Type::Timer, "Running timer task for profile: {}", uid);
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
Self::emit_update_event(uid, true);
Self::emit_update_event(&uid, true);
let is_current = Config::profiles().await.latest_arc().current.as_ref() == Some(uid);
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(&uid);
logging!(
info,
Type::Timer,
@@ -495,7 +484,7 @@ impl Timer {
is_current
);
feat::update_profile(uid, None, is_current, false).await
feat::update_profile(uid.clone(), None, Some(is_current)).await
})
.await
{
@@ -520,17 +509,7 @@ impl Timer {
}
// Emit completed event
Self::emit_update_event(uid, false);
}
async fn wait_until_sysopt(max_wait: Duration) {
let _ = timeout(max_wait, async {
while !Sysopt::global().is_initialed() {
logging!(warn, Type::Timer, "Waiting for Sysopt to be initialized...");
sleep(Duration::from_millis(30)).await;
}
})
.await;
Self::emit_update_event(&uid, false);
}
}

View File

@@ -1,12 +1,5 @@
use rust_i18n::t;
use std::{borrow::Cow, sync::Arc};
fn to_arc_str(value: Cow<'static, str>) -> Arc<str> {
match value {
Cow::Borrowed(s) => Arc::from(s),
Cow::Owned(s) => Arc::from(s.into_boxed_str()),
}
}
use crate::utils::i18n::t;
use std::sync::Arc;
macro_rules! define_menu {
($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => {
@@ -18,10 +11,9 @@ macro_rules! define_menu {
pub struct MenuIds;
impl MenuTexts {
pub fn new() -> Self {
Self {
$($field: to_arc_str(t!($text)),)+
}
pub async fn new() -> Self {
let ($($field,)+) = futures::join!($(t($text),)+);
Self { $($field,)+ }
}
}
@@ -32,26 +24,24 @@ macro_rules! define_menu {
}
define_menu! {
dashboard => DASHBOARD, "tray_dashboard", "tray.dashboard",
rule_mode => RULE_MODE, "tray_rule_mode", "tray.ruleMode",
global_mode => GLOBAL_MODE, "tray_global_mode", "tray.globalMode",
direct_mode => DIRECT_MODE, "tray_direct_mode", "tray.directMode",
profiles => PROFILES, "tray_profiles", "tray.profiles",
proxies => PROXIES, "tray_proxies", "tray.proxies",
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "tray.lightweightMode",
copy_env => COPY_ENV, "tray_copy_env", "tray.copyEnv",
conf_dir => CONF_DIR, "tray_conf_dir", "tray.confDir",
core_dir => CORE_DIR, "tray_core_dir", "tray.coreDir",
logs_dir => LOGS_DIR, "tray_logs_dir", "tray.logsDir",
open_dir => OPEN_DIR, "tray_open_dir", "tray.openDir",
app_log => APP_LOG, "tray_app_log", "tray.appLog",
core_log => CORE_LOG, "tray_core_log", "tray.coreLog",
restart_clash => RESTART_CLASH, "tray_restart_clash", "tray.restartClash",
restart_app => RESTART_APP, "tray_restart_app", "tray.restartApp",
verge_version => VERGE_VERSION, "tray_verge_version", "tray.vergeVersion",
more => MORE, "tray_more", "tray.more",
exit => EXIT, "tray_exit", "tray.exit",
dashboard => DASHBOARD, "tray_dashboard", "Dashboard",
rule_mode => RULE_MODE, "tray_rule_mode", "Rule Mode",
global_mode => GLOBAL_MODE, "tray_global_mode", "Global Mode",
direct_mode => DIRECT_MODE, "tray_direct_mode", "Direct Mode",
profiles => PROFILES, "tray_profiles", "Profiles",
proxies => PROXIES, "tray_proxies", "Proxies",
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "System Proxy",
tun_mode => TUN_MODE, "tray_tun_mode", "TUN Mode",
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "Close All Connections",
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "LightWeight Mode",
copy_env => COPY_ENV, "tray_copy_env", "Copy Env",
conf_dir => CONF_DIR, "tray_conf_dir", "Conf Dir",
core_dir => CORE_DIR, "tray_core_dir", "Core Dir",
logs_dir => LOGS_DIR, "tray_logs_dir", "Logs Dir",
open_dir => OPEN_DIR, "tray_open_dir", "Open Dir",
restart_clash => RESTART_CLASH, "tray_restart_clash", "Restart Clash Core",
restart_app => RESTART_APP, "tray_restart_app", "Restart App",
verge_version => VERGE_VERSION, "tray_verge_version", "Verge Version",
more => MORE, "tray_more", "More",
exit => EXIT, "tray_exit", "Exit",
}

View File

@@ -1,11 +1,10 @@
use once_cell::sync::OnceCell;
use tauri::Emitter;
use tauri::tray::TrayIconBuilder;
use tauri_plugin_mihomo::models::Proxies;
use tokio::fs;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::config::PrfSelected;
use crate::core::service;
use crate::module::lightweight;
use crate::process::AsyncHandler;
use crate::utils::window_manager::WindowManager;
@@ -15,7 +14,7 @@ use crate::{
feat, logging,
module::lightweight::is_in_lightweight_mode,
singleton_lazy,
utils::{dirs::find_target_icons, i18n},
utils::{dirs::find_target_icons, i18n::t},
};
use super::handle;
@@ -24,10 +23,9 @@ use futures::future::join_all;
use parking_lot::Mutex;
use smartstring::alias::String;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::{
fs,
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
@@ -56,18 +54,15 @@ fn get_tray_click_debounce() -> &'static Mutex<Instant> {
fn should_handle_tray_click() -> bool {
let debounce_lock = get_tray_click_debounce();
let mut last_click = debounce_lock.lock();
let now = Instant::now();
if now.duration_since(*debounce_lock.lock()) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
*debounce_lock.lock() = now;
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
*last_click = now;
true
} else {
logging!(
debug,
Type::Tray,
"托盘点击被防抖机制忽略,距离上次点击 {}ms",
now.duration_since(*debounce_lock.lock()).as_millis()
);
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
now.duration_since(*last_click).as_millis());
false
}
}
@@ -86,11 +81,11 @@ pub struct Tray {
impl TrayState {
pub async fn get_common_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_arc();
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
&& let Ok(Some(common_icon_path)) = find_target_icons("common")
&& let Ok(icon_data) = fs::read(common_icon_path).await
&& let Ok(icon_data) = fs::read(common_icon_path)
{
return (true, icon_data);
}
@@ -123,11 +118,11 @@ impl TrayState {
}
pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_arc();
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
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
&& let Ok(icon_data) = fs::read(sysproxy_icon_path).await
&& let Ok(icon_data) = fs::read(sysproxy_icon_path)
{
return (true, icon_data);
}
@@ -160,11 +155,11 @@ impl TrayState {
}
pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_arc();
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
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun")
&& let Ok(icon_data) = fs::read(tun_icon_path).await
&& let Ok(icon_data) = fs::read(tun_icon_path)
{
return (true, icon_data);
}
@@ -198,7 +193,7 @@ impl TrayState {
impl Default for Tray {
fn default() -> Self {
Self {
Tray {
last_menu_update: Mutex::new(None),
menu_updating: AtomicBool::new(false),
}
@@ -211,7 +206,7 @@ singleton_lazy!(Tray, TRAY, Tray::default);
impl Tray {
pub async fn init(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘初始化");
log::debug!(target: "app", "应用正在退出,跳过托盘初始化");
return Ok(());
}
@@ -219,15 +214,11 @@ impl Tray {
match self.create_tray_from_handle(app_handle).await {
Ok(_) => {
logging!(info, Type::Tray, "System tray created successfully");
log::info!(target: "app", "System tray created successfully");
}
Err(e) => {
// Don't return error, let application continue running without tray
logging!(
warn,
Type::Tray,
"System tray creation failed: {e}, Application will continue running without tray icon",
);
log::warn!(target: "app", "System tray creation failed: {}, Application will continue running without tray icon", e);
}
}
// TODO: 初始化时,暂时使用此方法更新系统托盘菜单,有效避免代理节点菜单空白
@@ -238,12 +229,12 @@ impl Tray {
/// 更新托盘点击行为
pub async fn update_click_behavior(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘点击行为更新");
log::debug!(target: "app", "应用正在退出,跳过托盘点击行为更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event = tray_event.unwrap_or_else(|| "main_window".into());
let tray = app_handle
.tray_by_id("main")
@@ -258,7 +249,7 @@ impl Tray {
/// 更新托盘菜单
pub async fn update_menu(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘菜单更新");
log::debug!(target: "app", "应用正在退出,跳过托盘菜单更新");
return Ok(());
}
// 调整最小更新间隔,确保状态及时刷新
@@ -303,24 +294,24 @@ impl Tray {
}
async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
let verge = Config::verge().await.latest_arc();
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 tun_mode_available = cmd::system::is_admin().unwrap_or_default()
|| service::is_service_available().await.is_ok();
let mode = {
Config::clash()
.await
.latest_arc()
.latest_ref()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule")
.to_owned()
};
let profiles_config = Config::profiles().await;
let profiles_arc = profiles_config.latest_arc();
let profile_uid_and_name = profiles_arc.all_profile_uid_and_name().unwrap_or_default();
let profile_uid_and_name = Config::profiles()
.await
.data_mut()
.all_profile_uid_and_name()
.unwrap_or_default();
let is_lightweight_mode = is_in_lightweight_mode();
match app_handle.tray_by_id("main") {
@@ -331,21 +322,16 @@ impl Tray {
Some(mode.as_str()),
*system_proxy,
*tun_mode,
tun_mode_available,
profile_uid_and_name,
is_lightweight_mode,
)
.await?,
));
logging!(debug, Type::Tray, "托盘菜单更新成功");
log::debug!(target: "app", "托盘菜单更新成功");
Ok(())
}
None => {
logging!(
warn,
Type::Tray,
"Failed to update tray menu: tray not found"
);
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
Ok(())
}
}
@@ -355,7 +341,7 @@ impl Tray {
#[cfg(target_os = "macos")]
pub async fn update_icon(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
return Ok(());
}
@@ -364,16 +350,12 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
logging!(
warn,
Type::Tray,
"Failed to update tray icon: tray not found"
);
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
return Ok(());
}
};
let verge = Config::verge().await.latest_arc();
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);
@@ -398,7 +380,7 @@ impl Tray {
#[cfg(not(target_os = "macos"))]
pub async fn update_icon(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
return Ok(());
}
@@ -407,16 +389,12 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
logging!(
warn,
Type::Tray,
"Failed to update tray icon: tray not found"
);
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
return Ok(());
}
};
let verge = Config::verge().await.latest_arc();
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);
@@ -431,18 +409,34 @@ impl Tray {
Ok(())
}
/// 更新托盘显示状态的函数
pub async fn update_tray_display(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘显示状态更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
let _tray = app_handle
.tray_by_id("main")
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
// 更新菜单
self.update_menu().await?;
Ok(())
}
/// 更新托盘提示
pub async fn update_tooltip(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘提示更新");
log::debug!(target: "app", "应用正在退出,跳过托盘提示更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
i18n::sync_locale().await;
let verge = Config::verge().await.latest_arc();
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);
@@ -456,9 +450,9 @@ impl Tray {
let mut current_profile_name = "None".into();
{
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
if let Some(current_profile_uid) = profiles.get_current()
&& let Ok(profile) = profiles.get_item(current_profile_uid)
&& let Ok(profile) = profiles.get_item(&current_profile_uid)
{
current_profile_name = match &profile.name {
Some(profile_name) => profile_name.to_string(),
@@ -468,9 +462,9 @@ impl Tray {
}
// Get localized strings before using them
let sys_proxy_text = rust_i18n::t!("tray.tooltip.systemProxy");
let tun_text = rust_i18n::t!("tray.tooltip.tun");
let profile_text = rust_i18n::t!("tray.tooltip.profile");
let sys_proxy_text = t("SysProxy").await;
let tun_text = t("TUN").await;
let profile_text = t("Profile").await;
let v = env!("CARGO_PKG_VERSION");
let reassembled_version = v.split_once('+').map_or_else(
@@ -492,11 +486,7 @@ impl Tray {
if let Some(tray) = app_handle.tray_by_id("main") {
let _ = tray.set_tooltip(Some(&tooltip));
} else {
logging!(
warn,
Type::Tray,
"Failed to update tray tooltip: tray not found"
);
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
}
Ok(())
@@ -504,10 +494,12 @@ impl Tray {
pub async fn update_part(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新");
log::debug!(target: "app", "应用正在退出,跳过托盘局部更新");
return Ok(());
}
self.update_menu().await?;
// self.update_menu().await?;
// 更新轻量模式显示状态
self.update_tray_display().await?;
self.update_icon().await?;
self.update_tooltip().await?;
Ok(())
@@ -515,11 +507,11 @@ impl Tray {
pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘创建");
log::debug!(target: "app", "应用正在退出,跳过托盘创建");
return Ok(());
}
logging!(info, Type::Tray, "正在从AppHandle创建系统托盘");
log::info!(target: "app", "正在从AppHandle创建系统托盘");
// 获取图标
let icon_bytes = TrayState::get_common_tray_icon().await.1;
@@ -532,7 +524,7 @@ impl Tray {
#[cfg(any(target_os = "macos", target_os = "windows"))]
let show_menu_on_left_click = {
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
tray_event.as_str() == "tray_menu"
};
@@ -563,9 +555,9 @@ impl Tray {
}
AsyncHandler::spawn(|| async move {
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
log::debug!(target: "app", "tray event: {tray_event:?}");
if let TrayIconEvent::Click {
button: MouseButton::Left,
@@ -578,6 +570,9 @@ impl Tray {
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;
@@ -597,6 +592,23 @@ impl Tray {
});
});
tray.on_menu_event(on_menu_event);
log::info!(target: "app", "系统托盘创建成功");
Ok(())
}
// 托盘统一的状态更新函数
pub async fn update_all_states(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘状态更新");
return Ok(());
}
// 确保所有状态更新完成
self.update_tray_display().await?;
// self.update_menu().await?;
self.update_icon().await?;
self.update_tooltip().await?;
Ok(())
}
}
@@ -627,21 +639,23 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
async fn create_profile_menu_item(
app_handle: &AppHandle,
profile_uid_and_name: Vec<(&String, &String)>,
profile_uid_and_name: Vec<(String, String)>,
) -> Result<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
.latest_arc()
.is_current_profile_index(profile_uid);
.latest_ref()
.is_current_profile_index(profile_uid.clone());
CheckMenuItem::with_id(
&app_handle,
format!("profiles_{profile_uid}"),
profile_name.as_str(),
t(&profile_name).await,
true,
is_current_profile,
None::<&str>,
@@ -712,9 +726,7 @@ fn create_subcreate_proxy_menu_item(
is_selected,
None::<&str>,
)
.map_err(|e| {
logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e)
})
.map_err(|e| log::warn!(target: "app", "创建代理菜单项失败: {}", e))
.ok()
})
.collect();
@@ -756,12 +768,7 @@ fn create_subcreate_proxy_menu_item(
let insertion_index = submenus.len();
submenus.push((group_name.into(), insertion_index, submenu));
} else {
logging!(
warn,
Type::Tray,
"Failed to create proxy group submenu: {}",
group_name
);
log::warn!(target: "app", "创建代理组子菜单失败: {}", group_name);
}
}
}
@@ -830,21 +837,18 @@ async fn create_tray_menu(
mode: Option<&str>,
system_proxy_enabled: bool,
tun_mode_enabled: bool,
tun_mode_available: bool,
profile_uid_and_name: Vec<(&String, &String)>,
profile_uid_and_name: Vec<(String, String)>,
is_lightweight_mode: bool,
) -> Result<tauri::menu::Menu<Wry>> {
let current_proxy_mode = mode.unwrap_or("");
i18n::sync_locale().await;
// 获取当前配置文件的选中代理组信息
let current_profile_selected = {
let profiles_config = Config::profiles().await;
let profiles_ref = profiles_config.latest_arc();
let profiles_ref = profiles_config.latest_ref();
profiles_ref
.get_current()
.and_then(|uid| profiles_ref.get_item(uid).ok())
.and_then(|uid| profiles_ref.get_item(&uid).ok())
.and_then(|profile| profile.selected.clone())
.unwrap_or_default()
};
@@ -887,7 +891,7 @@ async fn create_tray_menu(
.collect::<HashMap<String, usize>>()
});
let verge_settings = Config::verge().await.latest_arc();
let verge_settings = Config::verge().await.latest_ref().clone();
let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false);
let version = env!("CARGO_PKG_VERSION");
@@ -898,7 +902,7 @@ async fn create_tray_menu(
create_profile_menu_item(app_handle, profile_uid_and_name).await?;
// Pre-fetch all localized strings
let texts = MenuTexts::new();
let texts = &MenuTexts::new().await;
// Convert to references only when needed
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
.iter()
@@ -976,7 +980,7 @@ async fn create_tray_menu(
app_handle,
MenuIds::TUN_MODE,
&texts.tun_mode,
tun_mode_available,
true,
tun_mode_enabled,
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
)?;
@@ -1030,34 +1034,12 @@ async fn create_tray_menu(
None::<&str>,
)?;
let open_app_log = &MenuItem::with_id(
app_handle,
MenuIds::APP_LOG,
&texts.app_log,
true,
None::<&str>,
)?;
let open_core_log = &MenuItem::with_id(
app_handle,
MenuIds::CORE_LOG,
&texts.core_log,
true,
None::<&str>,
)?;
let open_dir = &Submenu::with_id_and_items(
app_handle,
MenuIds::OPEN_DIR,
&texts.open_dir,
true,
&[
open_app_dir,
open_core_dir,
open_logs_dir,
open_app_log,
open_core_log,
],
&[open_app_dir, open_core_dir, open_logs_dir],
)?;
let restart_clash = &MenuItem::with_id(
@@ -1156,7 +1138,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
feat::change_clash_mode(mode.into()).await;
}
MenuIds::DASHBOARD => {
logging!(info, Type::Tray, "托盘菜单点击: 打开窗口");
log::info!(target: "app", "托盘菜单点击: 打开窗口");
if !should_handle_tray_click() {
return;
@@ -1173,11 +1155,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
MenuIds::CLOSE_ALL_CONNECTIONS => {
if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await {
logging!(
error,
Type::Tray,
"Failed to close all connections from tray: {err}"
);
log::error!(target: "app", "Failed to close all connections from tray: {err}");
}
}
MenuIds::COPY_ENV => feat::copy_clash_env().await,
@@ -1191,12 +1169,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
MenuIds::LOGS_DIR => {
let _ = cmd::open_logs_dir().await;
}
MenuIds::APP_LOG => {
let _ = cmd::open_app_log().await;
}
MenuIds::CORE_LOG => {
let _ = cmd::open_core_log().await;
}
MenuIds::RESTART_CLASH => feat::restart_clash_core().await,
MenuIds::RESTART_APP => feat::restart_app().await,
MenuIds::LIGHTWEIGHT_MODE => {
@@ -1218,27 +1190,47 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
id if id.starts_with("proxy_") => {
// proxy_{group_name}_{proxy_name}
let rest = match id.strip_prefix("proxy_") {
Some(r) => r,
None => return,
};
let (group_name, proxy_name) = match rest.split_once('_') {
Some((g, p)) => (g, p),
None => return,
};
feat::switch_proxy_node(group_name, proxy_name).await;
}
_ => {
logging!(
debug,
Type::Tray,
"Unhandled tray menu event: {:?}",
event.id
);
let parts: Vec<&str> = id.splitn(3, '_').collect();
if parts.len() == 3 && parts[0] == "proxy" {
let group_name = parts[1];
let proxy_name = parts[2];
match handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await
{
Ok(_) => {
log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name);
let _ = handle::Handle::app_handle()
.emit("verge://refresh-proxy-config", ());
}
Err(e) => {
log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e);
// Fallback to IPC update
if (handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await)
.is_ok()
{
log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name);
let app_handle = handle::Handle::app_handle();
let _ = app_handle.emit("verge://force-refresh-proxies", ());
}
}
}
}
}
_ => {}
}
// We dont expected to refresh tray state here
// as the inner handle function (SHOULD) already takes care of it
// Ensure tray state update is awaited and properly handled
if let Err(e) = Tray::global().update_all_states().await {
log::warn!(target: "app", "更新托盘状态失败: {e}");
}
});
}

View File

@@ -1,9 +1,9 @@
use anyhow::Result;
use scopeguard::defer;
use smartstring::alias::String;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri_plugin_shell::ShellExt;
use tokio::fs;
use crate::config::{Config, ConfigType};
use crate::core::handle;
@@ -16,7 +16,7 @@ pub struct CoreConfigValidator {
}
impl CoreConfigValidator {
pub const fn new() -> Self {
pub fn new() -> Self {
Self {
is_processing: AtomicBool::new(false),
}
@@ -33,16 +33,19 @@ impl CoreConfigValidator {
impl CoreConfigValidator {
/// 检查文件是否为脚本文件
async fn is_script_file(path: &str) -> Result<bool> {
fn is_script_file<P>(path: P) -> Result<bool>
where
P: AsRef<Path> + std::fmt::Display,
{
// 1. 先通过扩展名快速判断
if has_ext(path, "yaml") || has_ext(path, "yml") {
if has_ext(&path, "yaml") || has_ext(&path, "yml") {
return Ok(false); // YAML文件不是脚本文件
} else if has_ext(path, "js") {
} else if has_ext(&path, "js") {
return Ok(true); // JS文件是脚本文件
}
// 2. 读取文件内容
let content = match fs::read_to_string(path).await {
let content = match std::fs::read_to_string(&path) {
Ok(content) => content,
Err(err) => {
logging!(
@@ -112,11 +115,11 @@ impl CoreConfigValidator {
}
/// 只进行文件语法检查,不进行完整验证
async fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> {
fn validate_file_syntax(config_path: &str) -> Result<(bool, String)> {
logging!(info, Type::Validate, "开始检查文件: {}", config_path);
// 读取文件内容
let content = match fs::read_to_string(config_path).await {
let content = match std::fs::read_to_string(config_path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read file: {err}").into();
@@ -141,9 +144,9 @@ impl CoreConfigValidator {
}
/// 验证脚本文件语法
async fn validate_script_file(path: &str) -> Result<(bool, String)> {
fn validate_script_file(path: &str) -> Result<(bool, String)> {
// 读取脚本内容
let content = match fs::read_to_string(path).await {
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read script file: {err}").into();
@@ -213,14 +216,14 @@ impl CoreConfigValidator {
"检测到Merge文件仅进行语法检查: {}",
config_path
);
return Self::validate_file_syntax(config_path).await;
return Self::validate_file_syntax(config_path);
}
// 检查是否为脚本文件
let is_script = if config_path.ends_with(".js") {
true
} else {
match Self::is_script_file(config_path).await {
match Self::is_script_file(config_path) {
Ok(result) => result,
Err(err) => {
// 如果无法确定文件类型尝试使用Clash内核验证
@@ -243,7 +246,7 @@ impl CoreConfigValidator {
"检测到脚本文件使用JavaScript验证: {}",
config_path
);
return Self::validate_script_file(config_path).await;
return Self::validate_script_file(config_path);
}
// 对YAML配置文件使用Clash内核验证
@@ -266,7 +269,7 @@ impl CoreConfigValidator {
logging!(info, Type::Validate, "开始验证配置文件: {}", config_path);
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
logging!(info, Type::Validate, "使用内核: {}", clash_core);
let app_handle = handle::Handle::app_handle();
@@ -275,43 +278,41 @@ impl CoreConfigValidator {
logging!(info, Type::Validate, "验证目录: {}", app_dir_str);
// 使用子进程运行clash验证配置
let command = app_handle.shell().sidecar(clash_core.as_str())?.args([
"-t",
"-d",
app_dir_str,
"-f",
config_path,
]);
let output = command.output().await?;
let output = app_handle
.shell()
.sidecar(clash_core.as_str())?
.args(["-t", "-d", app_dir_str, "-f", config_path])
.output()
.await?;
let status = &output.status;
let stderr = &output.stderr;
let stdout = &output.stdout;
let stderr = std::string::String::from_utf8_lossy(&output.stderr);
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
// 检查进程退出状态和错误输出
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
let has_error = !status.success() || contains_any_keyword(stderr, &error_keywords);
let has_error =
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
logging!(info, Type::Validate, "-------- 验证结果 --------");
if !stderr.is_empty() {
logging!(info, Type::Validate, "stderr输出:\n{:?}", stderr);
logging!(info, Type::Validate, "stderr输出:\n{}", stderr);
}
if has_error {
logging!(info, Type::Validate, "发现错误,开始处理错误信息");
let error_msg: String = if !stdout.is_empty() {
str::from_utf8(stdout).unwrap_or_default().into()
let error_msg = if !stdout.is_empty() {
stdout.into()
} else if !stderr.is_empty() {
str::from_utf8(stderr).unwrap_or_default().into()
} else if let Some(code) = status.code() {
format!("验证进程异常退出,退出码: {code}").into()
stderr.into()
} else if let Some(code) = output.status.code() {
format!("验证进程异常退出,退出码: {code}")
} else {
"验证进程被终止".into()
};
logging!(info, Type::Validate, "-------- 验证结束 --------");
Ok((false, error_msg)) // 返回错误消息给调用者处理
Ok((false, error_msg.into())) // 返回错误消息给调用者处理
} else {
logging!(info, Type::Validate, "验证成功");
logging!(info, Type::Validate, "-------- 验证结束 --------");
@@ -344,23 +345,6 @@ fn has_ext<P: AsRef<std::path::Path>>(path: P, ext: &str) -> bool {
.unwrap_or(false)
}
fn contains_any_keyword<'a>(buf: &'a [u8], keywords: &'a [&str]) -> bool {
for &kw in keywords {
let needle = kw.as_bytes();
if needle.is_empty() {
continue;
}
let mut i = 0;
while i + needle.len() <= buf.len() {
if &buf[i..i + needle.len()] == needle {
return true;
}
i += 1;
}
}
false
}
singleton_lazy!(
CoreConfigValidator,
CORECONFIGVALIDATOR,

View File

@@ -5,7 +5,7 @@ use crate::{
};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
use std::fs;
#[derive(Debug, Clone)]
pub struct ChainItem {
@@ -70,7 +70,7 @@ pub trait AsyncChainItemFrom {
}
impl AsyncChainItemFrom for Option<ChainItem> {
async fn from_async(item: &PrfItem) -> Self {
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_else(|| "".into());
@@ -83,7 +83,7 @@ impl AsyncChainItemFrom for Option<ChainItem> {
match itype {
"script" => Some(ChainItem {
uid,
data: ChainType::Script(fs::read_to_string(path).await.ok()?.into()),
data: ChainType::Script(fs::read_to_string(path).ok()?.into()),
}),
"merge" => Some(ChainItem {
uid,
@@ -116,21 +116,22 @@ impl AsyncChainItemFrom for Option<ChainItem> {
}
impl ChainItem {
/// 内建支持一些脚本
pub fn builtin() -> Vec<(ChainSupport, Self)> {
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
// meta 的一些处理
let meta_guard =
Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组
let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
let hy_alpn =
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
// meta 的一些处理
let meta_guard_alpha =
Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组
let hy_alpn_alpha =
Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
vec![
(ChainSupport::ClashMeta, hy_alpn),
@@ -153,7 +154,8 @@ impl ChainSupport {
match core {
Some(core) => matches!(
(self, core.as_str()),
(Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha")
(ChainSupport::ClashMeta, "verge-mihomo")
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
),
None => true,
}

View File

@@ -1,5 +1,3 @@
use crate::{logging, utils::logging::Type};
use super::use_lowercase;
use serde_yaml_ng::{self, Mapping, Value};
@@ -16,16 +14,12 @@ fn deep_merge(a: &mut Value, b: &Value) {
pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
let mut config = Value::from(config);
let merge = use_lowercase(merge);
let merge = use_lowercase(merge.clone());
deep_merge(&mut config, &Value::from(merge));
config.as_mapping().cloned().unwrap_or_else(|| {
logging!(
error,
Type::Core,
"Failed to convert merged config to mapping, using empty mapping"
);
log::error!("Failed to convert merged config to mapping, using empty mapping");
Mapping::new()
})
}

View File

@@ -6,14 +6,10 @@ pub mod seq;
mod tun;
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
use crate::constants;
use crate::utils::dirs;
use crate::{config::Config, utils::tmpl};
use crate::{logging, utils::logging::Type};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use tokio::fs;
type ResultLog = Vec<(String, String)>;
#[derive(Debug)]
@@ -44,49 +40,12 @@ struct ProfileItems {
profile_name: String,
}
impl Default for ProfileItems {
fn default() -> Self {
Self {
config: Default::default(),
profile_name: Default::default(),
merge_item: ChainItem {
uid: "".into(),
data: ChainType::Merge(Mapping::new()),
},
script_item: ChainItem {
uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
},
rules_item: ChainItem {
uid: "".into(),
data: ChainType::Rules(SeqMap::default()),
},
proxies_item: ChainItem {
uid: "".into(),
data: ChainType::Proxies(SeqMap::default()),
},
groups_item: ChainItem {
uid: "".into(),
data: ChainType::Groups(SeqMap::default()),
},
global_merge: ChainItem {
uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()),
},
global_script: ChainItem {
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
},
}
}
}
async fn get_config_values() -> ConfigValues {
let clash_config = { Config::clash().await.latest_arc().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().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
(
Some(verge.get_valid_clash_core()),
verge.enable_tun_mode.unwrap_or(false),
@@ -100,14 +59,14 @@ async fn get_config_values() -> ConfigValues {
#[cfg(not(target_os = "windows"))]
let redir_enabled = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.verge_redir_enabled.unwrap_or(false)
};
#[cfg(target_os = "linux")]
let tproxy_enabled = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.verge_tproxy_enabled.unwrap_or(false)
};
@@ -126,43 +85,33 @@ async fn get_config_values() -> ConfigValues {
}
}
#[allow(clippy::cognitive_complexity)]
async fn collect_profile_items() -> ProfileItems {
// 从profiles里拿东西 - 先收集需要的数据,然后释放锁
let (current, merge_uid, script_uid, rules_uid, proxies_uid, groups_uid, name) = {
let (
current,
merge_uid,
script_uid,
rules_uid,
proxies_uid,
groups_uid,
_current_profile_uid,
name,
) = {
let current = {
let profiles = Config::profiles().await;
let profiles_clone = profiles.latest_arc();
let profiles_clone = profiles.latest_ref().clone();
profiles_clone.current_mapping().await.unwrap_or_default()
};
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let current_profile_uid = match profiles_ref.get_current() {
Some(uid) => uid.clone(),
None => return ProfileItems::default(),
};
let profiles_ref = profiles.latest_ref();
let current_item = match profiles_ref.get_item_arc(&current_profile_uid) {
Some(item) => item,
None => return ProfileItems::default(),
};
let merge_uid = current_item
.current_merge()
.unwrap_or_else(|| "Merge".into());
let script_uid = current_item
.current_script()
.unwrap_or_else(|| "Script".into());
let rules_uid = current_item
.current_rules()
.unwrap_or_else(|| "Rules".into());
let proxies_uid = current_item
.current_proxies()
.unwrap_or_else(|| "Proxies".into());
let groups_uid = current_item
.current_groups()
.unwrap_or_else(|| "Groups".into());
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 name = profiles_ref
.get_item(&current_profile_uid)
@@ -177,6 +126,7 @@ async fn collect_profile_items() -> ProfileItems {
rules_uid,
proxies_uid,
groups_uid,
current_profile_uid,
name,
)
};
@@ -185,7 +135,7 @@ async fn collect_profile_items() -> ProfileItems {
let merge_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(&merge_uid).ok().cloned()
};
if let Some(item) = item {
@@ -202,7 +152,7 @@ async fn collect_profile_items() -> ProfileItems {
let script_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(&script_uid).ok().cloned()
};
if let Some(item) = item {
@@ -219,7 +169,7 @@ async fn collect_profile_items() -> ProfileItems {
let rules_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(&rules_uid).ok().cloned()
};
if let Some(item) = item {
@@ -236,7 +186,7 @@ async fn collect_profile_items() -> ProfileItems {
let proxies_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(&proxies_uid).ok().cloned()
};
if let Some(item) = item {
@@ -253,7 +203,7 @@ async fn collect_profile_items() -> ProfileItems {
let groups_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(&groups_uid).ok().cloned()
};
if let Some(item) = item {
@@ -270,8 +220,8 @@ async fn collect_profile_items() -> ProfileItems {
let global_merge = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
profiles.get_item("Merge").ok().cloned()
let profiles = profiles.latest_ref();
profiles.get_item(&"Merge".into()).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -287,8 +237,8 @@ async fn collect_profile_items() -> ProfileItems {
let global_script = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
profiles.get_item("Script").ok().cloned()
let profiles = profiles.latest_ref();
profiles.get_item(&"Script".into()).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
@@ -330,7 +280,7 @@ fn process_global_items(
if let ChainType::Script(script) = global_script.data {
let mut logs = vec![];
match use_script(script, config.to_owned(), profile_name) {
match use_script(script, config.to_owned(), profile_name.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
@@ -440,7 +390,7 @@ async fn merge_default_config(
if key.as_str() == Some("external-controller") {
let enable_external_controller = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_external_controller
.unwrap_or(false);
@@ -470,14 +420,14 @@ fn apply_builtin_scripts(
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
.map(|(_, c)| c)
.for_each(|item| {
logging!(debug, Type::Core, "run builtin script {}", item.uid);
log::debug!(target: "app", "run builtin script {}", item.uid);
if let ChainType::Script(script) = item.data {
match use_script(script, config.to_owned(), "".into()) {
Ok((res_config, _)) => {
config = res_config;
}
Err(err) => {
logging!(error, Type::Core, "builtin script error `{err}`");
log::error!(target: "app", "builtin script error `{err}`");
}
}
}
@@ -487,29 +437,34 @@ fn apply_builtin_scripts(
config
}
async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping {
if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping {
if enable_dns_settings {
use crate::utils::dirs;
use std::fs;
if dns_path.exists()
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path).await
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
{
if let Some(hosts_value) = dns_config.get("hosts")
&& hosts_value.is_mapping()
if let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join("dns_config.yaml");
if dns_path.exists()
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path)
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
{
config.insert("hosts".into(), hosts_value.clone());
logging!(info, Type::Core, "apply hosts configuration");
}
if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into());
logging!(info, Type::Core, "apply dns_config.yaml (dns section)");
if let Some(hosts_value) = dns_config.get("hosts")
&& hosts_value.is_mapping()
{
config.insert("hosts".into(), hosts_value.clone());
log::info!(target: "app", "apply hosts configuration");
}
if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into());
log::info!(target: "app", "apply dns_config.yaml (dns section)");
}
} else {
config.insert("dns".into(), dns_config.into());
log::info!(target: "app", "apply dns_config.yaml");
}
} else {
config.insert("dns".into(), dns_config.into());
logging!(info, Type::Core, "apply dns_config.yaml");
}
}
}
@@ -585,7 +540,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
config = use_sort(config);
// dns settings
config = apply_dns_settings(config, enable_dns_settings).await;
config = apply_dns_settings(config, enable_dns_settings);
let mut exists_set = HashSet::new();
exists_set.extend(exists_keys);

View File

@@ -62,7 +62,7 @@ pub fn use_script(
});"#,
));
let config = use_lowercase(config);
let config = use_lowercase(config.clone());
let config_str = serde_json::to_string(&config)?;
// 仅处理 name 参数中的特殊字符

View File

@@ -2,8 +2,6 @@ use serde_yaml_ng::{Mapping, Value};
#[cfg(target_os = "macos")]
use crate::process::AsyncHandler;
#[cfg(target_os = "linux")]
use crate::{logging, utils::logging::Type};
macro_rules! revise {
($map: expr, $key: expr, $val: expr) => {
@@ -44,10 +42,9 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
if should_override {
revise!(tun_val, "stack", "mixed");
logging!(
warn,
Type::Network,
"Warning: gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility"
log::warn!(
target: "app",
"gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility"
);
}
}

View File

@@ -2,7 +2,6 @@ use crate::{
config::{Config, IVerge},
core::backup,
logging, logging_error,
process::AsyncHandler,
utils::{
dirs::{PathBufExec, app_home_dir, local_backup_dir},
logging::Type,
@@ -13,8 +12,7 @@ use chrono::Utc;
use reqwest_dav::list_cmd::ListFile;
use serde::Serialize;
use smartstring::alias::String;
use std::path::PathBuf;
use tokio::fs;
use std::{fs, path::PathBuf};
#[derive(Debug, Serialize)]
pub struct LocalBackupFile {
@@ -26,7 +24,7 @@ pub struct LocalBackupFile {
/// Create a backup and upload to WebDAV
pub async fn create_backup_and_upload_webdav() -> Result<()> {
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
logging!(error, Type::Backup, "Failed to create backup: {err:#?}");
err
})?;
@@ -78,7 +76,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().await;
let verge_data = verge.latest_arc();
let verge_data = verge.latest_ref().clone();
let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
@@ -99,14 +97,12 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
})?;
// extract zip file
let value = backup_storage_path.clone();
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??;
let mut zip = zip::ZipArchive::new(file)?;
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
zip.extract(app_home_dir()?)?;
logging_error!(
Type::Backup,
super::patch_verge(
&IVerge {
IVerge {
webdav_url,
webdav_username,
webdav_password,
@@ -123,7 +119,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
/// Create a backup and save to local storage
pub async fn create_local_backup() -> Result<()> {
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
logging!(
error,
Type::Backup,
@@ -135,7 +131,7 @@ pub async fn create_local_backup() -> Result<()> {
let backup_dir = local_backup_dir()?;
let target_path = backup_dir.join(file_name.as_str());
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await {
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()) {
logging!(
error,
Type::Backup,
@@ -155,12 +151,12 @@ pub async fn create_local_backup() -> Result<()> {
Ok(())
}
async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
if let Some(parent) = to.parent() {
fs::create_dir_all(parent).await?;
fs::create_dir_all(parent)?;
}
match fs::rename(&from, &to).await {
match fs::rename(&from, &to) {
Ok(_) => Ok(()),
Err(rename_err) => {
// Attempt copy + remove as fallback, covering cross-device moves
@@ -169,11 +165,8 @@ async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
Type::Backup,
"Failed to rename backup file directly, fallback to copy/remove: {rename_err:#?}"
);
fs::copy(&from, &to)
.await
.map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?;
fs::copy(&from, &to).map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?;
fs::remove_file(&from)
.await
.map_err(|err| anyhow!("Failed to remove temp backup file: {err:#?}"))?;
Ok(())
}
@@ -181,25 +174,24 @@ async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
}
/// List local backups
pub async fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
pub fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
let backup_dir = local_backup_dir()?;
if !backup_dir.exists() {
return Ok(vec![]);
}
let mut backups = Vec::new();
let mut dir = fs::read_dir(&backup_dir).await?;
while let Some(entry) = dir.next_entry().await? {
for entry in fs::read_dir(&backup_dir)? {
let entry = entry?;
let path = entry.path();
let metadata = entry.metadata().await?;
if !metadata.is_file() {
if !path.is_file() {
continue;
}
let file_name = match path.file_name().and_then(|name| name.to_str()) {
Some(name) => name,
None => continue,
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
let metadata = entry.metadata()?;
let last_modified = metadata
.modified()
.map(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339())
@@ -241,23 +233,18 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
return Err(anyhow!("Backup file not found: {}", filename));
}
let (webdav_url, webdav_username, webdav_password) = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
(
verge.webdav_url.clone(),
verge.webdav_username.clone(),
verge.webdav_password.clone(),
)
};
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();
let webdav_password = verge_data.webdav_password.clone();
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
let mut zip = zip::ZipArchive::new(file)?;
let mut zip = zip::ZipArchive::new(fs::File::open(&target_path)?)?;
zip.extract(app_home_dir()?)?;
logging_error!(
Type::Backup,
super::patch_verge(
&IVerge {
IVerge {
webdav_url,
webdav_username,
webdav_password,
@@ -271,7 +258,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
}
/// Export local backup file to user selected destination
pub async fn export_local_backup(filename: String, destination: String) -> Result<()> {
pub fn export_local_backup(filename: String, destination: String) -> Result<()> {
let backup_dir = local_backup_dir()?;
let source_path = backup_dir.join(filename.as_str());
if !source_path.exists() {
@@ -280,11 +267,10 @@ pub async fn export_local_backup(filename: String, destination: String) -> Resul
let dest_path = PathBuf::from(destination.as_str());
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).await?;
fs::create_dir_all(parent)?;
}
fs::copy(&source_path, &dest_path)
.await
.map(|_| ())
.map_err(|err| anyhow!("Failed to export backup file: {err:#?}"))?;
Ok(())

View File

@@ -1,7 +1,7 @@
use crate::{
config::Config,
core::{CoreManager, handle, tray},
logging, logging_error,
logging_error,
process::AsyncHandler,
utils::{self, logging::Type, resolve},
};
@@ -17,7 +17,7 @@ pub async fn restart_clash_core() {
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
logging!(error, Type::Core, "{err}");
log::error!(target:"app", "{err}");
}
}
}
@@ -30,7 +30,7 @@ pub async fn restart_app() {
"restart_app::error",
format!("Failed to cleanup resources: {err}"),
);
logging!(error, Type::Core, "Restart failed during cleanup: {err}");
log::error!(target:"app", "Restart failed during cleanup: {err}");
return;
}
@@ -47,11 +47,10 @@ fn after_change_clash_mode() {
for connection in connections_array {
let _ = mihomo.close_connection(&connection.id).await;
}
drop(mihomo);
}
}
Err(err) => {
logging!(error, Type::Core, "Failed to get connections: {err}");
log::error!(target: "app", "Failed to get connections: {err}");
}
}
});
@@ -65,7 +64,7 @@ pub async fn change_clash_mode(mode: String) {
let json_value = serde_json::json!({
"mode": mode
});
logging!(debug, Type::Core, "change clash mode to {mode}");
log::debug!(target: "app", "change clash mode to {mode}");
match handle::Handle::mihomo()
.await
.patch_base_config(&json_value)
@@ -73,12 +72,10 @@ pub async fn change_clash_mode(mode: String) {
{
Ok(_) => {
// 更新订阅
Config::clash()
.await
.edit_draft(|d| d.patch_config(mapping));
Config::clash().await.data_mut().patch_config(mapping);
// 分离数据获取和异步调用
let clash_data = Config::clash().await.data_arc();
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, tray::Tray::global().update_menu().await);
@@ -87,14 +84,14 @@ pub async fn change_clash_mode(mode: String) {
let is_auto_close_connection = Config::verge()
.await
.data_arc()
.data_mut()
.auto_close_connection
.unwrap_or(false);
if is_auto_close_connection {
after_change_clash_mode();
}
}
Err(err) => logging!(error, Type::Core, "{err}"),
Err(err) => log::error!(target: "app", "{err}"),
}
}
@@ -105,7 +102,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
let tun_mode = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_tun_mode
.unwrap_or(false);
@@ -126,7 +123,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
match response {
Ok(response) => {
logging!(trace, Type::Network, "test_delay response: {response:#?}");
log::trace!(target: "app", "test_delay response: {response:#?}");
if response.status().is_success() {
Ok(start.elapsed().as_millis() as u32)
} else {
@@ -134,7 +131,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
}
}
Err(err) => {
logging!(trace, Type::Network, "test_delay error: {err:#?}");
log::trace!(target: "app", "test_delay error: {err:#?}");
Err(err)
}
}

View File

@@ -3,7 +3,7 @@ use crate::{
core::{CoreManager, handle, hotkey, sysopt, tray},
logging_error,
module::lightweight,
utils::{draft::SharedBox, logging::Type},
utils::logging::Type,
};
use anyhow::Result;
use serde_yaml_ng::Mapping;
@@ -12,7 +12,8 @@ use serde_yaml_ng::Mapping;
pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash()
.await
.edit_draft(|d| d.patch_config(patch.clone()));
.draft_mut()
.patch_config(patch.clone());
let res = {
// 激活订阅
@@ -24,9 +25,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
logging_error!(Type::Tray, tray::Tray::global().update_menu().await);
logging_error!(Type::Tray, tray::Tray::global().update_icon().await);
}
Config::runtime()
.await
.edit_draft(|d| d.patch_config(patch));
Config::runtime().await.draft_mut().patch_config(patch);
CoreManager::global().update_config().await?;
}
handle::Handle::refresh_clash();
@@ -36,7 +35,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
Ok(()) => {
Config::clash().await.apply();
// 分离数据获取和异步调用
let clash_data = Config::clash().await.data_arc();
let clash_data = Config::clash().await.data_mut().clone();
clash_data.save_config().await?;
Ok(())
}
@@ -191,9 +190,7 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<(
handle::Handle::refresh_clash();
}
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
Config::verge()
.await
.edit_draft(|d| d.enable_global_hotkey = patch.enable_global_hotkey);
Config::verge().await.draft_mut().enable_global_hotkey = patch.enable_global_hotkey;
handle::Handle::refresh_verge();
}
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
@@ -229,12 +226,15 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<(
Ok(())
}
pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
Config::verge().await.edit_draft(|d| d.patch_config(patch));
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge()
.await
.draft_mut()
.patch_config(patch.clone());
let update_flags = determine_update_flags(patch);
let update_flags = determine_update_flags(&patch);
let process_flag_result: std::result::Result<(), anyhow::Error> = {
process_terminated_flags(update_flags, patch).await?;
process_terminated_flags(update_flags, &patch).await?;
Ok(())
};
@@ -245,14 +245,8 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
Config::verge().await.apply();
if !not_save_file {
// 分离数据获取和异步调用
let verge_data = Config::verge().await.data_arc();
let verge_data = Config::verge().await.data_mut().clone();
verge_data.save_file().await?;
}
Ok(())
}
pub async fn fetch_verge_config() -> Result<SharedBox<IVerge>> {
let draft = Config::verge().await;
let data = draft.data_arc();
Ok(data)
}

View File

@@ -2,125 +2,52 @@ use crate::{
cmd,
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
core::{CoreManager, handle, tray},
logging, logging_error,
logging,
utils::logging::Type,
};
use anyhow::{Result, bail};
use smartstring::alias::String;
use tauri::Emitter;
/// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) {
logging_error!(
Type::Config,
cmd::patch_profiles_config_by_profile_index(profile_index).await
);
}
pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
match handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await
{
match cmd::patch_profiles_config_by_profile_index(profile_index).await {
Ok(_) => {
logging!(
info,
Type::Tray,
"切换代理成功: {} -> {}",
group_name,
proxy_name
);
let _ = handle::Handle::app_handle().emit("verge://refresh-proxy-config", ());
let _ = tray::Tray::global().update_menu().await;
return;
let result = tray::Tray::global().update_menu().await;
if let Err(err) = result {
logging!(error, Type::Tray, "更新菜单失败: {}", err);
}
}
Err(err) => {
logging!(
error,
Type::Tray,
"切换代理失败: {} -> {}, 错误: {:?}",
group_name,
proxy_name,
err
);
}
}
match handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await
{
Ok(_) => {
logging!(
info,
Type::Tray,
"代理切换回退成功: {} -> {}",
group_name,
proxy_name
);
let _ = tray::Tray::global().update_menu().await;
}
Err(err) => {
logging!(
error,
Type::Tray,
"代理切换最终失败: {} -> {}, 错误: {:?}",
group_name,
proxy_name,
err
);
log::error!(target: "app", "{err}");
}
}
}
async fn should_update_profile(
uid: &String,
ignore_auto_update: bool,
) -> Result<Option<(String, Option<PrfOption>)>> {
async fn should_update_profile(uid: String) -> Result<Option<(String, Option<PrfOption>)>> {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let item = profiles.get_item(uid)?;
let profiles = profiles.latest_ref();
let item = profiles.get_item(&uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote {
logging!(
info,
Type::Config,
"[订阅更新] {uid} 不是远程订阅,跳过更新"
);
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
Ok(None)
} else if item.url.is_none() {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] {uid} 缺少URL无法更新"
);
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL无法更新");
bail!("failed to get the profile item url");
} else if !ignore_auto_update
&& !item
.option
.as_ref()
.and_then(|o| o.allow_auto_update)
.unwrap_or(true)
} else if !item
.option
.as_ref()
.and_then(|o| o.allow_auto_update)
.unwrap_or(true)
{
logging!(
info,
Type::Config,
"[订阅更新] {} 禁止自动更新,跳过更新",
uid
);
log::info!(target: "app", "[订阅更新] {} 禁止自动更新,跳过更新", uid);
Ok(None)
} else {
logging!(
info,
Type::Config,
log::info!(target: "app",
"[订阅更新] {} 是远程订阅URL: {}",
uid,
item.url
.clone()
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
);
Ok(Some((
item.url
@@ -132,111 +59,79 @@ async fn should_update_profile(
}
async fn perform_profile_update(
uid: &String,
url: &String,
opt: Option<&PrfOption>,
option: Option<&PrfOption>,
uid: String,
url: String,
opt: Option<PrfOption>,
option: Option<PrfOption>,
) -> Result<bool> {
logging!(info, Type::Config, "[订阅更新] 开始下载新的订阅内容");
let mut merged_opt = PrfOption::merge(opt, option);
let is_current = {
let profiles = Config::profiles().await;
profiles.latest_arc().is_current_profile_index(uid)
};
let profiles = Config::profiles().await;
let profiles_arc = profiles.latest_arc();
let profile_name = profiles_arc
.get_name_by_uid(uid)
.cloned()
.unwrap_or_else(|| String::from("UnKown Profile"));
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
let mut last_err;
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(info, Type::Config, "[订阅更新] 更新订阅配置成功");
profiles_draft_update_item_safe(uid, &mut item).await?;
return Ok(is_current);
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => {
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles().await;
profiles_draft_update_item_safe(uid.clone(), item).await?;
let is_current = Some(uid.clone()) == profiles.latest_ref().get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
Ok(is_current)
}
Err(err) => {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err}尝试使用Clash代理更新"
);
last_err = err;
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err}尝试使用Clash代理更新");
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
let original_with_proxy = merged_opt.as_ref().and_then(|o| o.with_proxy);
let original_self_proxy = merged_opt.as_ref().and_then(|o| o.self_proxy);
let mut fallback_opt = merged_opt.unwrap_or_default();
fallback_opt.with_proxy = Some(false);
fallback_opt.self_proxy = Some(true);
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
Ok(mut item) => {
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
if let Some(option) = item.option.as_mut() {
option.with_proxy = original_with_proxy;
option.self_proxy = original_self_proxy;
}
let profiles = Config::profiles().await;
profiles_draft_update_item_safe(uid.clone(), item.clone()).await?;
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.data_ref().get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
Ok(is_current)
}
Err(retry_err) => {
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{retry_err}"),
);
Err(retry_err)
}
}
}
}
merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(true);
merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(false);
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(
info,
Type::Config,
"[订阅更新] 使用 Clash代理 更新订阅配置成功"
);
profiles_draft_update_item_safe(uid, &mut item).await?;
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
drop(last_err);
return Ok(is_current);
}
Err(err) => {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err}尝试使用Clash代理更新"
);
last_err = err;
}
}
merged_opt.get_or_insert_with(PrfOption::default).self_proxy = Some(false);
merged_opt.get_or_insert_with(PrfOption::default).with_proxy = Some(true);
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
Ok(mut item) => {
logging!(
info,
Type::Config,
"[订阅更新] 使用 系统代理 更新订阅配置成功"
);
profiles_draft_update_item_safe(uid, &mut item).await?;
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
drop(last_err);
return Ok(is_current);
}
Err(err) => {
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err},尝试使用系统代理更新"
);
last_err = err;
}
}
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{profile_name} - {last_err}"),
);
Ok(is_current)
}
pub async fn update_profile(
uid: &String,
option: Option<&PrfOption>,
auto_refresh: bool,
ignore_auto_update: bool,
uid: String,
option: Option<PrfOption>,
auto_refresh: Option<bool>,
) -> Result<()> {
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
let url_opt = should_update_profile(uid, ignore_auto_update).await?;
let auto_refresh = auto_refresh.unwrap_or(true);
let url_opt = should_update_profile(uid.clone()).await?;
let should_refresh = match url_opt {
Some((url, opt)) => {
perform_profile_update(uid, &url, opt.as_ref(), option).await? && auto_refresh
perform_profile_update(uid.clone(), url, opt, option).await? && auto_refresh
}
None => auto_refresh,
};
@@ -251,7 +146,7 @@ pub async fn update_profile(
Err(err) => {
logging!(error, Type::Config, "[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("update_failed", format!("{err}"));
logging!(error, Type::Config, "{err}");
log::error!(target: "app", "{err}");
}
}
}

View File

@@ -1,8 +1,6 @@
use crate::{
config::{Config, IVerge},
core::handle,
logging,
utils::logging::Type,
};
use std::env;
use tauri_plugin_clipboard_manager::ClipboardExt;
@@ -10,23 +8,19 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
/// Toggle system proxy on/off
pub async fn toggle_system_proxy() {
let verge = Config::verge().await;
let enable = verge.latest_arc().enable_system_proxy.unwrap_or(false);
let auto_close_connection = verge.latest_arc().auto_close_connection.unwrap_or(false);
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false);
let auto_close_connection = verge.latest_ref().auto_close_connection.unwrap_or(false);
// 如果当前系统代理即将关闭且自动关闭连接设置为true则关闭所有连接
if enable
&& auto_close_connection
&& let Err(err) = handle::Handle::mihomo().await.close_all_connections().await
{
logging!(
error,
Type::ProxyMode,
"Failed to close all connections: {err}"
);
log::error!(target: "app", "Failed to close all connections: {err}");
}
let patch_result = super::patch_verge(
&IVerge {
IVerge {
enable_system_proxy: Some(!enable),
..IVerge::default()
},
@@ -36,17 +30,17 @@ pub async fn toggle_system_proxy() {
match patch_result {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => logging!(error, Type::ProxyMode, "{err}"),
Err(err) => log::error!(target: "app", "{err}"),
}
}
/// Toggle TUN mode on/off
pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().await.latest_arc().enable_tun_mode;
let enable = Config::verge().await.data_mut().enable_tun_mode;
let enable = enable.unwrap_or(false);
match super::patch_verge(
&IVerge {
IVerge {
enable_tun_mode: Some(!enable),
..IVerge::default()
},
@@ -55,7 +49,7 @@ pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => logging!(error, Type::ProxyMode, "{err}"),
Err(err) => log::error!(target: "app", "{err}"),
}
}
@@ -66,7 +60,7 @@ pub async fn copy_clash_env() {
Ok(ip) => ip.into(),
Err(_) => Config::verge()
.await
.latest_arc()
.latest_ref()
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".into()),
@@ -76,7 +70,7 @@ pub async fn copy_clash_env() {
let port = {
Config::verge()
.await
.latest_arc()
.latest_ref()
.verge_mixed_port
.unwrap_or(7897)
};
@@ -84,7 +78,7 @@ pub async fn copy_clash_env() {
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
let cliboard = app_handle.clipboard();
let env_type = { Config::verge().await.latest_arc().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 => {
@@ -110,16 +104,12 @@ pub async fn copy_clash_env() {
}
"fish" => format!("set -x http_proxy {http_proxy}; set -x https_proxy {http_proxy}"),
_ => {
logging!(
error,
Type::ProxyMode,
"copy_clash_env: Invalid env type! {env_type}"
);
log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}");
return;
}
};
if cliboard.write_text(export_text).is_err() {
logging!(error, Type::ProxyMode, "Failed to write to clipboard");
log::error!(target: "app", "Failed to write to clipboard");
}
}

View File

@@ -14,7 +14,7 @@ pub async fn open_or_close_dashboard() {
async fn open_or_close_dashboard_internal() {
let _ = lightweight::exit_lightweight_mode().await;
let result = WindowManager::toggle_main_window().await;
logging!(info, Type::Window, "Window toggle result: {result:?}");
log::info!(target: "app", "Window toggle result: {result:?}");
}
pub async fn quit() {
@@ -47,7 +47,7 @@ pub async fn clean_async() -> bool {
let tun_task = async {
let tun_enabled = Config::verge()
.await
.latest_arc()
.data_ref()
.enable_tun_mode
.unwrap_or(false);
@@ -71,20 +71,16 @@ pub async fn clean_async() -> bool {
.await
{
Ok(Ok(_)) => {
logging!(info, Type::Window, "TUN模式已禁用");
log::info!(target: "app", "TUN模式已禁用");
true
}
Ok(Err(e)) => {
logging!(warn, Type::Window, "Warning: 禁用TUN模式失败: {e}");
log::warn!(target: "app", "禁用TUN模式失败: {e}");
// 超时不阻塞退出
true
}
Err(_) => {
logging!(
warn,
Type::Window,
"Warning: 禁用TUN模式超时可能系统正在关机继续退出流程"
);
log::warn!(target: "app", "禁用TUN模式超时可能系统正在关机继续退出流程");
true
}
}
@@ -100,12 +96,12 @@ pub async fn clean_async() -> bool {
// 检查系统代理是否开启
let sys_proxy_enabled = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_system_proxy
.unwrap_or(false);
if !sys_proxy_enabled {
logging!(info, Type::Window, "系统代理未启用,跳过重置");
log::info!(target: "app", "系统代理未启用,跳过重置");
return true;
}
@@ -114,23 +110,19 @@ pub async fn clean_async() -> bool {
if is_shutting_down {
// sysproxy-rs 操作注册表(避免.exe的dll错误)
logging!(
info,
Type::Window,
"检测到正在关机syspro-rs操作注册表关闭系统代理"
);
log::info!(target: "app", "检测到正在关机syspro-rs操作注册表关闭系统代理");
match Sysproxy::get_system_proxy() {
Ok(mut sysproxy) => {
sysproxy.enable = false;
if let Err(e) = sysproxy.set_system_proxy() {
logging!(warn, Type::Window, "Warning: 关机时关闭系统代理失败: {e}");
log::warn!(target: "app", "关机时关闭系统代理失败: {e}");
} else {
logging!(info, Type::Window, "系统代理已关闭(通过注册表)");
log::info!(target: "app", "系统代理已关闭(通过注册表)");
}
}
Err(e) => {
logging!(warn, Type::Window, "Warning: 关机时获取代理设置失败: {e}");
log::warn!(target: "app", "关机时获取代理设置失败: {e}");
}
}
@@ -144,7 +136,7 @@ pub async fn clean_async() -> bool {
}
// 正常退出:使用 sysproxy.exe 重置代理
logging!(info, Type::Window, "sysproxy.exe重置系统代理");
log::info!(target: "app", "sysproxy.exe重置系统代理");
match timeout(
Duration::from_secs(2),
@@ -153,19 +145,15 @@ pub async fn clean_async() -> bool {
.await
{
Ok(Ok(_)) => {
logging!(info, Type::Window, "系统代理已重置");
log::info!(target: "app", "系统代理已重置");
true
}
Ok(Err(e)) => {
logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}");
log::warn!(target: "app", "重置系统代理失败: {e}");
true
}
Err(_) => {
logging!(
warn,
Type::Window,
"Warning: 重置系统代理超时,继续退出流程"
);
log::warn!(target: "app", "重置系统代理超时,继续退出流程");
true
}
}
@@ -176,16 +164,16 @@ pub async fn clean_async() -> bool {
{
let sys_proxy_enabled = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_system_proxy
.unwrap_or(false);
if !sys_proxy_enabled {
logging!(info, Type::Window, "系统代理未启用,跳过重置");
log::info!(target: "app", "系统代理未启用,跳过重置");
return true;
}
logging!(info, Type::Window, "开始重置系统代理...");
log::info!(target: "app", "开始重置系统代理...");
match timeout(
Duration::from_millis(1500),
@@ -194,15 +182,15 @@ pub async fn clean_async() -> bool {
.await
{
Ok(Ok(_)) => {
logging!(info, Type::Window, "系统代理已重置");
log::info!(target: "app", "系统代理已重置");
true
}
Ok(Err(e)) => {
logging!(warn, Type::Window, "Warning: 重置系统代理失败: {e}");
log::warn!(target: "app", "重置系统代理失败: {e}");
true
}
Err(_) => {
logging!(warn, Type::Window, "Warning: 重置系统代理超时,继续退出");
log::warn!(target: "app", "重置系统代理超时,继续退出");
true
}
}
@@ -218,15 +206,11 @@ pub async fn clean_async() -> bool {
match timeout(stop_timeout, CoreManager::global().stop_core()).await {
Ok(_) => {
logging!(info, Type::Window, "core已停止");
log::info!(target: "app", "core已停止");
true
}
Err(_) => {
logging!(
warn,
Type::Window,
"Warning: 停止core超时可能系统正在关机继续退出"
);
log::warn!(target: "app", "停止core超时可能系统正在关机继续退出");
true
}
}
@@ -242,11 +226,11 @@ pub async fn clean_async() -> bool {
.await
{
Ok(_) => {
logging!(info, Type::Window, "DNS设置已恢复");
log::info!(target: "app", "DNS设置已恢复");
true
}
Err(_) => {
logging!(warn, Type::Window, "Warning: 恢复DNS设置超时");
log::warn!(target: "app", "恢复DNS设置超时");
false
}
}
@@ -316,7 +300,7 @@ pub async fn hide() {
let enable_auto_light_weight_mode = Config::verge()
.await
.latest_arc()
.data_mut()
.enable_auto_light_weight_mode
.unwrap_or(false);

View File

@@ -10,9 +10,6 @@ mod feat;
mod module;
mod process;
pub mod utils;
use crate::constants::files;
#[cfg(target_os = "macos")]
use crate::module::lightweight;
#[cfg(target_os = "linux")]
use crate::utils::linux;
#[cfg(target_os = "macos")]
@@ -22,21 +19,20 @@ use crate::{
process::AsyncHandler,
utils::{resolve, server},
};
use anyhow::Result;
use config::Config;
use once_cell::sync::OnceCell;
use rust_i18n::i18n;
use tauri::{AppHandle, Manager};
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
use utils::logging::Type;
i18n!("locales", fallback = "zh");
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
/// Application initialization helper functions
mod app_init {
use anyhow::Result;
use super::*;
/// Initialize singleton monitoring for other instances
@@ -95,14 +91,14 @@ mod app_init {
}
app.deep_link().on_open_url(|event| {
let urls = event.urls();
AsyncHandler::spawn(move || async move {
if let Some(url) = urls.first()
&& let Err(e) = resolve::resolve_scheme(url.as_ref()).await
{
logging!(error, Type::Setup, "Failed to resolve scheme: {}", e);
}
});
let url = event.urls().first().map(|u| u.to_string());
if let Some(url) = url {
AsyncHandler::spawn(|| async {
if let Err(e) = resolve::resolve_scheme(url.into()).await {
logging!(error, Type::Setup, "Failed to resolve scheme: {}", e);
}
});
}
});
Ok(())
@@ -119,7 +115,7 @@ mod app_init {
{
auto_start_plugin_builder = auto_start_plugin_builder
.macos_launcher(MacosLauncher::LaunchAgent)
.app_name(&app.config().identifier);
.app_name(app.config().identifier.clone());
}
app.handle().plugin(auto_start_plugin_builder.build())?;
Ok(())
@@ -129,7 +125,7 @@ mod app_init {
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
logging!(info, Type::Setup, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename(files::WINDOW_STATE)
.with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
.build();
app.handle().plugin(window_state_plugin)?;
@@ -145,8 +141,6 @@ mod app_init {
cmd::open_logs_dir,
cmd::open_web_url,
cmd::open_core_dir,
cmd::open_app_log,
cmd::open_core_log,
cmd::get_portable_flag,
cmd::get_network_interfaces,
cmd::get_system_hostname,
@@ -291,11 +285,6 @@ pub fn run() {
pub async fn handle_reopen(has_visible_windows: bool) {
handle::Handle::global().init();
if lightweight::is_in_lightweight_mode() {
lightweight::exit_lightweight_mode().await;
return;
}
if !has_visible_windows {
handle::Handle::global().set_activation_policy_regular();
let _ = WindowManager::show_main_window().await;
@@ -322,7 +311,7 @@ pub fn run() {
AsyncHandler::spawn(move || async move {
let is_enable_global_hotkey = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
@@ -337,7 +326,10 @@ pub fn run() {
.register_system_hotkey(SystemHotkey::CmdW)
.await;
}
let _ = hotkey::Hotkey::global().init(true).await;
if !is_enable_global_hotkey {
let _ = hotkey::Hotkey::global().init().await;
}
return;
}
@@ -354,21 +346,13 @@ pub fn run() {
});
}
#[cfg(target_os = "macos")]
pub fn handle_window_destroyed() {
use crate::core::hotkey::SystemHotkey;
AsyncHandler::spawn(move || async move {
#[cfg(target_os = "macos")]
{
use crate::core::hotkey::SystemHotkey;
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
let is_enable_global_hotkey = Config::verge()
.await
.latest_arc()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
let _ = hotkey::Hotkey::global().reset();
}
});
}
}
}
@@ -448,7 +432,6 @@ pub fn run() {
tauri::WindowEvent::Focused(focused) => {
event_handlers::handle_window_focus(focused);
}
#[cfg(target_os = "macos")]
tauri::WindowEvent::Destroyed => {
event_handlers::handle_window_destroyed();
}

View File

@@ -4,11 +4,12 @@ fn main() {
console_subscriber::init();
// Check for --no-tray command line argument
#[cfg(target_os = "linux")]
if std::env::args().any(|x| x == "--no-tray") {
let args: Vec<String> = std::env::args().collect();
if args.contains(&"--no-tray".into()) {
unsafe {
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
}
}
app_lib::run();
}

View File

@@ -28,75 +28,108 @@ enum LightweightState {
impl From<u8> for LightweightState {
fn from(v: u8) -> Self {
match v {
1 => Self::In,
2 => Self::Exiting,
_ => Self::Normal,
1 => LightweightState::In,
2 => LightweightState::Exiting,
_ => LightweightState::Normal,
}
}
}
impl LightweightState {
const fn as_u8(self) -> u8 {
fn as_u8(self) -> u8 {
self as u8
}
}
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
static WINDOW_CLOSE_HANDLER: AtomicU32 = AtomicU32::new(0);
static WEBVIEW_FOCUS_HANDLER: AtomicU32 = AtomicU32::new(0);
fn set_state(new: LightweightState) {
LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release);
match new {
LightweightState::Normal => {
logging!(info, Type::Lightweight, "轻量模式已关闭");
}
LightweightState::In => {
logging!(info, Type::Lightweight, "轻量模式已开启");
}
LightweightState::Exiting => {
logging!(info, Type::Lightweight, "正在退出轻量模式");
}
}
}
#[inline]
fn get_state() -> LightweightState {
LIGHTWEIGHT_STATE.load(Ordering::Acquire).into()
}
#[inline]
fn try_transition(from: LightweightState, to: LightweightState) -> bool {
LIGHTWEIGHT_STATE
.compare_exchange(
from.as_u8(),
to.as_u8(),
Ordering::AcqRel,
Ordering::Relaxed,
)
.is_ok()
}
#[inline]
fn record_state_and_log(state: LightweightState) {
LIGHTWEIGHT_STATE.store(state.as_u8(), Ordering::Release);
match state {
LightweightState::Normal => logging!(info, Type::Lightweight, "轻量模式已关闭"),
LightweightState::In => logging!(info, Type::Lightweight, "轻量模式已开启"),
LightweightState::Exiting => logging!(info, Type::Lightweight, "正在退出轻量模式"),
}
}
#[inline]
// 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool {
get_state() == LightweightState::In
}
async fn refresh_lightweight_tray_state() {
if let Err(err) = Tray::global().update_menu().await {
logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}");
// 设置轻量模式状态(仅 Normal <-> In
async fn set_lightweight_mode(value: bool) {
let current = get_state();
if value && current != LightweightState::In {
set_state(LightweightState::In);
} else if !value && current != LightweightState::Normal {
set_state(LightweightState::Normal);
}
// 只有在状态可用时才触发托盘更新
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
}
}
pub async fn auto_lightweight_boot() -> Result<()> {
pub async fn run_once_auto_lightweight() {
let verge_config = Config::verge().await;
let is_enable_auto = verge_config
.data_arc()
let enable_auto = verge_config
.data_mut()
.enable_auto_light_weight_mode
.unwrap_or(false);
let is_silent_start = verge_config.data_arc().enable_silent_start.unwrap_or(false);
if is_enable_auto {
let is_silent_start = verge_config
.latest_ref()
.enable_silent_start
.unwrap_or(false);
if !(enable_auto && is_silent_start) {
logging!(
info,
Type::Lightweight,
"不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式"
);
return;
}
set_lightweight_mode(true).await;
enable_auto_light_weight_mode().await;
}
pub async fn auto_lightweight_mode_init() -> Result<()> {
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
let enable_auto = {
Config::verge()
.await
.latest_ref()
.enable_auto_light_weight_mode
}
.unwrap_or(false);
if enable_auto && !is_silent_start {
logging!(
info,
Type::Lightweight,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_state(LightweightState::Normal);
enable_auto_light_weight_mode().await;
}
if is_silent_start {
entry_lightweight_mode().await;
}
Ok(())
}
@@ -118,33 +151,60 @@ pub fn disable_auto_light_weight_mode() {
}
pub async fn entry_lightweight_mode() -> bool {
if !try_transition(LightweightState::Normal, LightweightState::In) {
logging!(debug, Type::Lightweight, "无需进入轻量模式,跳过调用");
refresh_lightweight_tray_state().await;
// 尝试从 Normal -> In
if LIGHTWEIGHT_STATE
.compare_exchange(
LightweightState::Normal as u8,
LightweightState::In as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用");
return false;
}
record_state_and_log(LightweightState::In);
WindowManager::destroy_main_window();
set_lightweight_mode(true).await;
let _ = cancel_light_weight_timer();
refresh_lightweight_tray_state().await;
// 回到 In
set_state(LightweightState::In);
true
}
// 添加从轻量模式恢复的函数
pub async fn exit_lightweight_mode() -> bool {
if !try_transition(LightweightState::In, LightweightState::Exiting) {
// 尝试从 In -> Exiting
if LIGHTWEIGHT_STATE
.compare_exchange(
LightweightState::In as u8,
LightweightState::Exiting as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
logging!(
debug,
info,
Type::Lightweight,
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
);
refresh_lightweight_tray_state().await;
return false;
}
record_state_and_log(LightweightState::Exiting);
WindowManager::show_main_window().await;
set_lightweight_mode(false).await;
let _ = cancel_light_weight_timer();
record_state_and_log(LightweightState::Normal);
refresh_lightweight_tray_state().await;
// 回到 Normal
set_state(LightweightState::Normal);
logging!(info, Type::Lightweight, "轻量模式退出完成");
true
}
@@ -155,81 +215,70 @@ pub async fn add_light_weight_timer() {
fn setup_window_close_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler_id = window.listen("tauri://close-requested", move |_event| {
let handler = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async {
if let Err(e) = setup_light_weight_timer().await {
logging!(
warn,
Type::Lightweight,
"Warning: Failed to setup light weight timer: {e}"
);
log::warn!("Failed to setup light weight timer: {e}");
}
}));
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
});
WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release);
WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release);
}
}
fn cancel_window_close_listener() {
if let Some(window) = handle::Handle::get_window() {
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 {
window.unlisten(id);
logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, "取消了窗口关闭监听");
}
}
}
fn setup_webview_focus_listener() {
if let Some(window) = handle::Handle::get_window() {
let handler_id = window.listen("tauri://focus", move |_event| {
let handler = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer());
logging!(
debug,
info,
Type::Lightweight,
"监听到窗口获得焦点,取消轻量模式计时"
);
});
WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release);
WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release);
}
}
fn cancel_webview_focus_listener() {
if let Some(window) = handle::Handle::get_window() {
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 {
window.unlisten(id);
logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, "取消了窗口焦点监听");
}
}
}
async fn setup_light_weight_timer() -> Result<()> {
if let Err(e) = Timer::global().init().await {
return Err(e).context("failed to initialize timer");
}
Timer::global().init().await?;
let once_by_minutes = Config::verge()
.await
.data_arc()
.latest_ref()
.auto_light_weight_minutes
.unwrap_or(10);
{
let timer_map = Timer::global().timer_map.read();
if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) {
logging!(debug, Type::Timer, "轻量模式计时器已存在,跳过创建");
return Ok(());
}
}
// 获取task_id
let task_id = {
Timer::global()
.timer_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
};
// 创建任务
let task = TaskBuilder::default()
.set_task_id(task_id)
.set_maximum_parallel_runnable_num(1)
@@ -240,6 +289,7 @@ async fn setup_light_weight_timer() -> Result<()> {
})
.context("failed to create timer task")?;
// 添加任务到定时器
{
let delay_timer = Timer::global().delay_timer.write();
delay_timer
@@ -247,6 +297,7 @@ async fn setup_light_weight_timer() -> Result<()> {
.context("failed to add timer task")?;
}
// 更新任务映射
{
let mut timer_map = Timer::global().timer_map.write();
let timer_task = crate::core::timer::TimerTask {
@@ -268,17 +319,14 @@ async fn setup_light_weight_timer() -> Result<()> {
}
fn cancel_light_weight_timer() -> Result<()> {
let value = Timer::global()
.timer_map
.write()
.remove(LIGHT_WEIGHT_TASK_UID);
if let Some(task) = value {
Timer::global()
.delay_timer
.write()
let mut timer_map = Timer::global().timer_map.write();
let delay_timer = Timer::global().delay_timer.write();
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) {
delay_timer
.remove_task(task.task_id)
.context("failed to remove timer task")?;
logging!(debug, Type::Timer, "计时器已取消");
logging!(info, Type::Timer, "计时器已取消");
}
Ok(())

View File

@@ -1,26 +1,19 @@
use anyhow::Result;
use tauri_plugin_shell::process::CommandChild;
use crate::{logging, utils::logging::Type};
#[derive(Debug)]
pub struct CommandChildGuard(Option<CommandChild>);
impl Drop for CommandChildGuard {
fn drop(&mut self) {
if let Err(err) = self.kill() {
logging!(
error,
Type::Service,
"Failed to kill child process: {}",
err
);
log::error!(target: "app", "Failed to kill child process: {}", err);
}
}
}
impl CommandChildGuard {
pub const fn new(child: CommandChild) -> Self {
pub fn new(child: CommandChild) -> Self {
Self(Some(child))
}

View File

@@ -1,7 +1,7 @@
#[cfg(target_os = "windows")]
use crate::{logging, utils::logging::Type};
#[cfg(target_os = "windows")]
use anyhow::{Result, anyhow};
#[cfg(target_os = "windows")]
use log::info;
#[cfg(target_os = "windows")]
use std::{os::windows::process::CommandExt, path::Path, path::PathBuf};
@@ -49,15 +49,15 @@ pub async fn create_shortcut() -> Result<()> {
.remove_if_exists()
.await
.inspect(|_| {
logging!(info, Type::Setup, "成功移除旧启动快捷方式");
info!(target: "app", "成功移除旧启动快捷方式");
})
.inspect_err(|err| {
logging!(error, Type::Setup, "移除旧启动快捷方式失败: {err}");
log::error!(target: "app", "移除旧启动快捷方式失败: {err}");
});
// 如果新快捷方式已存在,直接返回成功
if new_shortcut_path.exists() {
logging!(info, Type::Setup, "启动快捷方式已存在");
info!(target: "app", "启动快捷方式已存在");
return Ok(());
}
@@ -83,7 +83,7 @@ pub async fn create_shortcut() -> Result<()> {
return Err(anyhow!("创建快捷方式失败: {}", error_msg));
}
logging!(info, Type::Setup, "成功创建启动快捷方式");
info!(target: "app", "成功创建启动快捷方式");
Ok(())
}
@@ -102,22 +102,22 @@ pub async fn remove_shortcut() -> Result<()> {
.remove_if_exists()
.await
.inspect(|_| {
logging!(info, Type::Setup, "成功删除旧启动快捷方式");
info!(target: "app", "成功删除旧启动快捷方式");
removed_any = true;
})
.inspect_err(|err| {
logging!(error, Type::Setup, "删除旧启动快捷方式失败: {err}");
log::error!(target: "app", "删除旧启动快捷方式失败: {err}");
});
let _ = new_shortcut_path
.remove_if_exists()
.await
.inspect(|_| {
logging!(info, Type::Setup, "成功删除启动快捷方式");
info!(target: "app", "成功删除启动快捷方式");
removed_any = true;
})
.inspect_err(|err| {
logging!(error, Type::Setup, "删除启动快捷方式失败: {err}");
log::error!(target: "app", "删除启动快捷方式失败: {err}");
});
Ok(())

View File

@@ -1,13 +1,7 @@
use crate::{
core::{CoreManager, handle, manager::RunningMode},
logging,
utils::logging::Type,
};
use crate::{core::handle, logging, utils::logging::Type};
use anyhow::Result;
use async_trait::async_trait;
use once_cell::sync::OnceCell;
#[cfg(unix)]
use std::iter;
use std::{fs, path::PathBuf};
use tauri::Manager;
@@ -26,6 +20,7 @@ pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
pub static CLASH_CONFIG: &str = "config.yaml";
pub static VERGE_CONFIG: &str = "verge.yaml";
pub static PROFILE_YAML: &str = "profiles.yaml";
pub static DNS_CONFIG: &str = "dns_config.yaml";
/// init portable flag
pub fn init_portable_flag() -> Result<()> {
@@ -63,11 +58,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
match app_handle.path().data_dir() {
Ok(dir) => Ok(dir.join(APP_ID)),
Err(e) => {
logging!(
error,
Type::File,
"Failed to get the app home directory: {e}"
);
log::error!(target: "app", "Failed to get the app home directory: {e}");
Err(anyhow::anyhow!("Failed to get the app homedirectory"))
}
}
@@ -81,11 +72,7 @@ pub fn app_resources_dir() -> Result<PathBuf> {
match app_handle.path().resource_dir() {
Ok(dir) => Ok(dir.join("resources")),
Err(e) => {
logging!(
error,
Type::File,
"Failed to get the resource directory: {e}"
);
log::error!(target: "app", "Failed to get the resource directory: {e}");
Err(anyhow::anyhow!("Failed to get the resource directory"))
}
}
@@ -135,11 +122,6 @@ pub fn app_logs_dir() -> Result<PathBuf> {
Ok(app_home_dir()?.join("logs"))
}
// latest verge log
pub fn app_latest_log() -> Result<PathBuf> {
Ok(app_logs_dir()?.join("latest.log"))
}
/// local backups dir
pub fn local_backup_dir() -> Result<PathBuf> {
let dir = app_home_dir()?.join(BACKUP_DIR);
@@ -185,15 +167,6 @@ pub fn service_log_dir() -> Result<PathBuf> {
Ok(log_dir)
}
pub fn clash_latest_log() -> Result<PathBuf> {
match *CoreManager::global().get_running_mode() {
RunningMode::Service => Ok(service_log_dir()?.join("service_latest.log")),
RunningMode::Sidecar | RunningMode::NotRunning => {
Ok(sidecar_log_dir()?.join("sidecar_latest.log"))
}
}
}
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
let path_str = path
.as_os_str()
@@ -228,7 +201,8 @@ pub fn get_encryption_key() -> Result<Vec<u8>> {
#[cfg(unix)]
pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
iter::once("/tmp")
["/tmp"]
.iter()
.map(PathBuf::from)
.find(|path| path.exists())
.or_else(|| {
@@ -237,11 +211,7 @@ pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
if home_config.exists() || fs::create_dir_all(&home_config).is_ok() {
Some(home_config)
} else {
logging!(
error,
Type::File,
"Failed to create safe directory: {home_config:?}"
);
log::error!(target: "app", "Failed to create safe directory: {home_config:?}");
None
}
})

View File

@@ -1,369 +1,178 @@
use parking_lot::RwLock;
use std::sync::Arc;
pub type SharedBox<T> = Arc<Box<T>>;
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
use parking_lot::{
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard,
RwLockUpgradableReadGuard, RwLockWriteGuard,
};
/// Draft 管理committed 与 optional draft 都以 Arc<Box<T>> 存储,
// (committed_snapshot, optional_draft_snapshot)
#[derive(Debug, Clone)]
pub struct Draft<T: Clone> {
inner: Arc<RwLock<DraftInner<T>>>,
pub struct Draft<T: Clone + ToOwned> {
inner: Arc<RwLock<(T, Option<T>)>>,
}
impl<T: Clone> Draft<T> {
pub fn new(data: T) -> Self {
impl<T: Clone + ToOwned> From<T> for Draft<T> {
fn from(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
inner: Arc::new(RwLock::new((data, None))),
}
}
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc
pub fn data_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
Arc::clone(&guard.0)
}
/// Implements draft management for `Box<T>`, allowing for safe concurrent editing and committing of draft data.
/// # Type Parameters
/// - `T`: The underlying data type, which must implement `Clone` and `ToOwned`.
///
/// # Methods
/// - `data_mut`: Returns a mutable reference to the committed data.
/// - `data_ref`: Returns an immutable reference to the committed data.
/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists.
/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data.
/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed.
/// - `discard`: Discards the draft data and returns it if it existed.
impl<T: Clone + ToOwned> Draft<Box<T>> {
/// 可写正式数据
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
}
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
/// 这也是零拷贝:只 clone Arc不 clone T
pub fn latest_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
guard
/// 返回正式数据的只读视图(不包含草稿)
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
}
/// 创建或获取草稿并返回可写引用
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
let guard = self.inner.upgradable_read();
if guard.1.is_none() {
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
guard.1 = Some(guard.0.clone());
return RwLockWriteGuard::map(guard, |inner| {
inner.1.as_mut().unwrap_or_else(|| {
unreachable!("Draft was just created above, this should never fail")
})
});
}
// 已存在草稿,升级为写锁映射
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
inner
.1
.as_mut()
.unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()"))
})
}
/// 零拷贝只读视图:返回草稿(若存在)或正式值
pub fn latest_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| {
inner.1.as_ref().unwrap_or(&inner.0)
})
}
/// 提交草稿,返回旧正式数据
pub fn apply(&self) -> Option<Box<T>> {
let mut inner = self.inner.write();
inner
.1
.as_ref()
.cloned()
.unwrap_or_else(|| Arc::clone(&guard.0))
.take()
.map(|draft| std::mem::replace(&mut inner.0, draft))
}
/// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T
/// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T
/// - 若草稿被其他读者共享Arc::make_mut 会做一次 T.clone最小必要拷贝
pub fn edit_draft<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut T) -> R,
{
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
let mut guard = self.inner.write();
let mut draft_arc = if guard.1.is_none() {
Arc::clone(&guard.0)
} else {
#[allow(clippy::unwrap_used)]
guard.1.take().unwrap()
};
drop(guard);
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
// 对 Box<T> 解引用得到 &mut T
let result = f(&mut **boxed);
// 恢复修改后的草稿 Arc
self.inner.write().1 = Some(draft_arc);
result
/// 丢弃草稿,返回被丢弃的草稿
pub fn discard(&self) -> Option<Box<T>> {
self.inner.write().1.take()
}
/// 将草稿提交到已提交位置(替换),并清除草稿
pub fn apply(&self) {
let mut guard = self.inner.write();
if let Some(d) = guard.1.take() {
guard.0 = d;
}
}
/// 丢弃草稿(如果存在)
pub fn discard(&self) {
let mut guard = self.inner.write();
guard.1 = None;
}
/// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
/// 异步修改正式数据,闭包直接获得 Box<T> 所有权
pub async fn with_data_modify<F, Fut, R, E>(&self, f: F) -> Result<R, E>
where
T: Send + Sync + 'static,
F: FnOnce(Box<T>) -> Fut + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), E>> + Send,
E: From<anyhow::Error>,
{
// 读取已提交快照cheap Arc clone, 然后得到 Box<T> 所有权 via clone
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T不可避免
let local: Box<T> = {
// 克隆正式数据
let local = {
let guard = self.inner.read();
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone
(*guard.0).clone()
guard.0.clone()
};
// 异步闭包执行,返回修改后的 Box<T> 和业务结果 R
let (new_local, res) = f(local).await?;
// 将新的 Box<T> 放到已提交位置(包进 Arc
self.inner.write().0 = Arc::new(new_local);
// 写回正式数据
let mut guard = self.inner.write();
guard.0 = new_local;
Ok(res)
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
#[test]
fn test_draft_box() {
use crate::config::IVerge;
#[derive(Clone, Debug, Default, PartialEq)]
struct IVerge {
enable_auto_launch: Option<bool>,
enable_tun_mode: Option<bool>,
// 1. 创建 Draft<Box<IVerge>>
let verge = Box::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..IVerge::default()
});
let draft = Draft::from(verge);
// 2. 读取正式数据data_mut
{
let data = draft.data_mut();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
}
// Minimal single-threaded executor for immediately-ready futures
fn block_on_ready<F: Future>(fut: F) -> F::Output {
fn no_op_raw_waker() -> RawWaker {
fn clone(_: *const ()) -> RawWaker {
no_op_raw_waker()
}
fn wake(_: *const ()) {}
fn wake_by_ref(_: *const ()) {}
fn drop(_: *const ()) {}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
RawWaker::new(std::ptr::null(), &VTABLE)
}
let waker = unsafe { Waker::from_raw(no_op_raw_waker()) };
let mut cx = Context::from_waker(&waker);
let mut fut = Box::pin(fut);
loop {
match Pin::as_mut(&mut fut).poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => std::thread::yield_now(),
}
}
// 3. 初次获取草稿draft_mut 会自动 clone 一份)
{
let draft_view = draft.draft_mut();
assert_eq!(draft_view.enable_auto_launch, Some(true));
assert_eq!(draft_view.enable_tun_mode, Some(false));
}
#[test]
fn test_draft_basic_flow() {
let verge = IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
};
let draft = Draft::new(verge);
// 读取正式数据data_arc
{
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
}
// 修改草稿(使用 edit_draft
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
});
// 正式数据未变
{
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
}
// 草稿已变
{
let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(false));
assert_eq!(latest.enable_tun_mode, Some(true));
}
// 提交草稿
draft.apply();
// 正式数据已更新
{
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
}
// 新一轮草稿并修改
draft.edit_draft(|d| {
d.enable_auto_launch = Some(true);
});
{
let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(true));
assert_eq!(latest.enable_tun_mode, Some(true));
}
// 丢弃草稿
draft.discard();
// 丢弃后再次创建草稿,会从已提交重新 clone
{
draft.edit_draft(|d| {
// 原 committed 是 enable_auto_launch = Some(false)
assert_eq!(d.enable_auto_launch, Some(false));
// 再修改一下
d.enable_tun_mode = Some(false);
});
// 草稿中值已修改,但正式数据仍是 apply 后的值
let data = draft.data_arc();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
}
// 4. 修改草稿
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
}
#[test]
fn test_arc_pointer_behavior_on_edit_and_apply() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
});
// 正式数据未变
assert_eq!(draft.data_mut().enable_auto_launch, Some(true));
assert_eq!(draft.data_mut().enable_tun_mode, Some(false));
// 初始 latest == committed
let committed = draft.data_arc();
let latest = draft.latest_arc();
assert!(std::sync::Arc::ptr_eq(&committed, &latest));
// 第一次 edit由于与 committed 共享Arc::make_mut 会克隆
draft.edit_draft(|d| d.enable_tun_mode = Some(true));
let committed_after_first_edit = draft.data_arc();
let draft_after_first_edit = draft.latest_arc();
assert!(!std::sync::Arc::ptr_eq(
&committed_after_first_edit,
&draft_after_first_edit
));
// 提交会把 committed 指向草稿的 Arc
let prev_draft_ptr = std::sync::Arc::as_ptr(&draft_after_first_edit);
draft.apply();
let committed_after_apply = draft.data_arc();
assert_eq!(
std::sync::Arc::as_ptr(&committed_after_apply),
prev_draft_ptr
);
// 第二次编辑:此时草稿唯一持有(无其它引用),不应再克隆
// 获取草稿 Arc 的指针并立即丢弃本地引用,避免增加 strong_count
draft.edit_draft(|d| d.enable_auto_launch = Some(false));
let latest1 = draft.latest_arc();
let latest1_ptr = std::sync::Arc::as_ptr(&latest1);
drop(latest1); // 确保只有 Draft 内部持有草稿 Arc
// 再次编辑uniqueArc::make_mut 不应克隆)
draft.edit_draft(|d| d.enable_tun_mode = Some(false));
let latest2 = draft.latest_arc();
let latest2_ptr = std::sync::Arc::as_ptr(&latest2);
assert_eq!(latest1_ptr, latest2_ptr, "Unique edit should not clone Arc");
assert_eq!(latest2.enable_auto_launch, Some(false));
assert_eq!(latest2.enable_tun_mode, Some(false));
}
#[test]
fn test_discard_restores_latest_to_committed() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 创建草稿并修改
draft.edit_draft(|d| d.enable_auto_launch = Some(true));
let committed = draft.data_arc();
let latest = draft.latest_arc();
assert!(!std::sync::Arc::ptr_eq(&committed, &latest));
// 丢弃草稿后 latest 应回到 committed
draft.discard();
let committed2 = draft.data_arc();
let latest2 = draft.latest_arc();
assert!(std::sync::Arc::ptr_eq(&committed2, &latest2));
assert_eq!(latest2.enable_auto_launch, Some(false));
}
#[test]
fn test_edit_draft_returns_closure_result() {
let draft = Draft::new(IVerge::default());
let ret = draft.edit_draft(|d| {
d.enable_tun_mode = Some(true);
123usize
});
assert_eq!(ret, 123);
let latest = draft.latest_arc();
// 草稿已变
{
let latest = draft.latest_ref();
assert_eq!(latest.enable_auto_launch, Some(false));
assert_eq!(latest.enable_tun_mode, Some(true));
}
#[test]
fn test_with_data_modify_ok_and_replaces_committed() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 5. 提交草稿
assert!(draft.apply().is_some()); // 第一次提交应有返回
assert!(draft.apply().is_none()); // 第二次提交返回 None
// 使用 with_data_modify 异步(立即就绪)地更新 committed
let res = block_on_ready(draft.with_data_modify(|mut v| async move {
v.enable_auto_launch = Some(true);
Ok((Box::new(*v), "done")) // Dereference v to get Box<T>
}));
assert_eq!(
{
#[allow(clippy::unwrap_used)]
res.unwrap()
},
"done"
);
let committed = draft.data_arc();
assert_eq!(committed.enable_auto_launch, Some(true));
assert_eq!(committed.enable_tun_mode, Some(false));
// 正式数据已更新
{
let data = draft.data_mut();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
}
#[test]
fn test_with_data_modify_error_propagation() {
let draft = Draft::new(IVerge::default());
#[allow(clippy::unwrap_used)]
let err = block_on_ready(draft.with_data_modify(|v| async move {
drop(v);
Err::<(Box<IVerge>, ()), _>(anyhow!("boom"))
}))
.unwrap_err();
assert_eq!(format!("{err}"), "boom");
// 6. 新建并修改下一轮草稿
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(true);
}
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
#[test]
fn test_with_data_modify_does_not_touch_existing_draft() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 7. 丢弃草稿
assert!(draft.discard().is_some()); // 第一次丢弃返回 Some
assert!(draft.discard().is_none()); // 再次丢弃返回 None
// 创建草稿并修改
draft.edit_draft(|d| {
d.enable_auto_launch = Some(true);
d.enable_tun_mode = Some(true);
});
let draft_before = draft.latest_arc();
let draft_before_ptr = std::sync::Arc::as_ptr(&draft_before);
// 同时通过 with_data_modify 修改 committed
#[allow(clippy::unwrap_used)]
block_on_ready(draft.with_data_modify(|mut v| async move {
v.enable_auto_launch = Some(false); // 与草稿不同
Ok((Box::new(*v), ())) // Dereference v to get Box<T>
}))
.unwrap();
// 草稿应保持不变
let draft_after = draft.latest_arc();
assert_eq!(
std::sync::Arc::as_ptr(&draft_after),
draft_before_ptr,
"Existing draft should not be replaced by with_data_modify"
);
assert_eq!(draft_after.enable_auto_launch, Some(true));
assert_eq!(draft_after.enable_tun_mode, Some(true));
// 丢弃草稿后 latest == committed且 committed 为异步修改结果
draft.discard();
let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(false));
assert_eq!(latest.enable_tun_mode, Some(false));
}
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false));
}

View File

@@ -1,4 +1,4 @@
use crate::{config::with_encryption, enhance::seq::SeqMap, logging, utils::logging::Type};
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
use anyhow::{Context, Result, anyhow, bail};
use nanoid::nanoid;
use serde::{Serialize, de::DeserializeOwned};
@@ -13,7 +13,7 @@ pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
let yaml_str = tokio::fs::read_to_string(path).await?;
Ok(with_encryption(|| async { serde_yaml_ng::from_str::<T>(&yaml_str) }).await?)
Ok(serde_yaml_ng::from_str::<T>(&yaml_str)?)
}
/// read mapping from yaml
@@ -65,7 +65,7 @@ pub async fn save_yaml<T: Serialize + Sync>(
data: &T,
prefix: Option<&str>,
) -> Result<()> {
let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?;
let data_str = serde_yaml_ng::to_string(data)?;
let yaml_str = match prefix {
Some(prefix) => format!("{prefix}\n\n{data_str}"),

View File

@@ -1,99 +1,114 @@
use crate::config::Config;
use crate::{config::Config, utils::dirs};
use once_cell::sync::Lazy;
use smartstring::alias::String;
use std::{
collections::HashMap,
fs,
path::PathBuf,
sync::{Arc, RwLock},
};
use sys_locale;
const DEFAULT_LANGUAGE: &str = "zh";
fn supported_languages_internal() -> Vec<&'static str> {
rust_i18n::available_locales!()
}
type TranslationMap = (String, HashMap<String, Arc<str>>);
const fn fallback_language() -> &'static str {
DEFAULT_LANGUAGE
}
fn locale_alias(locale: &str) -> Option<&'static str> {
match locale {
"ja" | "ja-jp" | "jp" => Some("jp"),
"zh" | "zh-cn" | "zh-hans" | "zh-sg" | "zh-my" | "zh-chs" => Some("zh"),
"zh-tw" | "zh-hk" | "zh-hant" | "zh-mo" | "zh-cht" => Some("zhtw"),
_ => None,
}
}
fn resolve_supported_language(language: &str) -> Option<String> {
if language.is_empty() {
return None;
}
let normalized = language.to_lowercase().replace('_', "-");
let mut candidates: Vec<String> = Vec::new();
let mut push_candidate = |candidate: String| {
if !candidate.is_empty()
&& !candidates
.iter()
.any(|existing| existing.eq_ignore_ascii_case(&candidate))
{
candidates.push(candidate);
}
};
let segments: Vec<&str> = normalized.split('-').collect();
for i in (1..=segments.len()).rev() {
let prefix = segments[..i].join("-");
if let Some(alias) = locale_alias(&prefix) {
push_candidate(alias.to_string());
}
push_candidate(prefix);
}
let supported = supported_languages_internal();
candidates.into_iter().find(|candidate| {
supported
.iter()
.any(|&lang| lang.eq_ignore_ascii_case(candidate))
})
}
fn system_language() -> String {
sys_locale::get_locale()
.as_deref()
.and_then(resolve_supported_language)
.unwrap_or_else(|| fallback_language().to_string())
fn get_locales_dir() -> Option<PathBuf> {
dirs::app_resources_dir()
.map(|resource_path| resource_path.join("locales"))
.ok()
}
pub fn get_supported_languages() -> Vec<String> {
supported_languages_internal()
.into_iter()
.map(|lang| lang.to_string())
.collect()
}
let mut languages = Vec::new();
pub fn set_locale(language: &str) {
let lang =
resolve_supported_language(language).unwrap_or_else(|| fallback_language().to_string());
rust_i18n::set_locale(&lang);
if let Some(locales_dir) = get_locales_dir()
&& let Ok(entries) = fs::read_dir(locales_dir)
{
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str()
&& let Some(lang) = file_name.strip_suffix(".json")
{
languages.push(lang.into());
}
}
}
if languages.is_empty() {
languages.push(DEFAULT_LANGUAGE.into());
}
languages
}
pub async fn current_language() -> String {
Config::verge()
.await
.latest_arc()
.latest_ref()
.language
.clone()
.filter(|lang| !lang.is_empty())
.and_then(|lang| resolve_supported_language(&lang))
.unwrap_or_else(system_language)
.as_deref()
.map(String::from)
.unwrap_or_else(get_system_language)
}
pub async fn sync_locale() -> String {
let language = current_language().await;
set_locale(&language);
language
static TRANSLATIONS: Lazy<RwLock<TranslationMap>> = Lazy::new(|| {
let lang = get_system_language();
let map = load_lang_file(&lang).unwrap_or_default();
RwLock::new((lang, map))
});
fn load_lang_file(lang: &str) -> Option<HashMap<String, Arc<str>>> {
let locales_dir = get_locales_dir()?;
let file_path = locales_dir.join(format!("{lang}.json"));
fs::read_to_string(file_path)
.ok()
.and_then(|content| serde_json::from_str::<HashMap<String, String>>(&content).ok())
.map(|map| {
map.into_iter()
.map(|(k, v)| (k, Arc::from(v.as_str())))
.collect()
})
}
pub const fn default_language() -> &'static str {
fallback_language()
fn get_system_language() -> String {
sys_locale::get_locale()
.map(|locale| locale.to_lowercase())
.and_then(|locale| locale.split(['_', '-']).next().map(String::from))
.filter(|lang| get_supported_languages().contains(lang))
.unwrap_or_else(|| DEFAULT_LANGUAGE.into())
}
pub async fn t(key: &str) -> Arc<str> {
let current_lang = current_language().await;
{
if let Ok(cache) = TRANSLATIONS.read()
&& cache.0 == current_lang
&& let Some(text) = cache.1.get(key)
{
return Arc::clone(text);
}
}
if let Some(new_map) = load_lang_file(&current_lang)
&& let Ok(mut cache) = TRANSLATIONS.write()
{
*cache = (current_lang.clone(), new_map);
if let Some(text) = cache.1.get(key) {
return Arc::clone(text);
}
}
if current_lang != DEFAULT_LANGUAGE
&& let Some(default_map) = load_lang_file(DEFAULT_LANGUAGE)
&& let Ok(mut cache) = TRANSLATIONS.write()
{
*cache = (DEFAULT_LANGUAGE.into(), default_map);
if let Some(text) = cache.1.get(key) {
return Arc::clone(text);
}
}
Arc::from(key)
}

View File

@@ -1,9 +1,8 @@
// #[cfg(not(feature = "tracing"))]
#[cfg(not(feature = "tracing"))]
#[cfg(not(feature = "tauri-dev"))]
use crate::utils::logging::NoModuleFilter;
use crate::{
config::*,
constants,
core::handle,
logging,
process::AsyncHandler,
@@ -31,7 +30,7 @@ pub async fn init_logger() -> Result<()> {
// TODO 提供 runtime 级别实时修改
let (log_level, log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_arc();
let verge = verge_guard.latest_ref();
(
verge.get_log_level(),
verge.app_log_max_size.unwrap_or(128),
@@ -49,9 +48,7 @@ pub async fn init_logger() -> Result<()> {
#[cfg(feature = "tracing")]
spec.module("tauri", log::LevelFilter::Debug);
#[cfg(feature = "tracing")]
spec.module("wry", log::LevelFilter::Off);
#[cfg(feature = "tracing")]
spec.module("tauri_plugin_mihomo", log::LevelFilter::Off);
spec.module("wry", log::LevelFilter::Debug);
let spec = spec.build();
let logger = Logger::with(spec)
@@ -69,12 +66,6 @@ pub async fn init_logger() -> Result<()> {
);
#[cfg(not(feature = "tracing"))]
let logger = logger.filter(Box::new(NoModuleFilter(&["wry", "tauri"])));
#[cfg(feature = "tracing")]
let logger = logger.filter(Box::new(NoModuleFilter(&[
"wry",
"tauri_plugin_mihomo",
"kode_bridge",
])));
let _handle = logger.start()?;
@@ -89,7 +80,7 @@ pub async fn init_logger() -> Result<()> {
pub async fn sidecar_writer() -> Result<FileLogWriter> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_arc();
let verge = verge_guard.latest_ref();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
@@ -117,7 +108,7 @@ pub async fn sidecar_writer() -> Result<FileLogWriter> {
pub async fn service_writer_config() -> Result<WriterConfig> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_arc();
let verge = verge_guard.latest_ref();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
@@ -142,7 +133,7 @@ pub async fn delete_log() -> Result<()> {
let auto_log_clean = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.auto_log_clean.unwrap_or(0)
};
@@ -313,7 +304,7 @@ async fn init_dns_config() -> Result<()> {
// 检查DNS配置文件是否存在
let app_dir = dirs::app_home_dir()?;
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
let dns_path = app_dir.join("dns_config.yaml");
if !dns_path.exists() {
logging!(info, Type::Setup, "Creating default DNS config file");
@@ -373,7 +364,7 @@ async fn initialize_config_files() -> Result<()> {
if let Ok(path) = dirs::profiles_path()
&& !path.exists()
{
let template = IProfiles::default();
let template = IProfiles::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
@@ -438,8 +429,26 @@ pub async fn init_resources() -> Result<()> {
let src_path = res_dir.join(file);
let dest_path = app_dir.join(file);
let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
match fs::copy(&src, &dest).await {
Ok(_) => {
logging!(debug, Type::Setup, "resources copied '{}'", file);
}
Err(err) => {
logging!(
error,
Type::Setup,
"failed to copy resources '{}' to '{:?}', {}",
file,
dest,
err
);
}
};
};
if src_path.exists() && !dest_path.exists() {
handle_copy(&src_path, &dest_path, file).await;
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
continue;
}
@@ -449,12 +458,12 @@ pub async fn init_resources() -> Result<()> {
match (src_modified, dest_modified) {
(Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified {
handle_copy(&src_path, &dest_path, file).await;
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
}
}
_ => {
logging!(debug, Type::Setup, "failed to get modified '{}'", file);
handle_copy(&src_path, &dest_path, file).await;
handle_copy(src_path.clone(), dest_path.clone(), (*file).into()).await;
}
};
}
@@ -506,7 +515,7 @@ pub fn init_scheme() -> Result<()> {
Ok(())
}
#[cfg(target_os = "macos")]
pub const fn init_scheme() -> Result<()> {
pub fn init_scheme() -> Result<()> {
Ok(())
}
@@ -517,7 +526,7 @@ pub async fn startup_script() -> Result<()> {
let app_handle = handle::Handle::app_handle();
let script_path = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.startup_script.clone().unwrap_or_else(|| "".into())
};
@@ -554,21 +563,3 @@ pub async fn startup_script() -> Result<()> {
Ok(())
}
async fn handle_copy(src: &PathBuf, dest: &PathBuf, file: &str) {
match fs::copy(src, dest).await {
Ok(_) => {
logging!(debug, Type::Setup, "resources copied '{}'", file);
}
Err(err) => {
logging!(
error,
Type::Setup,
"failed to copy resources '{}' to '{:?}', {}",
file,
dest,
err
);
}
};
}

View File

@@ -19,7 +19,7 @@ struct IntelGpuDetection {
}
impl IntelGpuDetection {
const fn should_disable_dmabuf(&self) -> bool {
fn should_disable_dmabuf(&self) -> bool {
self.intel_is_primary || self.inconclusive
}
}
@@ -41,7 +41,7 @@ enum NvidiaDmabufDisableReason {
}
impl NvidiaGpuDetection {
const fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
if !session.is_wayland {
return None;
}
@@ -144,11 +144,11 @@ impl DmabufOverrides {
}
}
const fn has_env_override(&self) -> bool {
fn has_env_override(&self) -> bool {
self.dmabuf_override.is_some()
}
const fn should_override_env(&self, decision: &DmabufDecision) -> bool {
fn should_override_env(&self, decision: &DmabufDecision) -> bool {
if self.user_preference.is_some() {
return true;
}

View File

@@ -29,6 +29,7 @@ pub enum Type {
Lightweight,
Network,
ProxyMode,
// Cache,
Validate,
ClashVergeRev,
}
@@ -36,24 +37,25 @@ pub enum Type {
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cmd => write!(f, "[Cmd]"),
Self::Core => write!(f, "[Core]"),
Self::Config => write!(f, "[Config]"),
Self::Setup => write!(f, "[Setup]"),
Self::System => write!(f, "[System]"),
Self::Service => write!(f, "[Service]"),
Self::Hotkey => write!(f, "[Hotkey]"),
Self::Window => write!(f, "[Window]"),
Self::Tray => write!(f, "[Tray]"),
Self::Timer => write!(f, "[Timer]"),
Self::Frontend => write!(f, "[Frontend]"),
Self::Backup => write!(f, "[Backup]"),
Self::File => write!(f, "[File]"),
Self::Lightweight => write!(f, "[Lightweight]"),
Self::Network => write!(f, "[Network]"),
Self::ProxyMode => write!(f, "[ProxMode]"),
Self::Validate => write!(f, "[Validate]"),
Self::ClashVergeRev => write!(f, "[ClashVergeRev]"),
Type::Cmd => write!(f, "[Cmd]"),
Type::Core => write!(f, "[Core]"),
Type::Config => write!(f, "[Config]"),
Type::Setup => write!(f, "[Setup]"),
Type::System => write!(f, "[System]"),
Type::Service => write!(f, "[Service]"),
Type::Hotkey => write!(f, "[Hotkey]"),
Type::Window => write!(f, "[Window]"),
Type::Tray => write!(f, "[Tray]"),
Type::Timer => write!(f, "[Timer]"),
Type::Frontend => write!(f, "[Frontend]"),
Type::Backup => write!(f, "[Backup]"),
Type::File => write!(f, "[File]"),
Type::Lightweight => write!(f, "[Lightweight]"),
Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"),
// Type::Cache => write!(f, "[Cache]"),
Type::Validate => write!(f, "[Validate]"),
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"),
}
}
}
@@ -80,6 +82,15 @@ macro_rules! log_err {
};
}
#[macro_export]
macro_rules! trace_err {
($result: expr, $err_str: expr) => {
if let Err(err) = $result {
log::trace!(target: "app", "{}, err {}", $err_str, err);
}
}
}
/// wrap the anyhow error
/// transform the error to String
#[macro_export]

View File

@@ -1,7 +1,6 @@
use crate::config::Config;
use anyhow::Result;
use base64::{Engine as _, engine::general_purpose};
use isahc::config::DnsCache;
use isahc::prelude::*;
use isahc::{HttpClient, config::SslOption};
use isahc::{
@@ -26,7 +25,7 @@ pub struct HttpResponse {
}
impl HttpResponse {
pub const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
pub fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
Self {
status,
headers,
@@ -34,11 +33,11 @@ impl HttpResponse {
}
}
pub const fn status(&self) -> StatusCode {
pub fn status(&self) -> StatusCode {
self.status
}
pub const fn headers(&self) -> &HeaderMap {
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
@@ -80,7 +79,8 @@ impl NetworkManager {
}
async fn record_connection_error(&self, error: &str) {
*self.last_connection_error.lock().await = Some((Instant::now(), error.into()));
let mut last_error = self.last_connection_error.lock().await;
*last_error = Some((Instant::now(), error.into()));
let mut count = self.connection_error_count.lock().await;
*count += 1;
@@ -88,11 +88,13 @@ impl NetworkManager {
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 count > 5 {
return true;
}
if let Some((time, _)) = &*self.last_connection_error.lock().await
if let Some((time, _)) = &*last_error_guard
&& time.elapsed() < Duration::from_secs(30)
&& count > 2
{
@@ -116,15 +118,18 @@ impl NetworkManager {
accept_invalid_certs: bool,
timeout_secs: Option<u64>,
) -> Result<HttpClient> {
let proxy_uri_clone = proxy_uri.clone();
let headers_clone = default_headers.clone();
{
let mut builder = HttpClient::builder();
builder = match proxy_uri {
builder = match proxy_uri_clone {
Some(uri) => builder.proxy(Some(uri)),
None => builder.proxy(None),
};
for (name, value) in default_headers.iter() {
for (name, value) in headers_clone.iter() {
builder = builder.default_header(name, value);
}
@@ -138,12 +143,6 @@ impl NetworkManager {
builder = builder.redirect_policy(RedirectPolicy::Follow);
// 禁用缓存,不关心连接复用
builder = builder.connection_cache_size(0);
// 禁用 DNS 缓存,避免因 DNS 变化导致的问题
builder = builder.dns_cache(DnsCache::Disable);
Ok(builder.build()?)
}
}
@@ -159,10 +158,10 @@ impl NetworkManager {
ProxyType::None => None,
ProxyType::Localhost => {
let port = {
let verge_port = Config::verge().await.latest_arc().verge_mixed_port;
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
match verge_port {
Some(port) => port,
None => Config::clash().await.latest_arc().get_mixed_port(),
None => Config::clash().await.latest_ref().get_mixed_port(),
}
};
let proxy_scheme = format!("http://127.0.0.1:{port}");

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