Compare commits

...
Author SHA1 Message Date
Tunglies fa74e79f40 fix: update version from 2.4.3-rc to 2.4.3 in package.json, Cargo.toml, Cargo.lock, and tauri.conf.json 2025-11-08 19:10:29 +08:00
Tunglies 6017423a49 chore: update version to 2.4.3-rc and enhance release notes 2025-11-08 18:31:22 +08:00
Tunglies 538cba5a33 fix: return correct type in get_profiles function 2025-11-08 17:07:48 +08:00
oomeow 76bf91ff8a fix: import profile panic (#5346)
* fix: import profile panic

docs: update UPDATELOG.md

chore: update

* perf: spawn import profile
2025-11-07 22:46:30 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 758c21e656 chore(deps): update dependency vitest to ^4.0.8 (#5349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 21:51:09 +08:00
Slinetrac 5759bed687 refactor(linux): replace users crate with libc::geteuid for root detection
Closes #5333
2025-11-07 18:58:13 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 9cc2f44379 chore(deps): update dependency vite to ^7.2.2 (#5342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 16:22:28 +08:00
rozanandSlinetrac aa6a1c8650 add Korean README and update language selectors (#5338)
* docs(i18n): add Korean README and update language selectors

* style: prettier

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2025-11-07 12:06:48 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 5b3dbaaec6 chore(deps): update dependency i18next to ^25.6.1 (#5331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 10:49:11 +08:00
oomeow 81e35e2ead chore: optimize (#5319)
* chore: optimize and reduce spawn

* chore: optimize
2025-11-07 06:06:09 +08:00
Tunglies 19b3899a1b refactor: improve notification handling and avoid backend loop with empty messages 2025-11-07 00:54:16 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 333440535b chore(deps): update dependency vite to ^7.2.1 (#5324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 14:17:21 +08:00
Tunglies 409b16b49f refactor: streamline admin check logic and improve get_running_mode return type (#5325) 2025-11-06 13:37:11 +08:00
Tunglies 9a1465ec4d refactor: replace AtomicI64 with Instant for app start time tracking and simplify uptime calculation 2025-11-06 12:59:05 +08:00
Tunglies 04fc9640c6 refactor: move handle_window_destroyed function under macOS conditional compilation 2025-11-06 11:31:15 +08:00
Slinetrac 966693c9cb fix: clippy lint 2025-11-06 11:21:12 +08:00
Tunglies 363fa98891 refactor: enhance YouTube Premium check logic and streamline response handling 2025-11-06 11:03:35 +08:00
Tunglies a0f7fb7952 refactor: update linting rules and optimize directory handling in Unix 2025-11-06 10:52:14 +08:00
Tunglies 5a8e83cd49 refactor: change function definitions to const for improved performance and clarity 2025-11-06 10:47:25 +08:00
Tunglies aaf3ebe547 refactor: enhance error handling in Timer and update server argument collection 2025-11-06 10:42:21 +08:00
Tunglies 651513c826 refactor: optimize error message handling and improve cloning in various functions 2025-11-06 10:26:40 +08:00
Tunglies 69a706b438 refactor: streamline error handling and resource management in various modules 2025-11-06 10:19:06 +08:00
Tunglies 671ac2ebed refactor: replace type references with Self in various structs and methods for consistency 2025-11-06 10:19:06 +08:00
Tunglies 70236f781c refactor: simplify uid assignment in PrfItem from_merge and from_script methods 2025-11-06 10:19:05 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> ccffc0a4fa chore(deps): update npm dependencies (#5321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 09:36:30 +08:00
Tunglies 21de5cbb0b refactor: remove unnecessary emit calls in switch_proxy_node and enhance profile switch notifications 2025-11-06 00:30:43 +08:00
Tunglies 6eaf999240 refactor: optimize worker loop to use try_recv for non-blocking event processing #5318 2025-11-05 22:59:22 +08:00
Tunglies 1bd2bde096 refactor: streamline lightweight mode initialization and logging levels 2025-11-05 18:57:04 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> cf89ac381a chore(deps): update npm dependencies (#5317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 18:47:43 +08:00
Tunglies ea41e71f72 refactor: replace EncryptionGuard with with_encryption for better async context handling 2025-11-05 16:56:04 +08:00
Tunglies 8c3e9c9ea9 feat: implement encryption guard for secure data handling in YAML operations 2025-11-05 15:05:19 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7145096729 chore(deps): update npm dependencies (#5311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 12:13:07 +08:00
Tunglies 3e2f605e77 fix: improve error handling and logging in various modules 2025-11-05 02:11:43 +08:00
Tunglies 28483ff9db fix: simplify error handling in toggle_proxy_profile by logging errors directly 2025-11-04 23:05:03 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 5c207a8a7d chore(deps): update npm dependencies to ^7.3.5 (#5310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 23:05:03 +08:00
Tunglies ab136e463f fix: change error handling in patch_profiles_config to return false when a switch is in progress
fix: improve error handling in patch_profiles_config to prevent requests during profile switching

fix: change error handling in patch_profiles_config to return false when a switch is in progress

fix: ensure CURRENT_SWITCHING_PROFILE is reset after config updates in perform_config_update and patch_profiles_config
2025-11-04 23:05:01 +08:00
Tunglies 97769cf93a feat: add methods to retrieve current subscription details in PrfItem and refactor profile handling in IProfiles 2025-11-04 12:03:03 +08:00
Tunglies 0306f73841 fix: replace update_tray_display with update_menu for consistency across tray updates 2025-11-04 12:03:03 +08:00
Tunglies 0a03867352 fix: optimize ownership handling in patch macro for better performance 2025-11-04 12:03:03 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 28a78464e1 chore(deps): update npm dependencies (#5303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 12:00:47 +08:00
Tunglies bae584b1ab perf: optimize profile handle memory usage 2025-11-04 10:05:17 +08:00
Tunglies a5e6a35dea fix: update IVerge struct to skip serializing optional fields and adjust types for latency timeout and proxy layout column 2025-11-04 08:16:02 +08:00
Tunglies b86ceb26f6 fix: streamline verge configuration fetching and patching functions 2025-11-04 08:01:33 +08:00
Tunglies b70d45b66a fix: update profile handling to apply and discard changes correctly 2025-11-04 07:58:14 +08:00
Tunglies ebd7f457d2 fix: improve error handling in port settings save function #4707 2025-11-04 07:17:14 +08:00
Tunglies 72d94c62f4 Revert "modify the port detection range to prevent the use of system ports (#3587)"
This reverts commit d3dbc11b1b.
2025-11-04 07:11:44 +08:00
Tunglies fe757ed984 docs(autobuild): Assets URL correction for rpm package 2025-11-04 06:23:53 +08:00
Tunglies 2287ea5f0b Refactor configuration access to use latest_arc() instead of latest_ref()
- Updated multiple instances in the codebase to replace calls to latest_ref() with latest_arc() for improved performance and memory management.
- This change affects various modules including validate, enhance, feat (backup, clash, config, profile, proxy, window), utils (draft, i18n, init, network, resolve, server).
- Ensured that all references to configuration data are now using the new arc-based approach to enhance concurrency and reduce cloning overhead.

refactor: update imports to explicitly include ClashInfo and Config in command files
2025-11-04 06:06:20 +08:00
Slinetrac 48a19f99e2 docs: UPDATELOG.md 2025-11-03 20:54:46 +08:00
❤是纱雾酱哟~ 1003118468 docs(release): Assets URL correction (#5294)
- Fix mis-spelling of assets URL to Linux RPM package
2025-11-03 20:45:06 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 940717712a chore(deps): update dependency @types/node to ^24.10.0 (#5292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 20:44:58 +08:00
76 changed files with 1956 additions and 1822 deletions
+2 -2
View File
@@ -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) - [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 ./路径 安装 #### 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 ### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html) - [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -578,7 +578,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) - [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 ./路径 安装 #### 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 ### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html) - [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
+13 -4
View File
@@ -5,6 +5,7 @@ on:
# ! 不再使用 workflow_dispatch 触发。 # ! 不再使用 workflow_dispatch 触发。
# workflow_dispatch: # workflow_dispatch:
push: push:
# -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新
tags: tags:
- "v*.*.*" - "v*.*.*"
permissions: write-all permissions: write-all
@@ -143,7 +144,7 @@ jobs:
name: "Clash Verge Rev ${{ env.TAG_NAME }}" name: "Clash Verge Rev ${{ env.TAG_NAME }}"
body_path: release.txt body_path: release.txt
draft: false draft: false
prerelease: false prerelease: ${{ contains(github.ref_name, '-rc') }}
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
# generate_release_notes: true # generate_release_notes: true
@@ -213,7 +214,8 @@ jobs:
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v0 # 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
uses: tauri-apps/tauri-action@v0.5.23
env: env:
NODE_OPTIONS: "--max_old_space_size=4096" NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -230,7 +232,7 @@ jobs:
releaseName: "Clash Verge Rev ${{ github.ref_name }}" releaseName: "Clash Verge Rev ${{ github.ref_name }}"
releaseBody: "Draft release, will be updated later." releaseBody: "Draft release, will be updated later."
releaseDraft: true releaseDraft: true
prerelease: false prerelease: ${{ contains(github.ref_name, '-rc') }}
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
includeUpdaterJson: true includeUpdaterJson: true
@@ -354,6 +356,7 @@ jobs:
name: "Clash Verge Rev v${{env.VERSION}}" name: "Clash Verge Rev v${{env.VERSION}}"
body: "See release notes for detailed changelog." body: "See release notes for detailed changelog."
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
prerelease: ${{ contains(github.ref_name, '-rc') }}
files: | files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
@@ -409,7 +412,8 @@ jobs:
- name: Tauri build - name: Tauri build
id: build id: build
uses: tauri-apps/tauri-action@v0 # 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
uses: tauri-apps/tauri-action@v0.5.23
env: env:
NODE_OPTIONS: "--max_old_space_size=4096" NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -446,6 +450,7 @@ jobs:
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}" name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
body: "See release notes for detailed changelog." body: "See release notes for detailed changelog."
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
prerelease: ${{ contains(github.ref_name, '-rc') }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle - name: Portable Bundle
@@ -454,6 +459,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update: release-update:
if: ${{ !contains(github.ref_name, '-rc') }}
name: Release Update name: Release Update
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [update_tag] needs: [update_tag]
@@ -480,6 +486,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update-for-fixed-webview2: release-update-for-fixed-webview2:
if: ${{ !contains(github.ref_name, '-rc') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [update_tag] needs: [update_tag]
steps: steps:
@@ -505,6 +512,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget: submit-to-winget:
if: ${{ !contains(github.ref_name, '-rc') }}
name: Submit to Winget name: Submit to Winget
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [update_tag, release-update] needs: [update_tag, release-update]
@@ -528,6 +536,7 @@ jobs:
token: ${{ secrets.WINGET_TOKEN }} token: ${{ secrets.WINGET_TOKEN }}
notify-telegram: notify-telegram:
if: ${{ !contains(github.ref_name, '-rc') }}
name: Notify Telegram name: Notify Telegram
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
+2 -1
View File
@@ -15,7 +15,8 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
<a href="./docs/README_en.md">English</a> · <a href="./docs/README_en.md">English</a> ·
<a href="./docs/README_es.md">Español</a> · <a href="./docs/README_es.md">Español</a> ·
<a href="./docs/README_ru.md">Русский</a> · <a href="./docs/README_ru.md">Русский</a> ·
<a href="./docs/README_ja.md">日本語</a> <a href="./docs/README_ja.md">日本語</a> ·
<a href="./docs/README_ko.md">한국어</a>
</p> </p>
## Preview ## Preview
+7 -1
View File
@@ -1,6 +1,9 @@
## v2.4.3 ## v2.4.3
感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献 **发行代号:澜**
代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。
特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
### 🐞 修复问题 ### 🐞 修复问题
@@ -36,6 +39,8 @@
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步 - 修复 macOS 从 Dock 栏退出轻量模式状态不同步
- 修复 Linux 系统主题切换不生效 - 修复 Linux 系统主题切换不生效
- 修复 `允许自动更新` 字段使手动订阅刷新失效 - 修复 `允许自动更新` 字段使手动订阅刷新失效
- 修复轻量模式托盘状态不同步
- 修复一键导入订阅导致应用卡死崩溃的问题
<details> <details>
<summary><strong> ✨ 新增功能 </strong></summary> <summary><strong> ✨ 新增功能 </strong></summary>
@@ -86,6 +91,7 @@
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题 - 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单 - Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书` - 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
</details> </details>
+2 -1
View File
@@ -15,7 +15,8 @@ A Clash Meta GUI built with <a href="https://github.com/tauri-apps/tauri">Tauri<
<a href="./README_en.md">English</a> · <a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> · <a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> · <a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> <a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p> </p>
## Preview ## Preview
+2 -1
View File
@@ -15,7 +15,8 @@ Una interfaz gráfica para Clash Meta construida con <a href="https://github.com
<a href="./README_en.md">English</a> · <a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> · <a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> · <a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> <a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p> </p>
## Vista previa ## Vista previa
+2 -1
View File
@@ -15,7 +15,8 @@
<a href="./README_en.md">English</a> · <a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> · <a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> · <a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> <a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p> </p>
## プレビュー ## プレビュー
+125
View File
@@ -0,0 +1,125 @@
<h1 align="center">
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
<br>
<a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>의 후속 프로젝트
<br>
</h1>
<h3 align="center">
<a href="https://github.com/tauri-apps/tauri">Tauri</a>로 제작된 Clash Meta GUI.
</h3>
<p align="center">
언어:
<a href="../README.md">简体中文</a> ·
<a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p>
## 미리보기
| 다크 | 라이트 |
| ------------------------------------ | --------------------------------------- |
| ![다크 미리보기](./preview_dark.png) | ![라이트 미리보기](./preview_light.png) |
## 설치
[릴리스 페이지](https://github.com/clash-verge-rev/clash-verge-rev/releases)에서 사용 중인 플랫폼에 맞는 설치 프로그램을 다운로드하세요.<br>
Windows (x64/x86), Linux (x64/arm64), macOS 10.15+ (Intel/Apple)을 지원합니다.
#### 릴리스 채널 선택
| 채널 | 설명 | 링크 |
| :---------- | :----------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | 안정 릴리스. 신뢰성이 높아 일상 사용에 적합합니다. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha (EOL) | 퍼블리시 파이프라인 검증에 사용되었던 구 테스트 채널입니다. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | 롤링 빌드 채널. 테스트와 피드백 용도로 권장되며, 실험적인 변경이 포함될 수 있습니다. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### 설치 가이드 및 FAQ
설치 방법, 트러블슈팅, 자주 묻는 질문은 [프로젝트 문서](https://clash-verge-rev.github.io/)를 참고하세요.
---
### 텔레그램 채널
업데이트 공지는 [@clash_verge_rev](https://t.me/clash_verge_re)에서 확인하세요.
## 프로모션
#### [Doggygo VPN — 고성능 글로벌 가속기](https://verge.dginv.click/#/register?code=oaxsAGo6)
- 무료 체험, 할인 요금제, 스트리밍 해제, 선도적인 Hysteria 프로토콜 지원을 갖춘 고성능 해외 네트워크 서비스
- Clash Verge 전용 초대 링크로 가입 시 3일간 매일 1GB 무료 체험 제공: [가입하기](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Clash Verge 전용 20% 할인 코드: `verge20` (선착순 500회)
- 월 15.8위안부터 160GB 제공, 연간 결제 시 추가 20% 할인
- 해외 팀 운영, 높은 신뢰성, 최대 50% 커미션
- 로드밸런싱 클러스터, 고속 전용 회선(구 클라이언트 호환), 매우 낮은 지연, 4K도 쾌적
- 세계 최초 `Hysteria2` 프로토콜 지원 — Clash Verge 클라이언트와 최적의 궁합
- 스트리밍 및 ChatGPT 접근 지원
- 공식 사이트: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
#### 빌드 인프라 스폰서 — [YXVM 전용 서버](https://yxvm.com/aff.php?aff=827)
본 프로젝트의 빌드 및 릴리스는 YXVM 전용 서버에서 구동됩니다. 빠른 다운로드와 경쾌한 사용감은 탄탄한 하드웨어 덕분입니다.
🧩 YXVM 전용 서버 하이라이트:
- 🌎 최적화된 글로벌 라우팅으로 대폭 빨라진 다운로드
- 🔧 공유 VPS가 아닌 베어메탈 자원으로 최대 성능 제공
- 🧠 프록시 워크로드, Web/CDN 호스팅, CI/CD, 고부하 작업에 적합
- 💡 CN2 / IEPL 등 다양한 데이터센터 옵션, 즉시 사용 가능
- 📦 본 프로젝트가 사용하는 구성도 판매 중 — 동일한 환경을 사용할 수 있습니다
- 🎯 동일한 빌드 환경이 필요하다면 [지금 YXVM 서버 주문](https://yxvm.com/aff.php?aff=827)
## 기능
- 고성능 Rust와 Tauri 2 프레임워크 기반 데스크톱 앱
- 내장 [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) 코어, `Alpha` 채널 전환 지원
- 테마 색상, 프록시 그룹/트레이 아이콘, `CSS Injection` 등 세련된 UI 커스터마이징
- 프로필 관리(병합 및 스크립트 보조), 구성 문법 힌트 제공
- 시스템 프록시 제어, 가드 모드, `TUN`(가상 네트워크 어댑터) 지원
- 노드/규칙 시각 편집기
- WebDAV 기반 설정 백업 및 동기화
### FAQ
플랫폼별 가이드는 [FAQ 페이지](https://clash-verge-rev.github.io/faq/windows.html)에서 확인하세요.
### 후원
[Clash Verge Rev 개발 후원](https://github.com/sponsors/clash-verge-rev)
## 개발
자세한 기여 가이드는 [CONTRIBUTING.md](../CONTRIBUTING.md)를 참고하세요.
**Tauri** 필수 구성 요소를 설치한 뒤 아래 명령으로 개발 서버를 실행합니다:
```shell
pnpm i
pnpm run prebuild
pnpm dev
```
## 기여
Issue와 Pull Request를 환영합니다!
## 감사의 말
Clash Verge Rev는 다음 프로젝트에 기반하거나 영향을 받았습니다:
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Windows / macOS / Linux용 Tauri 기반 Clash GUI
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): 웹 프론트엔드로 더 작고 빠르고 안전한 데스크톱 앱을 빌드
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Go로 작성된 규칙 기반 터널
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Go로 작성된 규칙 기반 터널
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Windows / macOS용 Clash GUI
- [vitejs/vite](https://github.com/vitejs/vite): 차세대 프론트엔드 툴링, 매우 빠른 DX
## 라이선스
GPL-3.0 라이선스. 자세한 내용은 [LICENSE](../LICENSE)를 참고하세요.
+2 -1
View File
@@ -15,7 +15,8 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
<a href="./README_en.md">English</a> · <a href="./README_en.md">English</a> ·
<a href="./README_es.md">Español</a> · <a href="./README_es.md">Español</a> ·
<a href="./README_ru.md">Русский</a> · <a href="./README_ru.md">Русский</a> ·
<a href="./README_ja.md">日本語</a> <a href="./README_ja.md">日本語</a> ·
<a href="./README_ko.md">한국어</a>
</p> </p>
## Предпросмотр ## Предпросмотр
+15 -15
View File
@@ -40,10 +40,10 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.3.4", "@mui/icons-material": "^7.3.5",
"@mui/lab": "7.0.0-beta.17", "@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.4", "@mui/material": "^7.3.5",
"@mui/x-data-grid": "^8.16.0", "@mui/x-data-grid": "^8.17.0",
"@tauri-apps/api": "2.9.0", "@tauri-apps/api": "2.9.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-dialog": "^2.4.2",
@@ -54,10 +54,10 @@
"@tauri-apps/plugin-updater": "2.9.0", "@tauri-apps/plugin-updater": "2.9.0",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.9.6", "ahooks": "^3.9.6",
"axios": "^1.13.1", "axios": "^1.13.2",
"dayjs": "1.11.19", "dayjs": "1.11.19",
"foxact": "^0.2.49", "foxact": "^0.2.49",
"i18next": "^25.6.0", "i18next": "^25.6.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -68,7 +68,7 @@
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-error-boundary": "6.0.0", "react-error-boundary": "6.0.0",
"react-hook-form": "^7.66.0", "react-hook-form": "^7.66.0",
"react-i18next": "16.2.3", "react-i18next": "16.2.4",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0", "react-monaco-editor": "0.59.0",
"react-router": "^7.9.5", "react-router": "^7.9.5",
@@ -80,20 +80,20 @@
"devDependencies": { "devDependencies": {
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
"@eslint-react/eslint-plugin": "^2.3.1", "@eslint-react/eslint-plugin": "^2.3.1",
"@eslint/js": "^9.39.0", "@eslint/js": "^9.39.1",
"@tauri-apps/cli": "2.9.2", "@tauri-apps/cli": "2.9.3",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^24.9.2", "@types/node": "^24.10.0",
"@types/react": "19.2.2", "@types/react": "19.2.2",
"@types/react-dom": "19.2.2", "@types/react-dom": "19.2.2",
"@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react-swc": "^4.2.0", "@vitejs/plugin-react-swc": "^4.2.1",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
"commander": "^14.0.2", "commander": "^14.0.2",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"eslint": "^9.39.0", "eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4", "eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1", "eslint-plugin-import-x": "^4.16.1",
@@ -112,13 +112,13 @@
"prettier": "^3.6.2", "prettier": "^3.6.2",
"sass": "^1.93.3", "sass": "^1.93.3",
"tar": "^7.5.2", "tar": "^7.5.2",
"terser": "^5.44.0", "terser": "^5.44.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.46.2", "typescript-eslint": "^8.46.3",
"vite": "^7.1.12", "vite": "^7.2.2",
"vite-plugin-monaco-editor-esm": "^2.0.2", "vite-plugin-monaco-editor-esm": "^2.0.2",
"vite-plugin-svgr": "^4.5.0", "vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.6" "vitest": "^4.0.8"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx,js,jsx}": [ "*.{ts,tsx,js,jsx}": [
+425 -425
View File
File diff suppressed because it is too large Load Diff
-11
View File
@@ -1158,7 +1158,6 @@ dependencies = [
"tauri-plugin-window-state", "tauri-plugin-window-state",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"users",
"warp", "warp",
"winapi", "winapi",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -8699,16 +8698,6 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "users"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
dependencies = [
"libc",
"log",
]
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"
+20 -3
View File
@@ -112,9 +112,6 @@ windows-sys = { version = "0.61.2", features = [
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
] } ] }
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
signal-hook = "0.3.18" signal-hook = "0.3.18"
@@ -226,3 +223,23 @@ needless_raw_string_hashes = "deny" # Too many in existing code
or_fun_call = "deny" or_fun_call = "deny"
cognitive_complexity = "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"
+28 -23
View File
@@ -7,13 +7,13 @@ use app_lib::config::IVerge;
use app_lib::utils::Draft as DraftNew; use app_lib::utils::Draft as DraftNew;
/// 创建测试数据 /// 创建测试数据
fn make_draft() -> DraftNew<Box<IVerge>> { fn make_draft() -> DraftNew<IVerge> {
let verge = Box::new(IVerge { let verge = IVerge {
enable_auto_launch: Some(true), enable_auto_launch: Some(true),
enable_tun_mode: Some(false), enable_tun_mode: Some(false),
..Default::default() ..Default::default()
}); };
DraftNew::from(verge) DraftNew::new(verge)
} }
pub fn bench_draft(c: &mut Criterion) { pub fn bench_draft(c: &mut Criterion) {
@@ -30,18 +30,17 @@ pub fn bench_draft(c: &mut Criterion) {
group.bench_function("data_mut", |b| { group.bench_function("data_mut", |b| {
b.iter(|| { b.iter(|| {
let draft = black_box(make_draft()); let draft = black_box(make_draft());
let mut data = draft.data_mut(); draft.edit_draft(|d| d.enable_tun_mode = Some(true));
data.enable_tun_mode = Some(true); black_box(&draft.latest_arc().enable_tun_mode);
black_box(&data.enable_tun_mode);
}); });
}); });
group.bench_function("draft_mut_first", |b| { group.bench_function("draft_mut_first", |b| {
b.iter(|| { b.iter(|| {
let draft = black_box(make_draft()); let draft = black_box(make_draft());
let mut d = draft.draft_mut(); draft.edit_draft(|d| d.enable_auto_launch = Some(false));
d.enable_auto_launch = Some(false); let latest = draft.latest_arc();
black_box(&d.enable_auto_launch); black_box(&latest.enable_auto_launch);
}); });
}); });
@@ -49,20 +48,24 @@ pub fn bench_draft(c: &mut Criterion) {
b.iter(|| { b.iter(|| {
let draft = black_box(make_draft()); let draft = black_box(make_draft());
{ {
let mut first = draft.draft_mut(); draft.edit_draft(|d| {
first.enable_tun_mode = Some(true); d.enable_tun_mode = Some(true);
black_box(&first.enable_tun_mode); });
let latest1 = draft.latest_arc();
black_box(&latest1.enable_tun_mode);
} }
let mut second = draft.draft_mut(); draft.edit_draft(|d| {
second.enable_tun_mode = Some(false); d.enable_tun_mode = Some(false);
black_box(&second.enable_tun_mode); });
let latest2 = draft.latest_arc();
black_box(&latest2.enable_tun_mode);
}); });
}); });
group.bench_function("latest_ref", |b| { group.bench_function("latest_arc", |b| {
b.iter(|| { b.iter(|| {
let draft = black_box(make_draft()); let draft = black_box(make_draft());
let latest = draft.latest_ref(); let latest = draft.latest_arc();
black_box(&latest.enable_auto_launch); black_box(&latest.enable_auto_launch);
}); });
}); });
@@ -71,8 +74,9 @@ pub fn bench_draft(c: &mut Criterion) {
b.iter(|| { b.iter(|| {
let draft = black_box(make_draft()); let draft = black_box(make_draft());
{ {
let mut d = draft.draft_mut(); draft.edit_draft(|d| {
d.enable_auto_launch = Some(false); d.enable_auto_launch = Some(false);
});
} }
draft.apply(); draft.apply();
black_box(&draft); black_box(&draft);
@@ -83,8 +87,9 @@ pub fn bench_draft(c: &mut Criterion) {
b.iter(|| { b.iter(|| {
let draft = black_box(make_draft()); let draft = black_box(make_draft());
{ {
let mut d = draft.draft_mut(); draft.edit_draft(|d| {
d.enable_auto_launch = Some(false); d.enable_auto_launch = Some(false);
});
} }
draft.discard(); draft.discard();
black_box(&draft); black_box(&draft);
@@ -95,7 +100,7 @@ pub fn bench_draft(c: &mut Criterion) {
b.to_async(&rt).iter(|| async { b.to_async(&rt).iter(|| async {
let draft = black_box(make_draft()); let draft = black_box(make_draft());
let _: Result<(), anyhow::Error> = draft let _: Result<(), anyhow::Error> = draft
.with_data_modify::<_, _, _, anyhow::Error>(|mut box_data| async move { .with_data_modify::<_, _, _>(|mut box_data| async move {
box_data.enable_auto_launch = box_data.enable_auto_launch =
Some(!box_data.enable_auto_launch.unwrap_or(false)); Some(!box_data.enable_auto_launch.unwrap_or(false));
Ok((box_data, ())) Ok((box_data, ()))
+7 -12
View File
@@ -2,11 +2,11 @@ use super::CmdResult;
use crate::utils::dirs; use crate::utils::dirs;
use crate::{ use crate::{
cmd::StringifyErr, cmd::StringifyErr,
config::Config, config::{ClashInfo, Config},
constants, constants,
core::{CoreManager, handle, validate::CoreConfigValidator}, core::{CoreManager, handle, validate::CoreConfigValidator},
}; };
use crate::{config::*, feat, logging, utils::logging::Type}; use crate::{feat, logging, utils::logging::Type};
use compact_str::CompactString; use compact_str::CompactString;
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
use smartstring::alias::String; use smartstring::alias::String;
@@ -22,7 +22,7 @@ pub async fn copy_clash_env() -> CmdResult {
/// 获取Clash信息 /// 获取Clash信息
#[tauri::command] #[tauri::command]
pub async fn get_clash_info() -> CmdResult<ClashInfo> { pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().await.latest_ref().get_client_info()) Ok(Config::clash().await.latest_arc().get_client_info())
} }
/// 修改Clash配置 /// 修改Clash配置
@@ -141,12 +141,6 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
/// 应用或撤销DNS配置 /// 应用或撤销DNS配置
#[tauri::command] #[tauri::command]
pub async fn apply_dns_config(apply: bool) -> CmdResult { pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{
config::Config,
core::{CoreManager, handle},
utils::dirs,
};
if apply { if apply {
// 读取DNS配置文件 // 读取DNS配置文件
let dns_path = dirs::app_home_dir() let dns_path = dirs::app_home_dir()
@@ -175,7 +169,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
patch.insert("dns".into(), patch_config.into()); patch.insert("dns".into(), patch_config.into());
// 应用DNS配置到运行时配置 // 应用DNS配置到运行时配置
Config::runtime().await.draft_mut().patch_config(patch); Config::runtime().await.edit_draft(|d| {
d.patch_config(patch);
});
// 重新生成配置 // 重新生成配置
Config::generate().await.stringify_err_log(|err| { Config::generate().await.stringify_err_log(|err| {
@@ -193,7 +189,6 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
})?; })?;
logging!(info, Type::Config, "DNS config successfully applied"); logging!(info, Type::Config, "DNS config successfully applied");
handle::Handle::refresh_clash();
} else { } else {
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件) // 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
logging!( logging!(
@@ -216,9 +211,9 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
})?; })?;
logging!(info, Type::Config, "Config regenerated successfully"); logging!(info, Type::Config, "Config regenerated successfully");
handle::Handle::refresh_clash();
} }
handle::Handle::refresh_clash();
Ok(()) Ok(())
} }
@@ -13,19 +13,23 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
Ok(response) => { Ok(response) => {
if let Ok(body) = response.text().await { if let Ok(body) = response.text().await {
let body_lower = body.to_lowercase(); 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") { if body_lower.contains("youtube premium is not available in your country") {
return UnlockItem { status = "No";
name: "Youtube Premium".to_string(), } else if body_lower.contains("ad-free") {
status: "No".to_string(), match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
region: None, Ok(re) => {
check_time: Some(get_local_date_string()), if let Some(caps) = re.captures(&body)
}; && let Some(m) = caps.get(1)
} {
let country_code = m.as_str().trim();
if body_lower.contains("ad-free") { let emoji = country_code_to_emoji(country_code);
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) { region = Some(format!("{emoji}{country_code}"));
Ok(re) => re, status = "Yes";
}
}
Err(e) => { Err(e) => {
logging!( logging!(
error, error,
@@ -33,34 +37,14 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
"Failed to compile YouTube Premium regex: {}", "Failed to compile YouTube Premium regex: {}",
e 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 { UnlockItem {
name: "Youtube Premium".to_string(), name: "Youtube Premium".to_string(),
status: "Failed".to_string(), status: status.to_string(),
region: None, region,
check_time: Some(get_local_date_string()), check_time: Some(get_local_date_string()),
} }
} else { } else {
+52 -50
View File
@@ -26,8 +26,8 @@ static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
pub async fn get_profiles() -> CmdResult<IProfiles> { pub async fn get_profiles() -> CmdResult<IProfiles> {
logging!(debug, Type::Cmd, "获取配置文件列表"); logging!(debug, Type::Cmd, "获取配置文件列表");
let draft = Config::profiles().await; let draft = Config::profiles().await;
let latest = draft.latest_ref(); let data = (**draft.data_arc()).clone();
Ok((**latest).clone()) Ok(data)
} }
/// 增强配置文件 /// 增强配置文件
@@ -99,9 +99,11 @@ 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(_) => { Ok(_) => {
logging!(info, Type::Cmd, "重新排序配置文件"); logging!(info, Type::Cmd, "重新排序配置文件");
Config::profiles().await.apply();
Ok(()) Ok(())
} }
Err(err) => { Err(err) => {
Config::profiles().await.discard();
logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err); logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err);
Err(format!("重新排序配置文件失败: {}", err).into()) Err(format!("重新排序配置文件失败: {}", err).into())
} }
@@ -119,12 +121,16 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid); logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid.clone()); handle::Handle::notify_profile_changed(uid.clone());
} }
Config::profiles().await.apply();
Ok(()) Ok(())
} }
Err(err) => match err.to_string().as_str() { Err(err) => {
"the file already exists" => Err("the file already exists".into()), Config::profiles().await.discard();
_ => Err(format!("add profile error: {err}").into()), match err.to_string().as_str() {
}, "the file already exists" => Err("the file already exists".into()),
_ => Err(format!("add profile error: {err}").into()),
}
}
} }
} }
@@ -132,8 +138,12 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
#[tauri::command] #[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult { pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
match feat::update_profile(&index, option.as_ref(), true, true).await { match feat::update_profile(&index, option.as_ref(), true, true).await {
Ok(_) => Ok(()), Ok(_) => {
let _: () = Config::profiles().await.apply();
Ok(())
}
Err(e) => { Err(e) => {
Config::profiles().await.discard();
logging!(error, Type::Cmd, "{}", e); logging!(error, Type::Cmd, "{}", e);
Err(e.to_string().into()) Err(e.to_string().into())
} }
@@ -143,12 +153,11 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件 /// 删除配置文件
#[tauri::command] #[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult { pub async fn delete_profile(index: String) -> CmdResult {
println!("delete_profile: {}", index);
// 使用Send-safe helper函数 // 使用Send-safe helper函数
let should_update = profiles_delete_item_safe(&index).await.stringify_err()?; let should_update = profiles_delete_item_safe(&index).await.stringify_err()?;
profiles_save_file_safe().await.stringify_err()?; profiles_save_file_safe().await.stringify_err()?;
if should_update { if should_update {
Config::profiles().await.apply();
match CoreManager::global().update_config().await { match CoreManager::global().update_config().await {
Ok(_) => { Ok(_) => {
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
@@ -172,7 +181,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
// 获取目标配置文件路径 // 获取目标配置文件路径
let config_file_result = { let config_file_result = {
let profiles_config = Config::profiles().await; let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_ref(); let profiles_data = profiles_config.latest_arc();
match profiles_data.get_item(new_profile) { match profiles_data.get_item(new_profile) {
Ok(item) => { Ok(item) => {
if let Some(file) = &item.file { if let Some(file) = &item.file {
@@ -234,7 +243,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
); );
handle::Handle::notice_message( handle::Handle::notice_message(
"config_validate::yaml_syntax_error", "config_validate::yaml_syntax_error",
error_msg.clone(), error_msg,
); );
Err(()) Err(())
} }
@@ -243,7 +252,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
logging!(error, Type::Cmd, "{}", error_msg); logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message( handle::Handle::notice_message(
"config_validate::yaml_parse_error", "config_validate::yaml_parse_error",
error_msg.clone(), error_msg,
); );
Err(()) Err(())
} }
@@ -252,19 +261,13 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
Ok(Err(err)) => { Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}"); let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, "{}", error_msg); logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message( handle::Handle::notice_message("config_validate::file_read_error", error_msg);
"config_validate::file_read_error",
error_msg.clone(),
);
Err(()) Err(())
} }
Err(_) => { Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string(); let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, "{}", error_msg); logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message( handle::Handle::notice_message("config_validate::file_read_timeout", error_msg);
"config_validate::file_read_timeout",
error_msg.clone(),
);
Err(()) Err(())
} }
} }
@@ -274,17 +277,15 @@ 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); logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
let restore_profiles = IProfiles { let restore_profiles = IProfiles {
current: Some(prev_profile), current: Some(prev_profile.to_owned()),
items: None, items: None,
}; };
Config::profiles() Config::profiles()
.await .await
.draft_mut() .edit_draft(|d| d.patch_config(&restore_profiles));
.patch_config(&restore_profiles)
.stringify_err()?;
Config::profiles().await.apply(); Config::profiles().await.apply();
crate::process::AsyncHandler::spawn(|| async move { crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = profiles_save_file_safe().await { if let Err(e) = profiles_save_file_safe().await {
@@ -295,7 +296,7 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
Ok(()) Ok(())
} }
async fn handle_success(current_value: Option<String>) -> CmdResult<bool> { async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
Config::profiles().await.apply(); Config::profiles().await.apply();
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
@@ -311,9 +312,9 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}"); logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
} }
if let Some(current) = &current_value { if let Some(current) = current_value {
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current,); logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current);
handle::Handle::notify_profile_changed(current.clone()); handle::Handle::notify_profile_changed(current.to_owned());
} }
Ok(true) Ok(true)
@@ -321,7 +322,7 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
async fn handle_validation_failure( async fn handle_validation_failure(
error_msg: String, error_msg: String,
current_profile: Option<String>, current_profile: Option<&String>,
) -> CmdResult<bool> { ) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg); logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
Config::profiles().await.discard(); Config::profiles().await.discard();
@@ -339,7 +340,7 @@ async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
Ok(false) Ok(false)
} }
async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> { async fn handle_timeout(current_profile: Option<&String>) -> CmdResult<bool> {
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞"; let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(error, Type::Cmd, "{}", timeout_msg); logging!(error, Type::Cmd, "{}", timeout_msg);
Config::profiles().await.discard(); Config::profiles().await.discard();
@@ -351,9 +352,12 @@ async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
} }
async fn perform_config_update( async fn perform_config_update(
current_value: Option<String>, current_value: Option<&String>,
current_profile: Option<String>, current_profile: Option<&String>,
) -> CmdResult<bool> { ) -> CmdResult<bool> {
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
let update_result = tokio::time::timeout( let update_result = tokio::time::timeout(
Duration::from_secs(30), Duration::from_secs(30),
CoreManager::global().update_config(), CoreManager::global().update_config(),
@@ -376,13 +380,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
.is_err() .is_err()
{ {
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求"); logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Err("switch_in_progress".into()); return Ok(false);
} }
defer! { let target_profile = profiles.current.as_ref();
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
let target_profile = profiles.current.clone();
logging!( logging!(
info, info,
@@ -392,21 +393,22 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
); );
// 保存当前配置,以便在验证失败时恢复 // 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().await.latest_ref().current.clone(); let previous_profile = Config::profiles().await.data_arc().current.clone();
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile); logging!(info, Type::Cmd, "当前配置: {:?}", previous_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误 // 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref() if let Some(switch_to_profile) = target_profile
&& current_profile.as_ref() != Some(new_profile) && previous_profile.as_ref() != Some(switch_to_profile)
&& validate_new_profile(new_profile).await.is_err() && validate_new_profile(switch_to_profile).await.is_err()
{ {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
return Ok(false); return Ok(false);
} }
Config::profiles()
.await
.edit_draft(|d| d.patch_config(&profiles));
let _ = Config::profiles().await.draft_mut().patch_config(&profiles); perform_config_update(target_profile, previous_profile.as_ref()).await
let current_value = profiles.current.clone();
perform_config_update(current_value, current_profile).await
} }
/// 根据profile name修改profiles /// 根据profile name修改profiles
@@ -426,7 +428,7 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
// 保存修改前检查是否有更新 update_interval // 保存修改前检查是否有更新 update_interval
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
&& let Some(new_option) = profile.option.as_ref() && let Some(new_option) = profile.option.as_ref()
{ {
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval); let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
@@ -465,7 +467,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
#[tauri::command] #[tauri::command]
pub async fn view_profile(index: String) -> CmdResult { pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref(); let profiles_ref = profiles.latest_arc();
let file = profiles_ref let file = profiles_ref
.get_item(&index) .get_item(&index)
.stringify_err()? .stringify_err()?
@@ -488,7 +490,7 @@ pub async fn view_profile(index: String) -> CmdResult {
pub async fn read_profile_file(index: String) -> CmdResult<String> { pub async fn read_profile_file(index: String) -> CmdResult<String> {
let item = { let item = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref(); let profiles_ref = profiles.latest_arc();
PrfItem { PrfItem {
file: profiles_ref.get_item(&index).stringify_err()?.file.clone(), file: profiles_ref.get_item(&index).stringify_err()?.file.clone(),
..Default::default() ..Default::default()
+6 -8
View File
@@ -8,14 +8,14 @@ use std::collections::HashMap;
/// 获取运行时配置 /// 获取运行时配置
#[tauri::command] #[tauri::command]
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> { pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().await.latest_ref().config.clone()) Ok(Config::runtime().await.latest_arc().config.clone())
} }
/// 获取运行时YAML配置 /// 获取运行时YAML配置
#[tauri::command] #[tauri::command]
pub async fn get_runtime_yaml() -> CmdResult<String> { pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime().await; let runtime = Config::runtime().await;
let runtime = runtime.latest_ref(); let runtime = runtime.latest_arc();
let config = runtime.config.as_ref(); let config = runtime.config.as_ref();
config config
@@ -31,19 +31,19 @@ pub async fn get_runtime_yaml() -> CmdResult<String> {
/// 获取运行时存在的键 /// 获取运行时存在的键
#[tauri::command] #[tauri::command]
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> { pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().await.latest_ref().exists_keys.clone()) Ok(Config::runtime().await.latest_arc().exists_keys.clone())
} }
/// 获取运行时日志 /// 获取运行时日志
#[tauri::command] #[tauri::command]
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> { pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().await.latest_ref().chain_logs.clone()) Ok(Config::runtime().await.latest_arc().chain_logs.clone())
} }
#[tauri::command] #[tauri::command]
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> { pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
let runtime = Config::runtime().await; let runtime = Config::runtime().await;
let runtime = runtime.latest_ref(); let runtime = runtime.latest_arc();
let config = runtime let config = runtime
.config .config
@@ -98,9 +98,7 @@ pub async fn update_proxy_chain_config_in_runtime(
) -> CmdResult<()> { ) -> CmdResult<()> {
{ {
let runtime = Config::runtime().await; let runtime = Config::runtime().await;
let mut draft = runtime.draft_mut(); runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
draft.update_proxy_chain_config(proxy_chain_config);
drop(draft);
runtime.apply(); runtime.apply();
} }
+1 -1
View File
@@ -20,7 +20,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
// 在异步操作前获取必要元数据并释放锁 // 在异步操作前获取必要元数据并释放锁
let (rel_path, is_merge_file) = { let (rel_path, is_merge_file) = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_ref(); let profiles_guard = profiles.latest_arc();
let item = profiles_guard.get_item(&index).stringify_err()?; let item = profiles_guard.get_item(&index).stringify_err()?;
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge"); let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
let path = item.file.clone().ok_or("file field is null")?; let path = item.file.clone().ok_or("file field is null")?;
+20 -52
View File
@@ -1,26 +1,28 @@
use std::sync::Arc;
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
core::{CoreManager, handle}, core::{CoreManager, handle, manager::RunningMode},
logging, logging,
module::sysinfo::PlatformSpecification, module::sysinfo::PlatformSpecification,
utils::logging::Type, utils::logging::Type,
}; };
#[cfg(target_os = "windows")]
use deelevate::{PrivilegeLevel, Token};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{
sync::atomic::{AtomicI64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
use tauri_plugin_clipboard_manager::ClipboardExt; use tauri_plugin_clipboard_manager::ClipboardExt;
use tokio::time::Instant;
// 存储应用启动时间的全局变量 // 存储应用启动时间的全局变量
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| { static APP_START_TIME: Lazy<Instant> = Lazy::new(Instant::now);
// 获取当前系统时间,转换为毫秒级时间戳 #[cfg(not(target_os = "windows"))]
let now = SystemTime::now() static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| unsafe { libc::geteuid() } == 0);
.duration_since(UNIX_EPOCH) #[cfg(target_os = "windows")]
.unwrap_or_default() static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| {
.as_millis() as i64; Token::with_current_process()
.and_then(|token| token.privilege_level())
AtomicI64::new(now) .map(|level| level != PrivilegeLevel::NotPrivileged)
.unwrap_or(false)
}); });
#[tauri::command] #[tauri::command]
@@ -45,52 +47,18 @@ pub async fn get_system_info() -> CmdResult<String> {
/// 获取当前内核运行模式 /// 获取当前内核运行模式
#[tauri::command] #[tauri::command]
pub async fn get_running_mode() -> Result<String, String> { pub async fn get_running_mode() -> Result<Arc<RunningMode>, String> {
Ok(CoreManager::global().get_running_mode().to_string()) Ok(CoreManager::global().get_running_mode())
} }
/// 获取应用的运行时间(毫秒) /// 获取应用的运行时间(毫秒)
#[tauri::command] #[tauri::command]
pub fn get_app_uptime() -> CmdResult<i64> { pub fn get_app_uptime() -> CmdResult<u128> {
let start_time = APP_START_TIME.load(Ordering::Relaxed); Ok(APP_START_TIME.elapsed().as_millis())
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
Ok(now - start_time)
} }
/// 检查应用是否以管理员身份运行 /// 检查应用是否以管理员身份运行
#[tauri::command] #[tauri::command]
#[cfg(target_os = "windows")]
pub fn is_admin() -> CmdResult<bool> { pub fn is_admin() -> CmdResult<bool> {
use deelevate::{PrivilegeLevel, Token}; Ok(*APPS_RUN_AS_ADMIN)
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)
}
} }
+1 -1
View File
@@ -17,7 +17,7 @@ mod platform {
mod platform { mod platform {
use super::CmdResult; use super::CmdResult;
pub fn invoke_uwp_tool() -> CmdResult { pub const fn invoke_uwp_tool() -> CmdResult {
Ok(()) Ok(())
} }
} }
+3 -9
View File
@@ -1,16 +1,10 @@
use super::CmdResult; use super::CmdResult;
use crate::{cmd::StringifyErr, config::*, feat}; use crate::{cmd::StringifyErr, config::IVerge, feat, utils::draft::SharedBox};
/// 获取Verge配置 /// 获取Verge配置
#[tauri::command] #[tauri::command]
pub async fn get_verge_config() -> CmdResult<IVergeResponse> { pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
let verge = Config::verge().await; feat::fetch_verge_config().await.stringify_err()
let verge_data = {
let ref_data = verge.latest_ref();
ref_data.clone()
};
let verge_response = IVergeResponse::from(verge_data);
Ok(verge_response)
} }
/// 修改Verge配置 /// 修改Verge配置
+8 -8
View File
@@ -1,5 +1,9 @@
use super::CmdResult; use super::CmdResult;
use crate::{cmd::StringifyErr, config::*, core, feat}; use crate::{
cmd::StringifyErr,
config::{Config, IVerge},
core, feat,
};
use reqwest_dav::list_cmd::ListFile; use reqwest_dav::list_cmd::ListFile;
use smartstring::alias::String; use smartstring::alias::String;
@@ -12,15 +16,11 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
webdav_password: Some(password), webdav_password: Some(password),
..IVerge::default() ..IVerge::default()
}; };
Config::verge().await.draft_mut().patch_config(&patch); Config::verge().await.edit_draft(|e| e.patch_config(&patch));
Config::verge().await.apply(); 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.stringify_err()?;
verge_data
.save_file()
.await
.map_err(|err| err.to_string())?;
core::backup::WebDavClient::global().reset(); core::backup::WebDavClient::global().reset();
Ok(()) Ok(())
} }
+1 -1
View File
@@ -300,7 +300,7 @@ impl IClashTemp {
// 检查 enable_external_controller 设置,用于运行时配置生成 // 检查 enable_external_controller 设置,用于运行时配置生成
let enable_external_controller = Config::verge() let enable_external_controller = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_external_controller .enable_external_controller
.unwrap_or(false); .unwrap_or(false);
+38 -36
View File
@@ -15,40 +15,40 @@ use tokio::sync::OnceCell;
use tokio::time::sleep; use tokio::time::sleep;
pub struct Config { pub struct Config {
clash_config: Draft<Box<IClashTemp>>, clash_config: Draft<IClashTemp>,
verge_config: Draft<Box<IVerge>>, verge_config: Draft<IVerge>,
profiles_config: Draft<Box<IProfiles>>, profiles_config: Draft<IProfiles>,
runtime_config: Draft<Box<IRuntime>>, runtime_config: Draft<IRuntime>,
} }
impl Config { impl Config {
pub async fn global() -> &'static Config { pub async fn global() -> &'static Self {
static CONFIG: OnceCell<Config> = OnceCell::const_new(); static CONFIG: OnceCell<Config> = OnceCell::const_new();
CONFIG CONFIG
.get_or_init(|| async { .get_or_init(|| async {
Config { Self {
clash_config: Draft::from(Box::new(IClashTemp::new().await)), clash_config: Draft::new(IClashTemp::new().await),
verge_config: Draft::from(Box::new(IVerge::new().await)), verge_config: Draft::new(IVerge::new().await),
profiles_config: Draft::from(Box::new(IProfiles::new().await)), profiles_config: Draft::new(IProfiles::new().await),
runtime_config: Draft::from(Box::new(IRuntime::new())), runtime_config: Draft::new(IRuntime::new()),
} }
}) })
.await .await
} }
pub async fn clash() -> Draft<Box<IClashTemp>> { pub async fn clash() -> Draft<IClashTemp> {
Self::global().await.clash_config.clone() Self::global().await.clash_config.clone()
} }
pub async fn verge() -> Draft<Box<IVerge>> { pub async fn verge() -> Draft<IVerge> {
Self::global().await.verge_config.clone() Self::global().await.verge_config.clone()
} }
pub async fn profiles() -> Draft<Box<IProfiles>> { pub async fn profiles() -> Draft<IProfiles> {
Self::global().await.profiles_config.clone() Self::global().await.profiles_config.clone()
} }
pub async fn runtime() -> Draft<Box<IRuntime>> { pub async fn runtime() -> Draft<IRuntime> {
Self::global().await.runtime_config.clone() Self::global().await.runtime_config.clone()
} }
@@ -60,13 +60,15 @@ impl Config {
if !cmd::system::is_admin().unwrap_or_default() if !cmd::system::is_admin().unwrap_or_default()
&& service::is_service_available().await.is_err() && service::is_service_available().await.is_err()
{ {
let verge = Config::verge().await; let verge = Self::verge().await;
verge.draft_mut().enable_tun_mode = Some(false); verge.edit_draft(|d| {
d.enable_tun_mode = Some(false);
});
verge.apply(); verge.apply();
let _ = tray::Tray::global().update_tray_display().await; let _ = tray::Tray::global().update_menu().await;
// 分离数据获取和异步调用避免Send问题 // 分离数据获取和异步调用避免Send问题
let verge_data = Config::verge().await.latest_ref().clone(); let verge_data = Self::verge().await.latest_arc();
logging_error!(Type::Core, verge_data.save_file().await); logging_error!(Type::Core, verge_data.save_file().await);
} }
@@ -83,11 +85,11 @@ impl Config {
// Ensure "Merge" and "Script" profile items exist, adding them if missing. // Ensure "Merge" and "Script" profile items exist, adding them if missing.
async fn ensure_default_profile_items() -> Result<()> { async fn ensure_default_profile_items() -> Result<()> {
let profiles = Self::profiles().await; let profiles = Self::profiles().await;
if profiles.latest_ref().get_item("Merge").is_err() { if profiles.latest_arc().get_item("Merge").is_err() {
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?; let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
profiles_append_item_safe(merge_item).await?; profiles_append_item_safe(merge_item).await?;
} }
if profiles.latest_ref().get_item("Script").is_err() { if profiles.latest_arc().get_item("Script").is_err() {
let script_item = &mut PrfItem::from_script(Some("Script".into()))?; let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
profiles_append_item_safe(script_item).await?; profiles_append_item_safe(script_item).await?;
} }
@@ -152,27 +154,27 @@ impl Config {
ConfigType::Check => dirs::app_home_dir()?.join(files::CHECK_CONFIG), ConfigType::Check => dirs::app_home_dir()?.join(files::CHECK_CONFIG),
}; };
let runtime = Config::runtime().await; let runtime = Self::runtime().await;
let config = runtime let runtime_arc = runtime.latest_arc();
.latest_ref() let config = runtime_arc
.config .config
.as_ref() .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) Ok(path)
} }
pub async fn generate() -> Result<()> { pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await; let (config, exists_keys, logs) = enhance::enhance().await;
**Config::runtime().await.draft_mut() = IRuntime { Self::runtime().await.edit_draft(|d| {
config: Some(config), *d = IRuntime {
exists_keys, config: Some(config),
chain_logs: logs, exists_keys,
}; chain_logs: logs,
}
});
Ok(()) Ok(())
} }
@@ -187,11 +189,11 @@ impl Config {
}; };
let operation = || async { let operation = || async {
if Config::runtime().await.latest_ref().config.is_some() { if Self::runtime().await.latest_arc().config.is_some() {
return Ok::<(), BackoffError<anyhow::Error>>(()); return Ok::<(), BackoffError<anyhow::Error>>(());
} }
Config::generate().await.map_err(BackoffError::transient) Self::generate().await.map_err(BackoffError::transient)
}; };
if let Err(e) = backoff::future::retry(backoff_strategy, operation).await { if let Err(e) = backoff::future::retry(backoff_strategy, operation).await {
@@ -228,7 +230,7 @@ mod tests {
#[test] #[test]
#[allow(unused_variables)] #[allow(unused_variables)]
fn test_draft_size_non_boxed() { fn test_draft_size_non_boxed() {
let draft = Draft::from(IRuntime::new()); let draft = Draft::new(IRuntime::new());
let iruntime_size = std::mem::size_of_val(&draft); let iruntime_size = std::mem::size_of_val(&draft);
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>()); assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
} }
@@ -236,7 +238,7 @@ mod tests {
#[test] #[test]
#[allow(unused_variables)] #[allow(unused_variables)]
fn test_draft_size_boxed() { fn test_draft_size_boxed() {
let draft = Draft::from(Box::new(IRuntime::new())); let draft = Draft::new(Box::new(IRuntime::new()));
let box_iruntime_size = std::mem::size_of_val(&draft); let box_iruntime_size = std::mem::size_of_val(&draft);
assert_eq!( assert_eq!(
box_iruntime_size, box_iruntime_size,
+38 -25
View File
@@ -5,9 +5,16 @@ use aes_gcm::{
}; };
use base64::{Engine, engine::general_purpose::STANDARD}; use base64::{Engine, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::Cell;
use std::future::Future;
const NONCE_LENGTH: usize = 12; 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 /// Encrypt data
#[allow(deprecated)] #[allow(deprecated)]
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> { pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
@@ -59,39 +66,45 @@ where
T: Serialize, T: Serialize,
S: Serializer, S: Serializer,
{ {
// 如果序列化失败,返回 None if is_encryption_active() {
let json = match serde_json::to_string(value) { let json = serde_json::to_string(value).map_err(serde::ser::Error::custom)?;
Ok(j) => j, let encrypted = encrypt_data(&json).map_err(serde::ser::Error::custom)?;
Err(_) => return serializer.serialize_none(), serializer.serialize_str(&encrypted)
}; } else {
value.serialize(serializer)
// 如果加密失败,返回 None
match encrypt_data(&json) {
Ok(encrypted) => serializer.serialize_str(&encrypted),
Err(_) => serializer.serialize_none(),
} }
} }
/// Deserialize decrypted function /// Deserialize decrypted function
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error> pub fn deserialize_encrypted<'a, D, T>(deserializer: D) -> Result<T, D::Error>
where where
T: for<'de> Deserialize<'de> + Default, T: for<'de> Deserialize<'de> + Default,
D: Deserializer<'a>, D: Deserializer<'a>,
{ {
// 如果反序列化字符串失败,返回默认值 if is_encryption_active() {
let encrypted = match String::deserialize(deserializer) { let encrypted_opt: Option<String> = Option::deserialize(deserializer)?;
Ok(s) => s,
Err(_) => return Ok(T::default()),
};
// 如果解密失败,返回默认值 match encrypted_opt {
let decrypted_string = match decrypt_data(&encrypted) { Some(encrypted) if !encrypted.is_empty() => {
Ok(data) => data, let decrypted_string =
Err(_) => return Ok(T::default()), decrypt_data(&encrypted).map_err(serde::de::Error::custom)?;
}; serde_json::from_str(&decrypted_string).map_err(serde::de::Error::custom)
// 如果 JSON 解析失败,返回默认值 }
match serde_json::from_str(&decrypted_string) { _ => Ok(T::default()),
Ok(value) => Ok(value), }
Err(_) => Ok(T::default()), } else {
T::deserialize(deserializer)
} }
} }
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)
}
+70 -74
View File
@@ -152,7 +152,7 @@ impl PrfOption {
impl PrfItem { impl PrfItem {
/// From partial item /// From partial item
/// must contain `itype` /// must contain `itype`
pub async fn from(item: &PrfItem, file_data: Option<String>) -> Result<PrfItem> { pub async fn from(item: &Self, file_data: Option<String>) -> Result<Self> {
if item.itype.is_none() { if item.itype.is_none() {
bail!("type should not be null"); bail!("type should not be null");
} }
@@ -170,13 +170,13 @@ impl PrfItem {
let name = item.name.as_ref(); let name = item.name.as_ref();
let desc = item.desc.as_ref(); let desc = item.desc.as_ref();
let option = item.option.as_ref(); let option = item.option.as_ref();
PrfItem::from_url(url, name, desc, option).await Self::from_url(url, name, desc, option).await
} }
"local" => { "local" => {
let name = item.name.clone().unwrap_or_else(|| "Local File".into()); let name = item.name.clone().unwrap_or_else(|| "Local File".into());
let desc = item.desc.clone().unwrap_or_else(|| "".into()); let desc = item.desc.clone().unwrap_or_else(|| "".into());
let option = item.option.as_ref(); let option = item.option.as_ref();
PrfItem::from_local(name, desc, file_data, option).await Self::from_local(name, desc, file_data, option).await
} }
typ => bail!("invalid profile item type \"{typ}\""), typ => bail!("invalid profile item type \"{typ}\""),
} }
@@ -189,7 +189,7 @@ impl PrfItem {
desc: String, desc: String,
file_data: Option<String>, file_data: Option<String>,
option: Option<&PrfOption>, option: Option<&PrfOption>,
) -> Result<PrfItem> { ) -> Result<Self> {
let uid = help::get_uid("L").into(); let uid = help::get_uid("L").into();
let file = format!("{uid}.yaml").into(); let file = format!("{uid}.yaml").into();
let opt_ref = option.as_ref(); let opt_ref = option.as_ref();
@@ -201,31 +201,31 @@ impl PrfItem {
let mut groups = opt_ref.and_then(|o| o.groups.clone()); let mut groups = opt_ref.and_then(|o| o.groups.clone());
if merge.is_none() { if merge.is_none() {
let merge_item = &mut PrfItem::from_merge(None)?; let merge_item = &mut Self::from_merge(None)?;
profiles::profiles_append_item_safe(merge_item).await?; profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone(); merge = merge_item.uid.clone();
} }
if script.is_none() { if script.is_none() {
let script_item = &mut PrfItem::from_script(None)?; let script_item = &mut Self::from_script(None)?;
profiles::profiles_append_item_safe(script_item).await?; profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone(); script = script_item.uid.clone();
} }
if rules.is_none() { if rules.is_none() {
let rules_item = &mut PrfItem::from_rules()?; let rules_item = &mut Self::from_rules()?;
profiles::profiles_append_item_safe(rules_item).await?; profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone(); rules = rules_item.uid.clone();
} }
if proxies.is_none() { if proxies.is_none() {
let proxies_item = &mut PrfItem::from_proxies()?; let proxies_item = &mut Self::from_proxies()?;
profiles::profiles_append_item_safe(proxies_item).await?; profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone(); proxies = proxies_item.uid.clone();
} }
if groups.is_none() { if groups.is_none() {
let groups_item = &mut PrfItem::from_groups()?; let groups_item = &mut Self::from_groups()?;
profiles::profiles_append_item_safe(groups_item).await?; profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone(); groups = groups_item.uid.clone();
} }
Ok(PrfItem { Ok(Self {
uid: Some(uid), uid: Some(uid),
itype: Some("local".into()), itype: Some("local".into()),
name: Some(name), name: Some(name),
@@ -256,7 +256,7 @@ impl PrfItem {
name: Option<&String>, name: Option<&String>,
desc: Option<&String>, desc: Option<&String>,
option: Option<&PrfOption>, option: Option<&PrfOption>,
) -> Result<PrfItem> { ) -> Result<Self> {
let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false)); 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)); let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false));
let accept_invalid_certs = let accept_invalid_certs =
@@ -393,32 +393,32 @@ impl PrfItem {
} }
if merge.is_none() { if merge.is_none() {
let merge_item = &mut PrfItem::from_merge(None)?; let merge_item = &mut Self::from_merge(None)?;
profiles::profiles_append_item_safe(merge_item).await?; profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone(); merge = merge_item.uid.clone();
} }
if script.is_none() { if script.is_none() {
let script_item = &mut PrfItem::from_script(None)?; let script_item = &mut Self::from_script(None)?;
profiles::profiles_append_item_safe(script_item).await?; profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone(); script = script_item.uid.clone();
} }
if rules.is_none() { if rules.is_none() {
let rules_item = &mut PrfItem::from_rules()?; let rules_item = &mut Self::from_rules()?;
profiles::profiles_append_item_safe(rules_item).await?; profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone(); rules = rules_item.uid.clone();
} }
if proxies.is_none() { if proxies.is_none() {
let proxies_item = &mut PrfItem::from_proxies()?; let proxies_item = &mut Self::from_proxies()?;
profiles::profiles_append_item_safe(proxies_item).await?; profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone(); proxies = proxies_item.uid.clone();
} }
if groups.is_none() { if groups.is_none() {
let groups_item = &mut PrfItem::from_groups()?; let groups_item = &mut Self::from_groups()?;
profiles::profiles_append_item_safe(groups_item).await?; profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone(); groups = groups_item.uid.clone();
} }
Ok(PrfItem { Ok(Self {
uid: Some(uid), uid: Some(uid),
itype: Some("remote".into()), itype: Some("remote".into()),
name: Some(name), name: Some(name),
@@ -445,116 +445,85 @@ impl PrfItem {
/// ## Merge type (enhance) /// ## Merge type (enhance)
/// create the enhanced item by using `merge` rule /// create the enhanced item by using `merge` rule
pub fn from_merge(uid: Option<String>) -> Result<PrfItem> { pub fn from_merge(uid: Option<String>) -> Result<Self> {
let mut id = help::get_uid("m").into(); let (id, template) = if let Some(uid) = uid {
let mut template = tmpl::ITEM_MERGE_EMPTY.into(); (uid, tmpl::ITEM_MERGE.into())
if let Some(uid) = uid { } else {
id = uid; (help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
template = tmpl::ITEM_MERGE.into(); };
}
let file = format!("{id}.yaml").into(); let file = format!("{id}.yaml").into();
Ok(PrfItem { Ok(Self {
uid: Some(id), uid: Some(id),
itype: Some("merge".into()), itype: Some("merge".into()),
name: None,
desc: None,
file: Some(file), file: Some(file),
url: None,
selected: None,
extra: None,
option: None,
home: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(template), file_data: Some(template),
..Default::default()
}) })
} }
/// ## Script type (enhance) /// ## Script type (enhance)
/// create the enhanced item by using javascript quick.js /// create the enhanced item by using javascript quick.js
pub fn from_script(uid: Option<String>) -> Result<PrfItem> { pub fn from_script(uid: Option<String>) -> Result<Self> {
let mut id = help::get_uid("s").into(); let id = if let Some(uid) = uid {
if let Some(uid) = uid { uid
id = uid; } else {
} help::get_uid("s").into()
};
let file = format!("{id}.js").into(); // js ext let file = format!("{id}.js").into(); // js ext
Ok(Self {
Ok(PrfItem {
uid: Some(id), uid: Some(id),
itype: Some("script".into()), itype: Some("script".into()),
name: None,
desc: None,
file: Some(file), file: Some(file),
url: None,
home: None,
selected: None,
extra: None,
option: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_SCRIPT.into()), file_data: Some(tmpl::ITEM_SCRIPT.into()),
..Default::default()
}) })
} }
/// ## Rules type (enhance) /// ## Rules type (enhance)
pub fn from_rules() -> Result<PrfItem> { pub fn from_rules() -> Result<Self> {
let uid = help::get_uid("r").into(); let uid = help::get_uid("r").into();
let file = format!("{uid}.yaml").into(); // yaml ext let file = format!("{uid}.yaml").into(); // yaml ext
Ok(PrfItem { Ok(Self {
uid: Some(uid), uid: Some(uid),
itype: Some("rules".into()), itype: Some("rules".into()),
name: None,
desc: None,
file: Some(file), file: Some(file),
url: None,
home: None,
selected: None,
extra: None,
option: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_RULES.into()), file_data: Some(tmpl::ITEM_RULES.into()),
..Default::default()
}) })
} }
/// ## Proxies type (enhance) /// ## Proxies type (enhance)
pub fn from_proxies() -> Result<PrfItem> { pub fn from_proxies() -> Result<Self> {
let uid = help::get_uid("p").into(); let uid = help::get_uid("p").into();
let file = format!("{uid}.yaml").into(); // yaml ext let file = format!("{uid}.yaml").into(); // yaml ext
Ok(PrfItem { Ok(Self {
uid: Some(uid), uid: Some(uid),
itype: Some("proxies".into()), itype: Some("proxies".into()),
name: None,
desc: None,
file: Some(file), file: Some(file),
url: None,
home: None,
selected: None,
extra: None,
option: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_PROXIES.into()), file_data: Some(tmpl::ITEM_PROXIES.into()),
..Default::default()
}) })
} }
/// ## Groups type (enhance) /// ## Groups type (enhance)
pub fn from_groups() -> Result<PrfItem> { pub fn from_groups() -> Result<Self> {
let uid = help::get_uid("g").into(); let uid = help::get_uid("g").into();
let file = format!("{uid}.yaml").into(); // yaml ext let file = format!("{uid}.yaml").into(); // yaml ext
Ok(PrfItem { Ok(Self {
uid: Some(uid), uid: Some(uid),
itype: Some("groups".into()), itype: Some("groups".into()),
name: None,
desc: None,
file: Some(file), file: Some(file),
url: None,
home: None,
selected: None,
extra: None,
option: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_GROUPS.into()), file_data: Some(tmpl::ITEM_GROUPS.into()),
..Default::default()
}) })
} }
@@ -584,7 +553,34 @@ impl PrfItem {
} }
} }
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())
}
}
// 向前兼容,默认为订阅启用自动更新 // 向前兼容,默认为订阅启用自动更新
fn default_allow_auto_update() -> Option<bool> { const fn default_allow_auto_update() -> Option<bool> {
Some(true) Some(true)
} }
+44 -109
View File
@@ -8,7 +8,7 @@ use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
use smartstring::alias::String; use smartstring::alias::String;
use std::collections::HashSet; use std::{collections::HashSet, sync::Arc};
use tokio::fs; use tokio::fs;
/// Define the `profiles.yaml` schema /// Define the `profiles.yaml` schema
@@ -32,7 +32,7 @@ pub struct CleanupResult {
macro_rules! patch { macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => { ($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() { if ($rv.$key).is_some() {
$lv.$key = $rv.$key.clone(); $lv.$key = $rv.$key.to_owned();
} }
}; };
} }
@@ -50,28 +50,26 @@ impl IProfiles {
} }
None None
} }
pub async fn new() -> Self { pub async fn new() -> Self {
match dirs::profiles_path() { let path = match dirs::profiles_path() {
Ok(path) => match help::read_yaml::<Self>(&path).await { Ok(p) => p,
Ok(mut profiles) => { Err(err) => {
if profiles.items.is_none() { logging!(error, Type::Config, "{err}");
profiles.items = Some(vec![]); 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());
} }
// 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
} }
Err(err) => { profiles
logging!(error, Type::Config, "{err}"); }
Self::default()
}
},
Err(err) => { Err(err) => {
logging!(error, Type::Config, "{err}"); logging!(error, Type::Config, "{err}");
Self::default() Self::default()
@@ -89,7 +87,7 @@ impl IProfiles {
} }
/// 只修改current,valid和chain /// 只修改current,valid和chain
pub fn patch_config(&mut self, patch: &IProfiles) -> Result<()> { pub fn patch_config(&mut self, patch: &Self) {
if self.items.is_none() { if self.items.is_none() {
self.items = Some(vec![]); self.items = Some(vec![]);
} }
@@ -102,16 +100,14 @@ impl IProfiles {
self.current = some_uid.cloned(); self.current = some_uid.cloned();
} }
} }
Ok(())
} }
pub fn get_current(&self) -> Option<String> { pub const fn get_current(&self) -> Option<&String> {
self.current.clone() self.current.as_ref()
} }
/// get items ref /// get items ref
pub fn get_items(&self) -> Option<&Vec<PrfItem>> { pub const fn get_items(&self) -> Option<&Vec<PrfItem>> {
self.items.as_ref() self.items.as_ref()
} }
@@ -132,6 +128,15 @@ impl IProfiles {
bail!("failed to get the profile item \"uid:{}\"", uid_str); 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()))
})
}
/// append new item /// append new item
/// if the file_data is some /// if the file_data is some
/// then should save the data to file /// then should save the data to file
@@ -357,88 +362,18 @@ 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指向的 /// 判断profile是否是current指向的
pub fn is_current_profile_index(&self, index: &String) -> bool { pub fn is_current_profile_index(&self, index: &String) -> bool {
self.current.as_ref() == Some(index) self.current.as_ref() == Some(index)
} }
/// 获取所有的profiles(uid,名称) /// 获取所有的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| { self.items.as_ref().map(|items| {
items items
.iter() .iter()
.filter_map(|e| { .filter_map(|e| {
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) { if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) {
Some((uid, name)) Some((uid, name))
} else { } else {
None None
@@ -449,11 +384,11 @@ impl IProfiles {
} }
/// 通过 uid 获取名称 /// 通过 uid 获取名称
pub fn get_name_by_uid(&self, uid: &String) -> Option<String> { pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> {
if let Some(items) = &self.items { if let Some(items) = &self.items {
for item in items { for item in items {
if item.uid.as_ref() == Some(uid) { if item.uid.as_ref() == Some(uid) {
return item.name.clone(); return item.name.as_ref();
} }
} }
} }
@@ -551,14 +486,14 @@ impl IProfiles {
} }
/// 获取所有 active profile 关联的文件名 /// 获取所有 active profile 关联的文件名
fn get_all_active_files(&self) -> HashSet<String> { fn get_all_active_files(&self) -> HashSet<&str> {
let mut active_files = HashSet::new(); let mut active_files: HashSet<&str> = HashSet::new();
if let Some(items) = &self.items { if let Some(items) = &self.items {
for item in items { for item in items {
// 收集所有类型 profile 的文件 // 收集所有类型 profile 的文件
if let Some(file) = &item.file { if let Some(file) = &item.file {
active_files.insert(file.clone()); active_files.insert(file);
} }
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件 // 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
@@ -571,35 +506,35 @@ impl IProfiles {
&& let Ok(merge_item) = self.get_item(merge_uid) && let Ok(merge_item) = self.get_item(merge_uid)
&& let Some(file) = &merge_item.file && let Some(file) = &merge_item.file
{ {
active_files.insert(file.clone()); active_files.insert(file);
} }
if let Some(script_uid) = &option.script if let Some(script_uid) = &option.script
&& let Ok(script_item) = self.get_item(script_uid) && let Ok(script_item) = self.get_item(script_uid)
&& let Some(file) = &script_item.file && let Some(file) = &script_item.file
{ {
active_files.insert(file.clone()); active_files.insert(file);
} }
if let Some(rules_uid) = &option.rules if let Some(rules_uid) = &option.rules
&& let Ok(rules_item) = self.get_item(rules_uid) && let Ok(rules_item) = self.get_item(rules_uid)
&& let Some(file) = &rules_item.file && let Some(file) = &rules_item.file
{ {
active_files.insert(file.clone()); active_files.insert(file);
} }
if let Some(proxies_uid) = &option.proxies if let Some(proxies_uid) = &option.proxies
&& let Ok(proxies_item) = self.get_item(proxies_uid) && let Ok(proxies_item) = self.get_item(proxies_uid)
&& let Some(file) = &proxies_item.file && let Some(file) = &proxies_item.file
{ {
active_files.insert(file.clone()); active_files.insert(file);
} }
if let Some(groups_uid) = &option.groups if let Some(groups_uid) = &option.groups
&& let Ok(groups_item) = self.get_item(groups_uid) && let Ok(groups_item) = self.get_item(groups_uid)
&& let Some(file) = &groups_item.file && let Some(file) = &groups_item.file
{ {
active_files.insert(file.clone()); active_files.insert(file);
} }
} }
} }
+17 -167
View File
@@ -46,19 +46,24 @@ pub struct IVerge {
pub enable_memory_usage: Option<bool>, pub enable_memory_usage: Option<bool>,
/// enable group icon /// enable group icon
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_group_icon: Option<bool>, pub enable_group_icon: Option<bool>,
/// common tray icon /// common tray icon
#[serde(skip_serializing_if = "Option::is_none")]
pub common_tray_icon: Option<bool>, pub common_tray_icon: Option<bool>,
/// tray icon /// tray icon
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[serde(skip_serializing_if = "Option::is_none")]
pub tray_icon: Option<String>, pub tray_icon: Option<String>,
/// menu icon /// menu icon
#[serde(skip_serializing_if = "Option::is_none")]
pub menu_icon: Option<String>, pub menu_icon: Option<String>,
/// menu order /// menu order
#[serde(skip_serializing_if = "Option::is_none")]
pub menu_order: Option<Vec<String>>, pub menu_order: Option<Vec<String>>,
/// sysproxy tray icon /// sysproxy tray icon
@@ -115,6 +120,7 @@ pub struct IVerge {
/// hotkey map /// hotkey map
/// format: {func},{key} /// format: {func},{key}
#[serde(skip_serializing_if = "Option::is_none")]
pub hotkeys: Option<Vec<String>>, pub hotkeys: Option<Vec<String>>,
/// enable global hotkey /// enable global hotkey
@@ -134,7 +140,7 @@ pub struct IVerge {
pub default_latency_test: Option<String>, pub default_latency_test: Option<String>,
/// 默认的延迟测试超时时间 /// 默认的延迟测试超时时间
pub default_latency_timeout: Option<i32>, pub default_latency_timeout: Option<i16>,
/// 是否自动检测当前节点延迟 /// 是否自动检测当前节点延迟
pub enable_auto_delay_detection: Option<bool>, pub enable_auto_delay_detection: Option<bool>,
@@ -143,7 +149,7 @@ pub struct IVerge {
pub enable_builtin_enhanced: Option<bool>, pub enable_builtin_enhanced: Option<bool>,
/// proxy 页面布局 列数 /// proxy 页面布局 列数
pub proxy_layout_column: Option<i32>, pub proxy_layout_column: Option<u8>,
/// 测试站列表 /// 测试站列表
pub test_list: Option<Vec<IVergeTestItem>>, pub test_list: Option<Vec<IVergeTestItem>>,
@@ -202,6 +208,7 @@ pub struct IVerge {
)] )]
pub webdav_password: Option<String>, pub webdav_password: Option<String>,
#[serde(skip)]
pub enable_tray_speed: Option<bool>, pub enable_tray_speed: Option<bool>,
// pub enable_tray_icon: Option<bool>, // pub enable_tray_icon: Option<bool>,
@@ -255,7 +262,7 @@ impl IVerge {
/// 验证并修正配置文件中的clash_core值 /// 验证并修正配置文件中的clash_core值
pub async fn validate_and_fix_config() -> Result<()> { pub async fn validate_and_fix_config() -> Result<()> {
let config_path = dirs::verge_path()?; let config_path = dirs::verge_path()?;
let mut config = match help::read_yaml::<IVerge>(&config_path).await { let mut config = match help::read_yaml::<Self>(&config_path).await {
Ok(config) => config, Ok(config) => config,
Err(_) => Self::template(), Err(_) => Self::template(),
}; };
@@ -304,7 +311,7 @@ impl IVerge {
} }
/// 配置修正后重新加载配置 /// 配置修正后重新加载配置
async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> { async fn reload_config_after_fix(updated_config: Self) -> Result<()> {
logging!( logging!(
info, info,
Type::Config, Type::Config,
@@ -313,7 +320,9 @@ impl IVerge {
); );
let config_draft = Config::verge().await; let config_draft = Config::verge().await;
**config_draft.draft_mut() = updated_config; config_draft.edit_draft(|d| {
*d = updated_config;
});
config_draft.apply(); config_draft.apply();
Ok(()) Ok(())
@@ -342,7 +351,7 @@ impl IVerge {
pub async fn new() -> Self { pub async fn new() -> Self {
match dirs::verge_path() { match dirs::verge_path() {
Ok(path) => match help::read_yaml::<IVerge>(&path).await { Ok(path) => match help::read_yaml::<Self>(&path).await {
Ok(mut config) => { Ok(mut config) => {
// compatibility // compatibility
if let Some(start_page) = config.start_page.clone() if let Some(start_page) = config.start_page.clone()
@@ -437,7 +446,7 @@ impl IVerge {
/// patch verge config /// patch verge config
/// only save to file /// only save to file
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn patch_config(&mut self, patch: &IVerge) { pub fn patch_config(&mut self, patch: &Self) {
macro_rules! patch { macro_rules! patch {
($key: tt) => { ($key: tt) => {
if patch.$key.is_some() { if patch.$key.is_some() {
@@ -522,7 +531,7 @@ impl IVerge {
patch!(enable_external_controller); patch!(enable_external_controller);
} }
pub fn get_singleton_port() -> u16 { pub const fn get_singleton_port() -> u16 {
crate::constants::network::ports::SINGLETON_SERVER crate::constants::network::ports::SINGLETON_SERVER
} }
@@ -543,162 +552,3 @@ 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,
}
}
}
impl From<Box<IVerge>> for IVergeResponse {
fn from(verge: Box<IVerge>) -> Self {
IVergeResponse::from(*verge)
}
}
+15 -12
View File
@@ -347,11 +347,12 @@ impl AsyncProxyQuery {
&mut buffer_size, &mut buffer_size,
); );
let mut proxy_server = String::new(); let proxy_server = if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len()); let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]); String::from_utf16_lossy(&buffer[..end_pos])
} } else {
String::new()
};
// 读取代理绕过列表 // 读取代理绕过列表
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>(); let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
@@ -368,14 +369,16 @@ impl AsyncProxyQuery {
&mut bypass_buffer_size, &mut bypass_buffer_size,
); );
let mut bypass_list = String::new(); let bypass_list =
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 { if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
let end_pos = bypass_buffer let end_pos = bypass_buffer
.iter() .iter()
.position(|&x| x == 0) .position(|&x| x == 0)
.unwrap_or(bypass_buffer.len()); .unwrap_or(bypass_buffer.len());
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]); String::from_utf16_lossy(&bypass_buffer[..end_pos])
} } else {
String::new()
};
RegCloseKey(hkey); RegCloseKey(hkey);
+12 -10
View File
@@ -45,12 +45,12 @@ enum Operation {
} }
impl Operation { impl Operation {
fn timeout(&self) -> u64 { const fn timeout(&self) -> u64 {
match self { match self {
Operation::Upload => TIMEOUT_UPLOAD, Self::Upload => TIMEOUT_UPLOAD,
Operation::Download => TIMEOUT_DOWNLOAD, Self::Download => TIMEOUT_DOWNLOAD,
Operation::List => TIMEOUT_LIST, Self::List => TIMEOUT_LIST,
Operation::Delete => TIMEOUT_DELETE, Self::Delete => TIMEOUT_DELETE,
} }
} }
} }
@@ -61,9 +61,9 @@ pub struct WebDavClient {
} }
impl WebDavClient { impl WebDavClient {
pub fn global() -> &'static WebDavClient { pub fn global() -> &'static Self {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new(); static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| WebDavClient { WEBDAV_CLIENT.get_or_init(|| Self {
config: Arc::new(ArcSwapOption::new(None)), config: Arc::new(ArcSwapOption::new(None)),
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))), clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
}) })
@@ -87,7 +87,7 @@ impl WebDavClient {
(*cfg_arc).clone() (*cfg_arc).clone()
} else { } else {
// 释放锁后获取异步配置 // 释放锁后获取异步配置
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.data_arc();
if verge.webdav_url.is_none() if verge.webdav_url.is_none()
|| verge.webdav_username.is_none() || verge.webdav_username.is_none()
|| verge.webdav_password.is_none() || verge.webdav_password.is_none()
@@ -99,11 +99,13 @@ impl WebDavClient {
let config = WebDavConfig { let config = WebDavConfig {
url: verge url: verge
.webdav_url .webdav_url
.as_ref()
.cloned()
.unwrap_or_default() .unwrap_or_default()
.trim_end_matches('/') .trim_end_matches('/')
.into(), .into(),
username: verge.webdav_username.unwrap_or_default(), username: verge.webdav_username.as_ref().cloned().unwrap_or_default(),
password: verge.webdav_password.unwrap_or_default(), password: verge.webdav_password.as_ref().cloned().unwrap_or_default(),
}; };
// 存储配置到 ArcSwapOption // 存储配置到 ArcSwapOption
+6 -6
View File
@@ -75,7 +75,7 @@ struct ProxyConfig {
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new); static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
impl EventDrivenProxyManager { impl EventDrivenProxyManager {
pub fn global() -> &'static EventDrivenProxyManager { pub fn global() -> &'static Self {
&PROXY_MANAGER &PROXY_MANAGER
} }
@@ -387,7 +387,7 @@ impl EventDrivenProxyManager {
async fn get_proxy_config() -> ProxyConfig { async fn get_proxy_config() -> ProxyConfig {
let (sys_enabled, pac_enabled, guard_enabled, guard_duration) = { let (sys_enabled, pac_enabled, guard_enabled, guard_duration) = {
let verge_config = Config::verge().await; let verge_config = Config::verge().await;
let verge = verge_config.latest_ref(); let verge = verge_config.latest_arc();
( (
verge.enable_system_proxy.unwrap_or(false), verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false), verge.proxy_auto_config.unwrap_or(false),
@@ -406,7 +406,7 @@ impl EventDrivenProxyManager {
async fn get_expected_pac_config() -> Autoproxy { async fn get_expected_pac_config() -> Autoproxy {
let proxy_host = { let proxy_host = {
let verge_config = Config::verge().await; let verge_config = Config::verge().await;
let verge = verge_config.latest_ref(); let verge = verge_config.latest_arc();
verge verge
.proxy_host .proxy_host
.clone() .clone()
@@ -424,13 +424,13 @@ impl EventDrivenProxyManager {
let (verge_mixed_port, proxy_host) = { let (verge_mixed_port, proxy_host) = {
let verge_config = Config::verge().await; let verge_config = Config::verge().await;
let verge_ref = verge_config.latest_ref(); let verge_ref = verge_config.latest_arc();
(verge_ref.verge_mixed_port, verge_ref.proxy_host.clone()) (verge_ref.verge_mixed_port, verge_ref.proxy_host.clone())
}; };
let default_port = { let default_port = {
let clash_config = Config::clash().await; let clash_config = Config::clash().await;
clash_config.latest_ref().get_mixed_port() clash_config.latest_arc().get_mixed_port()
}; };
let port = verge_mixed_port.unwrap_or(default_port); let port = verge_mixed_port.unwrap_or(default_port);
@@ -450,7 +450,7 @@ impl EventDrivenProxyManager {
use crate::constants::bypass; use crate::constants::bypass;
let verge_config = Config::verge().await; let verge_config = Config::verge().await;
let verge = verge_config.latest_ref(); let verge = verge_config.latest_arc();
let use_default = verge.use_default_bypass.unwrap_or(true); let use_default = verge.use_default_bypass.unwrap_or(true);
let custom = verge.system_proxy_bypass.as_deref().unwrap_or(""); let custom = verge.system_proxy_bypass.as_deref().unwrap_or("");
+3 -3
View File
@@ -102,14 +102,14 @@ impl Handle {
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid }); Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
} }
// TODO 利用 &str 等缩短 Clone
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) { pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
let handle = Self::global(); let handle = Self::global();
let status_str = status.into(); let status_str = status.into();
let msg_str = msg.into(); let msg_str = msg.into();
if !*handle.startup_completed.read() { if !*handle.startup_completed.read() {
let mut errors = handle.startup_errors.write(); handle.startup_errors.write().push(ErrorMessage {
errors.push(ErrorMessage {
status: status_str, status: status_str,
message: msg_str, message: msg_str,
}); });
@@ -158,7 +158,7 @@ impl Handle {
.spawn(move || { .spawn(move || {
thread::sleep(timing::STARTUP_ERROR_DELAY); thread::sleep(timing::STARTUP_ERROR_DELAY);
let handle = Handle::global(); let handle = Self::global();
if handle.is_exiting() { if handle.is_exiting() {
return; return;
} }
+42 -52
View File
@@ -28,16 +28,16 @@ pub enum HotkeyFunction {
impl fmt::Display for HotkeyFunction { impl fmt::Display for HotkeyFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self { let s = match self {
HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard", Self::OpenOrCloseDashboard => "open_or_close_dashboard",
HotkeyFunction::ClashModeRule => "clash_mode_rule", Self::ClashModeRule => "clash_mode_rule",
HotkeyFunction::ClashModeGlobal => "clash_mode_global", Self::ClashModeGlobal => "clash_mode_global",
HotkeyFunction::ClashModeDirect => "clash_mode_direct", Self::ClashModeDirect => "clash_mode_direct",
HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy", Self::ToggleSystemProxy => "toggle_system_proxy",
HotkeyFunction::ToggleTunMode => "toggle_tun_mode", Self::ToggleTunMode => "toggle_tun_mode",
HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode", Self::EntryLightweightMode => "entry_lightweight_mode",
HotkeyFunction::Quit => "quit", Self::Quit => "quit",
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
HotkeyFunction::Hide => "hide", Self::Hide => "hide",
}; };
write!(f, "{s}") write!(f, "{s}")
} }
@@ -48,16 +48,16 @@ impl FromStr for HotkeyFunction {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() { match s.trim() {
"open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard), "open_or_close_dashboard" => Ok(Self::OpenOrCloseDashboard),
"clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule), "clash_mode_rule" => Ok(Self::ClashModeRule),
"clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal), "clash_mode_global" => Ok(Self::ClashModeGlobal),
"clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect), "clash_mode_direct" => Ok(Self::ClashModeDirect),
"toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy), "toggle_system_proxy" => Ok(Self::ToggleSystemProxy),
"toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode), "toggle_tun_mode" => Ok(Self::ToggleTunMode),
"entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode), "entry_lightweight_mode" => Ok(Self::EntryLightweightMode),
"quit" => Ok(HotkeyFunction::Quit), "quit" => Ok(Self::Quit),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
"hide" => Ok(HotkeyFunction::Hide), "hide" => Ok(Self::Hide),
_ => bail!("invalid hotkey function: {}", s), _ => bail!("invalid hotkey function: {}", s),
} }
} }
@@ -75,8 +75,8 @@ pub enum SystemHotkey {
impl fmt::Display for SystemHotkey { impl fmt::Display for SystemHotkey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self { let s = match self {
SystemHotkey::CmdQ => "CMD+Q", Self::CmdQ => "CMD+Q",
SystemHotkey::CmdW => "CMD+W", Self::CmdW => "CMD+W",
}; };
write!(f, "{s}") write!(f, "{s}")
} }
@@ -84,10 +84,10 @@ impl fmt::Display for SystemHotkey {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
impl SystemHotkey { impl SystemHotkey {
pub fn function(self) -> HotkeyFunction { pub const fn function(self) -> HotkeyFunction {
match self { match self {
SystemHotkey::CmdQ => HotkeyFunction::Quit, Self::CmdQ => HotkeyFunction::Quit,
SystemHotkey::CmdW => HotkeyFunction::Hide, Self::CmdW => HotkeyFunction::Hide,
} }
} }
} }
@@ -211,50 +211,40 @@ impl Hotkey {
let is_quit = matches!(function, HotkeyFunction::Quit); 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; if event.state == ShortcutState::Pressed {
let event_owned = event; logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey_event);
let function_owned = function; let hotkey = hotkey_event.key;
let is_quit_owned = is_quit; if hotkey == Code::KeyQ && is_quit {
if let Some(window) = handle::Handle::get_window()
AsyncHandler::spawn(move || async move { && window.is_focused().unwrap_or(false)
if event_owned.state == ShortcutState::Pressed { {
logging!( logging!(debug, Type::Hotkey, "Executing quit function");
debug, Self::execute_function(function);
Type::Hotkey, }
"Hotkey pressed: {:?}", } else {
hotkey_event_owned AsyncHandler::spawn(move || async move {
);
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
if let Some(window) = handle::Handle::get_window()
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned);
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly"); logging!(debug, Type::Hotkey, "Executing function directly");
let is_enable_global_hotkey = Config::verge() let is_enable_global_hotkey = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_global_hotkey .enable_global_hotkey
.unwrap_or(true); .unwrap_or(true);
if is_enable_global_hotkey { if is_enable_global_hotkey {
Self::execute_function(function_owned); Self::execute_function(function);
} else { } else {
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
let is_visible = WindowManager::is_main_window_visible(); let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused(); let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible { if is_focused && is_visible {
Self::execute_function(function_owned); Self::execute_function(function);
} }
} }
} });
} }
}); }
})?; })?;
logging!( logging!(
@@ -274,7 +264,7 @@ singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
impl Hotkey { impl Hotkey {
pub async fn init(&self, skip: bool) -> Result<()> { pub async fn init(&self, skip: bool) -> Result<()> {
let verge = Config::verge().await; let verge = Config::verge().await;
let enable_global_hotkey = !skip && verge.latest_ref().enable_global_hotkey.unwrap_or(true); let enable_global_hotkey = !skip && verge.latest_arc().enable_global_hotkey.unwrap_or(true);
logging!( logging!(
debug, debug,
@@ -284,7 +274,7 @@ impl Hotkey {
); );
// Extract hotkeys data before async operations // Extract hotkeys data before async operations
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned(); let hotkeys = verge.latest_arc().hotkeys.as_ref().cloned();
if let Some(hotkeys) = hotkeys { if let Some(hotkeys) = hotkeys {
logging!( logging!(
+9 -7
View File
@@ -17,13 +17,15 @@ impl CoreManager {
use crate::constants::files::RUNTIME_CONFIG; use crate::constants::files::RUNTIME_CONFIG;
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
let clash_config = Config::clash().await.latest_ref().0.clone(); let clash_config = &Config::clash().await.latest_arc().0;
**Config::runtime().await.draft_mut() = IRuntime { Config::runtime().await.edit_draft(|d| {
config: Some(clash_config.clone()), *d = IRuntime {
exists_keys: vec![], config: Some(clash_config.to_owned()),
chain_logs: Default::default(), exists_keys: vec![],
}; chain_logs: Default::default(),
}
});
help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?; help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?;
handle::Handle::notice_message(error_key, error_msg); handle::Handle::notice_message(error_key, error_msg);
@@ -135,7 +137,7 @@ impl CoreManager {
} }
} }
fn is_connection_io_error(kind: std::io::ErrorKind) -> bool { const fn is_connection_io_error(kind: std::io::ErrorKind) -> bool {
matches!( matches!(
kind, kind,
std::io::ErrorKind::ConnectionAborted std::io::ErrorKind::ConnectionAborted
+7 -4
View File
@@ -47,10 +47,12 @@ impl CoreManager {
return Err(format!("Invalid clash core: {}", clash_core).into()); return Err(format!("Invalid clash core: {}", clash_core).into());
} }
Config::verge().await.draft_mut().clash_core = clash_core.to_owned().into(); Config::verge().await.edit_draft(|d| {
d.clash_core = Some(clash_core.to_owned());
});
Config::verge().await.apply(); Config::verge().await.apply();
let verge_data = Config::verge().await.latest_ref().clone(); let verge_data = Config::verge().await.latest_arc();
verge_data.save_file().await.map_err(|e| e.to_string())?; verge_data.save_file().await.map_err(|e| e.to_string())?;
let run_path = Config::generate_file(ConfigType::Run) let run_path = Config::generate_file(ConfigType::Run)
@@ -66,7 +68,8 @@ impl CoreManager {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
self.wait_for_service_if_needed().await; self.wait_for_service_if_needed().await;
let mode = match SERVICE_MANAGER.lock().await.current() { let value = SERVICE_MANAGER.lock().await.current();
let mode = match value {
ServiceStatus::Ready => RunningMode::Service, ServiceStatus::Ready => RunningMode::Service,
_ => RunningMode::Sidecar, _ => RunningMode::Sidecar,
}; };
@@ -82,7 +85,7 @@ impl CoreManager {
let needs_service = Config::verge() let needs_service = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_tun_mode .enable_tun_mode
.unwrap_or(false); .unwrap_or(false);
+13 -5
View File
@@ -32,7 +32,7 @@ impl CoreManager {
let config_file = Config::generate_file(crate::config::ConfigType::Run).await?; let config_file = Config::generate_file(crate::config::ConfigType::Run).await?;
let app_handle = handle::Handle::app_handle(); let app_handle = handle::Handle::app_handle();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core(); let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?; let config_dir = dirs::app_home_dir()?;
let (mut rx, child) = app_handle let (mut rx, child) = app_handle
@@ -62,8 +62,12 @@ impl CoreManager {
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => { | tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let mut now = DeferredNow::default(); let mut now = DeferredNow::default();
let message = CompactString::from(String::from_utf8_lossy(&line).as_ref()); let message = CompactString::from(String::from_utf8_lossy(&line).as_ref());
let w = shared_writer.lock().await; write_sidecar_log(
write_sidecar_log(w, &mut now, Level::Error, &message); shared_writer.lock().await,
&mut now,
Level::Error,
&message,
);
CLASH_LOGGER.append_log(message).await; CLASH_LOGGER.append_log(message).await;
} }
tauri_plugin_shell::process::CommandEvent::Terminated(term) => { tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
@@ -75,8 +79,12 @@ impl CoreManager {
} else { } else {
CompactString::from("Process terminated") CompactString::from("Process terminated")
}; };
let w = shared_writer.lock().await; write_sidecar_log(
write_sidecar_log(w, &mut now, Level::Info, &message); shared_writer.lock().await,
&mut now,
Level::Info,
&message,
);
CLASH_LOGGER.clear_logs().await; CLASH_LOGGER.clear_logs().await;
break; break;
} }
+16 -19
View File
@@ -1,3 +1,4 @@
use super::handle::Handle;
use crate::{ use crate::{
constants::{retry, timing}, constants::{retry, timing},
logging, logging,
@@ -11,7 +12,7 @@ use std::{
mpsc, mpsc,
}, },
thread, thread,
time::Instant, time::{Duration, Instant},
}; };
use tauri::{Emitter, WebviewWindow}; use tauri::{Emitter, WebviewWindow};
@@ -91,30 +92,26 @@ impl NotificationSystem {
} }
fn worker_loop(rx: mpsc::Receiver<FrontendEvent>) { fn worker_loop(rx: mpsc::Receiver<FrontendEvent>) {
use super::handle::Handle; loop {
let handle = Handle::global();
let handle = Handle::global(); if handle.is_exiting() {
break;
while !handle.is_exiting() { }
match rx.recv() { match rx.recv_timeout(Duration::from_millis(1_000)) {
Ok(event) => Self::process_event(handle, event), Ok(event) => Self::process_event(handle, event),
Err(e) => { Err(mpsc::RecvTimeoutError::Timeout) => (),
logging!( Err(mpsc::RecvTimeoutError::Disconnected) => break,
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) { fn process_event(handle: &super::handle::Handle, event: FrontendEvent) {
let system_guard = handle.notification_system.read(); let binding = handle.notification_system.read();
let Some(system) = system_guard.as_ref() else { let system = match binding.as_ref() {
return; Some(s) => s,
None => return,
}; };
if system.should_skip_event(&event) { if system.should_skip_event(&event) {
+20 -13
View File
@@ -122,7 +122,6 @@ async fn reinstall_service() -> Result<()> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
async fn uninstall_service() -> Result<()> { async fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, "uninstall service"); logging!(info, Type::Service, "uninstall service");
use users::get_effective_uid;
let uninstall_path = let uninstall_path =
tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall"); tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall");
@@ -134,13 +133,14 @@ async fn uninstall_service() -> Result<()> {
let uninstall_shell: String = uninstall_path.to_string_lossy().replace(" ", "\\ "); let uninstall_shell: String = uninstall_path.to_string_lossy().replace(" ", "\\ ");
let elevator = crate::utils::help::linux_elevator(); let elevator = crate::utils::help::linux_elevator();
let status = match get_effective_uid() { let status = if linux_running_as_root() {
0 => StdCommand::new(uninstall_shell).status()?, StdCommand::new(&uninstall_path).status()?
_ => StdCommand::new(elevator.clone()) } else {
StdCommand::new(elevator)
.arg("sh") .arg("sh")
.arg("-c") .arg("-c")
.arg(uninstall_shell) .arg(uninstall_shell)
.status()?, .status()?
}; };
logging!( logging!(
info, info,
@@ -163,7 +163,6 @@ async fn uninstall_service() -> Result<()> {
#[allow(clippy::unused_async)] #[allow(clippy::unused_async)]
async fn install_service() -> Result<()> { async fn install_service() -> Result<()> {
logging!(info, Type::Service, "install service"); logging!(info, Type::Service, "install service");
use users::get_effective_uid;
let install_path = let install_path =
tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-install"); tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-install");
@@ -175,13 +174,14 @@ async fn install_service() -> Result<()> {
let install_shell: String = install_path.to_string_lossy().replace(" ", "\\ "); let install_shell: String = install_path.to_string_lossy().replace(" ", "\\ ");
let elevator = crate::utils::help::linux_elevator(); let elevator = crate::utils::help::linux_elevator();
let status = match get_effective_uid() { let status = if linux_running_as_root() {
0 => StdCommand::new(install_shell).status()?, StdCommand::new(&install_path).status()?
_ => StdCommand::new(elevator.clone()) } else {
StdCommand::new(elevator)
.arg("sh") .arg("sh")
.arg("-c") .arg("-c")
.arg(install_shell) .arg(install_shell)
.status()?, .status()?
}; };
logging!( logging!(
info, info,
@@ -218,6 +218,13 @@ async fn reinstall_service() -> Result<()> {
} }
} }
#[cfg(target_os = "linux")]
fn linux_running_as_root() -> bool {
const ROOT_UID: u32 = 0;
unsafe { libc::geteuid() == ROOT_UID }
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
async fn uninstall_service() -> Result<()> { async fn uninstall_service() -> Result<()> {
use crate::utils::i18n::t; use crate::utils::i18n::t;
@@ -353,7 +360,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
logging!(info, Type::Service, "尝试使用现有服务启动核心"); logging!(info, Type::Service, "尝试使用现有服务启动核心");
let verge_config = Config::verge().await; let verge_config = Config::verge().await;
let clash_core = verge_config.latest_ref().get_valid_clash_core(); let clash_core = verge_config.latest_arc().get_valid_clash_core();
drop(verge_config); drop(verge_config);
let bin_ext = if cfg!(windows) { ".exe" } else { "" }; let bin_ext = if cfg!(windows) { ".exe" } else { "" };
@@ -449,7 +456,7 @@ impl ServiceManager {
Self(ServiceStatus::Unavailable("Need Checks".into())) Self(ServiceStatus::Unavailable("Need Checks".into()))
} }
pub fn config() -> Option<clash_verge_service_ipc::IpcConfig> { pub const fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
Some(clash_verge_service_ipc::IpcConfig { Some(clash_verge_service_ipc::IpcConfig {
default_timeout: Duration::from_millis(30), default_timeout: Duration::from_millis(30),
retry_delay: Duration::from_millis(250), retry_delay: Duration::from_millis(250),
@@ -532,7 +539,7 @@ impl ServiceManager {
return Err(anyhow::anyhow!("服务不可用: {}", reason)); return Err(anyhow::anyhow!("服务不可用: {}", reason));
} }
} }
let _ = tray::Tray::global().update_tray_display().await; let _ = tray::Tray::global().update_menu().await;
Ok(()) Ok(())
} }
} }
+7 -7
View File
@@ -31,12 +31,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 { async fn get_bypass() -> String {
let use_default = Config::verge() let use_default = Config::verge()
.await .await
.latest_ref() .latest_arc()
.use_default_bypass .use_default_bypass
.unwrap_or(true); .unwrap_or(true);
let res = { let res = {
let verge = Config::verge().await; let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_arc();
verge.system_proxy_bypass.clone() verge.system_proxy_bypass.clone()
}; };
let custom_bypass = match res { let custom_bypass = match res {
@@ -84,7 +84,7 @@ async fn execute_sysproxy_command(args: Vec<std::string::String>) -> Result<()>
impl Default for Sysopt { impl Default for Sysopt {
fn default() -> Self { fn default() -> Self {
Sysopt { Self {
initialed: AtomicBool::new(false), initialed: AtomicBool::new(false),
update_sysproxy: AtomicBool::new(false), update_sysproxy: AtomicBool::new(false),
reset_sysproxy: AtomicBool::new(false), reset_sysproxy: AtomicBool::new(false),
@@ -124,17 +124,17 @@ impl Sysopt {
} }
let port = { let port = {
let verge_port = Config::verge().await.latest_ref().verge_mixed_port; let verge_port = Config::verge().await.latest_arc().verge_mixed_port;
match verge_port { match verge_port {
Some(port) => port, Some(port) => port,
None => Config::clash().await.latest_ref().get_mixed_port(), None => Config::clash().await.latest_arc().get_mixed_port(),
} }
}; };
let pac_port = IVerge::get_singleton_port(); let pac_port = IVerge::get_singleton_port();
let (sys_enable, pac_enable, proxy_host) = { let (sys_enable, pac_enable, proxy_host) = {
let verge = Config::verge().await; let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_arc();
( (
verge.enable_system_proxy.unwrap_or(false), verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false), verge.proxy_auto_config.unwrap_or(false),
@@ -266,7 +266,7 @@ impl Sysopt {
/// update the startup /// update the startup
pub async fn update_launch(&self) -> Result<()> { pub async fn update_launch(&self) -> Result<()> {
let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch }; let enable_auto_launch = { Config::verge().await.latest_arc().enable_auto_launch };
let is_enable = enable_auto_launch.unwrap_or(false); let is_enable = enable_auto_launch.unwrap_or(false);
logging!( logging!(
info, info,
+18 -21
View File
@@ -46,7 +46,7 @@ singleton!(Timer, TIMER_INSTANCE);
impl Timer { impl Timer {
fn new() -> Self { fn new() -> Self {
Timer { Self {
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())), delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
timer_map: Arc::new(RwLock::new(HashMap::new())), timer_map: Arc::new(RwLock::new(HashMap::new())),
timer_count: AtomicU64::new(1), timer_count: AtomicU64::new(1),
@@ -100,7 +100,7 @@ impl Timer {
// Collect profiles that need immediate update // Collect profiles that need immediate update
let profiles_to_update = let profiles_to_update =
if let Some(items) = Config::profiles().await.latest_ref().get_items() { if let Some(items) = Config::profiles().await.latest_arc().get_items() {
items items
.iter() .iter()
.filter_map(|item| { .filter_map(|item| {
@@ -154,19 +154,17 @@ impl Timer {
/// 每 3 秒更新系统托盘菜单,总共执行 3 次 /// 每 3 秒更新系统托盘菜单,总共执行 3 次
pub fn add_update_tray_menu_task(&self) -> Result<()> { pub fn add_update_tray_menu_task(&self) -> Result<()> {
let tid = self.timer_count.fetch_add(1, Ordering::SeqCst); let tid = self.timer_count.fetch_add(1, Ordering::SeqCst);
let delay_timer = self.delay_timer.write();
let task = TaskBuilder::default() let task = TaskBuilder::default()
.set_task_id(tid) .set_task_id(tid)
.set_maximum_parallel_runnable_num(1) .set_maximum_parallel_runnable_num(1)
.set_frequency_count_down_by_seconds(3, 3) .set_frequency_count_down_by_seconds(3, 3)
.spawn_async_routine(|| async move { .spawn_async_routine(|| async move {
logging!(debug, Type::Timer, "Updating tray menu"); logging!(debug, Type::Timer, "Updating tray menu");
crate::core::tray::Tray::global() crate::core::tray::Tray::global().update_menu().await
.update_tray_display()
.await
}) })
.context("failed to create update tray menu timer task")?; .context("failed to create update tray menu timer task")?;
delay_timer self.delay_timer
.write()
.add_task(task) .add_task(task)
.context("failed to add update tray menu timer task")?; .context("failed to add update tray menu timer task")?;
Ok(()) Ok(())
@@ -195,14 +193,12 @@ impl Timer {
// Perform sync operations while holding locks // 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 { for (uid, diff) in diff_map {
match diff { match diff {
DiffFlag::Del(tid) => { DiffFlag::Del(tid) => {
timer_map.remove(&uid); self.timer_map.write().remove(&uid);
if let Err(e) = delay_timer.remove_task(tid) { let value = self.delay_timer.write().remove_task(tid);
if let Err(e) = value {
logging!( logging!(
warn, warn,
Type::Timer, Type::Timer,
@@ -222,12 +218,13 @@ impl Timer {
last_run: chrono::Local::now().timestamp(), last_run: chrono::Local::now().timestamp(),
}; };
timer_map.insert(uid.clone(), task); self.timer_map.write().insert(uid.clone(), task);
operations_to_add.push((uid, tid, interval)); operations_to_add.push((uid, tid, interval));
} }
DiffFlag::Mod(tid, interval) => { DiffFlag::Mod(tid, interval) => {
// Remove old task first // Remove old task first
if let Err(e) = delay_timer.remove_task(tid) { let value = self.delay_timer.write().remove_task(tid);
if let Err(e) = value {
logging!( logging!(
warn, warn,
Type::Timer, Type::Timer,
@@ -245,7 +242,7 @@ impl Timer {
last_run: chrono::Local::now().timestamp(), last_run: chrono::Local::now().timestamp(),
}; };
timer_map.insert(uid.clone(), task); self.timer_map.write().insert(uid.clone(), task);
operations_to_add.push((uid, tid, interval)); operations_to_add.push((uid, tid, interval));
} }
} }
@@ -255,8 +252,8 @@ impl Timer {
// Now perform async operations without holding locks // Now perform async operations without holding locks
for (uid, tid, interval) in operations_to_add { for (uid, tid, interval) in operations_to_add {
// Re-acquire locks for individual operations // Re-acquire locks for individual operations
let mut delay_timer = self.delay_timer.write(); let delay_timer = self.delay_timer.write();
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) { if let Err(e) = self.add_task(&delay_timer, uid.clone(), tid, interval) {
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e); logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
// Rollback on failure - remove from timer_map // Rollback on failure - remove from timer_map
@@ -273,7 +270,7 @@ impl Timer {
async fn gen_map(&self) -> HashMap<String, u64> { async fn gen_map(&self) -> HashMap<String, u64> {
let mut new_map = HashMap::new(); let mut new_map = HashMap::new();
if let Some(items) = Config::profiles().await.latest_ref().get_items() { if let Some(items) = Config::profiles().await.latest_arc().get_items() {
for item in items.iter() { for item in items.iter() {
if let Some(option) = item.option.as_ref() if let Some(option) = item.option.as_ref()
&& let Some(allow_auto_update) = option.allow_auto_update && let Some(allow_auto_update) = option.allow_auto_update
@@ -373,7 +370,7 @@ impl Timer {
/// Add a timer task with better error handling /// Add a timer task with better error handling
fn add_task( fn add_task(
&self, &self,
delay_timer: &mut DelayTimer, delay_timer: &DelayTimer,
uid: String, uid: String,
tid: TaskID, tid: TaskID,
minutes: u64, minutes: u64,
@@ -427,7 +424,7 @@ impl Timer {
// Get the profile updated timestamp - now safe to await // Get the profile updated timestamp - now safe to await
let items = { let items = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_ref(); let profiles_guard = profiles.latest_arc();
match profiles_guard.get_items() { match profiles_guard.get_items() {
Some(i) => i.clone(), Some(i) => i.clone(),
None => { None => {
@@ -489,7 +486,7 @@ impl Timer {
match tokio::time::timeout(std::time::Duration::from_secs(40), async { match tokio::time::timeout(std::time::Duration::from_secs(40), async {
Self::emit_update_event(uid, true); Self::emit_update_event(uid, true);
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(uid); let is_current = Config::profiles().await.latest_arc().current.as_ref() == Some(uid);
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
+63 -160
View File
@@ -1,5 +1,4 @@
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use tauri::Emitter;
use tauri::tray::TrayIconBuilder; use tauri::tray::TrayIconBuilder;
use tauri_plugin_mihomo::models::Proxies; use tauri_plugin_mihomo::models::Proxies;
use tokio::fs; use tokio::fs;
@@ -55,18 +54,17 @@ fn get_tray_click_debounce() -> &'static Mutex<Instant> {
fn should_handle_tray_click() -> bool { fn should_handle_tray_click() -> bool {
let debounce_lock = get_tray_click_debounce(); let debounce_lock = get_tray_click_debounce();
let mut last_click = debounce_lock.lock();
let now = Instant::now(); let now = Instant::now();
if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) { if now.duration_since(*debounce_lock.lock()) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) {
*last_click = now; *debounce_lock.lock() = now;
true true
} else { } else {
logging!( logging!(
debug, debug,
Type::Tray, Type::Tray,
"托盘点击被防抖机制忽略,距离上次点击 {}ms", "托盘点击被防抖机制忽略,距离上次点击 {}ms",
now.duration_since(*last_click).as_millis() now.duration_since(*debounce_lock.lock()).as_millis()
); );
false false
} }
@@ -86,7 +84,7 @@ pub struct Tray {
impl TrayState { impl TrayState {
pub async fn get_common_tray_icon() -> (bool, Vec<u8>) { pub async fn get_common_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_arc();
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false); let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon if is_common_tray_icon
&& let Ok(Some(common_icon_path)) = find_target_icons("common") && let Ok(Some(common_icon_path)) = find_target_icons("common")
@@ -123,7 +121,7 @@ impl TrayState {
} }
pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) { pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_arc();
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false); let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon if is_sysproxy_tray_icon
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") && let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
@@ -160,7 +158,7 @@ impl TrayState {
} }
pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) { pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_arc();
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false); let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon if is_tun_tray_icon
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun") && let Ok(Some(tun_icon_path)) = find_target_icons("tun")
@@ -198,7 +196,7 @@ impl TrayState {
impl Default for Tray { impl Default for Tray {
fn default() -> Self { fn default() -> Self {
Tray { Self {
last_menu_update: Mutex::new(None), last_menu_update: Mutex::new(None),
menu_updating: AtomicBool::new(false), menu_updating: AtomicBool::new(false),
} }
@@ -243,7 +241,7 @@ impl Tray {
} }
let app_handle = handle::Handle::app_handle(); let app_handle = handle::Handle::app_handle();
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() }; let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event = tray_event.unwrap_or_else(|| "main_window".into()); let tray_event = tray_event.unwrap_or_else(|| "main_window".into());
let tray = app_handle let tray = app_handle
.tray_by_id("main") .tray_by_id("main")
@@ -303,7 +301,7 @@ impl Tray {
} }
async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> { async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_arc();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); 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 = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let tun_mode_available = cmd::system::is_admin().unwrap_or_default() let tun_mode_available = cmd::system::is_admin().unwrap_or_default()
@@ -311,18 +309,16 @@ impl Tray {
let mode = { let mode = {
Config::clash() Config::clash()
.await .await
.latest_ref() .latest_arc()
.0 .0
.get("mode") .get("mode")
.map(|val| val.as_str().unwrap_or("rule")) .map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule") .unwrap_or("rule")
.to_owned() .to_owned()
}; };
let profile_uid_and_name = Config::profiles() let profiles_config = Config::profiles().await;
.await let profiles_arc = profiles_config.latest_arc();
.data_mut() let profile_uid_and_name = profiles_arc.all_profile_uid_and_name().unwrap_or_default();
.all_profile_uid_and_name()
.unwrap_or_default();
let is_lightweight_mode = is_in_lightweight_mode(); let is_lightweight_mode = is_in_lightweight_mode();
match app_handle.tray_by_id("main") { match app_handle.tray_by_id("main") {
@@ -375,7 +371,7 @@ impl Tray {
} }
}; };
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_arc();
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -418,7 +414,7 @@ impl Tray {
} }
}; };
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_arc();
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -433,24 +429,6 @@ impl Tray {
Ok(()) Ok(())
} }
/// 更新托盘显示状态的函数
pub async fn update_tray_display(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘显示状态更新");
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<()> { pub async fn update_tooltip(&self) -> Result<()> {
if handle::Handle::global().is_exiting() { if handle::Handle::global().is_exiting() {
@@ -460,7 +438,7 @@ impl Tray {
let app_handle = handle::Handle::app_handle(); let app_handle = handle::Handle::app_handle();
let verge = Config::verge().await.latest_ref().clone(); let verge = Config::verge().await.latest_arc();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); 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 = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -474,9 +452,9 @@ impl Tray {
let mut current_profile_name = "None".into(); let mut current_profile_name = "None".into();
{ {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_arc();
if let Some(current_profile_uid) = profiles.get_current() 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 { current_profile_name = match &profile.name {
Some(profile_name) => profile_name.to_string(), Some(profile_name) => profile_name.to_string(),
@@ -525,9 +503,7 @@ impl Tray {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新"); logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新");
return Ok(()); return Ok(());
} }
// self.update_menu().await?; self.update_menu().await?;
// 更新轻量模式显示状态
self.update_tray_display().await?;
self.update_icon().await?; self.update_icon().await?;
self.update_tooltip().await?; self.update_tooltip().await?;
Ok(()) Ok(())
@@ -552,7 +528,7 @@ impl Tray {
#[cfg(any(target_os = "macos", target_os = "windows"))] #[cfg(any(target_os = "macos", target_os = "windows"))]
let show_menu_on_left_click = { let show_menu_on_left_click = {
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() }; let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into()); let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
tray_event.as_str() == "tray_menu" tray_event.as_str() == "tray_menu"
}; };
@@ -572,72 +548,38 @@ impl Tray {
let tray = builder.build(app_handle)?; let tray = builder.build(app_handle)?;
tray.on_tray_icon_event(|_app_handle, event| { tray.on_tray_icon_event(|_app_handle, event| {
// 忽略移动、进入和离开等无需处理的事件,避免不必要的刷新 if let TrayIconEvent::Click {
match event { button: MouseButton::Left,
TrayIconEvent::Move { .. } button_state: MouseButtonState::Down,
| TrayIconEvent::Enter { .. } ..
| TrayIconEvent::Leave { .. } => { } = event
return; {
} AsyncHandler::spawn(|| async move {
_ => {} let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
} let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
AsyncHandler::spawn(|| async move {
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:?}");
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
..
} = event
{
// 添加防抖检查,防止快速连击 // 添加防抖检查,防止快速连击
if !should_handle_tray_click() { if !should_handle_tray_click() {
return; return;
} }
use std::future::Future; match tray_event.as_str() {
use std::pin::Pin; "system_proxy" => feat::toggle_system_proxy().await,
"tun_mode" => feat::toggle_tun_mode(None).await,
let fut: Pin<Box<dyn Future<Output = ()> + Send>> = match tray_event.as_str() { "main_window" => {
"system_proxy" => Box::pin(async move {
feat::toggle_system_proxy().await;
}),
"tun_mode" => Box::pin(async move {
feat::toggle_tun_mode(None).await;
}),
"main_window" => Box::pin(async move {
if !lightweight::exit_lightweight_mode().await { if !lightweight::exit_lightweight_mode().await {
WindowManager::show_main_window().await; WindowManager::show_main_window().await;
}; };
}), }
_ => Box::pin(async move {}), _ => {}
}; };
fut.await; });
} }
});
}); });
tray.on_menu_event(on_menu_event); tray.on_menu_event(on_menu_event);
Ok(()) Ok(())
} }
// 托盘统一的状态更新函数
pub async fn update_all_states(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
logging!(debug, Type::Tray, "应用正在退出,跳过托盘状态更新");
return Ok(());
}
// 确保所有状态更新完成
self.update_tray_display().await?;
// self.update_menu().await?;
self.update_icon().await?;
self.update_tooltip().await?;
Ok(())
}
} }
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> { fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
@@ -666,7 +608,7 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<String, String> {
async fn create_profile_menu_item( async fn create_profile_menu_item(
app_handle: &AppHandle, app_handle: &AppHandle,
profile_uid_and_name: Vec<(String, String)>, profile_uid_and_name: Vec<(&String, &String)>,
) -> Result<Vec<CheckMenuItem<Wry>>> { ) -> Result<Vec<CheckMenuItem<Wry>>> {
let futures = profile_uid_and_name let futures = profile_uid_and_name
.iter() .iter()
@@ -675,7 +617,7 @@ async fn create_profile_menu_item(
async move { async move {
let is_current_profile = Config::profiles() let is_current_profile = Config::profiles()
.await .await
.latest_ref() .latest_arc()
.is_current_profile_index(profile_uid); .is_current_profile_index(profile_uid);
CheckMenuItem::with_id( CheckMenuItem::with_id(
&app_handle, &app_handle,
@@ -870,7 +812,7 @@ async fn create_tray_menu(
system_proxy_enabled: bool, system_proxy_enabled: bool,
tun_mode_enabled: bool, tun_mode_enabled: bool,
tun_mode_available: bool, tun_mode_available: bool,
profile_uid_and_name: Vec<(String, String)>, profile_uid_and_name: Vec<(&String, &String)>,
is_lightweight_mode: bool, is_lightweight_mode: bool,
) -> Result<tauri::menu::Menu<Wry>> { ) -> Result<tauri::menu::Menu<Wry>> {
let current_proxy_mode = mode.unwrap_or(""); let current_proxy_mode = mode.unwrap_or("");
@@ -878,10 +820,10 @@ async fn create_tray_menu(
// 获取当前配置文件的选中代理组信息 // 获取当前配置文件的选中代理组信息
let current_profile_selected = { let current_profile_selected = {
let profiles_config = Config::profiles().await; let profiles_config = Config::profiles().await;
let profiles_ref = profiles_config.latest_ref(); let profiles_ref = profiles_config.latest_arc();
profiles_ref profiles_ref
.get_current() .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()) .and_then(|profile| profile.selected.clone())
.unwrap_or_default() .unwrap_or_default()
}; };
@@ -924,7 +866,7 @@ async fn create_tray_menu(
.collect::<HashMap<String, usize>>() .collect::<HashMap<String, usize>>()
}); });
let verge_settings = Config::verge().await.latest_ref().clone(); let verge_settings = Config::verge().await.latest_arc();
let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false); let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false);
let version = env!("CARGO_PKG_VERSION"); let version = env!("CARGO_PKG_VERSION");
@@ -1255,66 +1197,27 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
} }
id if id.starts_with("proxy_") => { id if id.starts_with("proxy_") => {
// proxy_{group_name}_{proxy_name} // proxy_{group_name}_{proxy_name}
let parts: Vec<&str> = id.splitn(3, '_').collect(); let rest = match id.strip_prefix("proxy_") {
Some(r) => r,
if parts.len() == 3 && parts[0] == "proxy" { None => return,
let group_name = parts[1]; };
let proxy_name = parts[2]; let (group_name, proxy_name) = match rest.split_once('_') {
Some((g, p)) => (g, p),
match handle::Handle::mihomo() None => return,
.await };
.select_node_for_group(group_name, proxy_name) feat::switch_proxy_node(group_name, proxy_name).await;
.await }
{ _ => {
Ok(_) => { logging!(
logging!( debug,
info, Type::Tray,
Type::Tray, "Unhandled tray menu event: {:?}",
"切换代理成功: {} -> {}", event.id
group_name, );
proxy_name
);
let _ = handle::Handle::app_handle()
.emit("verge://refresh-proxy-config", ());
}
Err(e) => {
logging!(
error,
Type::Tray,
"切换代理失败: {} -> {}, 错误: {:?}",
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()
{
logging!(
info,
Type::Tray,
"代理切换回退成功: {} -> {}",
group_name,
proxy_name
);
let app_handle = handle::Handle::app_handle();
let _ = app_handle.emit("verge://force-refresh-proxies", ());
}
}
}
}
} }
_ => {}
} }
// Ensure tray state update is awaited and properly handled // We dont expected to refresh tray state here
if let Err(e) = Tray::global().update_all_states().await { // as the inner handle function (SHOULD) already takes care of it
logging!(warn, Type::Tray, "Failed to update tray state: {e}");
}
}); });
} }
+38 -19
View File
@@ -16,7 +16,7 @@ pub struct CoreConfigValidator {
} }
impl CoreConfigValidator { impl CoreConfigValidator {
pub fn new() -> Self { pub const fn new() -> Self {
Self { Self {
is_processing: AtomicBool::new(false), is_processing: AtomicBool::new(false),
} }
@@ -266,7 +266,7 @@ impl CoreConfigValidator {
logging!(info, Type::Validate, "开始验证配置文件: {}", config_path); logging!(info, Type::Validate, "开始验证配置文件: {}", config_path);
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core(); let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
logging!(info, Type::Validate, "使用内核: {}", clash_core); logging!(info, Type::Validate, "使用内核: {}", clash_core);
let app_handle = handle::Handle::app_handle(); let app_handle = handle::Handle::app_handle();
@@ -275,41 +275,43 @@ impl CoreConfigValidator {
logging!(info, Type::Validate, "验证目录: {}", app_dir_str); logging!(info, Type::Validate, "验证目录: {}", app_dir_str);
// 使用子进程运行clash验证配置 // 使用子进程运行clash验证配置
let output = app_handle let command = app_handle.shell().sidecar(clash_core.as_str())?.args([
.shell() "-t",
.sidecar(clash_core.as_str())? "-d",
.args(["-t", "-d", app_dir_str, "-f", config_path]) app_dir_str,
.output() "-f",
.await?; config_path,
]);
let output = command.output().await?;
let stderr = std::string::String::from_utf8_lossy(&output.stderr); let status = &output.status;
let stdout = std::string::String::from_utf8_lossy(&output.stdout); let stderr = &output.stderr;
let stdout = &output.stdout;
// 检查进程退出状态和错误输出 // 检查进程退出状态和错误输出
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"]; let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
let has_error = let has_error = !status.success() || contains_any_keyword(stderr, &error_keywords);
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
logging!(info, Type::Validate, "-------- 验证结果 --------"); logging!(info, Type::Validate, "-------- 验证结果 --------");
if !stderr.is_empty() { if !stderr.is_empty() {
logging!(info, Type::Validate, "stderr输出:\n{}", stderr); logging!(info, Type::Validate, "stderr输出:\n{:?}", stderr);
} }
if has_error { if has_error {
logging!(info, Type::Validate, "发现错误,开始处理错误信息"); logging!(info, Type::Validate, "发现错误,开始处理错误信息");
let error_msg = if !stdout.is_empty() { let error_msg: String = if !stdout.is_empty() {
stdout.into() str::from_utf8(stdout).unwrap_or_default().into()
} else if !stderr.is_empty() { } else if !stderr.is_empty() {
stderr.into() str::from_utf8(stderr).unwrap_or_default().into()
} else if let Some(code) = output.status.code() { } else if let Some(code) = status.code() {
format!("验证进程异常退出,退出码: {code}") format!("验证进程异常退出,退出码: {code}").into()
} else { } else {
"验证进程被终止".into() "验证进程被终止".into()
}; };
logging!(info, Type::Validate, "-------- 验证结束 --------"); logging!(info, Type::Validate, "-------- 验证结束 --------");
Ok((false, error_msg.into())) // 返回错误消息给调用者处理 Ok((false, error_msg)) // 返回错误消息给调用者处理
} else { } else {
logging!(info, Type::Validate, "验证成功"); logging!(info, Type::Validate, "验证成功");
logging!(info, Type::Validate, "-------- 验证结束 --------"); logging!(info, Type::Validate, "-------- 验证结束 --------");
@@ -342,6 +344,23 @@ fn has_ext<P: AsRef<std::path::Path>>(path: P, ext: &str) -> bool {
.unwrap_or(false) .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!( singleton_lazy!(
CoreConfigValidator, CoreConfigValidator,
CORECONFIGVALIDATOR, CORECONFIGVALIDATOR,
+7 -9
View File
@@ -70,7 +70,7 @@ pub trait AsyncChainItemFrom {
} }
impl AsyncChainItemFrom for Option<ChainItem> { impl AsyncChainItemFrom for Option<ChainItem> {
async fn from_async(item: &PrfItem) -> Option<ChainItem> { async fn from_async(item: &PrfItem) -> Self {
let itype = item.itype.as_ref()?.as_str(); let itype = item.itype.as_ref()?.as_str();
let file = item.file.clone()?; let file = item.file.clone()?;
let uid = item.uid.clone().unwrap_or_else(|| "".into()); let uid = item.uid.clone().unwrap_or_else(|| "".into());
@@ -116,22 +116,21 @@ impl AsyncChainItemFrom for Option<ChainItem> {
} }
impl ChainItem { impl ChainItem {
/// 内建支持一些脚本 /// 内建支持一些脚本
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> { pub fn builtin() -> Vec<(ChainSupport, Self)> {
// meta 的一些处理 // meta 的一些处理
let meta_guard = let meta_guard =
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组 // meta 1.13.2 alpn string 转 数组
let hy_alpn = let hy_alpn = 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"));
// meta 的一些处理 // meta 的一些处理
let meta_guard_alpha = let meta_guard_alpha =
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组 // meta 1.13.2 alpn string 转 数组
let hy_alpn_alpha = let hy_alpn_alpha =
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
vec![ vec![
(ChainSupport::ClashMeta, hy_alpn), (ChainSupport::ClashMeta, hy_alpn),
@@ -154,8 +153,7 @@ impl ChainSupport {
match core { match core {
Some(core) => matches!( Some(core) => matches!(
(self, core.as_str()), (self, core.as_str()),
(ChainSupport::ClashMeta, "verge-mihomo") (Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha")
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
), ),
None => true, None => true,
} }
+1 -1
View File
@@ -16,7 +16,7 @@ fn deep_merge(a: &mut Value, b: &Value) {
pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping { pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
let mut config = Value::from(config); let mut config = Value::from(config);
let merge = use_lowercase(merge.clone()); let merge = use_lowercase(merge);
deep_merge(&mut config, &Value::from(merge)); deep_merge(&mut config, &Value::from(merge));
+83 -37
View File
@@ -44,12 +44,49 @@ struct ProfileItems {
profile_name: String, 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 { async fn get_config_values() -> ConfigValues {
let clash_config = { Config::clash().await.latest_ref().0.clone() }; let clash_config = { Config::clash().await.latest_arc().0.clone() };
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = { let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
let verge = Config::verge().await; let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_arc();
( (
Some(verge.get_valid_clash_core()), Some(verge.get_valid_clash_core()),
verge.enable_tun_mode.unwrap_or(false), verge.enable_tun_mode.unwrap_or(false),
@@ -63,14 +100,14 @@ async fn get_config_values() -> ConfigValues {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let redir_enabled = { let redir_enabled = {
let verge = Config::verge().await; let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_arc();
verge.verge_redir_enabled.unwrap_or(false) verge.verge_redir_enabled.unwrap_or(false)
}; };
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let tproxy_enabled = { let tproxy_enabled = {
let verge = Config::verge().await; let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_arc();
verge.verge_tproxy_enabled.unwrap_or(false) verge.verge_tproxy_enabled.unwrap_or(false)
}; };
@@ -89,33 +126,43 @@ async fn get_config_values() -> ConfigValues {
} }
} }
#[allow(clippy::cognitive_complexity)]
async fn collect_profile_items() -> ProfileItems { async fn collect_profile_items() -> ProfileItems {
// 从profiles里拿东西 - 先收集需要的数据,然后释放锁 // 从profiles里拿东西 - 先收集需要的数据,然后释放锁
let ( let (current, merge_uid, script_uid, rules_uid, proxies_uid, groups_uid, name) = {
current,
merge_uid,
script_uid,
rules_uid,
proxies_uid,
groups_uid,
_current_profile_uid,
name,
) = {
let current = { let current = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles_clone = profiles.latest_ref().clone(); let profiles_clone = profiles.latest_arc();
profiles_clone.current_mapping().await.unwrap_or_default() profiles_clone.current_mapping().await.unwrap_or_default()
}; };
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref(); let profiles_ref = profiles.latest_arc();
let current_profile_uid = match profiles_ref.get_current() {
Some(uid) => uid.clone(),
None => return ProfileItems::default(),
};
let merge_uid = profiles_ref.current_merge().unwrap_or_default(); let current_item = match profiles_ref.get_item_arc(&current_profile_uid) {
let script_uid = profiles_ref.current_script().unwrap_or_default(); Some(item) => item,
let rules_uid = profiles_ref.current_rules().unwrap_or_default(); None => return ProfileItems::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 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 name = profiles_ref let name = profiles_ref
.get_item(&current_profile_uid) .get_item(&current_profile_uid)
@@ -130,7 +177,6 @@ async fn collect_profile_items() -> ProfileItems {
rules_uid, rules_uid,
proxies_uid, proxies_uid,
groups_uid, groups_uid,
current_profile_uid,
name, name,
) )
}; };
@@ -139,8 +185,8 @@ async fn collect_profile_items() -> ProfileItems {
let merge_item = { let merge_item = {
let item = { let item = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_arc();
profiles.get_item(merge_uid).ok().cloned() profiles.get_item(&merge_uid).ok().cloned()
}; };
if let Some(item) = item { if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await <Option<ChainItem>>::from_async(&item).await
@@ -156,8 +202,8 @@ async fn collect_profile_items() -> ProfileItems {
let script_item = { let script_item = {
let item = { let item = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_arc();
profiles.get_item(script_uid).ok().cloned() profiles.get_item(&script_uid).ok().cloned()
}; };
if let Some(item) = item { if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await <Option<ChainItem>>::from_async(&item).await
@@ -173,8 +219,8 @@ async fn collect_profile_items() -> ProfileItems {
let rules_item = { let rules_item = {
let item = { let item = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_arc();
profiles.get_item(rules_uid).ok().cloned() profiles.get_item(&rules_uid).ok().cloned()
}; };
if let Some(item) = item { if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await <Option<ChainItem>>::from_async(&item).await
@@ -190,8 +236,8 @@ async fn collect_profile_items() -> ProfileItems {
let proxies_item = { let proxies_item = {
let item = { let item = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_arc();
profiles.get_item(proxies_uid).ok().cloned() profiles.get_item(&proxies_uid).ok().cloned()
}; };
if let Some(item) = item { if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await <Option<ChainItem>>::from_async(&item).await
@@ -207,8 +253,8 @@ async fn collect_profile_items() -> ProfileItems {
let groups_item = { let groups_item = {
let item = { let item = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_arc();
profiles.get_item(groups_uid).ok().cloned() profiles.get_item(&groups_uid).ok().cloned()
}; };
if let Some(item) = item { if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await <Option<ChainItem>>::from_async(&item).await
@@ -224,7 +270,7 @@ async fn collect_profile_items() -> ProfileItems {
let global_merge = { let global_merge = {
let item = { let item = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_arc();
profiles.get_item("Merge").ok().cloned() profiles.get_item("Merge").ok().cloned()
}; };
if let Some(item) = item { if let Some(item) = item {
@@ -241,7 +287,7 @@ async fn collect_profile_items() -> ProfileItems {
let global_script = { let global_script = {
let item = { let item = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_arc();
profiles.get_item("Script").ok().cloned() profiles.get_item("Script").ok().cloned()
}; };
if let Some(item) = item { if let Some(item) = item {
@@ -284,7 +330,7 @@ fn process_global_items(
if let ChainType::Script(script) = global_script.data { if let ChainType::Script(script) = global_script.data {
let mut logs = vec![]; let mut logs = vec![];
match use_script(script, config.to_owned(), profile_name.to_owned()) { match use_script(script, config.to_owned(), profile_name) {
Ok((res_config, res_logs)) => { Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config)); exists_keys.extend(use_keys(&res_config));
config = res_config; config = res_config;
@@ -394,7 +440,7 @@ async fn merge_default_config(
if key.as_str() == Some("external-controller") { if key.as_str() == Some("external-controller") {
let enable_external_controller = Config::verge() let enable_external_controller = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_external_controller .enable_external_controller
.unwrap_or(false); .unwrap_or(false);
+1 -1
View File
@@ -62,7 +62,7 @@ pub fn use_script(
});"#, });"#,
)); ));
let config = use_lowercase(config.clone()); let config = use_lowercase(config);
let config_str = serde_json::to_string(&config)?; let config_str = serde_json::to_string(&config)?;
// 仅处理 name 参数中的特殊字符 // 仅处理 name 参数中的特殊字符
+2 -2
View File
@@ -78,7 +78,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
/// Restore WebDAV backup /// Restore WebDAV backup
pub async fn restore_webdav_backup(filename: String) -> Result<()> { pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge().await; let verge = Config::verge().await;
let verge_data = verge.latest_ref().clone(); let verge_data = verge.latest_arc();
let webdav_url = verge_data.webdav_url.clone(); let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone(); let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone(); let webdav_password = verge_data.webdav_password.clone();
@@ -243,7 +243,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
let (webdav_url, webdav_username, webdav_password) = { let (webdav_url, webdav_username, webdav_password) = {
let verge = Config::verge().await; let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_arc();
( (
verge.webdav_url.clone(), verge.webdav_url.clone(),
verge.webdav_username.clone(), verge.webdav_username.clone(),
+7 -4
View File
@@ -47,6 +47,7 @@ fn after_change_clash_mode() {
for connection in connections_array { for connection in connections_array {
let _ = mihomo.close_connection(&connection.id).await; let _ = mihomo.close_connection(&connection.id).await;
} }
drop(mihomo);
} }
} }
Err(err) => { Err(err) => {
@@ -72,10 +73,12 @@ pub async fn change_clash_mode(mode: String) {
{ {
Ok(_) => { Ok(_) => {
// 更新订阅 // 更新订阅
Config::clash().await.data_mut().patch_config(mapping); Config::clash()
.await
.edit_draft(|d| d.patch_config(mapping));
// 分离数据获取和异步调用 // 分离数据获取和异步调用
let clash_data = Config::clash().await.data_mut().clone(); let clash_data = Config::clash().await.data_arc();
if clash_data.save_config().await.is_ok() { if clash_data.save_config().await.is_ok() {
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
logging_error!(Type::Tray, tray::Tray::global().update_menu().await); logging_error!(Type::Tray, tray::Tray::global().update_menu().await);
@@ -84,7 +87,7 @@ pub async fn change_clash_mode(mode: String) {
let is_auto_close_connection = Config::verge() let is_auto_close_connection = Config::verge()
.await .await
.data_mut() .data_arc()
.auto_close_connection .auto_close_connection
.unwrap_or(false); .unwrap_or(false);
if is_auto_close_connection { if is_auto_close_connection {
@@ -102,7 +105,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
let tun_mode = Config::verge() let tun_mode = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_tun_mode .enable_tun_mode
.unwrap_or(false); .unwrap_or(false);
+17 -8
View File
@@ -3,7 +3,7 @@ use crate::{
core::{CoreManager, handle, hotkey, sysopt, tray}, core::{CoreManager, handle, hotkey, sysopt, tray},
logging_error, logging_error,
module::lightweight, module::lightweight,
utils::logging::Type, utils::{draft::SharedBox, logging::Type},
}; };
use anyhow::Result; use anyhow::Result;
use serde_yaml_ng::Mapping; use serde_yaml_ng::Mapping;
@@ -12,8 +12,7 @@ use serde_yaml_ng::Mapping;
pub async fn patch_clash(patch: Mapping) -> Result<()> { pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash() Config::clash()
.await .await
.draft_mut() .edit_draft(|d| d.patch_config(patch.clone()));
.patch_config(patch.clone());
let res = { let res = {
// 激活订阅 // 激活订阅
@@ -25,7 +24,9 @@ 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_menu().await);
logging_error!(Type::Tray, tray::Tray::global().update_icon().await); logging_error!(Type::Tray, tray::Tray::global().update_icon().await);
} }
Config::runtime().await.draft_mut().patch_config(patch); Config::runtime()
.await
.edit_draft(|d| d.patch_config(patch));
CoreManager::global().update_config().await?; CoreManager::global().update_config().await?;
} }
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
@@ -35,7 +36,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
Ok(()) => { Ok(()) => {
Config::clash().await.apply(); Config::clash().await.apply();
// 分离数据获取和异步调用 // 分离数据获取和异步调用
let clash_data = Config::clash().await.data_mut().clone(); let clash_data = Config::clash().await.data_arc();
clash_data.save_config().await?; clash_data.save_config().await?;
Ok(()) Ok(())
} }
@@ -190,7 +191,9 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<(
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 { if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
Config::verge().await.draft_mut().enable_global_hotkey = patch.enable_global_hotkey; Config::verge()
.await
.edit_draft(|d| d.enable_global_hotkey = patch.enable_global_hotkey);
handle::Handle::refresh_verge(); handle::Handle::refresh_verge();
} }
if (update_flags & (UpdateFlags::Launch as i32)) != 0 { if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
@@ -227,7 +230,7 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<(
} }
pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
Config::verge().await.draft_mut().patch_config(patch); Config::verge().await.edit_draft(|d| d.patch_config(patch));
let update_flags = determine_update_flags(patch); let update_flags = determine_update_flags(patch);
let process_flag_result: std::result::Result<(), anyhow::Error> = { let process_flag_result: std::result::Result<(), anyhow::Error> = {
@@ -242,8 +245,14 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
Config::verge().await.apply(); Config::verge().await.apply();
if !not_save_file { if !not_save_file {
// 分离数据获取和异步调用 // 分离数据获取和异步调用
let verge_data = Config::verge().await.data_ref().clone(); let verge_data = Config::verge().await.data_arc();
verge_data.save_file().await?; verge_data.save_file().await?;
} }
Ok(()) Ok(())
} }
pub async fn fetch_verge_config() -> Result<SharedBox<IVerge>> {
let draft = Config::verge().await;
let data = draft.data_arc();
Ok(data)
}
+68 -16
View File
@@ -2,23 +2,75 @@ use crate::{
cmd, cmd,
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe}, config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
core::{CoreManager, handle, tray}, core::{CoreManager, handle, tray},
logging, logging, logging_error,
utils::logging::Type, utils::logging::Type,
}; };
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use smartstring::alias::String; use smartstring::alias::String;
use tauri::Emitter;
/// Toggle proxy profile /// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) { pub async fn toggle_proxy_profile(profile_index: String) {
match cmd::patch_profiles_config_by_profile_index(profile_index).await { 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
{
Ok(_) => { Ok(_) => {
let result = tray::Tray::global().update_menu().await; logging!(
if let Err(err) = result { info,
logging!(error, Type::Tray, "更新菜单失败: {}", err); Type::Tray,
} "切换代理成功: {} -> {}",
group_name,
proxy_name
);
let _ = handle::Handle::app_handle().emit("verge://refresh-proxy-config", ());
let _ = tray::Tray::global().update_menu().await;
return;
} }
Err(err) => { Err(err) => {
logging!(error, Type::Tray, "{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
);
} }
} }
} }
@@ -28,7 +80,7 @@ async fn should_update_profile(
ignore_auto_update: bool, ignore_auto_update: bool,
) -> Result<Option<(String, Option<PrfOption>)>> { ) -> Result<Option<(String, Option<PrfOption>)>> {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_arc();
let item = profiles.get_item(uid)?; let item = profiles.get_item(uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote"); let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
@@ -89,15 +141,15 @@ async fn perform_profile_update(
let mut merged_opt = PrfOption::merge(opt, option); let mut merged_opt = PrfOption::merge(opt, option);
let is_current = { let is_current = {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
profiles.latest_ref().is_current_profile_index(uid) profiles.latest_arc().is_current_profile_index(uid)
};
let profile_name = {
let profiles = Config::profiles().await;
profiles
.latest_ref()
.get_name_by_uid(uid)
.unwrap_or_default()
}; };
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"));
let mut last_err; let mut last_err;
match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await { match PrfItem::from_url(url, None, None, merged_opt.as_ref()).await {
+6 -6
View File
@@ -10,8 +10,8 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
/// Toggle system proxy on/off /// Toggle system proxy on/off
pub async fn toggle_system_proxy() { pub async fn toggle_system_proxy() {
let verge = Config::verge().await; let verge = Config::verge().await;
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false); let enable = verge.latest_arc().enable_system_proxy.unwrap_or(false);
let auto_close_connection = verge.latest_ref().auto_close_connection.unwrap_or(false); let auto_close_connection = verge.latest_arc().auto_close_connection.unwrap_or(false);
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接 // 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
if enable if enable
@@ -42,7 +42,7 @@ pub async fn toggle_system_proxy() {
/// Toggle TUN mode on/off /// Toggle TUN mode on/off
pub async fn toggle_tun_mode(not_save_file: Option<bool>) { pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().await.data_mut().enable_tun_mode; let enable = Config::verge().await.latest_arc().enable_tun_mode;
let enable = enable.unwrap_or(false); let enable = enable.unwrap_or(false);
match super::patch_verge( match super::patch_verge(
@@ -66,7 +66,7 @@ pub async fn copy_clash_env() {
Ok(ip) => ip.into(), Ok(ip) => ip.into(),
Err(_) => Config::verge() Err(_) => Config::verge()
.await .await
.latest_ref() .latest_arc()
.proxy_host .proxy_host
.clone() .clone()
.unwrap_or_else(|| "127.0.0.1".into()), .unwrap_or_else(|| "127.0.0.1".into()),
@@ -76,7 +76,7 @@ pub async fn copy_clash_env() {
let port = { let port = {
Config::verge() Config::verge()
.await .await
.latest_ref() .latest_arc()
.verge_mixed_port .verge_mixed_port
.unwrap_or(7897) .unwrap_or(7897)
}; };
@@ -84,7 +84,7 @@ pub async fn copy_clash_env() {
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}"); let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
let cliboard = app_handle.clipboard(); let cliboard = app_handle.clipboard();
let env_type = { Config::verge().await.latest_ref().env_type.clone() }; let env_type = { Config::verge().await.latest_arc().env_type.clone() };
let env_type = match env_type { let env_type = match env_type {
Some(env_type) => env_type, Some(env_type) => env_type,
None => { None => {
+4 -4
View File
@@ -47,7 +47,7 @@ pub async fn clean_async() -> bool {
let tun_task = async { let tun_task = async {
let tun_enabled = Config::verge() let tun_enabled = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_tun_mode .enable_tun_mode
.unwrap_or(false); .unwrap_or(false);
@@ -100,7 +100,7 @@ pub async fn clean_async() -> bool {
// 检查系统代理是否开启 // 检查系统代理是否开启
let sys_proxy_enabled = Config::verge() let sys_proxy_enabled = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_system_proxy .enable_system_proxy
.unwrap_or(false); .unwrap_or(false);
@@ -176,7 +176,7 @@ pub async fn clean_async() -> bool {
{ {
let sys_proxy_enabled = Config::verge() let sys_proxy_enabled = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_system_proxy .enable_system_proxy
.unwrap_or(false); .unwrap_or(false);
@@ -316,7 +316,7 @@ pub async fn hide() {
let enable_auto_light_weight_mode = Config::verge() let enable_auto_light_weight_mode = Config::verge()
.await .await
.data_mut() .latest_arc()
.enable_auto_light_weight_mode .enable_auto_light_weight_mode
.unwrap_or(false); .unwrap_or(false);
+16 -17
View File
@@ -319,7 +319,7 @@ pub fn run() {
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
let is_enable_global_hotkey = Config::verge() let is_enable_global_hotkey = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_global_hotkey .enable_global_hotkey
.unwrap_or(true); .unwrap_or(true);
@@ -351,23 +351,21 @@ pub fn run() {
}); });
} }
#[cfg(target_os = "macos")]
pub fn handle_window_destroyed() { pub fn handle_window_destroyed() {
#[cfg(target_os = "macos")] use crate::core::hotkey::SystemHotkey;
{ AsyncHandler::spawn(move || async move {
use crate::core::hotkey::SystemHotkey; let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
AsyncHandler::spawn(move || async move { let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ); let is_enable_global_hotkey = Config::verge()
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW); .await
let is_enable_global_hotkey = Config::verge() .latest_arc()
.await .enable_global_hotkey
.latest_ref() .unwrap_or(true);
.enable_global_hotkey if !is_enable_global_hotkey {
.unwrap_or(true); let _ = hotkey::Hotkey::global().reset();
if !is_enable_global_hotkey { }
let _ = hotkey::Hotkey::global().reset(); });
}
});
}
} }
} }
@@ -447,6 +445,7 @@ pub fn run() {
tauri::WindowEvent::Focused(focused) => { tauri::WindowEvent::Focused(focused) => {
event_handlers::handle_window_focus(focused); event_handlers::handle_window_focus(focused);
} }
#[cfg(target_os = "macos")]
tauri::WindowEvent::Destroyed => { tauri::WindowEvent::Destroyed => {
event_handlers::handle_window_destroyed(); event_handlers::handle_window_destroyed();
} }
+2 -2
View File
@@ -4,8 +4,8 @@ fn main() {
console_subscriber::init(); console_subscriber::init();
// Check for --no-tray command line argument // Check for --no-tray command line argument
let args: Vec<String> = std::env::args().collect(); #[cfg(target_os = "linux")]
if args.contains(&"--no-tray".into()) { if std::env::args().any(|x| x == "--no-tray") {
unsafe { unsafe {
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1"); std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
} }
+28 -49
View File
@@ -28,15 +28,15 @@ enum LightweightState {
impl From<u8> for LightweightState { impl From<u8> for LightweightState {
fn from(v: u8) -> Self { fn from(v: u8) -> Self {
match v { match v {
1 => LightweightState::In, 1 => Self::In,
2 => LightweightState::Exiting, 2 => Self::Exiting,
_ => LightweightState::Normal, _ => Self::Normal,
} }
} }
} }
impl LightweightState { impl LightweightState {
fn as_u8(self) -> u8 { const fn as_u8(self) -> u8 {
self as u8 self as u8
} }
} }
@@ -79,40 +79,24 @@ pub fn is_in_lightweight_mode() -> bool {
} }
async fn refresh_lightweight_tray_state() { async fn refresh_lightweight_tray_state() {
if let Err(err) = Tray::global().update_tray_display().await { if let Err(err) = Tray::global().update_menu().await {
logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}"); logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}");
} }
} }
pub async fn auto_lightweight_boot() -> Result<()> { pub async fn auto_lightweight_boot() -> Result<()> {
let verge_config = Config::verge().await; let verge_config = Config::verge().await;
let enable_auto = verge_config let is_enable_auto = verge_config
.data_mut() .data_arc()
.enable_auto_light_weight_mode .enable_auto_light_weight_mode
.unwrap_or(false); .unwrap_or(false);
let is_silent_start = verge_config let is_silent_start = verge_config.data_arc().enable_silent_start.unwrap_or(false);
.latest_ref() if is_enable_auto {
.enable_silent_start enable_auto_light_weight_mode().await;
.unwrap_or(false); }
if is_silent_start { if is_silent_start {
logging!(info, Type::Lightweight, "静默启动:直接进入轻量模式"); entry_lightweight_mode().await;
let _ = entry_lightweight_mode().await;
return Ok(());
} }
if !enable_auto {
logging!(info, Type::Lightweight, "未开启自动轻量模式,跳过初始化");
return Ok(());
}
logging!(
info,
Type::Lightweight,
"非静默启动:注册自动轻量模式监听器"
);
enable_auto_light_weight_mode().await;
Ok(()) Ok(())
} }
@@ -135,7 +119,7 @@ pub fn disable_auto_light_weight_mode() {
pub async fn entry_lightweight_mode() -> bool { pub async fn entry_lightweight_mode() -> bool {
if !try_transition(LightweightState::Normal, LightweightState::In) { if !try_transition(LightweightState::Normal, LightweightState::In) {
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用"); logging!(debug, Type::Lightweight, "无需进入轻量模式,跳过调用");
refresh_lightweight_tray_state().await; refresh_lightweight_tray_state().await;
return false; return false;
} }
@@ -149,7 +133,7 @@ pub async fn entry_lightweight_mode() -> bool {
pub async fn exit_lightweight_mode() -> bool { pub async fn exit_lightweight_mode() -> bool {
if !try_transition(LightweightState::In, LightweightState::Exiting) { if !try_transition(LightweightState::In, LightweightState::Exiting) {
logging!( logging!(
info, debug,
Type::Lightweight, Type::Lightweight,
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用" "轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
); );
@@ -171,10 +155,6 @@ pub async fn add_light_weight_timer() {
fn setup_window_close_listener() { fn setup_window_close_listener() {
if let Some(window) = handle::Handle::get_window() { if let Some(window) = handle::Handle::get_window() {
let old_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
if old_id != 0 {
window.unlisten(old_id);
}
let handler_id = window.listen("tauri://close-requested", move |_event| { let handler_id = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async { std::mem::drop(AsyncHandler::spawn(|| async {
if let Err(e) = setup_light_weight_timer().await { if let Err(e) = setup_light_weight_timer().await {
@@ -196,21 +176,17 @@ fn cancel_window_close_listener() {
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 { if id != 0 {
window.unlisten(id); window.unlisten(id);
logging!(info, Type::Lightweight, "取消了窗口关闭监听"); logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
} }
} }
} }
fn setup_webview_focus_listener() { fn setup_webview_focus_listener() {
if let Some(window) = handle::Handle::get_window() { if let Some(window) = handle::Handle::get_window() {
let old_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
if old_id != 0 {
window.unlisten(old_id);
}
let handler_id = window.listen("tauri://focus", move |_event| { let handler_id = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer()); log_err!(cancel_light_weight_timer());
logging!( logging!(
info, debug,
Type::Lightweight, Type::Lightweight,
"监听到窗口获得焦点,取消轻量模式计时" "监听到窗口获得焦点,取消轻量模式计时"
); );
@@ -224,7 +200,7 @@ fn cancel_webview_focus_listener() {
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
if id != 0 { if id != 0 {
window.unlisten(id); window.unlisten(id);
logging!(info, Type::Lightweight, "取消了窗口焦点监听"); logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
} }
} }
} }
@@ -236,14 +212,14 @@ async fn setup_light_weight_timer() -> Result<()> {
let once_by_minutes = Config::verge() let once_by_minutes = Config::verge()
.await .await
.latest_ref() .data_arc()
.auto_light_weight_minutes .auto_light_weight_minutes
.unwrap_or(10); .unwrap_or(10);
{ {
let timer_map = Timer::global().timer_map.read(); let timer_map = Timer::global().timer_map.read();
if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) { if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) {
logging!(warn, Type::Timer, "轻量模式计时器已存在,跳过创建"); logging!(debug, Type::Timer, "轻量模式计时器已存在,跳过创建");
return Ok(()); return Ok(());
} }
} }
@@ -292,14 +268,17 @@ async fn setup_light_weight_timer() -> Result<()> {
} }
fn cancel_light_weight_timer() -> Result<()> { fn cancel_light_weight_timer() -> Result<()> {
let mut timer_map = Timer::global().timer_map.write(); let value = Timer::global()
let delay_timer = Timer::global().delay_timer.write(); .timer_map
.write()
if let Some(task) = timer_map.remove(LIGHT_WEIGHT_TASK_UID) { .remove(LIGHT_WEIGHT_TASK_UID);
delay_timer if let Some(task) = value {
Timer::global()
.delay_timer
.write()
.remove_task(task.task_id) .remove_task(task.task_id)
.context("failed to remove timer task")?; .context("failed to remove timer task")?;
logging!(info, Type::Timer, "计时器已取消"); logging!(debug, Type::Timer, "计时器已取消");
} }
Ok(()) Ok(())
+1 -1
View File
@@ -20,7 +20,7 @@ impl Drop for CommandChildGuard {
} }
impl CommandChildGuard { impl CommandChildGuard {
pub fn new(child: CommandChild) -> Self { pub const fn new(child: CommandChild) -> Self {
Self(Some(child)) Self(Some(child))
} }
+3 -2
View File
@@ -6,6 +6,8 @@ use crate::{
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
#[cfg(unix)]
use std::iter;
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use tauri::Manager; use tauri::Manager;
@@ -226,8 +228,7 @@ pub fn get_encryption_key() -> Result<Vec<u8>> {
#[cfg(unix)] #[cfg(unix)]
pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> { pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
["/tmp"] iter::once("/tmp")
.iter()
.map(PathBuf::from) .map(PathBuf::from)
.find(|path| path.exists()) .find(|path| path.exists())
.or_else(|| { .or_else(|| {
+315 -125
View File
@@ -1,179 +1,369 @@
use parking_lot::RwLock;
use std::sync::Arc; use std::sync::Arc;
use parking_lot::{ pub type SharedBox<T> = Arc<Box<T>>;
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard, type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
RwLockUpgradableReadGuard, RwLockWriteGuard,
};
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
// (committed_snapshot, optional_draft_snapshot)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Draft<T: Clone + ToOwned> { pub struct Draft<T: Clone> {
inner: Arc<RwLock<(T, Option<T>)>>, inner: Arc<RwLock<DraftInner<T>>>,
} }
impl<T: Clone + ToOwned> From<T> for Draft<T> { impl<T: Clone> Draft<T> {
fn from(data: T) -> Self { pub fn new(data: T) -> Self {
Self { Self {
inner: Arc::new(RwLock::new((data, None))), inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
} }
} }
} /// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
pub fn data_arc(&self) -> SharedBox<T> {
/// Implements draft management for `Box<T>`, allowing for safe concurrent editing and committing of draft data. let guard = self.inner.read();
/// # Type Parameters Arc::clone(&guard.0)
/// - `T`: The underlying data type, which must implement `Clone` and `ToOwned`.
///
/// # Methods
/// - `data_mut`: Returns a mutable 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_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
} }
/// 可写正式数据 /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> { /// 这也是零拷贝:只 clone Arc,不 clone T
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0) pub fn latest_arc(&self) -> SharedBox<T> {
let guard = self.inner.read();
guard
.1
.as_ref()
.cloned()
.unwrap_or_else(|| Arc::clone(&guard.0))
} }
/// 创建或获取草稿并返回可写引用 /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T)
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> { /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T;
let guard = self.inner.upgradable_read(); /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。
if guard.1.is_none() { pub fn edit_draft<F, R>(&self, f: F) -> R
let mut guard = RwLockUpgradableReadGuard::upgrade(guard); where
guard.1 = Some(guard.0.clone()); F: FnOnce(&mut T) -> R,
return RwLockWriteGuard::map(guard, |inner| { {
inner.1.as_mut().unwrap_or_else(|| { // 先获得写锁以创建或取出草稿 Arc 的可变引用位置
unreachable!("Draft was just created above, this should never fail") let mut guard = self.inner.write();
}) let mut draft_arc = if guard.1.is_none() {
}); Arc::clone(&guard.0)
} } else {
// 已存在草稿,升级为写锁映射 #[allow(clippy::unwrap_used)]
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| { guard.1.take().unwrap()
inner };
.1 drop(guard);
.as_mut() // Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
.unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()")) 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 latest_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| {
inner.1.as_ref().unwrap_or(&inner.0)
})
}
/// 提交草稿,返回旧正式数据
pub fn apply(&self) { pub fn apply(&self) {
let guard = self.inner.upgradable_read(); let mut guard = self.inner.write();
if guard.1.is_none() { if let Some(d) = guard.1.take() {
return; guard.0 = d;
}
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
if let Some(draft) = guard.1.take() {
guard.0 = draft;
} }
} }
/// 丢弃草稿,返回被丢弃的草稿 /// 丢弃草稿(如果存在)
pub fn discard(&self) { pub fn discard(&self) {
self.inner.write().1.take(); let mut guard = self.inner.write();
guard.1 = None;
} }
/// 异步修改正式数据,闭包直接获得 Box<T> 所有权 /// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
pub async fn with_data_modify<F, Fut, R, E>(&self, f: F) -> Result<R, E> /// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
where where
T: Send + Sync + 'static, T: Send + Sync + 'static,
F: FnOnce(Box<T>) -> Fut + Send, F: FnOnce(Box<T>) -> Fut + Send,
Fut: std::future::Future<Output = Result<(Box<T>, R), E>> + Send, Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
E: From<anyhow::Error>,
{ {
// 克隆正式数据 // 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
let local = { // 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
let local: Box<T> = {
let guard = self.inner.read(); let guard = self.inner.read();
guard.0.clone() // 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
(*guard.0).clone()
}; };
// 异步闭包执行,返回修改后的 Box<T> 和业务结果 R
let (new_local, res) = f(local).await?; let (new_local, res) = f(local).await?;
// 写回正式数据 // 将新的 Box<T> 放到已提交位置(包进 Arc)
let mut guard = self.inner.write(); self.inner.write().0 = Arc::new(new_local);
guard.0 = new_local;
Ok(res) Ok(res)
} }
} }
#[test] #[cfg(test)]
fn test_draft_box() { mod tests {
use crate::config::IVerge; use super::*;
use anyhow::anyhow;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
// 1. 创建 Draft<Box<IVerge>> #[derive(Clone, Debug, Default, PartialEq)]
let verge = Box::new(IVerge { struct IVerge {
enable_auto_launch: Some(true), enable_auto_launch: Option<bool>,
enable_tun_mode: Some(false), enable_tun_mode: Option<bool>,
..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));
} }
// 3. 初次获取草稿(draft_mut 会自动 clone 一份) // Minimal single-threaded executor for immediately-ready futures
{ fn block_on_ready<F: Future>(fut: F) -> F::Output {
let draft_view = draft.draft_mut(); fn no_op_raw_waker() -> RawWaker {
assert_eq!(draft_view.enable_auto_launch, Some(true)); fn clone(_: *const ()) -> RawWaker {
assert_eq!(draft_view.enable_tun_mode, Some(false)); 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(),
}
}
} }
// 4. 修改草稿 #[test]
{ fn test_draft_basic_flow() {
let mut d = draft.draft_mut(); let verge = IVerge {
d.enable_auto_launch = Some(false); enable_auto_launch: Some(true),
d.enable_tun_mode = 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));
}
} }
// 正式数据未变 #[test]
assert_eq!(draft.data_mut().enable_auto_launch, Some(true)); fn test_arc_pointer_behavior_on_edit_and_apply() {
assert_eq!(draft.data_mut().enable_tun_mode, Some(false)); let draft = Draft::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
});
// 草稿已变 // 初始 latest == committed
{ let committed = draft.data_arc();
let latest = draft.latest_ref(); let latest = draft.latest_arc();
assert_eq!(latest.enable_auto_launch, Some(false)); 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
// 再次编辑(unique,Arc::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();
assert_eq!(latest.enable_tun_mode, Some(true)); assert_eq!(latest.enable_tun_mode, Some(true));
} }
// 5. 提交草稿 #[test]
draft.apply(); fn test_with_data_modify_ok_and_replaces_committed() {
let draft = Draft::new(IVerge {
enable_auto_launch: Some(false),
enable_tun_mode: Some(false),
});
// 正式数据已更新 // 使用 with_data_modify 异步(立即就绪)地更新 committed
{ let res = block_on_ready(draft.with_data_modify(|mut v| async move {
let data = draft.data_mut(); v.enable_auto_launch = Some(true);
assert_eq!(data.enable_auto_launch, Some(false)); Ok((Box::new(*v), "done")) // Dereference v to get Box<T>
assert_eq!(data.enable_tun_mode, Some(true)); }));
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));
} }
// 6. 新建并修改下一轮草稿 #[test]
{ fn test_with_data_modify_error_propagation() {
let mut d = draft.draft_mut(); let draft = Draft::new(IVerge::default());
d.enable_auto_launch = Some(true);
#[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");
} }
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
// 7. 丢弃草稿 #[test]
draft.discard(); 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),
});
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone // 创建草稿并修改
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false)); 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));
}
} }
+3 -3
View File
@@ -1,4 +1,4 @@
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type}; use crate::{config::with_encryption, enhance::seq::SeqMap, logging, utils::logging::Type};
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use nanoid::nanoid; use nanoid::nanoid;
use serde::{Serialize, de::DeserializeOwned}; 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?; let yaml_str = tokio::fs::read_to_string(path).await?;
Ok(serde_yaml_ng::from_str::<T>(&yaml_str)?) Ok(with_encryption(|| async { serde_yaml_ng::from_str::<T>(&yaml_str) }).await?)
} }
/// read mapping from yaml /// read mapping from yaml
@@ -65,7 +65,7 @@ pub async fn save_yaml<T: Serialize + Sync>(
data: &T, data: &T,
prefix: Option<&str>, prefix: Option<&str>,
) -> Result<()> { ) -> Result<()> {
let data_str = serde_yaml_ng::to_string(data)?; let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?;
let yaml_str = match prefix { let yaml_str = match prefix {
Some(prefix) => format!("{prefix}\n\n{data_str}"), Some(prefix) => format!("{prefix}\n\n{data_str}"),
+1 -1
View File
@@ -43,7 +43,7 @@ pub fn get_supported_languages() -> Vec<String> {
pub async fn current_language() -> String { pub async fn current_language() -> String {
Config::verge() Config::verge()
.await .await
.latest_ref() .latest_arc()
.language .language
.as_deref() .as_deref()
.map(String::from) .map(String::from)
+16 -8
View File
@@ -1,4 +1,4 @@
#[cfg(not(feature = "tracing"))] // #[cfg(not(feature = "tracing"))]
#[cfg(not(feature = "tauri-dev"))] #[cfg(not(feature = "tauri-dev"))]
use crate::utils::logging::NoModuleFilter; use crate::utils::logging::NoModuleFilter;
use crate::{ use crate::{
@@ -31,7 +31,7 @@ pub async fn init_logger() -> Result<()> {
// TODO 提供 runtime 级别实时修改 // TODO 提供 runtime 级别实时修改
let (log_level, log_max_size, log_max_count) = { let (log_level, log_max_size, log_max_count) = {
let verge_guard = Config::verge().await; let verge_guard = Config::verge().await;
let verge = verge_guard.latest_ref(); let verge = verge_guard.latest_arc();
( (
verge.get_log_level(), verge.get_log_level(),
verge.app_log_max_size.unwrap_or(128), verge.app_log_max_size.unwrap_or(128),
@@ -49,7 +49,9 @@ pub async fn init_logger() -> Result<()> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
spec.module("tauri", log::LevelFilter::Debug); spec.module("tauri", log::LevelFilter::Debug);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
spec.module("wry", log::LevelFilter::Debug); spec.module("wry", log::LevelFilter::Off);
#[cfg(feature = "tracing")]
spec.module("tauri_plugin_mihomo", log::LevelFilter::Off);
let spec = spec.build(); let spec = spec.build();
let logger = Logger::with(spec) let logger = Logger::with(spec)
@@ -67,6 +69,12 @@ pub async fn init_logger() -> Result<()> {
); );
#[cfg(not(feature = "tracing"))] #[cfg(not(feature = "tracing"))]
let logger = logger.filter(Box::new(NoModuleFilter(&["wry", "tauri"]))); 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()?; let _handle = logger.start()?;
@@ -81,7 +89,7 @@ pub async fn init_logger() -> Result<()> {
pub async fn sidecar_writer() -> Result<FileLogWriter> { pub async fn sidecar_writer() -> Result<FileLogWriter> {
let (log_max_size, log_max_count) = { let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await; let verge_guard = Config::verge().await;
let verge = verge_guard.latest_ref(); let verge = verge_guard.latest_arc();
( (
verge.app_log_max_size.unwrap_or(128), verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8), verge.app_log_max_count.unwrap_or(8),
@@ -109,7 +117,7 @@ pub async fn sidecar_writer() -> Result<FileLogWriter> {
pub async fn service_writer_config() -> Result<WriterConfig> { pub async fn service_writer_config() -> Result<WriterConfig> {
let (log_max_size, log_max_count) = { let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await; let verge_guard = Config::verge().await;
let verge = verge_guard.latest_ref(); let verge = verge_guard.latest_arc();
( (
verge.app_log_max_size.unwrap_or(128), verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8), verge.app_log_max_count.unwrap_or(8),
@@ -134,7 +142,7 @@ pub async fn delete_log() -> Result<()> {
let auto_log_clean = { let auto_log_clean = {
let verge = Config::verge().await; let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_arc();
verge.auto_log_clean.unwrap_or(0) verge.auto_log_clean.unwrap_or(0)
}; };
@@ -498,7 +506,7 @@ pub fn init_scheme() -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn init_scheme() -> Result<()> { pub const fn init_scheme() -> Result<()> {
Ok(()) Ok(())
} }
@@ -509,7 +517,7 @@ pub async fn startup_script() -> Result<()> {
let app_handle = handle::Handle::app_handle(); let app_handle = handle::Handle::app_handle();
let script_path = { let script_path = {
let verge = Config::verge().await; let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_arc();
verge.startup_script.clone().unwrap_or_else(|| "".into()) verge.startup_script.clone().unwrap_or_else(|| "".into())
}; };
+4 -4
View File
@@ -19,7 +19,7 @@ struct IntelGpuDetection {
} }
impl IntelGpuDetection { impl IntelGpuDetection {
fn should_disable_dmabuf(&self) -> bool { const fn should_disable_dmabuf(&self) -> bool {
self.intel_is_primary || self.inconclusive self.intel_is_primary || self.inconclusive
} }
} }
@@ -41,7 +41,7 @@ enum NvidiaDmabufDisableReason {
} }
impl NvidiaGpuDetection { impl NvidiaGpuDetection {
fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> { const fn disable_reason(&self, session: &SessionEnv) -> Option<NvidiaDmabufDisableReason> {
if !session.is_wayland { if !session.is_wayland {
return None; return None;
} }
@@ -144,11 +144,11 @@ impl DmabufOverrides {
} }
} }
fn has_env_override(&self) -> bool { const fn has_env_override(&self) -> bool {
self.dmabuf_override.is_some() self.dmabuf_override.is_some()
} }
fn should_override_env(&self, decision: &DmabufDecision) -> bool { const fn should_override_env(&self, decision: &DmabufDecision) -> bool {
if self.user_preference.is_some() { if self.user_preference.is_some() {
return true; return true;
} }
+18 -18
View File
@@ -36,24 +36,24 @@ pub enum Type {
impl fmt::Display for Type { impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Type::Cmd => write!(f, "[Cmd]"), Self::Cmd => write!(f, "[Cmd]"),
Type::Core => write!(f, "[Core]"), Self::Core => write!(f, "[Core]"),
Type::Config => write!(f, "[Config]"), Self::Config => write!(f, "[Config]"),
Type::Setup => write!(f, "[Setup]"), Self::Setup => write!(f, "[Setup]"),
Type::System => write!(f, "[System]"), Self::System => write!(f, "[System]"),
Type::Service => write!(f, "[Service]"), Self::Service => write!(f, "[Service]"),
Type::Hotkey => write!(f, "[Hotkey]"), Self::Hotkey => write!(f, "[Hotkey]"),
Type::Window => write!(f, "[Window]"), Self::Window => write!(f, "[Window]"),
Type::Tray => write!(f, "[Tray]"), Self::Tray => write!(f, "[Tray]"),
Type::Timer => write!(f, "[Timer]"), Self::Timer => write!(f, "[Timer]"),
Type::Frontend => write!(f, "[Frontend]"), Self::Frontend => write!(f, "[Frontend]"),
Type::Backup => write!(f, "[Backup]"), Self::Backup => write!(f, "[Backup]"),
Type::File => write!(f, "[File]"), Self::File => write!(f, "[File]"),
Type::Lightweight => write!(f, "[Lightweight]"), Self::Lightweight => write!(f, "[Lightweight]"),
Type::Network => write!(f, "[Network]"), Self::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"), Self::ProxyMode => write!(f, "[ProxMode]"),
Type::Validate => write!(f, "[Validate]"), Self::Validate => write!(f, "[Validate]"),
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"), Self::ClashVergeRev => write!(f, "[ClashVergeRev]"),
} }
} }
} }
+9 -15
View File
@@ -26,7 +26,7 @@ pub struct HttpResponse {
} }
impl HttpResponse { impl HttpResponse {
pub fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self { pub const fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
Self { Self {
status, status,
headers, headers,
@@ -34,11 +34,11 @@ impl HttpResponse {
} }
} }
pub fn status(&self) -> StatusCode { pub const fn status(&self) -> StatusCode {
self.status self.status
} }
pub fn headers(&self) -> &HeaderMap { pub const fn headers(&self) -> &HeaderMap {
&self.headers &self.headers
} }
@@ -80,8 +80,7 @@ impl NetworkManager {
} }
async fn record_connection_error(&self, error: &str) { async fn record_connection_error(&self, error: &str) {
let mut last_error = self.last_connection_error.lock().await; *self.last_connection_error.lock().await = Some((Instant::now(), error.into()));
*last_error = Some((Instant::now(), error.into()));
let mut count = self.connection_error_count.lock().await; let mut count = self.connection_error_count.lock().await;
*count += 1; *count += 1;
@@ -89,13 +88,11 @@ impl NetworkManager {
async fn should_reset_clients(&self) -> bool { async fn should_reset_clients(&self) -> bool {
let count = *self.connection_error_count.lock().await; let count = *self.connection_error_count.lock().await;
let last_error_guard = self.last_connection_error.lock().await;
if count > 5 { if count > 5 {
return true; return true;
} }
if let Some((time, _)) = &*last_error_guard if let Some((time, _)) = &*self.last_connection_error.lock().await
&& time.elapsed() < Duration::from_secs(30) && time.elapsed() < Duration::from_secs(30)
&& count > 2 && count > 2
{ {
@@ -119,18 +116,15 @@ impl NetworkManager {
accept_invalid_certs: bool, accept_invalid_certs: bool,
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
) -> Result<HttpClient> { ) -> Result<HttpClient> {
let proxy_uri_clone = proxy_uri.clone();
let headers_clone = default_headers.clone();
{ {
let mut builder = HttpClient::builder(); let mut builder = HttpClient::builder();
builder = match proxy_uri_clone { builder = match proxy_uri {
Some(uri) => builder.proxy(Some(uri)), Some(uri) => builder.proxy(Some(uri)),
None => builder.proxy(None), None => builder.proxy(None),
}; };
for (name, value) in headers_clone.iter() { for (name, value) in default_headers.iter() {
builder = builder.default_header(name, value); builder = builder.default_header(name, value);
} }
@@ -165,10 +159,10 @@ impl NetworkManager {
ProxyType::None => None, ProxyType::None => None,
ProxyType::Localhost => { ProxyType::Localhost => {
let port = { let port = {
let verge_port = Config::verge().await.latest_ref().verge_mixed_port; let verge_port = Config::verge().await.latest_arc().verge_mixed_port;
match verge_port { match verge_port {
Some(port) => port, Some(port) => port,
None => Config::clash().await.latest_ref().get_mixed_port(), None => Config::clash().await.latest_arc().get_mixed_port(),
} }
}; };
let proxy_scheme = format!("http://127.0.0.1:{port}"); let proxy_scheme = format!("http://127.0.0.1:{port}");
+1 -1
View File
@@ -179,7 +179,7 @@ pub(super) async fn refresh_tray_menu() {
pub(super) async fn init_window() { pub(super) async fn init_window() {
let is_silent_start = Config::verge() let is_silent_start = Config::verge()
.await .await
.latest_ref() .latest_arc()
.enable_silent_start .enable_silent_start
.unwrap_or(false); .unwrap_or(false);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
+71 -45
View File
@@ -1,12 +1,14 @@
use std::time::Duration;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
use smartstring::alias::String; use smartstring::alias::String;
use tauri::Url; use tauri::Url;
use crate::{ use crate::{
config::{PrfItem, profiles}, config::{Config, PrfItem, profiles},
core::handle, core::handle,
logging, logging_error, logging,
utils::logging::Type, utils::logging::Type,
}; };
@@ -29,55 +31,79 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
} }
}; };
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" { let (url_param, name) =
let name_owned: Option<String> = link_parsed if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
.query_pairs() let name_owned: Option<String> = link_parsed
.find(|(key, _)| key == "name") .query_pairs()
.map(|(_, value)| value.into_owned().into()); .find(|(key, _)| key == "name")
let name = name_owned.as_ref(); .map(|(_, value)| value.into_owned().into());
let url_param = if let Some(query) = link_parsed.query() { let url_param = if let Some(query) = link_parsed.query() {
let prefix = "url="; let prefix = "url=";
if let Some(pos) = query.find(prefix) { if let Some(pos) = query.find(prefix) {
let raw_url = &query[pos + prefix.len()..]; let raw_url = &query[pos + prefix.len()..];
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string()) Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
} else {
None
}
} else { } else {
None None
} };
(url_param, name_owned)
} else { } else {
None (None, None)
}; };
match url_param { let url = if let Some(ref url) = url_param {
Some(ref url) => { url
logging!(info, Type::Config, "decoded subscription url: {url}"); } else {
match PrfItem::from_url(url.as_ref(), name, None, None).await { logging!(
Ok(mut item) => { error,
let uid = match item.uid.clone() { Type::Config,
Some(uid) => uid, "missing url parameter in deep link: {}",
None => { param_str
logging!(error, Type::Config, "Profile item missing UID"); );
handle::Handle::notice_message( return Ok(());
"import_sub_url::error", };
"Profile item missing UID".to_string(),
); let mut item = match PrfItem::from_url(url, name.as_ref(), None, None).await {
return Ok(()); Ok(item) => item,
} Err(e) => {
}; logging!(
let result = profiles::profiles_append_item_safe(&mut item).await; error,
logging_error!( Type::Config,
Type::Config, "failed to parse profile from url: {:?}",
"failed to import subscription url: {:?}", e
result );
); handle::Handle::notice_message("import_sub_url::error", e.to_string());
handle::Handle::notice_message("import_sub_url::ok", uid); return Ok(());
} }
Err(e) => { };
handle::Handle::notice_message("import_sub_url::error", e.to_string());
} let uid = item.uid.clone().unwrap_or_default();
} match profiles::profiles_append_item_safe(&mut item).await {
} Ok(_) => {
None => bail!("failed to get profile url"), Config::profiles().await.apply();
let _ = Config::profiles().await.data_arc().save_file().await;
handle::Handle::notice_message(
"import_sub_url::ok",
"", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
);
handle::Handle::refresh_verge();
handle::Handle::notify_profile_changed(uid.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
handle::Handle::notify_profile_changed(uid);
}
Err(e) => {
logging!(
error,
Type::Config,
"failed to import subscription url: {:?}",
e
);
Config::profiles().await.discard();
handle::Handle::notice_message("import_sub_url::error", e.to_string());
return Ok(());
} }
} }
+1 -3
View File
@@ -54,9 +54,7 @@ fn get_ui_ready_notify() -> &'static Arc<Notify> {
// 更新UI准备阶段 // 更新UI准备阶段
pub fn update_ui_ready_stage(stage: UiReadyStage) { pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state(); let state = get_ui_ready_state();
let mut stage_lock = state.stage.write(); *state.stage.write() = stage;
*stage_lock = stage;
// 如果是最终阶段,标记UI完全就绪 // 如果是最终阶段,标记UI完全就绪
if stage == UiReadyStage::Ready { if stage == UiReadyStage::Ready {
mark_ui_ready(); mark_ui_ready();
+1 -1
View File
@@ -22,7 +22,7 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
let app_handle = handle::Handle::app_handle(); let app_handle = handle::Handle::app_handle();
let config = Config::verge().await; let config = Config::verge().await;
let latest = config.latest_ref(); let latest = config.latest_arc();
let start_page = latest.start_page.as_deref().unwrap_or("/"); let start_page = latest.start_page.as_deref().unwrap_or("/");
match tauri::WebviewWindowBuilder::new( match tauri::WebviewWindowBuilder::new(
+9 -7
View File
@@ -31,6 +31,8 @@ pub async fn check_singleton() -> Result<()> {
let client = ClientBuilder::new() let client = ClientBuilder::new()
.timeout(Duration::from_millis(500)) .timeout(Duration::from_millis(500))
.build()?; .build()?;
// 需要确保 Send
#[allow(clippy::needless_collect)]
let argvs: Vec<std::string::String> = std::env::args().collect(); let argvs: Vec<std::string::String> = std::env::args().collect();
if argvs.len() > 1 { if argvs.len() > 1 {
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
@@ -89,15 +91,15 @@ pub fn embed_server() {
let clash_config = Config::clash().await; let clash_config = Config::clash().await;
let pac_content = verge_config let pac_content = verge_config
.latest_ref() .latest_arc()
.pac_file_content .pac_file_content
.clone() .clone()
.unwrap_or_else(|| DEFAULT_PAC.into()); .unwrap_or_else(|| DEFAULT_PAC.into());
let pac_port = verge_config let pac_port = verge_config
.latest_ref() .latest_arc()
.verge_mixed_port .verge_mixed_port
.unwrap_or_else(|| clash_config.latest_ref().get_mixed_port()); .unwrap_or_else(|| clash_config.latest_arc().get_mixed_port());
let pac = warp::path!("commands" / "pac").map(move || { let pac = warp::path!("commands" / "pac").map(move || {
let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}")); let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}"));
@@ -110,14 +112,14 @@ pub fn embed_server() {
// Use map instead of and_then to avoid Send issues // Use map instead of and_then to avoid Send issues
let scheme = warp::path!("commands" / "scheme") let scheme = warp::path!("commands" / "scheme")
.and(warp::query::<QueryParam>()) .and(warp::query::<QueryParam>())
.map(|query: QueryParam| { .and_then(|query: QueryParam| async move {
tokio::task::spawn_local(async move { AsyncHandler::spawn(|| async move {
logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await); logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await);
}); });
warp::reply::with_status::<std::string::String>( Ok::<_, warp::Rejection>(warp::reply::with_status::<std::string::String>(
"ok".to_string(), "ok".to_string(),
warp::http::StatusCode::OK, warp::http::StatusCode::OK,
) ))
}); });
let commands = visible.or(scheme).or(pac); let commands = visible.or(scheme).or(pac);
+1
View File
@@ -81,6 +81,7 @@ fn should_handle_window_operation() -> bool {
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) { if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
*last_operation = now; *last_operation = now;
drop(last_operation);
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release); WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
logging!(info, Type::Window, "[防抖] 窗口操作被允许执行"); logging!(info, Type::Window, "[防抖] 窗口操作被允许执行");
true true
@@ -71,8 +71,9 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
setOpen(false); setOpen(false);
showNotice("success", t("Port settings saved")); showNotice("success", t("Port settings saved"));
}, },
onError: () => { onError: (e) => {
showNotice("error", t("Failed to save port settings")); showNotice("error", e.message || t("Failed to save port settings"));
// showNotice("error", t("Failed to save port settings"));
}, },
}, },
); );
+10 -10
View File
@@ -70,8 +70,8 @@ export const useClashInfo = () => {
if (patch["redir-port"]) { if (patch["redir-port"]) {
const port = patch["redir-port"]; const port = patch["redir-port"];
if (port < 1111) { if (port < 1000) {
throw new Error("The port should not < 1111"); throw new Error("The port should not < 1000");
} }
if (port > 65536) { if (port > 65536) {
throw new Error("The port should not > 65536"); throw new Error("The port should not > 65536");
@@ -80,8 +80,8 @@ export const useClashInfo = () => {
if (patch["tproxy-port"]) { if (patch["tproxy-port"]) {
const port = patch["tproxy-port"]; const port = patch["tproxy-port"];
if (port < 1111) { if (port < 1000) {
throw new Error("The port should not < 1111"); throw new Error("The port should not < 1000");
} }
if (port > 65536) { if (port > 65536) {
throw new Error("The port should not > 65536"); throw new Error("The port should not > 65536");
@@ -90,8 +90,8 @@ export const useClashInfo = () => {
if (patch["mixed-port"]) { if (patch["mixed-port"]) {
const port = patch["mixed-port"]; const port = patch["mixed-port"];
if (port < 1111) { if (port < 1000) {
throw new Error("The port should not < 1111"); throw new Error("The port should not < 1000");
} }
if (port > 65536) { if (port > 65536) {
throw new Error("The port should not > 65536"); throw new Error("The port should not > 65536");
@@ -100,8 +100,8 @@ export const useClashInfo = () => {
if (patch["socks-port"]) { if (patch["socks-port"]) {
const port = patch["socks-port"]; const port = patch["socks-port"];
if (port < 1111) { if (port < 1000) {
throw new Error("The port should not < 1111"); throw new Error("The port should not < 1000");
} }
if (port > 65536) { if (port > 65536) {
throw new Error("The port should not > 65536"); throw new Error("The port should not > 65536");
@@ -110,8 +110,8 @@ export const useClashInfo = () => {
if (patch["port"]) { if (patch["port"]) {
const port = patch["port"]; const port = patch["port"];
if (port < 1111) { if (port < 1000) {
throw new Error("The port should not < 1111"); throw new Error("The port should not < 1000");
} }
if (port > 65536) { if (port > 65536) {
throw new Error("The port should not > 65536"); throw new Error("The port should not > 65536");
+4 -1
View File
@@ -11,7 +11,10 @@ export const handleNoticeMessage = (
) => { ) => {
const handlers: Record<string, () => void> = { const handlers: Record<string, () => void> = {
"import_sub_url::ok": () => { "import_sub_url::ok": () => {
navigate("/profile", { state: { current: msg } }); // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
// navigate("/profile", { state: { current: msg } });
navigate("/profile");
showNotice("success", t("Import Subscription Successful")); showNotice("success", t("Import Subscription Successful"));
}, },
"import_sub_url::error": () => { "import_sub_url::error": () => {
+4 -4
View File
@@ -13,15 +13,15 @@ import {
sortableKeyboardCoordinates, sortableKeyboardCoordinates,
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { import {
CheckBoxOutlineBlankRounded,
CheckBoxRounded,
ClearRounded, ClearRounded,
ContentPasteRounded, ContentPasteRounded,
DeleteRounded,
IndeterminateCheckBoxRounded,
LocalFireDepartmentRounded, LocalFireDepartmentRounded,
RefreshRounded, RefreshRounded,
TextSnippetOutlined, TextSnippetOutlined,
CheckBoxOutlineBlankRounded,
CheckBoxRounded,
IndeterminateCheckBoxRounded,
DeleteRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material"; import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material";
+1 -1
View File
@@ -801,7 +801,7 @@ interface IVergeConfig {
common_tray_icon?: boolean; common_tray_icon?: boolean;
sysproxy_tray_icon?: boolean; sysproxy_tray_icon?: boolean;
tun_tray_icon?: boolean; tun_tray_icon?: boolean;
enable_tray_speed?: boolean; // enable_tray_speed?: boolean;
// enable_tray_icon?: boolean; // enable_tray_icon?: boolean;
tray_inline_proxy_groups?: boolean; tray_inline_proxy_groups?: boolean;
enable_tun_mode?: boolean; enable_tun_mode?: boolean;