Compare commits

..

4 Commits

50 changed files with 652 additions and 1105 deletions

View File

@@ -90,7 +90,7 @@ jobs:
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
@@ -103,7 +103,7 @@ jobs:
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -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)
#### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)

View File

@@ -1,160 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [],
"categories": {
"correctness": "off"
},
"env": {
"builtin": true
},
"overrides": [
{
"files": ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
"rules": {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-undef": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error"
}
},
{
"files": ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}", "**/*.cts"],
"rules": {
"constructor-super": "off",
"getter-return": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-undef": "off",
"no-unreachable": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
},
{
"files": ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
"rules": {
"@typescript-eslint/ban-ts-comment": "error",
"no-array-constructor": "error",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-declaration-merging": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"no-unused-expressions": "error",
"no-unused-vars": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/triple-slash-reference": "error"
},
"plugins": ["typescript"]
},
{
"files": ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
"rules": {
"curly": "off",
"no-unexpected-multiline": "off",
"unicorn/empty-brace-spaces": "off",
"unicorn/no-nested-ternary": "off",
"unicorn/number-literal-case": "off"
},
"plugins": ["unicorn"]
},
{
"files": ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"@typescript-eslint/no-explicit-any": "off",
"no-case-declarations": "error",
"no-fallthrough": "error",
"no-empty": [
"warn",
{
"allowEmptyCatch": true
}
],
"no-unused-vars": "off"
},
"plugins": ["react", "typescript"],
"env": {
"browser": true,
"shared-node-browser": true
}
}
]
}

View File

@@ -1,6 +1,6 @@
## v2.4.3
感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
感谢 @Slinetrac, @oomeow 以及 @Lythrilla 的出色贡献
### 🐞 修复问题
@@ -36,7 +36,7 @@
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
- 修复 Linux 系统主题切换不生效
- 修复 `允许自动更新` 字段使手动订阅刷新失效
- 修复轻量模式托盘状态不同步
- 修复连接界面长时间显示后报错问题
<details>
<summary><strong> ✨ 新增功能 </strong></summary>

View File

@@ -43,7 +43,7 @@
"@mui/icons-material": "^7.3.4",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.4",
"@mui/x-data-grid": "^8.16.0",
"@mui/x-data-grid": "^7.29.9",
"@tauri-apps/api": "2.9.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.4.2",
@@ -84,7 +84,7 @@
"@tauri-apps/cli": "2.9.2",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.0",
"@types/node": "^24.9.2",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@vitejs/plugin-legacy": "^7.2.1",
@@ -109,7 +109,6 @@
"lint-staged": "^16.2.6",
"meta-json-schema": "^1.19.14",
"node-fetch": "^3.3.2",
"oxlint": "^1.25.0",
"prettier": "^3.6.2",
"sass": "^1.93.3",
"tar": "^7.5.2",

175
pnpm-lock.yaml generated
View File

@@ -36,8 +36,8 @@ importers:
specifier: ^7.3.4
version: 7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mui/x-data-grid':
specifier: ^8.16.0
version: 8.16.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
specifier: ^7.29.9
version: 7.29.9(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@tauri-apps/api':
specifier: 2.9.0
version: 2.9.0
@@ -154,8 +154,8 @@ importers:
specifier: ^4.17.12
version: 4.17.12
'@types/node':
specifier: ^24.10.0
version: 24.10.0
specifier: ^24.9.2
version: 24.9.2
'@types/react':
specifier: 19.2.2
version: 19.2.2
@@ -164,10 +164,10 @@ importers:
version: 19.2.2(@types/react@19.2.2)
'@vitejs/plugin-legacy':
specifier: ^7.2.1
version: 7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
version: 7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
'@vitejs/plugin-react-swc':
specifier: ^4.2.0
version: 4.2.0(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
version: 4.2.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
adm-zip:
specifier: ^0.5.16
version: 0.5.16
@@ -228,9 +228,6 @@ importers:
node-fetch:
specifier: ^3.3.2
version: 3.3.2
oxlint:
specifier: ^1.25.0
version: 1.25.0
prettier:
specifier: ^3.6.2
version: 3.6.2
@@ -251,16 +248,16 @@ importers:
version: 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
vite:
specifier: ^7.1.12
version: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
vite-plugin-monaco-editor-esm:
specifier: ^2.0.2
version: 2.0.2(monaco-editor@0.54.0)
vite-plugin-svgr:
specifier: ^4.5.0
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
vitest:
specifier: ^4.0.6
version: 4.0.6(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
version: 4.0.6(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
packages:
@@ -1242,8 +1239,8 @@ packages:
'@types/react':
optional: true
'@mui/x-data-grid@8.16.0':
resolution: {integrity: sha512-yJ+v+E1yI1HxrEUdOfgrUTCxobAFvotGggU6cy6MnM7c7/TPPg9d5mDzjzxb0imOCJ6WyiM/vtd5WKbY/5sUNw==}
'@mui/x-data-grid@7.29.9':
resolution: {integrity: sha512-RfK7Fnuu4eyv/4eD3MEB1xxZsx0xRBsofb1kifghIjyQV1EKAeRcwvczyrzQggj7ZRT5AqkwCzhLsZDvE5O0nQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.9.0
@@ -1258,19 +1255,12 @@ packages:
'@emotion/styled':
optional: true
'@mui/x-internals@8.16.0':
resolution: {integrity: sha512-JR53WOFqmQYQzurOpB0H91K7/9uMcte1ooxHxTLGB+97PgB+rKY6siRWvUALGS56XyPV+1a2ALI33hd2E7+Rgg==}
'@mui/x-internals@7.29.0':
resolution: {integrity: sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
'@mui/x-virtualizer@0.2.6':
resolution: {integrity: sha512-t45EHhD9kStSwIYMkqYYQIFbZNVQws9LRANktf0e/+j+MxsRTFk41r0rgiazMSOSugJlCuSh/H8xUUuMCZdtow==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -1334,46 +1324,6 @@ packages:
'@octokit/types@13.10.0':
resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==}
'@oxlint/darwin-arm64@1.25.0':
resolution: {integrity: sha512-OLx4XyUv5SO7k8y5FzJIoTKan+iKK53T1Ws8fBIl4zblUIWI66ZIqSVG2A2rxOBA7XfINqCz8UipGzOW9yzKcg==}
cpu: [arm64]
os: [darwin]
'@oxlint/darwin-x64@1.25.0':
resolution: {integrity: sha512-srndNPiliA0rchYKqYfOdqA9kqyVQ6YChK3XJe9Lxo/YG8tTJ5K65g2A5SHTT2s1Nm5DnQa5AKZH7w+7KI/m8A==}
cpu: [x64]
os: [darwin]
'@oxlint/linux-arm64-gnu@1.25.0':
resolution: {integrity: sha512-W9+DnHDbygprpGV586BolwWES+o2raOcSJv404nOFPQjWZ09efG24nuXrg/fpyoMQb4YoW2W1fvlnyMVU+ADcw==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-arm64-musl@1.25.0':
resolution: {integrity: sha512-1tIMpQhKlItm7uKzs3lluG7KorZR5ItoNKd1iFYF/IPmZ+i0/iuZ7MVWXRjBcgQMhMYSdfZpSVEdFKcFz2HDxA==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-x64-gnu@1.25.0':
resolution: {integrity: sha512-xVkmk/zkIulc5o0OUWY04DyBfKotnq9+60O9I5c0DpdKAELVLhZkLmct0apx3jAX6Z/3yYPzhc6Lw1Ia3jU3VQ==}
cpu: [x64]
os: [linux]
'@oxlint/linux-x64-musl@1.25.0':
resolution: {integrity: sha512-IeO10dZosJV58YzN0gckhRYac+FM9s5VCKUx2ghgbKR91z/bpSRcRl8Sy5cWTkcVwu3ZTikhK8aXC6j7XIqKNw==}
cpu: [x64]
os: [linux]
'@oxlint/win32-arm64@1.25.0':
resolution: {integrity: sha512-mpdiXZm2oNuSQAbTEPRDuSeR6v1DCD7Cl/xouR2ggHZu3AKZ4XYmm29hyrzIxrYVoQ/5j+182TGdOpGYn9xQJg==}
cpu: [arm64]
os: [win32]
'@oxlint/win32-x64@1.25.0':
resolution: {integrity: sha512-opoIACOkcFloWQO6dubBLbcWwW52ML8+3deFdr0WE0PeM9UXdLB0jRMuLsEnplmBoy9TRvmxDJ+Pw8xc2PsOfQ==}
cpu: [x64]
os: [win32]
'@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
engines: {node: '>= 10.0.0'}
@@ -1864,8 +1814,8 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@24.10.0':
resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==}
'@types/node@24.9.2':
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -3548,16 +3498,6 @@ packages:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
oxlint@1.25.0:
resolution: {integrity: sha512-O6iJ9xeuy9eQCi8/EghvsNO6lzSaUPs0FR1uLy51Exp3RkVpjvJKyPPhd9qv65KLnfG/BNd2HE/rH0NbEfVVzA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
oxlint-tsgolint: '>=0.4.0'
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -5534,18 +5474,18 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.2
'@mui/x-data-grid@8.16.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
'@mui/x-data-grid@7.29.9(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mui/system': 7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)
'@mui/utils': 7.3.3(@types/react@19.2.2)(react@19.2.0)
'@mui/x-internals': 8.16.0(@types/react@19.2.2)(react@19.2.0)
'@mui/x-virtualizer': 0.2.6(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mui/x-internals': 7.29.0(@types/react@19.2.2)(react@19.2.0)
clsx: 2.1.1
prop-types: 15.8.1
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
reselect: 5.1.1
use-sync-external-store: 1.6.0(react@19.2.0)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0)
@@ -5553,23 +5493,11 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
'@mui/x-internals@8.16.0(@types/react@19.2.2)(react@19.2.0)':
'@mui/x-internals@7.29.0(@types/react@19.2.2)(react@19.2.0)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.3(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
reselect: 5.1.1
use-sync-external-store: 1.6.0(react@19.2.0)
transitivePeerDependencies:
- '@types/react'
'@mui/x-virtualizer@0.2.6(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.3(@types/react@19.2.2)(react@19.2.0)
'@mui/x-internals': 8.16.0(@types/react@19.2.2)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
transitivePeerDependencies:
- '@types/react'
@@ -5650,30 +5578,6 @@ snapshots:
dependencies:
'@octokit/openapi-types': 24.2.0
'@oxlint/darwin-arm64@1.25.0':
optional: true
'@oxlint/darwin-x64@1.25.0':
optional: true
'@oxlint/linux-arm64-gnu@1.25.0':
optional: true
'@oxlint/linux-arm64-musl@1.25.0':
optional: true
'@oxlint/linux-x64-gnu@1.25.0':
optional: true
'@oxlint/linux-x64-musl@1.25.0':
optional: true
'@oxlint/win32-arm64@1.25.0':
optional: true
'@oxlint/win32-x64@1.25.0':
optional: true
'@parcel/watcher-android-arm64@2.5.1':
optional: true
@@ -6059,7 +5963,7 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/node@24.10.0':
'@types/node@24.9.2':
dependencies:
undici-types: 7.16.0
@@ -6237,7 +6141,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
'@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
'@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.28.4
'@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4)
@@ -6252,15 +6156,15 @@ snapshots:
regenerator-runtime: 0.14.1
systemjs: 6.15.1
terser: 5.44.0
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-react-swc@4.2.0(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
'@vitejs/plugin-react-swc@4.2.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.43
'@swc/core': 1.14.0
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- '@swc/helpers'
@@ -6273,13 +6177,13 @@ snapshots:
chai: 6.2.0
tinyrainbow: 3.0.3
'@vitest/mocker@4.0.6(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
'@vitest/mocker@4.0.6(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 4.0.6
estree-walker: 3.0.3
magic-string: 0.30.19
optionalDependencies:
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
'@vitest/pretty-format@4.0.6':
dependencies:
@@ -8157,17 +8061,6 @@ snapshots:
safe-push-apply: 1.0.0
optional: true
oxlint@1.25.0:
optionalDependencies:
'@oxlint/darwin-arm64': 1.25.0
'@oxlint/darwin-x64': 1.25.0
'@oxlint/linux-arm64-gnu': 1.25.0
'@oxlint/linux-arm64-musl': 1.25.0
'@oxlint/linux-x64-gnu': 1.25.0
'@oxlint/linux-x64-musl': 1.25.0
'@oxlint/win32-arm64': 1.25.0
'@oxlint/win32-x64': 1.25.0
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@@ -8932,18 +8825,18 @@ snapshots:
dependencies:
monaco-editor: 0.54.0
vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)):
vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
'@svgr/core': 8.1.0(typescript@5.9.3)
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- rollup
- supports-color
- typescript
vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
dependencies:
esbuild: 0.25.4
fdir: 6.5.0(picomatch@4.0.3)
@@ -8952,17 +8845,17 @@ snapshots:
rollup: 4.46.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.0
'@types/node': 24.9.2
fsevents: 2.3.3
jiti: 2.6.1
sass: 1.93.3
terser: 5.44.0
yaml: 2.8.1
vitest@4.0.6(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
vitest@4.0.6(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
dependencies:
'@vitest/expect': 4.0.6
'@vitest/mocker': 4.0.6(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
'@vitest/mocker': 4.0.6(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
'@vitest/pretty-format': 4.0.6
'@vitest/runner': 4.0.6
'@vitest/snapshot': 4.0.6
@@ -8979,11 +8872,11 @@ snapshots:
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
'@types/node': 24.10.0
'@types/node': 24.9.2
transitivePeerDependencies:
- jiti
- less

View File

@@ -40,6 +40,10 @@
"description": "Group all GitHub Actions updates into a single PR",
"matchManagers": ["github-actions"],
"groupName": "github actions"
},
{
"matchPackageNames": ["@mui/x-data-grid"],
"matchCurrentVersion": "<8.0.0"
}
],
"postUpdateOptions": ["pnpmDedupe", "updateCargoLock"],

7
src-tauri/Cargo.lock generated
View File

@@ -152,12 +152,6 @@ dependencies = [
"x11rb",
]
[[package]]
name = "arc-swap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "arraydeque"
version = "0.5.1"
@@ -1099,7 +1093,6 @@ version = "2.4.3"
dependencies = [
"aes-gcm",
"anyhow",
"arc-swap",
"async-trait",
"backoff",
"base64 0.22.1",

View File

@@ -86,7 +86,6 @@ smartstring = { version = "1.0.1", features = ["serde"] }
clash_verge_service_ipc = { version = "2.0.21", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.7.1"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"

View File

@@ -7,13 +7,13 @@ use app_lib::config::IVerge;
use app_lib::utils::Draft as DraftNew;
/// 创建测试数据
fn make_draft() -> DraftNew<IVerge> {
let verge = IVerge {
fn make_draft() -> DraftNew<Box<IVerge>> {
let verge = Box::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..Default::default()
};
DraftNew::new(verge)
});
DraftNew::from(verge)
}
pub fn bench_draft(c: &mut Criterion) {
@@ -30,17 +30,18 @@ pub fn bench_draft(c: &mut Criterion) {
group.bench_function("data_mut", |b| {
b.iter(|| {
let draft = black_box(make_draft());
draft.edit_draft(|d| d.enable_tun_mode = Some(true));
black_box(&draft.latest_arc().enable_tun_mode);
let mut data = draft.data_mut();
data.enable_tun_mode = Some(true);
black_box(&data.enable_tun_mode);
});
});
group.bench_function("draft_mut_first", |b| {
b.iter(|| {
let draft = black_box(make_draft());
draft.edit_draft(|d| d.enable_auto_launch = Some(false));
let latest = draft.latest_arc();
black_box(&latest.enable_auto_launch);
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
black_box(&d.enable_auto_launch);
});
});
@@ -48,24 +49,20 @@ pub fn bench_draft(c: &mut Criterion) {
b.iter(|| {
let draft = black_box(make_draft());
{
draft.edit_draft(|d| {
d.enable_tun_mode = Some(true);
});
let latest1 = draft.latest_arc();
black_box(&latest1.enable_tun_mode);
let mut first = draft.draft_mut();
first.enable_tun_mode = Some(true);
black_box(&first.enable_tun_mode);
}
draft.edit_draft(|d| {
d.enable_tun_mode = Some(false);
});
let latest2 = draft.latest_arc();
black_box(&latest2.enable_tun_mode);
let mut second = draft.draft_mut();
second.enable_tun_mode = Some(false);
black_box(&second.enable_tun_mode);
});
});
group.bench_function("latest_arc", |b| {
group.bench_function("latest_ref", |b| {
b.iter(|| {
let draft = black_box(make_draft());
let latest = draft.latest_arc();
let latest = draft.latest_ref();
black_box(&latest.enable_auto_launch);
});
});
@@ -74,9 +71,8 @@ pub fn bench_draft(c: &mut Criterion) {
b.iter(|| {
let draft = black_box(make_draft());
{
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
});
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
draft.apply();
black_box(&draft);
@@ -87,9 +83,8 @@ pub fn bench_draft(c: &mut Criterion) {
b.iter(|| {
let draft = black_box(make_draft());
{
draft.edit_draft(|d| {
d.enable_auto_launch = Some(false);
});
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
draft.discard();
black_box(&draft);
@@ -100,7 +95,7 @@ pub fn bench_draft(c: &mut Criterion) {
b.to_async(&rt).iter(|| async {
let draft = black_box(make_draft());
let _: Result<(), anyhow::Error> = draft
.with_data_modify::<_, _, _>(|mut box_data| async move {
.with_data_modify::<_, _, _, anyhow::Error>(|mut box_data| async move {
box_data.enable_auto_launch =
Some(!box_data.enable_auto_launch.unwrap_or(false));
Ok((box_data, ()))

View File

@@ -2,11 +2,11 @@ use super::CmdResult;
use crate::utils::dirs;
use crate::{
cmd::StringifyErr,
config::{ClashInfo, Config},
config::Config,
constants,
core::{CoreManager, handle, validate::CoreConfigValidator},
};
use crate::{feat, logging, utils::logging::Type};
use crate::{config::*, feat, logging, utils::logging::Type};
use compact_str::CompactString;
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
@@ -22,7 +22,7 @@ pub async fn copy_clash_env() -> CmdResult {
/// 获取Clash信息
#[tauri::command]
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().await.latest_arc().get_client_info())
Ok(Config::clash().await.latest_ref().get_client_info())
}
/// 修改Clash配置
@@ -141,6 +141,12 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
/// 应用或撤销DNS配置
#[tauri::command]
pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{
config::Config,
core::{CoreManager, handle},
utils::dirs,
};
if apply {
// 读取DNS配置文件
let dns_path = dirs::app_home_dir()
@@ -169,9 +175,7 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
patch.insert("dns".into(), patch_config.into());
// 应用DNS配置到运行时配置
Config::runtime().await.edit_draft(|d| {
d.patch_config(patch);
});
Config::runtime().await.draft_mut().patch_config(patch);
// 重新生成配置
Config::generate().await.stringify_err_log(|err| {

View File

@@ -1,6 +1,5 @@
use super::CmdResult;
use super::StringifyErr;
use crate::utils::draft::SharedBox;
use crate::{
config::{
Config, IProfiles, PrfItem, PrfOption,
@@ -16,19 +15,68 @@ use crate::{
ret_err,
utils::{dirs, help, logging::Type},
};
use scopeguard::defer;
use smartstring::alias::String;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
// 全局请求序列号跟踪,用于避免队列化执行
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
#[tauri::command]
pub async fn get_profiles() -> CmdResult<SharedBox<IProfiles>> {
logging!(debug, Type::Cmd, "获取配置文件列表");
let draft = Config::profiles().await;
let latest = draft.latest_arc();
Ok(latest)
pub async fn get_profiles() -> CmdResult<IProfiles> {
// 策略1: 尝试快速获取latest数据
let latest_result = tokio::time::timeout(Duration::from_millis(500), async {
let profiles = Config::profiles().await;
let latest = profiles.latest_ref();
IProfiles {
current: latest.current.clone(),
items: latest.items.clone(),
}
})
.await;
match latest_result {
Ok(profiles) => {
logging!(info, Type::Cmd, "快速获取配置列表成功");
return Ok(profiles);
}
Err(_) => {
logging!(warn, Type::Cmd, "快速获取配置超时(500ms)");
}
}
// 策略2: 如果快速获取失败尝试获取data()
let data_result = tokio::time::timeout(Duration::from_secs(2), async {
let profiles = Config::profiles().await;
let data = profiles.latest_ref();
IProfiles {
current: data.current.clone(),
items: data.items.clone(),
}
})
.await;
match data_result {
Ok(profiles) => {
logging!(info, Type::Cmd, "获取draft配置列表成功");
return Ok(profiles);
}
Err(join_err) => {
logging!(
error,
Type::Cmd,
"获取draft配置任务失败或超时: {}",
join_err
);
}
}
// 策略3: fallback尝试重新创建配置
logging!(warn, Type::Cmd, "所有获取配置策略都失败尝试fallback");
Ok(IProfiles::new().await)
}
/// 增强配置文件
@@ -173,7 +221,7 @@ async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
// 获取目标配置文件路径
let config_file_result = {
let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_arc();
let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) {
Ok(item) => {
if let Some(file) = &item.file {
@@ -283,7 +331,8 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
};
Config::profiles()
.await
.edit_draft(|d| d.patch_config(&restore_profiles))
.draft_mut()
.patch_config(restore_profiles)
.stringify_err()?;
Config::profiles().await.apply();
crate::process::AsyncHandler::spawn(|| async move {
@@ -295,7 +344,26 @@ async fn restore_previous_profile(prev_profile: String) -> CmdResult<()> {
Ok(())
}
async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
async fn handle_success(current_sequence: u64, current_value: Option<String>) -> CmdResult<bool> {
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
current_sequence,
latest_sequence
);
Config::profiles().await.discard();
return Ok(false);
}
logging!(
info,
Type::Cmd,
"配置更新成功,序列号: {}",
current_sequence
);
Config::profiles().await.apply();
handle::Handle::refresh_clash();
@@ -312,10 +380,17 @@ async fn handle_success(current_value: Option<String>) -> CmdResult<bool> {
}
if let Some(current) = &current_value {
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current,);
logging!(
info,
Type::Cmd,
"向前端发送配置变更事件: {}, 序列号: {}",
current,
current_sequence
);
handle::Handle::notify_profile_changed(current.clone());
}
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(true)
}
@@ -329,31 +404,53 @@ async fn handle_validation_failure(
restore_previous_profile(prev_profile).await?;
}
handle::Handle::notice_message("config_validate::error", error_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,);
async fn handle_update_error<E: std::fmt::Display>(e: E, current_sequence: u64) -> CmdResult<bool> {
logging!(
warn,
Type::Cmd,
"更新过程发生错误: {}, 序列号: {}",
e,
current_sequence
);
Config::profiles().await.discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn handle_timeout(current_profile: Option<String>) -> CmdResult<bool> {
async fn handle_timeout(current_profile: Option<String>, current_sequence: u64) -> CmdResult<bool> {
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(error, Type::Cmd, "{}", timeout_msg);
logging!(
error,
Type::Cmd,
"{}, 序列号: {}",
timeout_msg,
current_sequence
);
Config::profiles().await.discard();
if let Some(prev_profile) = current_profile {
restore_previous_profile(prev_profile).await?;
}
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
async fn perform_config_update(
current_sequence: u64,
current_value: Option<String>,
current_profile: Option<String>,
) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
"开始内核配置更新,序列号: {}",
current_sequence
);
let update_result = tokio::time::timeout(
Duration::from_secs(30),
CoreManager::global().update_config(),
@@ -361,38 +458,48 @@ async fn perform_config_update(
.await;
match update_result {
Ok(Ok((true, _))) => handle_success(current_value).await,
Ok(Ok((true, _))) => handle_success(current_sequence, current_value).await,
Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await,
Ok(Err(e)) => handle_update_error(e).await,
Err(_) => handle_timeout(current_profile).await,
Ok(Err(e)) => handle_update_error(e, current_sequence).await,
Err(_) => handle_timeout(current_profile, current_sequence).await,
}
}
/// 修改profiles的配置
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
if CURRENT_SWITCHING_PROFILE
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Err("switch_in_progress".into());
return Ok(false);
}
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
// 为当前请求分配序列号
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
let target_profile = profiles.current.clone();
logging!(
info,
Type::Cmd,
"开始修改配置文件目标profile: {:?}",
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
current_sequence,
target_profile
);
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().await.latest_arc().current.clone();
let current_profile = Config::profiles().await.latest_ref().current.clone();
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
@@ -400,16 +507,50 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
&& current_profile.as_ref() != Some(new_profile)
&& validate_new_profile(new_profile).await.is_err()
{
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
return Ok(false);
}
let _ = Config::profiles()
.await
.edit_draft(|d| d.patch_config(&profiles));
// 检查请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
// 更新profiles配置
logging!(
info,
Type::Cmd,
"正在更新配置草稿,序列号: {}",
current_sequence
);
let current_value = profiles.current.clone();
perform_config_update(current_value, current_profile).await
let _ = Config::profiles().await.draft_mut().patch_config(profiles);
// 在调用内核前再次验证请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
Config::profiles().await.discard();
return Ok(false);
}
perform_config_update(current_sequence, current_value, current_profile).await
}
/// 根据profile name修改profiles
@@ -429,7 +570,7 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
// 保存修改前检查是否有更新 update_interval
let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_ref().get_item(&index)
&& let Some(new_option) = profile.option.as_ref()
{
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
@@ -468,7 +609,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
#[tauri::command]
pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let profiles_ref = profiles.latest_ref();
let file = profiles_ref
.get_item(&index)
.stringify_err()?
@@ -491,7 +632,7 @@ pub async fn view_profile(index: String) -> CmdResult {
pub async fn read_profile_file(index: String) -> CmdResult<String> {
let item = {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let profiles_ref = profiles.latest_ref();
PrfItem {
file: profiles_ref.get_item(&index).stringify_err()?.file.clone(),
..Default::default()

View File

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

View File

@@ -20,7 +20,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
// 在异步操作前获取必要元数据并释放锁
let (rel_path, is_merge_file) = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_arc();
let profiles_guard = profiles.latest_ref();
let item = profiles_guard.get_item(&index).stringify_err()?;
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
let path = item.file.clone().ok_or("file field is null")?;

View File

@@ -1,16 +1,16 @@
use super::CmdResult;
use crate::{
cmd::StringifyErr,
config::{Config, IVerge},
feat,
utils::draft::SharedBox,
};
use crate::{cmd::StringifyErr, config::*, feat};
/// 获取Verge配置
#[tauri::command]
pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge().await;
Ok(verge.latest_arc())
let verge_data = {
let ref_data = verge.latest_ref();
ref_data.clone()
};
let verge_response = IVergeResponse::from(verge_data);
Ok(verge_response)
}
/// 修改Verge配置

View File

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

View File

@@ -300,7 +300,7 @@ impl IClashTemp {
// 检查 enable_external_controller 设置,用于运行时配置生成
let enable_external_controller = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_external_controller
.unwrap_or(false);

View File

@@ -15,10 +15,10 @@ use tokio::sync::OnceCell;
use tokio::time::sleep;
pub struct Config {
clash_config: Draft<IClashTemp>,
verge_config: Draft<IVerge>,
profiles_config: Draft<IProfiles>,
runtime_config: Draft<IRuntime>,
clash_config: Draft<Box<IClashTemp>>,
verge_config: Draft<Box<IVerge>>,
profiles_config: Draft<Box<IProfiles>>,
runtime_config: Draft<Box<IRuntime>>,
}
impl Config {
@@ -27,28 +27,28 @@ impl Config {
CONFIG
.get_or_init(|| async {
Config {
clash_config: Draft::new(IClashTemp::new().await),
verge_config: Draft::new(IVerge::new().await),
profiles_config: Draft::new(IProfiles::new().await),
runtime_config: Draft::new(IRuntime::new()),
clash_config: Draft::from(Box::new(IClashTemp::new().await)),
verge_config: Draft::from(Box::new(IVerge::new().await)),
profiles_config: Draft::from(Box::new(IProfiles::new().await)),
runtime_config: Draft::from(Box::new(IRuntime::new())),
}
})
.await
}
pub async fn clash() -> Draft<IClashTemp> {
pub async fn clash() -> Draft<Box<IClashTemp>> {
Self::global().await.clash_config.clone()
}
pub async fn verge() -> Draft<IVerge> {
pub async fn verge() -> Draft<Box<IVerge>> {
Self::global().await.verge_config.clone()
}
pub async fn profiles() -> Draft<IProfiles> {
pub async fn profiles() -> Draft<Box<IProfiles>> {
Self::global().await.profiles_config.clone()
}
pub async fn runtime() -> Draft<IRuntime> {
pub async fn runtime() -> Draft<Box<IRuntime>> {
Self::global().await.runtime_config.clone()
}
@@ -61,14 +61,12 @@ impl Config {
&& service::is_service_available().await.is_err()
{
let verge = Config::verge().await;
verge.edit_draft(|d| {
d.enable_tun_mode = Some(false);
});
verge.draft_mut().enable_tun_mode = Some(false);
verge.apply();
let _ = tray::Tray::global().update_tray_display().await;
// 分离数据获取和异步调用避免Send问题
let verge_data = Config::verge().await.latest_arc();
let verge_data = Config::verge().await.latest_ref().clone();
logging_error!(Type::Core, verge_data.save_file().await);
}
@@ -85,11 +83,11 @@ impl Config {
// Ensure "Merge" and "Script" profile items exist, adding them if missing.
async fn ensure_default_profile_items() -> Result<()> {
let profiles = Self::profiles().await;
if profiles.latest_arc().get_item("Merge").is_err() {
if profiles.latest_ref().get_item("Merge").is_err() {
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
profiles_append_item_safe(merge_item).await?;
}
if profiles.latest_arc().get_item("Script").is_err() {
if profiles.latest_ref().get_item("Script").is_err() {
let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
profiles_append_item_safe(script_item).await?;
}
@@ -156,7 +154,7 @@ impl Config {
let runtime = Config::runtime().await;
let config = runtime
.latest_arc()
.latest_ref()
.config
.as_ref()
.ok_or_else(|| anyhow!("failed to get runtime config"))?
@@ -170,13 +168,11 @@ impl Config {
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
Config::runtime().await.edit_draft(|d| {
*d = IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
}
});
**Config::runtime().await.draft_mut() = IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
};
Ok(())
}
@@ -191,7 +187,7 @@ impl Config {
};
let operation = || async {
if Config::runtime().await.latest_arc().config.is_some() {
if Config::runtime().await.latest_ref().config.is_some() {
return Ok::<(), BackoffError<anyhow::Error>>(());
}
@@ -232,7 +228,7 @@ mod tests {
#[test]
#[allow(unused_variables)]
fn test_draft_size_non_boxed() {
let draft = Draft::new(IRuntime::new());
let draft = Draft::from(IRuntime::new());
let iruntime_size = std::mem::size_of_val(&draft);
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
}
@@ -240,7 +236,7 @@ mod tests {
#[test]
#[allow(unused_variables)]
fn test_draft_size_boxed() {
let draft = Draft::new(Box::new(IRuntime::new()));
let draft = Draft::from(Box::new(IRuntime::new()));
let box_iruntime_size = std::mem::size_of_val(&draft);
assert_eq!(
box_iruntime_size,

View File

@@ -69,16 +69,23 @@ impl IProfiles {
}
Err(err) => {
logging!(error, Type::Config, "{err}");
Self::default()
Self::template()
}
},
Err(err) => {
logging!(error, Type::Config, "{err}");
Self::default()
Self::template()
}
}
}
pub fn template() -> Self {
Self {
items: Some(vec![]),
..Self::default()
}
}
pub async fn save_file(&self) -> Result<()> {
help::save_yaml(
&dirs::profiles_path()?,
@@ -89,17 +96,17 @@ impl IProfiles {
}
/// 只修改currentvalid和chain
pub fn patch_config(&mut self, patch: &IProfiles) -> Result<()> {
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
if let Some(current) = &patch.current
if let Some(current) = patch.current
&& let Some(items) = self.items.as_ref()
{
let some_uid = Some(current);
if items.iter().any(|e| e.uid.as_ref() == some_uid) {
self.current = some_uid.cloned();
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
}

View File

@@ -313,9 +313,7 @@ impl IVerge {
);
let config_draft = Config::verge().await;
config_draft.edit_draft(|d| {
*d = updated_config;
});
**config_draft.draft_mut() = updated_config;
config_draft.apply();
Ok(())
@@ -698,3 +696,9 @@ impl From<IVerge> for IVergeResponse {
}
}
}
impl From<Box<IVerge>> for IVergeResponse {
fn from(verge: Box<IVerge>) -> Self {
IVergeResponse::from(*verge)
}
}

View File

@@ -6,8 +6,8 @@ use crate::{
utils::{dirs, logging::Type},
};
use anyhow::Error;
use arc_swap::{ArcSwap, ArcSwapOption};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reqwest_dav::list_cmd::{ListEntity, ListFile};
use smartstring::alias::String;
use std::{
@@ -56,24 +56,24 @@ impl Operation {
}
pub struct WebDavClient {
config: Arc<ArcSwapOption<WebDavConfig>>,
clients: Arc<ArcSwap<HashMap<Operation, reqwest_dav::Client>>>,
config: Arc<Mutex<Option<WebDavConfig>>>,
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
}
impl WebDavClient {
pub fn global() -> &'static WebDavClient {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
config: Arc::new(ArcSwapOption::new(None)),
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
config: Arc::new(Mutex::new(None)),
clients: Arc::new(Mutex::new(HashMap::new())),
})
}
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
// 先尝试从缓存获取
{
let clients_map = self.clients.load();
if let Some(client) = clients_map.get(&op) {
let clients = self.clients.lock();
if let Some(client) = clients.get(&op) {
return Ok(client.clone());
}
}
@@ -81,13 +81,13 @@ impl WebDavClient {
// 获取或创建配置
let config = {
// 首先检查是否已有配置
let existing_config = self.config.load();
let existing_config = self.config.lock().as_ref().cloned();
if let Some(cfg_arc) = existing_config.clone() {
(*cfg_arc).clone()
if let Some(cfg) = existing_config {
cfg
} else {
// 释放锁后获取异步配置
let verge = Config::verge().await.latest_arc();
let verge = Config::verge().await.latest_ref().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
@@ -99,17 +99,15 @@ impl WebDavClient {
let config = WebDavConfig {
url: verge
.webdav_url
.as_ref()
.cloned()
.unwrap_or_default()
.trim_end_matches('/')
.into(),
username: verge.webdav_username.as_ref().cloned().unwrap_or_default(),
password: verge.webdav_password.as_ref().cloned().unwrap_or_default(),
username: verge.webdav_username.unwrap_or_default(),
password: verge.webdav_password.unwrap_or_default(),
};
// 存储配置到 ArcSwapOption
self.config.store(Some(Arc::new(config.clone())));
// 重新获取锁并存储配置
*self.config.lock() = Some(config.clone());
config
}
};
@@ -163,19 +161,18 @@ impl WebDavClient {
}
}
// 缓存客户端(替换 Arc<Mutex<HashMap<...>>> 的写法)
// 缓存客户端
{
let mut map = (**self.clients.load()).clone();
map.insert(op, client.clone());
self.clients.store(map.into());
let mut clients = self.clients.lock();
clients.insert(op, client.clone());
}
Ok(client)
}
pub fn reset(&self) {
self.config.store(None);
self.clients.store(Arc::new(HashMap::new()));
*self.config.lock() = None;
self.clients.lock().clear();
}
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {

View File

@@ -387,7 +387,7 @@ impl EventDrivenProxyManager {
async fn get_proxy_config() -> ProxyConfig {
let (sys_enabled, pac_enabled, guard_enabled, guard_duration) = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_arc();
let verge = verge_config.latest_ref();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
@@ -406,7 +406,7 @@ impl EventDrivenProxyManager {
async fn get_expected_pac_config() -> Autoproxy {
let proxy_host = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_arc();
let verge = verge_config.latest_ref();
verge
.proxy_host
.clone()
@@ -424,13 +424,13 @@ impl EventDrivenProxyManager {
let (verge_mixed_port, proxy_host) = {
let verge_config = Config::verge().await;
let verge_ref = verge_config.latest_arc();
let verge_ref = verge_config.latest_ref();
(verge_ref.verge_mixed_port, verge_ref.proxy_host.clone())
};
let default_port = {
let clash_config = Config::clash().await;
clash_config.latest_arc().get_mixed_port()
clash_config.latest_ref().get_mixed_port()
};
let port = verge_mixed_port.unwrap_or(default_port);
@@ -450,7 +450,7 @@ impl EventDrivenProxyManager {
use crate::constants::bypass;
let verge_config = Config::verge().await;
let verge = verge_config.latest_arc();
let verge = verge_config.latest_ref();
let use_default = verge.use_default_bypass.unwrap_or(true);
let custom = verge.system_proxy_bypass.as_deref().unwrap_or("");

View File

@@ -5,7 +5,7 @@ use crate::{
singleton_with_logging, utils::logging::Type,
};
use anyhow::{Result, bail};
use arc_swap::ArcSwap;
use parking_lot::Mutex;
use smartstring::alias::String;
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
@@ -93,13 +93,13 @@ impl SystemHotkey {
}
pub struct Hotkey {
current: ArcSwap<Vec<String>>,
current: Arc<Mutex<Vec<String>>>,
}
impl Hotkey {
fn new() -> Self {
Self {
current: ArcSwap::new(Arc::new(Vec::new())),
current: Arc::new(Mutex::new(Vec::new())),
}
}
@@ -237,7 +237,7 @@ impl Hotkey {
let is_enable_global_hotkey = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
@@ -272,9 +272,9 @@ impl Hotkey {
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
impl Hotkey {
pub async fn init(&self, skip: bool) -> Result<()> {
pub async fn init(&self) -> Result<()> {
let verge = Config::verge().await;
let enable_global_hotkey = !skip && verge.latest_arc().enable_global_hotkey.unwrap_or(true);
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
logging!(
debug,
@@ -283,8 +283,12 @@ impl Hotkey {
enable_global_hotkey
);
if !enable_global_hotkey {
return Ok(());
}
// Extract hotkeys data before async operations
let hotkeys = verge.latest_arc().hotkeys.as_ref().cloned();
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
if let Some(hotkeys) = hotkeys {
logging!(
@@ -340,7 +344,7 @@ impl Hotkey {
}
}
}
self.current.store(Arc::new(hotkeys));
self.current.lock().clone_from(&hotkeys);
} else {
logging!(debug, Type::Hotkey, "No hotkeys configured");
}
@@ -371,8 +375,8 @@ impl Hotkey {
pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
// Extract current hotkeys before async operations
let current_hotkeys = &*self.current.load();
let old_map = Self::get_map_from_vec(current_hotkeys);
let current_hotkeys = self.current.lock().clone();
let old_map = Self::get_map_from_vec(&current_hotkeys);
let new_map = Self::get_map_from_vec(&new_hotkeys);
let (del, add) = Self::get_diff(old_map, new_map);
@@ -386,7 +390,7 @@ impl Hotkey {
}
// Update the current hotkeys after all async operations
self.current.store(Arc::new(new_hotkeys));
*self.current.lock() = new_hotkeys;
Ok(())
}

View File

@@ -17,15 +17,13 @@ impl CoreManager {
use crate::constants::files::RUNTIME_CONFIG;
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
let clash_config = &Config::clash().await.latest_arc().0;
let clash_config = Config::clash().await.latest_ref().0.clone();
Config::runtime().await.edit_draft(|d| {
*d = IRuntime {
config: Some(clash_config.to_owned()),
exists_keys: vec![],
chain_logs: Default::default(),
}
});
**Config::runtime().await.draft_mut() = IRuntime {
config: Some(clash_config.clone()),
exists_keys: vec![],
chain_logs: Default::default(),
};
help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?;
handle::Handle::notice_message(error_key, error_msg);
@@ -41,20 +39,25 @@ impl CoreManager {
return Ok((true, String::new()));
}
let _permit = self
.update_semaphore
.try_acquire()
.map_err(|_| anyhow!("Config update already in progress"))?;
self.perform_config_update().await
}
fn should_update_config(&self) -> Result<bool> {
let now = Instant::now();
let last = self.get_last_update();
let mut last = self.last_update.lock();
if let Some(last_time) = last
&& now.duration_since(*last_time) < timing::CONFIG_UPDATE_DEBOUNCE
if let Some(last_time) = *last
&& now.duration_since(last_time) < timing::CONFIG_UPDATE_DEBOUNCE
{
return Ok(false);
}
self.set_last_update(now);
*last = Some(now);
Ok(true)
}

View File

@@ -47,12 +47,10 @@ impl CoreManager {
return Err(format!("Invalid clash core: {}", clash_core).into());
}
Config::verge().await.edit_draft(|d| {
d.clash_core = Some(clash_core.to_owned());
});
Config::verge().await.draft_mut().clash_core = clash_core.to_owned().into();
Config::verge().await.apply();
let verge_data = Config::verge().await.latest_arc();
let verge_data = Config::verge().await.latest_ref().clone();
verge_data.save_file().await.map_err(|e| e.to_string())?;
let run_path = Config::generate_file(ConfigType::Run)
@@ -84,7 +82,7 @@ impl CoreManager {
let needs_service = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_tun_mode
.unwrap_or(false);

View File

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

View File

@@ -32,7 +32,7 @@ impl CoreManager {
let config_file = Config::generate_file(crate::config::ConfigType::Run).await?;
let app_handle = handle::Handle::app_handle();
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
let (mut rx, child) = app_handle
@@ -93,7 +93,8 @@ impl CoreManager {
defer! {
self.set_running_mode(RunningMode::NotRunning);
}
if let Some(child) = self.take_child_sidecar() {
let mut state = self.state.lock();
if let Some(child) = state.child_sidecar.take() {
let pid = child.pid();
drop(child);
logging!(trace, Type::Core, "Sidecar stopped (PID: {:?})", pid);

View File

@@ -353,7 +353,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
logging!(info, Type::Service, "尝试使用现有服务启动核心");
let verge_config = Config::verge().await;
let clash_core = verge_config.latest_arc().get_valid_clash_core();
let clash_core = verge_config.latest_ref().get_valid_clash_core();
drop(verge_config);
let bin_ext = if cfg!(windows) { ".exe" } else { "" };

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 {
let use_default = Config::verge()
.await
.latest_arc()
.latest_ref()
.use_default_bypass
.unwrap_or(true);
let res = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.system_proxy_bypass.clone()
};
let custom_bypass = match res {
@@ -124,17 +124,17 @@ impl Sysopt {
}
let port = {
let verge_port = Config::verge().await.latest_arc().verge_mixed_port;
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
match verge_port {
Some(port) => port,
None => Config::clash().await.latest_arc().get_mixed_port(),
None => Config::clash().await.latest_ref().get_mixed_port(),
}
};
let pac_port = IVerge::get_singleton_port();
let (sys_enable, pac_enable, proxy_host) = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
@@ -266,7 +266,7 @@ impl Sysopt {
/// update the startup
pub async fn update_launch(&self) -> Result<()> {
let enable_auto_launch = { Config::verge().await.latest_arc().enable_auto_launch };
let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch };
let is_enable = enable_auto_launch.unwrap_or(false);
logging!(
info,

View File

@@ -100,7 +100,7 @@ impl Timer {
// Collect profiles that need immediate update
let profiles_to_update =
if let Some(items) = Config::profiles().await.latest_arc().get_items() {
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
items
.iter()
.filter_map(|item| {
@@ -273,7 +273,7 @@ impl Timer {
async fn gen_map(&self) -> HashMap<String, u64> {
let mut new_map = HashMap::new();
if let Some(items) = Config::profiles().await.latest_arc().get_items() {
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
for item in items.iter() {
if let Some(option) = item.option.as_ref()
&& let Some(allow_auto_update) = option.allow_auto_update
@@ -427,7 +427,7 @@ impl Timer {
// Get the profile updated timestamp - now safe to await
let items = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_arc();
let profiles_guard = profiles.latest_ref();
match profiles_guard.get_items() {
Some(i) => i.clone(),
None => {
@@ -489,7 +489,7 @@ impl Timer {
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
Self::emit_update_event(uid, true);
let is_current = Config::profiles().await.latest_arc().current.as_ref() == Some(uid);
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(uid);
logging!(
info,
Type::Timer,

View File

@@ -86,7 +86,7 @@ pub struct Tray {
impl TrayState {
pub async fn get_common_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_arc();
let verge = Config::verge().await.latest_ref().clone();
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
if is_common_tray_icon
&& let Ok(Some(common_icon_path)) = find_target_icons("common")
@@ -123,7 +123,7 @@ impl TrayState {
}
pub async fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_arc();
let verge = Config::verge().await.latest_ref().clone();
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
if is_sysproxy_tray_icon
&& let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy")
@@ -160,7 +160,7 @@ impl TrayState {
}
pub async fn get_tun_tray_icon() -> (bool, Vec<u8>) {
let verge = Config::verge().await.latest_arc();
let verge = Config::verge().await.latest_ref().clone();
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
if is_tun_tray_icon
&& let Ok(Some(tun_icon_path)) = find_target_icons("tun")
@@ -243,7 +243,7 @@ impl Tray {
}
let app_handle = handle::Handle::app_handle();
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event = tray_event.unwrap_or_else(|| "main_window".into());
let tray = app_handle
.tray_by_id("main")
@@ -303,7 +303,7 @@ impl Tray {
}
async fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
let verge = Config::verge().await.latest_arc();
let verge = Config::verge().await.latest_ref().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let tun_mode_available = cmd::system::is_admin().unwrap_or_default()
@@ -311,7 +311,7 @@ impl Tray {
let mode = {
Config::clash()
.await
.latest_arc()
.latest_ref()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
@@ -320,7 +320,7 @@ impl Tray {
};
let profile_uid_and_name = Config::profiles()
.await
.latest_arc()
.data_mut()
.all_profile_uid_and_name()
.unwrap_or_default();
let is_lightweight_mode = is_in_lightweight_mode();
@@ -375,7 +375,7 @@ impl Tray {
}
};
let verge = Config::verge().await.latest_arc();
let verge = Config::verge().await.latest_ref().clone();
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -418,7 +418,7 @@ impl Tray {
}
};
let verge = Config::verge().await.latest_arc();
let verge = Config::verge().await.latest_ref().clone();
let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -460,7 +460,7 @@ impl Tray {
let app_handle = handle::Handle::app_handle();
let verge = Config::verge().await.latest_arc();
let verge = Config::verge().await.latest_ref().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -474,7 +474,7 @@ impl Tray {
let mut current_profile_name = "None".into();
{
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
if let Some(current_profile_uid) = profiles.get_current()
&& let Ok(profile) = profiles.get_item(&current_profile_uid)
{
@@ -552,7 +552,7 @@ impl Tray {
#[cfg(any(target_os = "macos", target_os = "windows"))]
let show_menu_on_left_click = {
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
tray_event.as_str() == "tray_menu"
};
@@ -583,7 +583,7 @@ impl Tray {
}
AsyncHandler::spawn(|| async move {
let tray_event = { Config::verge().await.latest_arc().tray_event.clone() };
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into());
logging!(debug, Type::Tray, "tray event: {tray_event:?}");
@@ -675,7 +675,7 @@ async fn create_profile_menu_item(
async move {
let is_current_profile = Config::profiles()
.await
.latest_arc()
.latest_ref()
.is_current_profile_index(profile_uid);
CheckMenuItem::with_id(
&app_handle,
@@ -878,7 +878,7 @@ async fn create_tray_menu(
// 获取当前配置文件的选中代理组信息
let current_profile_selected = {
let profiles_config = Config::profiles().await;
let profiles_ref = profiles_config.latest_arc();
let profiles_ref = profiles_config.latest_ref();
profiles_ref
.get_current()
.and_then(|uid| profiles_ref.get_item(&uid).ok())
@@ -924,7 +924,7 @@ async fn create_tray_menu(
.collect::<HashMap<String, usize>>()
});
let verge_settings = Config::verge().await.latest_arc();
let verge_settings = Config::verge().await.latest_ref().clone();
let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false);
let version = env!("CARGO_PKG_VERSION");

View File

@@ -266,7 +266,7 @@ impl CoreConfigValidator {
logging!(info, Type::Validate, "开始验证配置文件: {}", config_path);
let clash_core = Config::verge().await.latest_arc().get_valid_clash_core();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
logging!(info, Type::Validate, "使用内核: {}", clash_core);
let app_handle = handle::Handle::app_handle();

View File

@@ -45,11 +45,11 @@ struct ProfileItems {
}
async fn get_config_values() -> ConfigValues {
let clash_config = { Config::clash().await.latest_arc().0.clone() };
let clash_config = { Config::clash().await.latest_ref().0.clone() };
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
(
Some(verge.get_valid_clash_core()),
verge.enable_tun_mode.unwrap_or(false),
@@ -63,14 +63,14 @@ async fn get_config_values() -> ConfigValues {
#[cfg(not(target_os = "windows"))]
let redir_enabled = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.verge_redir_enabled.unwrap_or(false)
};
#[cfg(target_os = "linux")]
let tproxy_enabled = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.verge_tproxy_enabled.unwrap_or(false)
};
@@ -103,12 +103,12 @@ async fn collect_profile_items() -> ProfileItems {
) = {
let current = {
let profiles = Config::profiles().await;
let profiles_clone = profiles.latest_arc();
let profiles_clone = profiles.latest_ref().clone();
profiles_clone.current_mapping().await.unwrap_or_default()
};
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let profiles_ref = profiles.latest_ref();
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
let script_uid = profiles_ref.current_script().unwrap_or_default();
@@ -139,7 +139,7 @@ async fn collect_profile_items() -> ProfileItems {
let merge_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(merge_uid).ok().cloned()
};
if let Some(item) = item {
@@ -156,7 +156,7 @@ async fn collect_profile_items() -> ProfileItems {
let script_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(script_uid).ok().cloned()
};
if let Some(item) = item {
@@ -173,7 +173,7 @@ async fn collect_profile_items() -> ProfileItems {
let rules_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(rules_uid).ok().cloned()
};
if let Some(item) = item {
@@ -190,7 +190,7 @@ async fn collect_profile_items() -> ProfileItems {
let proxies_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(proxies_uid).ok().cloned()
};
if let Some(item) = item {
@@ -207,7 +207,7 @@ async fn collect_profile_items() -> ProfileItems {
let groups_item = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item(groups_uid).ok().cloned()
};
if let Some(item) = item {
@@ -224,7 +224,7 @@ async fn collect_profile_items() -> ProfileItems {
let global_merge = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item("Merge").ok().cloned()
};
if let Some(item) = item {
@@ -241,7 +241,7 @@ async fn collect_profile_items() -> ProfileItems {
let global_script = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
profiles.get_item("Script").ok().cloned()
};
if let Some(item) = item {
@@ -394,7 +394,7 @@ async fn merge_default_config(
if key.as_str() == Some("external-controller") {
let enable_external_controller = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_external_controller
.unwrap_or(false);

View File

@@ -78,7 +78,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
/// Restore WebDAV backup
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge().await;
let verge_data = verge.latest_arc();
let verge_data = verge.latest_ref().clone();
let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
@@ -241,15 +241,11 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
return Err(anyhow!("Backup file not found: {}", filename));
}
let (webdav_url, webdav_username, webdav_password) = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
(
verge.webdav_url.clone(),
verge.webdav_username.clone(),
verge.webdav_password.clone(),
)
};
let verge = Config::verge().await;
let verge_data = verge.latest_ref().clone();
let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
let mut zip = zip::ZipArchive::new(file)?;

View File

@@ -72,12 +72,10 @@ pub async fn change_clash_mode(mode: String) {
{
Ok(_) => {
// 更新订阅
Config::clash()
.await
.edit_draft(|d| d.patch_config(mapping));
Config::clash().await.data_mut().patch_config(mapping);
// 分离数据获取和异步调用
let clash_data = Config::clash().await.data_arc();
let clash_data = Config::clash().await.data_mut().clone();
if clash_data.save_config().await.is_ok() {
handle::Handle::refresh_clash();
logging_error!(Type::Tray, tray::Tray::global().update_menu().await);
@@ -86,7 +84,7 @@ pub async fn change_clash_mode(mode: String) {
let is_auto_close_connection = Config::verge()
.await
.data_arc()
.data_mut()
.auto_close_connection
.unwrap_or(false);
if is_auto_close_connection {
@@ -104,7 +102,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
let tun_mode = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_tun_mode
.unwrap_or(false);

View File

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

View File

@@ -28,7 +28,7 @@ async fn should_update_profile(
ignore_auto_update: bool,
) -> Result<Option<(String, Option<PrfOption>)>> {
let profiles = Config::profiles().await;
let profiles = profiles.latest_arc();
let profiles = profiles.latest_ref();
let item = profiles.get_item(uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
@@ -89,12 +89,12 @@ async fn perform_profile_update(
let mut merged_opt = PrfOption::merge(opt, option);
let is_current = {
let profiles = Config::profiles().await;
profiles.latest_arc().is_current_profile_index(uid)
profiles.latest_ref().is_current_profile_index(uid)
};
let profile_name = {
let profiles = Config::profiles().await;
profiles
.latest_arc()
.latest_ref()
.get_name_by_uid(uid)
.unwrap_or_default()
};

View File

@@ -10,8 +10,8 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
/// Toggle system proxy on/off
pub async fn toggle_system_proxy() {
let verge = Config::verge().await;
let enable = verge.latest_arc().enable_system_proxy.unwrap_or(false);
let auto_close_connection = verge.latest_arc().auto_close_connection.unwrap_or(false);
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false);
let auto_close_connection = verge.latest_ref().auto_close_connection.unwrap_or(false);
// 如果当前系统代理即将关闭且自动关闭连接设置为true则关闭所有连接
if enable
@@ -42,7 +42,7 @@ pub async fn toggle_system_proxy() {
/// Toggle TUN mode on/off
pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().await.latest_arc().enable_tun_mode;
let enable = Config::verge().await.data_mut().enable_tun_mode;
let enable = enable.unwrap_or(false);
match super::patch_verge(
@@ -66,7 +66,7 @@ pub async fn copy_clash_env() {
Ok(ip) => ip.into(),
Err(_) => Config::verge()
.await
.latest_arc()
.latest_ref()
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".into()),
@@ -76,7 +76,7 @@ pub async fn copy_clash_env() {
let port = {
Config::verge()
.await
.latest_arc()
.latest_ref()
.verge_mixed_port
.unwrap_or(7897)
};
@@ -84,7 +84,7 @@ pub async fn copy_clash_env() {
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
let cliboard = app_handle.clipboard();
let env_type = { Config::verge().await.latest_arc().env_type.clone() };
let env_type = { Config::verge().await.latest_ref().env_type.clone() };
let env_type = match env_type {
Some(env_type) => env_type,
None => {

View File

@@ -47,7 +47,7 @@ pub async fn clean_async() -> bool {
let tun_task = async {
let tun_enabled = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_tun_mode
.unwrap_or(false);
@@ -100,7 +100,7 @@ pub async fn clean_async() -> bool {
// 检查系统代理是否开启
let sys_proxy_enabled = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_system_proxy
.unwrap_or(false);
@@ -176,7 +176,7 @@ pub async fn clean_async() -> bool {
{
let sys_proxy_enabled = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_system_proxy
.unwrap_or(false);
@@ -316,7 +316,7 @@ pub async fn hide() {
let enable_auto_light_weight_mode = Config::verge()
.await
.latest_arc()
.data_mut()
.enable_auto_light_weight_mode
.unwrap_or(false);

View File

@@ -319,7 +319,7 @@ pub fn run() {
AsyncHandler::spawn(move || async move {
let is_enable_global_hotkey = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
@@ -334,7 +334,10 @@ pub fn run() {
.register_system_hotkey(SystemHotkey::CmdW)
.await;
}
let _ = hotkey::Hotkey::global().init(true).await;
if !is_enable_global_hotkey {
let _ = hotkey::Hotkey::global().init().await;
}
return;
}
@@ -355,18 +358,8 @@ pub fn run() {
#[cfg(target_os = "macos")]
{
use crate::core::hotkey::SystemHotkey;
AsyncHandler::spawn(move || async move {
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
let is_enable_global_hotkey = Config::verge()
.await
.latest_arc()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
let _ = hotkey::Hotkey::global().reset();
}
});
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ);
let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW);
}
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
config::Config,
core::{handle, timer::Timer, tray::Tray},
core::{handle, timer::Timer},
log_err, logging,
process::AsyncHandler,
utils::logging::Type,
@@ -78,20 +78,14 @@ pub fn is_in_lightweight_mode() -> bool {
get_state() == LightweightState::In
}
async fn refresh_lightweight_tray_state() {
if let Err(err) = Tray::global().update_tray_display().await {
logging!(warn, Type::Lightweight, "更新托盘轻量模式状态失败: {err}");
}
}
pub async fn auto_lightweight_boot() -> Result<()> {
let verge_config = Config::verge().await;
let enable_auto = verge_config
.latest_arc()
.data_mut()
.enable_auto_light_weight_mode
.unwrap_or(false);
let is_silent_start = verge_config
.latest_arc()
.latest_ref()
.enable_silent_start
.unwrap_or(false);
@@ -136,13 +130,11 @@ pub fn disable_auto_light_weight_mode() {
pub async fn entry_lightweight_mode() -> bool {
if !try_transition(LightweightState::Normal, LightweightState::In) {
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用");
refresh_lightweight_tray_state().await;
return false;
}
record_state_and_log(LightweightState::In);
WindowManager::destroy_main_window();
let _ = cancel_light_weight_timer();
refresh_lightweight_tray_state().await;
true
}
@@ -153,14 +145,12 @@ pub async fn exit_lightweight_mode() -> bool {
Type::Lightweight,
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
);
refresh_lightweight_tray_state().await;
return false;
}
record_state_and_log(LightweightState::Exiting);
WindowManager::show_main_window().await;
let _ = cancel_light_weight_timer();
record_state_and_log(LightweightState::Normal);
refresh_lightweight_tray_state().await;
true
}
@@ -236,7 +226,7 @@ async fn setup_light_weight_timer() -> Result<()> {
let once_by_minutes = Config::verge()
.await
.latest_arc()
.latest_ref()
.auto_light_weight_minutes
.unwrap_or(10);

View File

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

View File

@@ -43,7 +43,7 @@ pub fn get_supported_languages() -> Vec<String> {
pub async fn current_language() -> String {
Config::verge()
.await
.latest_arc()
.latest_ref()
.language
.as_deref()
.map(String::from)

View File

@@ -31,7 +31,7 @@ pub async fn init_logger() -> Result<()> {
// TODO 提供 runtime 级别实时修改
let (log_level, log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_arc();
let verge = verge_guard.latest_ref();
(
verge.get_log_level(),
verge.app_log_max_size.unwrap_or(128),
@@ -81,7 +81,7 @@ pub async fn init_logger() -> Result<()> {
pub async fn sidecar_writer() -> Result<FileLogWriter> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_arc();
let verge = verge_guard.latest_ref();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
@@ -109,7 +109,7 @@ pub async fn sidecar_writer() -> Result<FileLogWriter> {
pub async fn service_writer_config() -> Result<WriterConfig> {
let (log_max_size, log_max_count) = {
let verge_guard = Config::verge().await;
let verge = verge_guard.latest_arc();
let verge = verge_guard.latest_ref();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
@@ -134,7 +134,7 @@ pub async fn delete_log() -> Result<()> {
let auto_log_clean = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.auto_log_clean.unwrap_or(0)
};
@@ -365,7 +365,7 @@ async fn initialize_config_files() -> Result<()> {
if let Ok(path) = dirs::profiles_path()
&& !path.exists()
{
let template = IProfiles::default();
let template = IProfiles::template();
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
@@ -509,7 +509,7 @@ pub async fn startup_script() -> Result<()> {
let app_handle = handle::Handle::app_handle();
let script_path = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
let verge = verge.latest_ref();
verge.startup_script.clone().unwrap_or_else(|| "".into())
};

View File

@@ -165,10 +165,10 @@ impl NetworkManager {
ProxyType::None => None,
ProxyType::Localhost => {
let port = {
let verge_port = Config::verge().await.latest_arc().verge_mixed_port;
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
match verge_port {
Some(port) => port,
None => Config::clash().await.latest_arc().get_mixed_port(),
None => Config::clash().await.latest_ref().get_mixed_port(),
}
};
let proxy_scheme = format!("http://127.0.0.1:{port}");

View File

@@ -120,7 +120,7 @@ pub(super) async fn init_timer() {
}
pub(super) async fn init_hotkey() {
logging_error!(Type::Setup, Hotkey::global().init(false).await);
logging_error!(Type::Setup, Hotkey::global().init().await);
}
pub(super) async fn init_auto_lightweight_boot() {
@@ -179,7 +179,7 @@ pub(super) async fn refresh_tray_menu() {
pub(super) async fn init_window() {
let is_silent_start = Config::verge()
.await
.latest_arc()
.latest_ref()
.enable_silent_start
.unwrap_or(false);
#[cfg(target_os = "macos")]

View File

@@ -22,7 +22,7 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
let app_handle = handle::Handle::app_handle();
let config = Config::verge().await;
let latest = config.latest_arc();
let latest = config.latest_ref();
let start_page = latest.start_page.as_deref().unwrap_or("/");
match tauri::WebviewWindowBuilder::new(

View File

@@ -89,15 +89,15 @@ pub fn embed_server() {
let clash_config = Config::clash().await;
let pac_content = verge_config
.latest_arc()
.latest_ref()
.pac_file_content
.clone()
.unwrap_or_else(|| DEFAULT_PAC.into());
let pac_port = verge_config
.latest_arc()
.latest_ref()
.verge_mixed_port
.unwrap_or_else(|| clash_config.latest_arc().get_mixed_port());
.unwrap_or_else(|| clash_config.latest_ref().get_mixed_port());
let pac = warp::path!("commands" / "pac").map(move || {
let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}"));

View File

@@ -3,7 +3,7 @@ import { ErrorBoundary, FallbackProps } from "react-error-boundary";
function ErrorFallback({ error }: FallbackProps) {
return (
<div role="alert" style={{ padding: 16 }}>
<div role="alert" style={{ padding: 16, height: "100%", overflow: "auto" }}>
<h4>Something went wrong:(</h4>
<pre>{error.message}</pre>

View File

@@ -1,13 +1,16 @@
import {
DataGrid,
GridActionsCellItem,
GridCloseIcon,
GridColDef,
GridColumnResizeParams,
useGridApiRef,
} from "@mui/x-data-grid";
import dayjs from "dayjs";
import { useLocalStorage } from "foxact/use-local-storage";
import { useLayoutEffect, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { closeConnections } from "tauri-plugin-mihomo-api";
import parseTraffic from "@/utils/parse-traffic";
import { truncateStr } from "@/utils/truncate-str";
@@ -21,129 +24,6 @@ export const ConnectionTable = (props: Props) => {
const { connections, onShowDetail } = props;
const { t } = useTranslation();
const apiRef = useGridApiRef();
useLayoutEffect(() => {
const PATCH_FLAG_KEY = "__clashPatchedPublishEvent" as const;
const ORIGINAL_KEY = "__clashOriginalPublishEvent" as const;
let isUnmounted = false;
let retryHandle: ReturnType<typeof setTimeout> | null = null;
let cleanupOriginal: (() => void) | null = null;
const scheduleRetry = () => {
if (isUnmounted || retryHandle !== null) return;
retryHandle = setTimeout(() => {
retryHandle = null;
ensurePatched();
}, 16);
};
// Safari occasionally emits grid events without an event object,
// and MUI expects `defaultMuiPrevented` to exist. Normalize here to avoid crashes.
const createFallbackEvent = () => {
const fallback = {
defaultMuiPrevented: false,
preventDefault() {
fallback.defaultMuiPrevented = true;
},
};
return fallback;
};
const ensureMuiEvent = (
value: unknown,
): {
defaultMuiPrevented: boolean;
preventDefault: () => void;
[key: string]: unknown;
} => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return createFallbackEvent();
}
const eventObject = value as {
defaultMuiPrevented?: unknown;
preventDefault?: () => void;
[key: string]: unknown;
};
if (typeof eventObject.defaultMuiPrevented !== "boolean") {
eventObject.defaultMuiPrevented = false;
}
if (typeof eventObject.preventDefault !== "function") {
eventObject.preventDefault = () => {
eventObject.defaultMuiPrevented = true;
};
}
return eventObject as {
defaultMuiPrevented: boolean;
preventDefault: () => void;
[key: string]: unknown;
};
};
const ensurePatched = () => {
if (isUnmounted) return;
const api = apiRef.current;
if (!api?.publishEvent) {
scheduleRetry();
return;
}
const metadataApi = api as unknown as typeof api &
Record<string, unknown>;
if (metadataApi[PATCH_FLAG_KEY] === true) return;
const originalPublishEvent = api.publishEvent;
// Use Proxy to create a more resilient wrapper that always normalizes events
const patchedPublishEvent = new Proxy(originalPublishEvent, {
apply(target, thisArg, rawArgs: unknown[]) {
rawArgs[2] = ensureMuiEvent(rawArgs[2]);
return Reflect.apply(
target as (...args: unknown[]) => unknown,
thisArg,
rawArgs,
);
},
}) as typeof originalPublishEvent;
api.publishEvent = patchedPublishEvent;
metadataApi[PATCH_FLAG_KEY] = true;
metadataApi[ORIGINAL_KEY] = originalPublishEvent;
cleanupOriginal = () => {
const storedOriginal = metadataApi[ORIGINAL_KEY] as
| typeof originalPublishEvent
| undefined;
api.publishEvent = (
typeof storedOriginal === "function"
? storedOriginal
: originalPublishEvent
) as typeof originalPublishEvent;
delete metadataApi[PATCH_FLAG_KEY];
delete metadataApi[ORIGINAL_KEY];
};
};
ensurePatched();
return () => {
isUnmounted = true;
if (retryHandle !== null) {
clearTimeout(retryHandle);
retryHandle = null;
}
if (cleanupOriginal) {
cleanupOriginal();
cleanupOriginal = null;
}
};
}, [apiRef]);
const [columnVisible, setColumnVisible] = useState<
Partial<Record<keyof IConnectionsItem, boolean>>
@@ -160,6 +40,29 @@ export const ConnectionTable = (props: Props) => {
const columns = useMemo<GridColDef[]>(() => {
return [
{
field: "actions",
type: "actions",
width: 30,
className: "actions",
getActions: ({ id }) => {
return [
<GridActionsCellItem
key={id.toString()}
icon={<GridCloseIcon />}
label="Close"
onClick={() => closeConnections(id.toString())}
color="inherit"
/>,
];
},
},
{
field: "type",
headerName: t("Type"),
width: columnWidths["type"] || 100,
minWidth: 100,
},
{
field: "host",
headerName: t("Host"),
@@ -239,12 +142,6 @@ export const ConnectionTable = (props: Props) => {
width: columnWidths["remoteDestination"] || 200,
minWidth: 130,
},
{
field: "type",
headerName: t("Type"),
width: columnWidths["type"] || 160,
minWidth: 100,
},
];
}, [columnWidths, t]);
@@ -266,6 +163,7 @@ export const ConnectionTable = (props: Props) => {
? `${metadata.destinationIP}:${metadata.destinationPort}`
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
return {
type: `${metadata.type}(${metadata.network})`,
id: each.id,
host: metadata.host
? `${metadata.host}:${metadata.destinationPort}`
@@ -280,7 +178,6 @@ export const ConnectionTable = (props: Props) => {
time: each.start,
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
remoteDestination: Destination,
type: `${metadata.type}(${metadata.network})`,
connectionData: each,
};
});
@@ -289,7 +186,6 @@ export const ConnectionTable = (props: Props) => {
return (
<DataGrid
apiRef={apiRef}
hideFooter
rows={connRows}
columns={columns}
onRowClick={(e) => onShowDetail(e.row.connectionData)}

View File

@@ -280,6 +280,7 @@ interface IProfileOption {
interface IProfilesConfig {
current?: string;
valid?: string[];
items?: IProfileItem[];
}