Update On Sun Oct 12 20:34:11 CEST 2025

This commit is contained in:
github-action[bot]
2025-10-12 20:34:12 +02:00
parent edd1a6a4f7
commit 25bca36c4c
173 changed files with 5800 additions and 6226 deletions
+1
View File
@@ -1148,3 +1148,4 @@ Update On Wed Oct 8 20:42:18 CEST 2025
Update On Thu Oct 9 20:40:34 CEST 2025
Update On Fri Oct 10 20:41:18 CEST 2025
Update On Sat Oct 11 20:34:43 CEST 2025
Update On Sun Oct 12 20:34:03 CEST 2025
+22 -22
View File
@@ -346,7 +346,7 @@ dependencies = [
"objc2-foundation 0.3.2",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
"wl-clipboard-rs",
"x11rb",
]
@@ -456,9 +456,9 @@ dependencies = [
[[package]]
name = "async-fs"
version = "2.1.3"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50"
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
dependencies = [
"async-lock",
"blocking",
@@ -1292,11 +1292,11 @@ dependencies = [
[[package]]
name = "camino"
version = "1.1.12"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5"
checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
dependencies = [
"serde",
"serde_core",
]
[[package]]
@@ -1688,7 +1688,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -2294,7 +2294,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.61.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2803,7 +2803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -4923,7 +4923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.53.3",
"windows-targets 0.48.5",
]
[[package]]
@@ -6369,12 +6369,12 @@ dependencies = [
[[package]]
name = "os_pipe"
version = "1.2.2"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.45.0",
]
[[package]]
@@ -7328,7 +7328,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -7546,9 +7546,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]]
name = "redb"
version = "3.0.2"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb1bfd2a09cb3c362dd10ea63427315cf3c79a84feb279394509981c4a3a91c"
checksum = "ae323eb086579a3769daa2c753bb96deb95993c534711e0dbe881b5192906a06"
dependencies = [
"libc",
]
@@ -7882,7 +7882,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -7895,7 +7895,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -9565,15 +9565,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.22.0"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.8",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -11310,7 +11310,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -19,7 +19,7 @@
"@mui/icons-material": "7.3.4",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "7.3.4",
"@mui/x-date-pickers": "8.12.0",
"@mui/x-date-pickers": "8.14.0",
"@nyanpasu/interface": "workspace:^",
"@nyanpasu/ui": "workspace:^",
"@tailwindcss/postcss": "4.1.14",
+37 -28
View File
@@ -240,8 +240,8 @@ importers:
specifier: 7.3.4
version: 7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@mui/x-date-pickers':
specifier: 8.12.0
version: 8.12.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
specifier: 8.14.0
version: 8.14.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@nyanpasu/interface':
specifier: workspace:^
version: link:../interface
@@ -289,7 +289,7 @@ importers:
version: 0.4.0
material-react-table:
specifier: npm:@greenhat616/material-react-table@4.0.0
version: '@greenhat616/material-react-table@4.0.0(db1adb8c2b3e72d3c5c81efe3e7a3e75)'
version: '@greenhat616/material-react-table@4.0.0(fafcd4476b8f739264ca8e438395b4cd)'
monaco-editor:
specifier: 0.54.0
version: 0.54.0
@@ -310,7 +310,7 @@ importers:
version: 1.6.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-hook-form-mui:
specifier: 8.0.0
version: 8.0.0(507fe0d2a0205cfe15d8e5dc9d4d832e)
version: 8.0.0(8428d39c3121399e2a450601900b76c4)
react-i18next:
specifier: 15.7.4
version: 15.7.4(i18next@25.5.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)
@@ -2061,8 +2061,8 @@ packages:
'@types/react':
optional: true
'@mui/x-date-pickers@8.12.0':
resolution: {integrity: sha512-CDcjdBNwMcTy3flZTCKZqSUS6deBFGKLqy3Vl6bgr5KTo8Vky2v+S+zNi56fv23Qs+P47GwpILcm3QZt/0BP0A==}
'@mui/x-date-pickers@8.14.0':
resolution: {integrity: sha512-fz8z1hoi1tbG1QUcZAkQdiO3GsCOpUeRfyANXDDzDN88L4VqwNEyrv0wmzmCfIX2sgur4gWwWJzMuCvauMgXRw==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.9.0
@@ -2098,8 +2098,8 @@ packages:
moment-jalaali:
optional: true
'@mui/x-internals@8.12.0':
resolution: {integrity: sha512-KCZgFHwuPg0v8I2gpjeC6k3eDRXPPX8RIGSNDXe8zSZ8dAw+p6Q2pzT9kKvctqCXSFK8ct/5YQwqx8Quhs8Ndg==}
'@mui/x-internals@8.14.0':
resolution: {integrity: sha512-esYyl61nuuFXiN631TWuPh2tqdoyTdBI/4UXgwH3rytF8jiWvy6prPBPRHEH1nvW3fgw9FoBI48FlOO+yEI8xg==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -8404,6 +8404,11 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
utf-8-validate@5.0.10:
resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
engines: {node: '>=6.14.2'}
@@ -10220,13 +10225,13 @@ snapshots:
'@fastify/busboy@2.1.1': {}
'@greenhat616/material-react-table@4.0.0(db1adb8c2b3e72d3c5c81efe3e7a3e75)':
'@greenhat616/material-react-table@4.0.0(fafcd4476b8f739264ca8e438395b4cd)':
dependencies:
'@emotion/react': 11.14.0(@types/react@19.1.14)(react@19.1.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1)
'@mui/icons-material': 7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react@19.1.1)
'@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@mui/x-date-pickers': 8.12.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@mui/x-date-pickers': 8.14.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tanstack/match-sorter-utils': 8.19.4
'@tanstack/react-table': 8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tanstack/react-virtual': 3.13.9(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -10415,8 +10420,8 @@ snapshots:
'@mui/private-theming@7.3.2(@types/react@19.1.14)(react@19.1.1)':
dependencies:
'@babel/runtime': 7.28.3
'@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1)
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.3(@types/react@19.1.14)(react@19.1.1)
prop-types: 15.8.1
react: 19.1.1
optionalDependencies:
@@ -10433,7 +10438,7 @@ snapshots:
'@mui/styled-engine@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(react@19.1.1)':
dependencies:
'@babel/runtime': 7.28.3
'@babel/runtime': 7.28.4
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/sheet': 1.4.0
@@ -10459,11 +10464,11 @@ snapshots:
'@mui/system@7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1)':
dependencies:
'@babel/runtime': 7.28.3
'@babel/runtime': 7.28.4
'@mui/private-theming': 7.3.2(@types/react@19.1.14)(react@19.1.1)
'@mui/styled-engine': 7.3.2(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(react@19.1.1)
'@mui/types': 7.4.6(@types/react@19.1.14)
'@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1)
'@mui/utils': 7.3.3(@types/react@19.1.14)(react@19.1.1)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
@@ -10491,7 +10496,7 @@ snapshots:
'@mui/types@7.4.6(@types/react@19.1.14)':
dependencies:
'@babel/runtime': 7.28.3
'@babel/runtime': 7.28.4
optionalDependencies:
'@types/react': 19.1.14
@@ -10503,7 +10508,7 @@ snapshots:
'@mui/utils@7.3.2(@types/react@19.1.14)(react@19.1.1)':
dependencies:
'@babel/runtime': 7.28.3
'@babel/runtime': 7.28.4
'@mui/types': 7.4.6(@types/react@19.1.14)
'@types/prop-types': 15.7.15
clsx: 2.1.1
@@ -10525,13 +10530,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.14
'@mui/x-date-pickers@8.12.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
'@mui/x-date-pickers@8.14.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@babel/runtime': 7.28.3
'@babel/runtime': 7.28.4
'@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@mui/system': 7.3.3(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1)
'@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1)
'@mui/x-internals': 8.12.0(@types/react@19.1.14)(react@19.1.1)
'@mui/utils': 7.3.3(@types/react@19.1.14)(react@19.1.1)
'@mui/x-internals': 8.14.0(@types/react@19.1.14)(react@19.1.1)
'@types/react-transition-group': 4.4.12(@types/react@19.1.14)
clsx: 2.1.1
prop-types: 15.8.1
@@ -10545,13 +10550,13 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
'@mui/x-internals@8.12.0(@types/react@19.1.14)(react@19.1.1)':
'@mui/x-internals@8.14.0(@types/react@19.1.14)(react@19.1.1)':
dependencies:
'@babel/runtime': 7.28.3
'@mui/utils': 7.3.2(@types/react@19.1.14)(react@19.1.1)
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.3(@types/react@19.1.14)(react@19.1.1)
react: 19.1.1
reselect: 5.1.1
use-sync-external-store: 1.5.0(react@19.1.1)
use-sync-external-store: 1.6.0(react@19.1.1)
transitivePeerDependencies:
- '@types/react'
@@ -16100,14 +16105,14 @@ snapshots:
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
react-hook-form-mui@8.0.0(507fe0d2a0205cfe15d8e5dc9d4d832e):
react-hook-form-mui@8.0.0(8428d39c3121399e2a450601900b76c4):
dependencies:
'@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react: 19.1.1
react-hook-form: 7.52.1(react@19.1.1)
optionalDependencies:
'@mui/icons-material': 7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@types/react@19.1.14)(react@19.1.1)
'@mui/x-date-pickers': 8.12.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@mui/x-date-pickers': 8.14.0(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(react@19.1.1))(@types/react@19.1.14)(dayjs@1.11.18)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-hook-form@7.52.1(react@19.1.1):
dependencies:
@@ -16408,7 +16413,7 @@ snapshots:
rtl-css-js@1.16.1:
dependencies:
'@babel/runtime': 7.28.3
'@babel/runtime': 7.28.4
run-parallel@1.2.0:
dependencies:
@@ -17496,6 +17501,10 @@ snapshots:
dependencies:
react: 19.1.1
use-sync-external-store@1.6.0(react@19.1.1):
dependencies:
react: 19.1.1
utf-8-validate@5.0.10:
dependencies:
node-gyp-build: 4.8.1
+1
View File
@@ -25,6 +25,7 @@ env:
TAG_CHANNEL: Alpha
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
HUSKY: 0
concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
+1
View File
@@ -11,6 +11,7 @@ env:
TAG_CHANNEL: AutoBuild
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
HUSKY: 0
concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+3
View File
@@ -9,6 +9,9 @@ on:
permissions:
contents: read
env:
HUSKY: 0
jobs:
cargo-check:
# Treat all Rust compiler warnings as errors
+1
View File
@@ -30,6 +30,7 @@ env:
TAG_CHANNEL: DeployTest
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
HUSKY: 0
concurrency:
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+3
View File
@@ -7,6 +7,9 @@ name: Check Formatting
on:
pull_request:
env:
HUSKY: 0
jobs:
rustfmt:
runs-on: ubuntu-latest
+2
View File
@@ -3,6 +3,8 @@ name: Clippy Lint
on:
pull_request:
workflow_dispatch:
env:
HUSKY: 0
jobs:
clippy:
+1
View File
@@ -11,6 +11,7 @@ permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
HUSKY: 0
concurrency:
# only allow per workflow per commit (and not pr) to run at a time
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
+3
View File
@@ -2,6 +2,9 @@ name: Updater CI
on: workflow_dispatch
permissions: write-all
env:
HUSKY: 0
jobs:
release-update:
runs-on: ubuntu-latest
+15 -17
View File
@@ -1,26 +1,24 @@
#!/bin/bash
set -euo pipefail
#pnpm pretty-quick --staged
echo "[pre-commit] Running lint-staged for JS/TS files..."
# Auto-fix staged JS/TS files, print warnings but don't fail commit
npx lint-staged || true
if git diff --cached --name-only | grep -q '^src/'; then
pnpm format:check
if [ $? -ne 0 ]; then
echo "Code format check failed in src/. Please fix formatting issues."
exit 1
fi
fi
# Check staged Rust files
RUST_FILES=$(git diff --cached --name-only | grep -E '^src-tauri/.*\.rs$' || true)
if [ -n "$RUST_FILES" ]; then
echo "[pre-commit] Running rustfmt and clippy on staged Rust files..."
cd src-tauri || exit
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cd src-tauri
# Auto-format Rust code
cargo fmt
if [ $? -ne 0 ]; then
echo "rustfmt failed to format the code. Please fix the issues and try again."
exit 1
fi
# Lint with clippy, print warnings but don't fail commit
cargo clippy || echo "⚠️ clippy found issues, but commit will continue."
cd ..
fi
#git add .
# 允许提交
echo "[pre-commit] Checks completed. Some warnings may exist, please review."
exit 0
+11 -12
View File
@@ -1,26 +1,24 @@
#!/bin/bash
set -euo pipefail
# $1: remote name (e.g., origin)
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
remote_name="$1"
# --- Rust clippy for staged files in src-tauri ---
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cargo clippy --manifest-path ./src-tauri/Cargo.toml
if [ $? -ne 0 ]; then
echo "Clippy found issues in src-tauri. Please fix them before pushing."
echo "[pre-push] Running clippy on src-tauri..."
cargo clippy --manifest-path ./src-tauri/Cargo.toml -- -D warnings || {
echo "Clippy found issues in src-tauri. Please fix them before pushing."
exit 1
fi
}
fi
# Only run format check if the remote exists and is the main repo
remote_name="$1"
# --- JS/TS format check only for main repo ---
if git remote get-url "$remote_name" >/dev/null 2>&1; then
remote_url=$(git remote get-url "$remote_name")
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
echo "[pre-push] Running pnpm format:check..."
pnpm format:check
if [ $? -ne 0 ]; then
if ! pnpm format:check; then
echo "❌ Code format check failed. Please fix formatting before pushing."
exit 1
fi
@@ -28,7 +26,8 @@ if git remote get-url "$remote_name" >/dev/null 2>&1; then
echo "[pre-push] Not pushing to target repo. Skipping format check."
fi
else
echo "[pre-push] Remote $remote_name does not exist. Skipping format check."
echo "[pre-push] Remote '$remote_name' does not exist. Skipping format check."
fi
echo "[pre-push] All checks passed."
exit 0
+2
View File
@@ -6,3 +6,5 @@ pnpm-lock.yaml
src-tauri/target/
src-tauri/gen/
target
+1 -1
View File
@@ -11,6 +11,6 @@
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf",
"endOfLine": "auto",
"embeddedLanguageFormatting": "auto"
}
+18 -1
View File
@@ -3,9 +3,12 @@
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.14**
- Linux 打包为 `.deb` `.rpm` 提供 pkexec 依赖项
- 支持前端修改日志(最大文件大小、最大保留数量)
- 新增链式代理图形化设置功能
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
- 监听关机事件,自动关闭系统代理
- 主界面“当前节点”卡片新增“延迟测试”按钮
- 新增批量选择配置文件功能
### 🚀 优化改进
@@ -17,6 +20,11 @@
- 优化 TUN 模式可用性的判断
- 移除流媒体检测的系统级提示(使用软件内通知)
- 优化后端 i18n 资源占用
- 改进 Linux 托盘支持并添加 `--no-tray` 选项
- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式
- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL
- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
### 🐞 修复问题
@@ -29,6 +37,15 @@
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
- 修复 Webdav 恢复备份不重启
- 修复 Linux 开机后无法正常代理需要手动设置
- 修复增加订阅或导入订阅文件时订阅页面无更新
- 修复系统代理守卫功能不工作
- 修复 KDE + Wayland 下多屏显示 UI 异常
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复 Linux GNOME/KDE 桌面下,应用主题颜色选择“系统”后,不随操作系统主题(Dark/Light)切换
## v2.4.2
+30 -13
View File
@@ -3,6 +3,7 @@
"version": "2.4.3",
"license": "GPL-3.0-only",
"scripts": {
"prepare": "husky || true",
"dev": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=full RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
@@ -24,10 +25,12 @@
"release:deploytest": "pnpm release-version deploytest",
"publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint -c eslint.config.ts --cache src",
"clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint -c eslint.config.ts --cache --cache-location .eslintcache src",
"lint:fix": "eslint -c eslint.config.ts --cache --cache-location .eslintcache --fix src",
"format": "prettier --write .",
"format:check": "prettier --check ."
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -39,7 +42,7 @@
"@mui/icons-material": "^7.3.4",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.4",
"@mui/x-data-grid": "^8.13.1",
"@mui/x-data-grid": "^8.14.0",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
@@ -53,25 +56,26 @@
"axios": "^1.12.2",
"dayjs": "1.11.18",
"foxact": "^0.2.49",
"i18next": "^25.5.3",
"i18next": "^25.6.0",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.53.0",
"monaco-editor": "^0.54.0",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.64.0",
"react-hook-form": "^7.65.0",
"react-i18next": "16.0.0",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router-dom": "7.9.3",
"react-router-dom": "7.9.4",
"react-virtuoso": "^4.14.1",
"swr": "^2.3.6",
"types-pac": "^1.0.3",
"zustand": "^5.0.8"
"zustand": "^5.0.8",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo"
},
"devDependencies": {
"@actions/github": "^6.0.1",
@@ -80,8 +84,8 @@
"@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.2.0",
"@types/react-dom": "19.2.0",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.1",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.0.4",
"adm-zip": "^0.5.16",
@@ -93,13 +97,15 @@
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-hooks": "^6.1.1",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.23",
"eslint-plugin-unused-imports": "^4.2.0",
"glob": "^11.0.3",
"globals": "^16.4.0",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"jiti": "^2.6.1",
"lint-staged": "^16.2.4",
"meta-json-schema": "^1.19.14",
"node-fetch": "^3.3.2",
"prettier": "^3.6.2",
@@ -107,11 +113,22 @@
"tar": "^7.5.1",
"terser": "^5.44.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.45.0",
"typescript-eslint": "^8.46.0",
"vite": "^7.1.9",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.5.0"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{css,scss,json,md}": [
"prettier --write",
"git add"
]
},
"type": "module",
"packageManager": "pnpm@9.13.2"
}
+511 -281
View File
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -1,14 +1,14 @@
import AdmZip from "adm-zip";
import { execSync } from "child_process";
import fs from "fs";
import fsp from "fs/promises";
import zlib from "zlib";
import { extract } from "tar";
import path from "path";
import AdmZip from "adm-zip";
import fetch from "node-fetch";
import { HttpsProxyAgent } from "https-proxy-agent";
import { execSync } from "child_process";
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
import { glob } from "glob";
import { HttpsProxyAgent } from "https-proxy-agent";
import fetch from "node-fetch";
import path from "path";
import { extract } from "tar";
import zlib from "zlib";
import { log_debug, log_error, log_info, log_success } from "./utils.mjs";
const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
@@ -383,8 +383,8 @@ const resolvePlugin = async () => {
const resolveServicePermission = async () => {
const serviceExecutables = [
"clash-verge-service*",
"install-service*",
"uninstall-service*",
"clash-verge-service-install*",
"clash-verge-service-uninstall*",
];
const resDir = path.join(cwd, "src-tauri/resources");
for (let f of serviceExecutables) {
@@ -430,7 +430,7 @@ async function resolveLocales() {
/**
* main
*/
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
@@ -445,8 +445,8 @@ const resolveInstall = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "install-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/install-service${ext}`,
file: "clash-verge-service-install" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
});
};
@@ -455,8 +455,8 @@ const resolveUninstall = () => {
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "uninstall-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
file: "clash-verge-service-uninstall" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
});
};
+514 -340
View File
File diff suppressed because it is too large Load Diff
+15 -11
View File
@@ -24,14 +24,14 @@ log = "0.4.28"
dunce = "1.0.5"
nanoid = "0.4"
chrono = "0.4.42"
sysinfo = { version = "0.37.1", features = ["network", "system"] }
sysinfo = { version = "0.37.2", features = ["network", "system"] }
boa_engine = "0.20.0"
serde_json = "1.0.145"
serde_yaml_ng = "0.10.0"
once_cell = "1.21.3"
port_scanner = "0.1.5"
delay_timer = "0.11.6"
parking_lot = "0.12.4"
parking_lot = "0.12.5"
percent-encoding = "2.3.2"
tokio = { version = "1.47.1", features = [
"rt-multi-thread",
@@ -41,7 +41,7 @@ tokio = { version = "1.47.1", features = [
] }
serde = { version = "1.0.228", features = ["derive"] }
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.3"
regex = "1.12.1"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
tauri = { version = "2.8.5", features = [
"protocol-asset",
@@ -58,20 +58,19 @@ tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.3"
tauri-plugin-window-state = "2.4.0"
zip = "5.1.1"
zip = "6.0.0"
reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.3.3"
futures = "0.3.31"
sys-locale = "0.3.2"
libc = "0.2.176"
libc = "0.2.177"
gethostname = "1.0.2"
hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
kode-bridge = "0.3.0"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.1"
tokio-stream = "0.1.17"
@@ -81,12 +80,17 @@ isahc = { version = "1.7.2", default-features = false, features = [
] }
backoff = { version = "0.4.0", features = ["tokio"] }
tauri-plugin-http = "2.5.2"
flexi_logger = "0.31.4"
cfg-if = "1.0.3"
nu-ansi-term = { version = "0.50.1", optional = true }
flexi_logger = "0.31.7"
console-subscriber = { version = "0.4.1", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
clash_verge_logger = { version = "0.1.0", git = "https://github.com/clash-verge-rev/clash-verge-logger" }
clash_verge_service_ipc = { version = "2.0.14", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
# clash_verge_service_ipc = { version = "2.0.14", features = [
# "client",
# ], path = "../../clash-verge-service-ipc" }
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
@@ -117,7 +121,7 @@ tauri-plugin-updater = "2.9.0"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = ["nu-ansi-term"]
verge-dev = ["clash_verge_logger/color"]
tauri-dev = []
tokio-trace = ["console-subscriber"]
@@ -24,6 +24,7 @@
{
"identifier": "http:default",
"allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }]
}
},
"mihomo:default"
]
}
@@ -1,4 +1,4 @@
#!/bin/bash
chmod +x /usr/bin/install-service
chmod +x /usr/bin/uninstall-service
chmod +x /usr/bin/clash-verge-service-install
chmod +x /usr/bin/clash-verge-service-uninstall
chmod +x /usr/bin/clash-verge-service
@@ -1,2 +1,2 @@
#!/bin/bash
/usr/bin/uninstall-service
/usr/bin/clash-verge-service-uninstall
-111
View File
@@ -1,111 +0,0 @@
use crate::singleton;
use anyhow::Result;
use dashmap::DashMap;
use serde_json::Value;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::OnceCell;
pub const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250);
pub struct CacheEntry<T> {
pub value: Arc<T>,
pub expires_at: Instant,
}
pub struct Cache<T> {
pub map: DashMap<String, Arc<OnceCell<Box<CacheEntry<T>>>>>,
}
impl<T> Cache<T> {
fn new() -> Self {
Cache {
map: DashMap::new(),
}
}
pub fn make_key(prefix: &str, id: &str) -> String {
format!("{prefix}:{id}")
}
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<T>
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
T: Send + Sync + 'static,
{
loop {
let now = Instant::now();
let key_cloned = key.clone();
// Get or create the cell
let cell = self
.map
.entry(key_cloned.clone())
.or_insert_with(|| Arc::new(OnceCell::new()))
.clone();
// Check if we have a valid cached entry
if let Some(entry) = cell.get() {
if entry.expires_at > now {
return Arc::clone(&entry.value);
}
// Entry is expired, remove it
self.map
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
continue; // Retry with fresh cell
}
// Try to set a new value
let value = fetch_fn().await;
let entry = Box::new(CacheEntry {
value: Arc::new(value),
expires_at: Instant::now() + ttl,
});
match cell.set(entry) {
Ok(_) => {
// Successfully set the value, it must exist now
if let Some(set_entry) = cell.get() {
return Arc::clone(&set_entry.value);
}
}
Err(_) => {
if let Some(existing_entry) = cell.get() {
if existing_entry.expires_at > Instant::now() {
return Arc::clone(&existing_entry.value);
}
self.map
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
}
}
}
}
}
// pub fn clean_key(&self, key: &str) {
// self.map.remove(key);
// }
// TODO
pub fn clean_default_keys(&self) {
// logging!(info, Type::Cache, "Cleaning proxies keys");
// let proxies_key = Self::make_key("proxies", "default");
// self.map.remove(&proxies_key);
// logging!(info, Type::Cache, "Cleaning providers keys");
// let providers_key = Self::make_key("providers", "default");
// self.map.remove(&providers_key);
// !The frontend goes crash if we clean the clash_config cache
// logging!(info, Type::Cache, "Cleaning clash config keys");
// let clash_config_key = Self::make_key("clash_config", "default");
// self.map.remove(&clash_config_key);
}
}
pub type CacheService = Cache<Result<String>>;
pub type CacheProxy = Cache<Value>;
singleton!(Cache<Value>, PROXY_INSTANCE);
singleton!(Cache<Result<String>>, SERVICE_INSTANCE);
-1
View File
@@ -191,7 +191,6 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
logging!(
info,
Type::Cmd,
true,
"Copying icon file path: {:?} -> file dist: {:?}",
path,
dest_path
+13 -337
View File
@@ -1,21 +1,15 @@
use std::collections::VecDeque;
use super::CmdResult;
use crate::{
cache::CacheProxy,
config::Config,
core::{CoreManager, handle},
};
use crate::{
config::*,
feat,
ipc::{self, IpcManager},
logging,
utils::logging::Type,
wrap_err,
core::{self, CoreManager, RunningMode, handle, logger},
};
use crate::{config::*, feat, logging, utils::logging::Type, wrap_err};
use serde_yaml_ng::Mapping;
use std::time::Duration;
// use std::time::Duration;
const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
// const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
/// 复制Clash环境变量
#[tauri::command]
@@ -112,20 +106,6 @@ pub async fn restart_core() -> CmdResult {
result
}
/// 获取代理延迟
#[tauri::command]
pub async fn clash_api_get_proxy_delay(
name: String,
url: Option<String>,
timeout: i32,
) -> CmdResult<serde_json::Value> {
wrap_err!(
IpcManager::global()
.test_proxy_delay(&name, url, timeout)
.await
)
}
/// 测试URL延迟
#[tauri::command]
pub async fn test_delay(url: String) -> CmdResult<u32> {
@@ -307,317 +287,13 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
}
}
/// 获取Clash版本信息
#[tauri::command]
pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_version().await)
}
/// 获取Clash配置
#[tauri::command]
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = CacheProxy::global();
let key = CacheProxy::make_key("clash_config", "default");
let value = cache
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
manager.get_config().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch clash config: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
}
/// 强制刷新Clash配置缓存
#[tauri::command]
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
let cache = CacheProxy::global();
let key = CacheProxy::make_key("clash_config", "default");
cache.map.remove(&key);
get_clash_config().await
}
/// 更新地理数据
#[tauri::command]
pub async fn update_geo_data() -> CmdResult {
wrap_err!(IpcManager::global().update_geo_data().await)
}
/// 升级Clash核心
#[tauri::command]
pub async fn upgrade_clash_core() -> CmdResult {
wrap_err!(IpcManager::global().upgrade_core().await)
}
/// 获取规则
#[tauri::command]
pub async fn get_clash_rules() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_rules().await)
}
/// 更新代理选择
#[tauri::command]
pub async fn update_proxy_choice(group: String, proxy: String) -> CmdResult {
wrap_err!(IpcManager::global().update_proxy(&group, &proxy).await)
}
/// 获取代理提供者
#[tauri::command]
pub async fn get_proxy_providers() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_providers_proxies().await)
}
/// 获取规则提供者
#[tauri::command]
pub async fn get_rule_providers() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_rule_providers().await)
}
/// 代理提供者健康检查
#[tauri::command]
pub async fn proxy_provider_health_check(name: String) -> CmdResult {
wrap_err!(
IpcManager::global()
.proxy_provider_health_check(&name)
.await
)
}
/// 更新代理提供者
#[tauri::command]
pub async fn update_proxy_provider(name: String) -> CmdResult {
wrap_err!(IpcManager::global().update_proxy_provider(&name).await)
}
/// 更新规则提供者
#[tauri::command]
pub async fn update_rule_provider(name: String) -> CmdResult {
wrap_err!(IpcManager::global().update_rule_provider(&name).await)
}
/// 获取连接
#[tauri::command]
pub async fn get_clash_connections() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_connections().await)
}
/// 删除连接
#[tauri::command]
pub async fn delete_clash_connection(id: String) -> CmdResult {
wrap_err!(IpcManager::global().delete_connection(&id).await)
}
/// 关闭所有连接
#[tauri::command]
pub async fn close_all_clash_connections() -> CmdResult {
wrap_err!(IpcManager::global().close_all_connections().await)
}
/// 获取流量数据 (使用新的IPC流式监控)
#[tauri::command]
pub async fn get_traffic_data() -> CmdResult<serde_json::Value> {
let traffic = crate::ipc::get_current_traffic().await;
let result = serde_json::json!({
"up": traffic.total_up,
"down": traffic.total_down,
"up_rate": traffic.up_rate,
"down_rate": traffic.down_rate,
"last_updated": traffic.last_updated.elapsed().as_secs()
});
Ok(result)
}
/// 获取内存数据 (使用新的IPC流式监控)
#[tauri::command]
pub async fn get_memory_data() -> CmdResult<serde_json::Value> {
let memory = crate::ipc::get_current_memory().await;
let usage_percent = if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
pub async fn get_clash_logs() -> CmdResult<VecDeque<String>> {
let logs = match core::CoreManager::global().get_running_mode() {
// TODO: 服务模式下日志获取接口
RunningMode::Service => VecDeque::new(),
RunningMode::Sidecar => logger::Logger::global().get_logs().clone(),
_ => VecDeque::new(),
};
let result = serde_json::json!({
"inuse": memory.inuse,
"oslimit": memory.oslimit,
"usage_percent": usage_percent,
"last_updated": memory.last_updated.elapsed().as_secs()
});
Ok(result)
}
/// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留)
#[tauri::command]
pub async fn start_traffic_service() -> CmdResult {
logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)");
// 新的IPC监控在首次访问时自动启动
// 触发一次访问以确保监控器已初始化
let _ = crate::ipc::get_current_traffic().await;
let _ = crate::ipc::get_current_memory().await;
logging!(info, Type::Ipc, "IPC流式监控已激活");
Ok(())
}
/// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留)
#[tauri::command]
pub async fn stop_traffic_service() -> CmdResult {
logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)");
// 新的IPC监控是持久的,无需显式停止
logging!(info, Type::Ipc, "IPC流式监控继续运行");
Ok(())
}
/// 获取格式化的流量数据 (包含单位,便于前端显示)
#[tauri::command]
pub async fn get_formatted_traffic_data() -> CmdResult<serde_json::Value> {
logging!(trace, Type::Ipc, "获取格式化流量数据");
let (up_rate, down_rate, total_up, total_down, is_fresh) =
crate::ipc::get_formatted_traffic().await;
let result = serde_json::json!({
"up_rate_formatted": up_rate,
"down_rate_formatted": down_rate,
"total_up_formatted": total_up,
"total_down_formatted": total_down,
"is_fresh": is_fresh
});
logging!(
debug,
Type::Ipc,
"格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"
);
Ok(result)
}
/// 获取格式化的内存数据 (包含单位,便于前端显示)
#[tauri::command]
pub async fn get_formatted_memory_data() -> CmdResult<serde_json::Value> {
logging!(info, Type::Ipc, "获取格式化内存数据");
let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await;
let result = serde_json::json!({
"inuse_formatted": inuse,
"oslimit_formatted": oslimit,
"usage_percent": usage_percent,
"is_fresh": is_fresh
});
logging!(
debug,
Type::Ipc,
"格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"
);
Ok(result)
}
/// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态)
#[tauri::command]
pub async fn get_system_monitor_overview() -> CmdResult<serde_json::Value> {
logging!(debug, Type::Ipc, "获取系统监控概览");
// 并发获取流量和内存数据
let (traffic, memory) = tokio::join!(
crate::ipc::get_current_traffic(),
crate::ipc::get_current_memory()
);
let (traffic_formatted, memory_formatted) = tokio::join!(
crate::ipc::get_formatted_traffic(),
crate::ipc::get_formatted_memory()
);
let traffic_is_fresh = traffic.last_updated.elapsed().as_secs() < 5;
let memory_is_fresh = memory.last_updated.elapsed().as_secs() < 10;
let result = serde_json::json!({
"traffic": {
"raw": {
"up": traffic.total_up,
"down": traffic.total_down,
"up_rate": traffic.up_rate,
"down_rate": traffic.down_rate
},
"formatted": {
"up_rate": traffic_formatted.0,
"down_rate": traffic_formatted.1,
"total_up": traffic_formatted.2,
"total_down": traffic_formatted.3
},
"is_fresh": traffic_is_fresh
},
"memory": {
"raw": {
"inuse": memory.inuse,
"oslimit": memory.oslimit,
"usage_percent": if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
}
},
"formatted": {
"inuse": memory_formatted.0,
"oslimit": memory_formatted.1,
"usage_percent": memory_formatted.2
},
"is_fresh": memory_is_fresh
},
"overall_status": if traffic_is_fresh && memory_is_fresh { "healthy" } else { "stale" }
});
Ok(result)
}
/// 获取代理组延迟
#[tauri::command]
pub async fn get_group_proxy_delays(
group_name: String,
url: Option<String>,
timeout: Option<i32>,
) -> CmdResult<serde_json::Value> {
wrap_err!(
IpcManager::global()
.get_group_proxy_delays(&group_name, url, timeout.unwrap_or(10000))
.await
)
}
/// 检查调试是否启用
#[tauri::command]
pub async fn is_clash_debug_enabled() -> CmdResult<bool> {
match IpcManager::global().is_debug_enabled().await {
Ok(enabled) => Ok(enabled),
Err(_) => Ok(false),
}
}
/// 垃圾回收
#[tauri::command]
pub async fn clash_gc() -> CmdResult {
wrap_err!(IpcManager::global().gc().await)
}
/// 获取日志 (使用新的流式实现)
#[tauri::command]
pub async fn get_clash_logs() -> CmdResult<serde_json::Value> {
Ok(ipc::get_logs_json().await)
}
/// 启动日志监控
#[tauri::command]
pub async fn start_logs_monitoring(level: Option<String>) -> CmdResult {
ipc::start_logs_monitoring(level).await;
Ok(())
}
/// 停止日志监控
#[tauri::command]
pub async fn stop_logs_monitoring() -> CmdResult {
ipc::stop_logs_monitoring().await;
Ok(())
}
/// 清除日志
#[tauri::command]
pub async fn clear_logs() -> CmdResult {
ipc::clear_logs().await;
Ok(())
Ok(logs)
}
+43 -71
View File
@@ -38,11 +38,11 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
match latest_result {
Ok(profiles) => {
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
logging!(info, Type::Cmd, "快速获取配置列表成功");
return Ok(profiles);
}
Err(_) => {
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
logging!(warn, Type::Cmd, "快速获取配置超时(500ms)");
}
}
@@ -59,14 +59,13 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
match data_result {
Ok(profiles) => {
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
logging!(info, Type::Cmd, "获取draft配置列表成功");
return Ok(profiles);
}
Err(join_err) => {
logging!(
error,
Type::Cmd,
true,
"获取draft配置任务失败或超时: {}",
join_err
);
@@ -74,12 +73,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
}
// 策略3: fallback,尝试重新创建配置
logging!(
warn,
Type::Cmd,
true,
"所有获取配置策略都失败,尝试fallback"
);
logging!(warn, Type::Cmd, "所有获取配置策略都失败,尝试fallback");
Ok(IProfiles::new().await)
}
@@ -101,11 +95,11 @@ pub async fn enhance_profiles() -> CmdResult {
/// 导入配置文件
#[tauri::command]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url);
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url);
let import_result = tokio::time::timeout(Duration::from_secs(60), async {
let item = PrfItem::from_url(&url, None, None, option).await?;
logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置");
logging!(info, Type::Cmd, "[导入订阅] 下载完成,开始保存配置");
let profiles = Config::profiles().await;
let pre_count = profiles
@@ -123,19 +117,13 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
.as_ref()
.map_or(0, |items| items.len());
if post_count <= pre_count {
logging!(
error,
Type::Cmd,
true,
"[导入订阅] 配置未增加,导入可能失败"
);
logging!(error, Type::Cmd, "[导入订阅] 配置未增加,导入可能失败");
return Err(anyhow::anyhow!("配置导入后数量未增加"));
}
logging!(
info,
Type::Cmd,
true,
"[导入订阅] 配置保存成功,数量: {} -> {}",
pre_count,
post_count
@@ -143,13 +131,7 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
// 立即发送配置变更通知
if let Some(uid) = &item.uid {
logging!(
info,
Type::Cmd,
true,
"[导入订阅] 发送配置变更通知: {}",
uid
);
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid.clone());
}
@@ -158,9 +140,9 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
crate::process::AsyncHandler::spawn(move || async move {
// 使用Send-safe helper函数
if let Err(e) = profiles_save_file_safe().await {
logging!(error, Type::Cmd, true, "[导入订阅] 保存配置文件失败: {}", e);
logging!(error, Type::Cmd, "[导入订阅] 保存配置文件失败: {}", e);
} else {
logging!(info, Type::Cmd, true, "[导入订阅] 配置文件保存成功");
logging!(info, Type::Cmd, "[导入订阅] 配置文件保存成功");
// 发送全局配置更新通知
if let Some(uid) = uid_clone {
@@ -177,15 +159,15 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
match import_result {
Ok(Ok(())) => {
logging!(info, Type::Cmd, true, "[导入订阅] 导入完成: {}", url);
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url);
Ok(())
}
Ok(Err(e)) => {
logging!(error, Type::Cmd, true, "[导入订阅] 导入失败: {}", e);
logging!(error, Type::Cmd, "[导入订阅] 导入失败: {}", e);
Err(format!("导入订阅失败: {e}"))
}
Err(_) => {
logging!(error, Type::Cmd, true, "[导入订阅] 导入超时(60秒): {}", url);
logging!(error, Type::Cmd, "[导入订阅] 导入超时(60秒): {}", url);
Err("导入订阅超时,请检查网络连接".into())
}
}
@@ -210,8 +192,15 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
/// 创建一个新的配置文件
#[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
match profiles_append_item_with_filedata_safe(item, file_data).await {
Ok(_) => Ok(()),
match profiles_append_item_with_filedata_safe(item.clone(), file_data).await {
Ok(_) => {
// 发送配置变更通知
if let Some(uid) = &item.uid {
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid.clone());
}
Ok(())
}
Err(err) => match err.to_string().as_str() {
"the file already exists" => Err("the file already exists".into()),
_ => Err(format!("add profile error: {err}")),
@@ -235,12 +224,15 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
// 使用Send-safe helper函数
let should_update = wrap_err!(profiles_delete_item_safe(index).await)?;
let should_update = wrap_err!(profiles_delete_item_safe(index.clone()).await)?;
if should_update {
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
// 发送配置变更通知
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index);
handle::Handle::notify_profile_changed(index);
}
Err(e) => {
log::error!(target: "app", "{}", e);
@@ -255,7 +247,7 @@ pub async fn delete_profile(index: String) -> CmdResult {
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
logging!(info, Type::Cmd, true, "当前正在切换配置,放弃请求");
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Ok(false);
}
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
@@ -267,7 +259,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
current_sequence,
target_profile
@@ -278,7 +269,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
@@ -288,13 +278,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().await.latest_ref().current.clone();
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
logging!(info, Type::Cmd, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref()
&& current_profile.as_ref() != Some(new_profile)
{
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile);
// 获取目标配置文件路径
let config_file_result = {
@@ -310,7 +300,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
}
Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e);
None
}
}
@@ -322,7 +312,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
error,
Type::Cmd,
true,
"目标配置文件不存在: {}",
file_path.display()
);
@@ -349,14 +338,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
match yaml_parse_result {
Ok(Ok(_)) => {
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
logging!(info, Type::Cmd, "目标配置文件语法正确");
}
Ok(Err(err)) => {
let error_msg = format!(" {err}");
logging!(
error,
Type::Cmd,
true,
"目标配置文件存在YAML语法错误:{}",
error_msg
);
@@ -368,7 +356,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
Err(join_err) => {
let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
&error_msg,
@@ -379,13 +367,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message("config_validate::file_read_error", &error_msg);
return Ok(false);
}
Err(_) => {
let error_msg = "读取配置文件超时(5秒)".to_string();
logging!(error, Type::Cmd, true, "{}", error_msg);
logging!(error, Type::Cmd, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
&error_msg,
@@ -402,7 +390,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
@@ -414,7 +401,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"正在更新配置草稿,序列号: {}",
current_sequence
);
@@ -429,7 +415,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
@@ -442,7 +427,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"开始内核配置更新,序列号: {}",
current_sequence
);
@@ -461,7 +445,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
current_sequence,
latest_sequence
@@ -473,7 +456,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"配置更新成功,序列号: {}",
current_sequence
);
@@ -481,11 +463,11 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
handle::Handle::refresh_clash();
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = super::proxy::force_refresh_proxies().await {
log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
}
});
// crate::process::AsyncHandler::spawn(|| async move {
// if let Err(e) = super::proxy::force_refresh_proxies().await {
// log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
// }
// });
if let Err(e) = Tray::global().update_tooltip().await {
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
@@ -505,7 +487,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"向前端发送配置变更事件: {}, 序列号: {}",
current,
current_sequence
@@ -517,17 +498,11 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
Ok(true)
}
Ok(Ok((false, error_msg))) => {
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
Config::profiles().await.discard();
// 如果验证失败,恢复到之前的配置
if let Some(prev_profile) = current_profile {
logging!(
info,
Type::Cmd,
true,
"尝试恢复到之前的配置: {}",
prev_profile
);
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
let restore_profiles = IProfiles {
current: Some(prev_profile),
items: None,
@@ -547,7 +522,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
});
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
logging!(info, Type::Cmd, "成功恢复到之前的配置");
}
// 发送验证错误通知
@@ -559,7 +534,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
warn,
Type::Cmd,
true,
"更新过程发生错误: {}, 序列号: {}",
e,
current_sequence
@@ -576,7 +550,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
error,
Type::Cmd,
true,
"{}, 序列号: {}",
timeout_msg,
current_sequence
@@ -587,7 +560,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(
info,
Type::Cmd,
true,
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
prev_profile,
current_sequence
@@ -615,7 +587,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
/// 根据profile name修改profiles
#[tauri::command]
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
logging!(info, Type::Cmd, "切换配置到: {}", profile_index);
let profiles = IProfiles {
current: Some(profile_index),
+2 -105
View File
@@ -1,59 +1,7 @@
use tauri::Emitter;
use super::CmdResult;
use crate::{
cache::CacheProxy,
core::{handle::Handle, tray::Tray},
ipc::IpcManager,
logging,
utils::logging::Type,
};
use std::time::Duration;
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let cache = CacheProxy::global();
let key = CacheProxy::make_key("proxies", "default");
let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
}
/// 强制刷新代理缓存用于profile切换
#[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let cache = CacheProxy::global();
let key = CacheProxy::make_key("proxies", "default");
cache.map.remove(&key);
get_proxies().await
}
#[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let cache = CacheProxy::global();
let key = CacheProxy::make_key("providers", "default");
let value = cache
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_providers_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
}
use crate::{logging, utils::logging::Type};
// TODO: 前端通过 emit 发送更新事件, tray 监听更新事件
/// 同步托盘和GUI的代理选择状态
#[tauri::command]
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
@@ -70,54 +18,3 @@ pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
}
}
}
/// 更新代理选择并同步托盘和GUI状态
#[tauri::command]
pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> {
match IpcManager::global().update_proxy(&group, &proxy).await {
Ok(_) => {
// println!("Proxy updated successfully: {} -> {}", group,proxy);
logging!(
info,
Type::Cmd,
"Proxy updated successfully: {} -> {}",
group,
proxy
);
let cache = CacheProxy::global();
let key = CacheProxy::make_key("proxies", "default");
cache.map.remove(&key);
if let Err(e) = Tray::global().update_menu().await {
logging!(error, Type::Cmd, "Failed to sync tray menu: {}", e);
}
if let Some(app_handle) = Handle::global().app_handle() {
let _ = app_handle.emit("verge://force-refresh-proxies", ());
let _ = app_handle.emit("verge://refresh-proxy-config", ());
}
logging!(
info,
Type::Cmd,
"Proxy and sync completed successfully: {} -> {}",
group,
proxy
);
Ok(())
}
Err(e) => {
println!("1111111111111111");
logging!(
error,
Type::Cmd,
"Failed to update proxy: {} -> {}, error: {}",
group,
proxy,
e
);
Err(e.to_string())
}
}
}
@@ -36,7 +36,6 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
logging!(
info,
Type::Config,
true,
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
file_path_str,
is_merge_file
@@ -47,7 +46,6 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
logging!(
info,
Type::Config,
true,
"[cmd配置save] 检测到merge文件,只进行语法验证"
);
match CoreManager::global()
@@ -55,12 +53,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
.await
{
Ok((true, _)) => {
logging!(
info,
Type::Config,
true,
"[cmd配置save] merge文件语法验证通过"
);
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
// 成功后尝试更新整体配置
match CoreManager::global().update_config().await {
Ok(_) => {
@@ -71,7 +64,6 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
@@ -83,7 +75,6 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
logging!(
warn,
Type::Config,
true,
"[cmd配置save] merge文件语法验证失败: {}",
error_msg
);
@@ -95,13 +86,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
return Ok(());
}
Err(e) => {
logging!(
error,
Type::Config,
true,
"[cmd配置save] 验证过程发生错误: {}",
e
);
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content).await)?;
return Err(e.to_string());
@@ -115,17 +100,11 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
.await
{
Ok((true, _)) => {
logging!(info, Type::Config, true, "[cmd配置save] 验证成功");
logging!(info, Type::Config, "[cmd配置save] 验证成功");
Ok(())
}
Ok((false, error_msg)) => {
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 验证失败: {}",
error_msg
);
logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content).await)?;
@@ -169,13 +148,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
Ok(())
}
Err(e) => {
logging!(
error,
Type::Config,
true,
"[cmd配置save] 验证过程发生错误: {}",
e
);
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content).await)?;
Err(e.to_string())
+1 -8
View File
@@ -1,9 +1,6 @@
use super::CmdResult;
use crate::{
core::{
CoreManager,
service::{self, SERVICE_MANAGER, ServiceStatus},
},
core::service::{self, SERVICE_MANAGER, ServiceStatus},
utils::i18n::t,
};
@@ -17,10 +14,6 @@ async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) ->
let emsg = format!("{} Service failed: {}", op_type, e);
return Err(t(emsg.as_str()).await);
}
if CoreManager::global().restart_core().await.is_err() {
let emsg = "Restart Core failed";
return Err(t(emsg).await);
}
Ok(())
}
+1 -3
View File
@@ -28,9 +28,7 @@ pub async fn export_diagnostic_info() -> CmdResult<()> {
let sysinfo = PlatformSpecification::new_sync();
let info = format!("{sysinfo:?}");
let app_handle = handle::Handle::global()
.app_handle()
.ok_or("Failed to get app handle")?;
let app_handle = handle::Handle::app_handle();
let cliboard = app_handle.clipboard();
if cliboard.write_text(info).is_err() {
logging!(error, Type::System, "Failed to write to clipboard");
+3 -20
View File
@@ -28,14 +28,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
"config_validate::script_error"
};
logging!(
warn,
Type::Config,
true,
"{} 验证失败: {}",
file_type,
error_msg
);
logging!(warn, Type::Config, "{} 验证失败: {}", file_type, error_msg);
handle::Handle::notice_message(status, error_msg);
}
}
@@ -43,7 +36,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
/// 验证指定脚本文件
#[tauri::command]
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
logging!(info, Type::Config, "验证脚本文件: {}", file_path);
match CoreManager::global()
.validate_config_file(&file_path, None)
@@ -58,7 +51,6 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
logging!(
error,
Type::Config,
true,
"验证脚本文件过程发生错误: {}",
error_msg
);
@@ -76,7 +68,6 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
logging!(
info,
Type::Config,
true,
"[通知] 处理{}验证错误: {}",
file_type,
error_msg
@@ -117,18 +108,10 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
}
};
logging!(
warn,
Type::Config,
true,
"{} 验证失败: {}",
file_type,
error_msg
);
logging!(warn, Type::Config, "{} 验证失败: {}", file_type, error_msg);
logging!(
info,
Type::Config,
true,
"[通知] 发送通知: status={}, msg={}",
status,
error_msg
@@ -51,6 +51,9 @@ impl IClashTemp {
let mut tun = Mapping::new();
let mut cors_map = Mapping::new();
tun.insert("enable".into(), false.into());
#[cfg(target_os = "linux")]
tun.insert("stack".into(), "mixed".into());
#[cfg(not(target_os = "linux"))]
tun.insert("stack".into(), "gvisor".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), false.into());
+67 -8
View File
@@ -6,9 +6,11 @@ use crate::{
utils::{dirs, help, logging::Type},
};
use anyhow::{Result, anyhow};
use backoff::{Error as BackoffError, ExponentialBackoff};
use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::OnceCell;
use tokio::time::{Duration, sleep};
use tokio::time::sleep;
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
@@ -73,9 +75,9 @@ impl Config {
}
// 生成运行时配置
if let Err(err) = Self::generate().await {
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
logging!(error, Type::Config, "生成运行时配置失败: {}", err);
} else {
logging!(info, Type::Config, true, "生成运行时配置成功");
logging!(info, Type::Config, "生成运行时配置成功");
}
// 生成运行时配置文件并验证
@@ -83,7 +85,7 @@ impl Config {
let validation_result = if config_result.is_ok() {
// 验证配置文件
logging!(info, Type::Config, true, "开始验证配置");
logging!(info, Type::Config, "开始验证配置");
match CoreManager::global().validate_config().await {
Ok((is_valid, error_msg)) => {
@@ -91,7 +93,6 @@ impl Config {
logging!(
warn,
Type::Config,
true,
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
error_msg
);
@@ -100,14 +101,14 @@ impl Config {
.await?;
Some(("config_validate::boot_error", error_msg))
} else {
logging!(info, Type::Config, true, "配置验证成功");
logging!(info, Type::Config, "配置验证成功");
// 前端没有必要知道验证成功的消息,也没有事件驱动
// Some(("config_validate::success", String::new()))
None
}
}
Err(err) => {
logging!(warn, Type::Config, true, "验证程执行失败: {}", err);
logging!(warn, Type::Config, "验证程执行失败: {}", err);
CoreManager::global()
.use_default_config("config_validate::process_terminated", "")
.await?;
@@ -115,7 +116,7 @@ impl Config {
}
}
} else {
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
logging!(warn, Type::Config, "生成配置文件失败,使用默认配置");
CoreManager::global()
.use_default_config("config_validate::error", "")
.await?;
@@ -163,6 +164,64 @@ impl Config {
Ok(())
}
pub async fn verify_config_initialization() {
logging!(info, Type::Setup, "Verifying config initialization...");
let backoff_strategy = ExponentialBackoff {
initial_interval: std::time::Duration::from_millis(100),
max_interval: std::time::Duration::from_secs(2),
max_elapsed_time: Some(std::time::Duration::from_secs(10)),
multiplier: 2.0,
..Default::default()
};
let operation = || async {
if Config::runtime().await.latest_ref().config.is_some() {
logging!(
info,
Type::Setup,
"Config initialization verified successfully"
);
return Ok::<(), BackoffError<anyhow::Error>>(());
}
logging!(
warn,
Type::Setup,
"Runtime config not found, attempting to regenerate..."
);
match Config::generate().await {
Ok(_) => {
logging!(info, Type::Setup, "Config successfully regenerated");
Ok(())
}
Err(e) => {
logging!(warn, Type::Setup, "Failed to generate config: {}", e);
Err(BackoffError::transient(e))
}
}
};
match backoff::future::retry(backoff_strategy, operation).await {
Ok(_) => {
logging!(
info,
Type::Setup,
"Config initialization verified with backoff retry"
);
}
Err(e) => {
logging!(
error,
Type::Setup,
"Failed to verify config initialization after retries: {}",
e
);
}
}
}
}
#[derive(Debug)]
@@ -297,7 +297,6 @@ impl IProfiles {
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
@@ -323,7 +322,6 @@ impl IProfiles {
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
@@ -349,7 +347,6 @@ impl IProfiles {
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
@@ -375,7 +372,6 @@ impl IProfiles {
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
@@ -401,7 +397,6 @@ impl IProfiles {
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
@@ -427,7 +422,6 @@ impl IProfiles {
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
+2 -11
View File
@@ -255,7 +255,6 @@ impl IVerge {
logging!(
warn,
Type::Config,
true,
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
core
);
@@ -266,7 +265,6 @@ impl IVerge {
logging!(
info,
Type::Config,
true,
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
);
config.clash_core = Some("verge-mihomo".to_string());
@@ -275,21 +273,15 @@ impl IVerge {
// 修正后保存配置
if needs_fix {
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
logging!(info, Type::Config, "正在保存修正后的配置文件...");
help::save_yaml(&config_path, &config, Some("# Clash Verge Config")).await?;
logging!(
info,
Type::Config,
true,
"配置文件修正完成,需要重新加载配置"
);
logging!(info, Type::Config, "配置文件修正完成,需要重新加载配置");
Self::reload_config_after_fix(config).await?;
} else {
logging!(
info,
Type::Config,
true,
"clash_core配置验证通过: {:?}",
config.clash_core
);
@@ -309,7 +301,6 @@ impl IVerge {
logging!(
info,
Type::Config,
true,
"内存配置已强制更新,新的clash_core: {:?}",
updated_config.clash_core
);
+13 -2
View File
@@ -127,13 +127,24 @@ impl WebDavClient {
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
.build()?;
// 尝试检查目录是否存在,如果不存在尝试创建,但创建失败不报错
// 尝试检查目录是否存在,如果不存在尝试创建
if client
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
.await
.is_err()
{
let _ = client.mkcol(dirs::BACKUP_DIR).await;
match client.mkcol(dirs::BACKUP_DIR).await {
Ok(_) => log::info!("Successfully created backup directory"),
Err(e) => {
log::warn!("Failed to create backup directory: {}", e);
// 清除缓存,强制下次重新尝试
self.reset();
return Err(anyhow::Error::msg(format!(
"Failed to create backup directory: {}",
e
)));
}
}
}
// 缓存客户端
+122 -119
View File
@@ -1,11 +1,14 @@
use crate::AsyncHandler;
use crate::core::logger::Logger;
use crate::process::CommandChildGuard;
use crate::utils::init::sidecar_writer;
use crate::utils::logging::SharedWriter;
use crate::{
config::*,
core::{
handle,
service::{self, SERVICE_MANAGER, ServiceStatus},
},
ipc::IpcManager,
logging, logging_error, singleton_lazy,
utils::{
dirs,
@@ -14,21 +17,21 @@ use crate::{
},
};
use anyhow::Result;
use chrono::Local;
use flexi_logger::DeferredNow;
use flexi_logger::writers::LogWriter;
use log::Record;
use parking_lot::Mutex;
use std::{
fmt,
fs::{File, create_dir_all},
io::Write,
path::PathBuf,
sync::Arc,
};
use tauri_plugin_shell::{ShellExt, process::CommandChild};
use std::{fmt, path::PathBuf, sync::Arc};
use tauri_plugin_shell::ShellExt;
// TODO:
// - 重构,提升模式切换速度
// - 内核启动添加启动 IPC 启动参数, `-ext-ctl-unix` / `-ext-ctl-pipe`, 运行时配置需要删除相关配置项
#[derive(Debug)]
pub struct CoreManager {
running: Arc<Mutex<RunningMode>>,
child_sidecar: Arc<Mutex<Option<CommandChild>>>,
child_sidecar: Arc<Mutex<Option<CommandChildGuard>>>,
}
/// 内核运行模式
@@ -54,6 +57,29 @@ impl fmt::Display for RunningMode {
use crate::config::IVerge;
fn write_sidecar_log(
writer: &dyn LogWriter,
now: &mut DeferredNow,
level: log::Level,
message: String,
) -> String {
let boxed = message.into_boxed_str();
let leaked: &'static mut str = Box::leak(boxed);
let leaked_ptr = leaked as *mut str;
{
let _ = writer.write(
now,
&Record::builder()
.args(format_args!("{}", &*leaked))
.level(level)
.target("sidecar")
.build(),
);
}
// SAFETY: `leaked` originated from `Box::leak` above; reboxing frees it immediately after use.
unsafe { String::from(Box::from_raw(leaked_ptr)) }
}
impl CoreManager {
/// 检查文件是否为脚本文件
fn is_script_file(&self, path: &str) -> Result<bool> {
@@ -71,7 +97,6 @@ impl CoreManager {
logging!(
warn,
Type::Config,
true,
"无法读取文件以检测类型: {}, 错误: {}",
path,
err
@@ -129,7 +154,6 @@ impl CoreManager {
logging!(
debug,
Type::Config,
true,
"无法确定文件类型,默认当作YAML处理: {}",
path
);
@@ -153,7 +177,7 @@ impl CoreManager {
}
/// 验证运行时配置
pub async fn validate_config(&self) -> Result<(bool, String)> {
logging!(info, Type::Config, true, "生成临时配置文件用于验证");
logging!(info, Type::Config, "生成临时配置文件用于验证");
let config_path = Config::generate_file(ConfigType::Check).await?;
let config_path = dirs::path_to_str(&config_path)?;
self.validate_config_internal(config_path).await
@@ -166,7 +190,7 @@ impl CoreManager {
) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过验证
if handle::Handle::global().is_exiting() {
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
logging!(info, Type::Core, "应用正在退出,跳过验证");
return Ok((true, String::new()));
}
@@ -182,7 +206,6 @@ impl CoreManager {
logging!(
info,
Type::Config,
true,
"检测到Merge文件,仅进行语法检查: {}",
config_path
);
@@ -200,7 +223,6 @@ impl CoreManager {
logging!(
warn,
Type::Config,
true,
"无法确定文件类型: {}, 错误: {}",
config_path,
err
@@ -214,7 +236,6 @@ impl CoreManager {
logging!(
info,
Type::Config,
true,
"检测到脚本文件,使用JavaScript验证: {}",
config_path
);
@@ -225,7 +246,6 @@ impl CoreManager {
logging!(
info,
Type::Config,
true,
"使用Clash内核验证配置文件: {}",
config_path
);
@@ -235,29 +255,19 @@ impl CoreManager {
async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过验证
if handle::Handle::global().is_exiting() {
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
logging!(info, Type::Core, "应用正在退出,跳过验证");
return Ok((true, String::new()));
}
logging!(
info,
Type::Config,
true,
"开始验证配置文件: {}",
config_path
);
logging!(info, Type::Config, "开始验证配置文件: {}", config_path);
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
logging!(info, Type::Config, "使用内核: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
let msg = "Failed to get app handle";
logging!(error, Type::Core, true, "{}", msg);
anyhow::anyhow!(msg)
})?;
let app_handle = handle::Handle::app_handle();
let app_dir = dirs::app_home_dir()?;
let app_dir_str = dirs::path_to_str(&app_dir)?;
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
logging!(info, Type::Config, "验证目录: {}", app_dir_str);
// 使用子进程运行clash验证配置
let output = app_handle
@@ -275,14 +285,14 @@ impl CoreManager {
let has_error =
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
logging!(info, Type::Config, true, "-------- 验证结果 --------");
logging!(info, Type::Config, "-------- 验证结果 --------");
if !stderr.is_empty() {
logging!(info, Type::Config, true, "stderr输出:\n{}", stderr);
logging!(info, Type::Config, "stderr输出:\n{}", stderr);
}
if has_error {
logging!(info, Type::Config, true, "发现错误,开始处理错误信息");
logging!(info, Type::Config, "发现错误,开始处理错误信息");
let error_msg = if !stdout.is_empty() {
stdout.to_string()
} else if !stderr.is_empty() {
@@ -293,38 +303,38 @@ impl CoreManager {
"验证进程被终止".to_string()
};
logging!(info, Type::Config, true, "-------- 验证结束 --------");
logging!(info, Type::Config, "-------- 验证结束 --------");
Ok((false, error_msg)) // 返回错误消息给调用者处理
} else {
logging!(info, Type::Config, true, "验证成功");
logging!(info, Type::Config, true, "-------- 验证结束 --------");
logging!(info, Type::Config, "验证成功");
logging!(info, Type::Config, "-------- 验证结束 --------");
Ok((true, String::new()))
}
}
/// 只进行文件语法检查,不进行完整验证
fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
logging!(info, Type::Config, "开始检查文件: {}", config_path);
// 读取文件内容
let content = match std::fs::read_to_string(config_path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read file: {err}");
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
logging!(error, Type::Config, "无法读取文件: {}", error_msg);
return Ok((false, error_msg));
}
};
// 对YAML文件尝试解析,只检查语法正确性
logging!(info, Type::Config, true, "进行YAML语法检查");
logging!(info, Type::Config, "进行YAML语法检查");
match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content) {
Ok(_) => {
logging!(info, Type::Config, true, "YAML语法检查通过");
logging!(info, Type::Config, "YAML语法检查通过");
Ok((true, String::new()))
}
Err(err) => {
// 使用标准化的前缀,以便错误处理函数能正确识别
let error_msg = format!("YAML syntax error: {err}");
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
logging!(error, Type::Config, "YAML语法错误: {}", error_msg);
Ok((false, error_msg))
}
}
@@ -336,13 +346,13 @@ impl CoreManager {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read script file: {err}");
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
logging!(warn, Type::Config, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
return Ok((false, error_msg));
}
};
logging!(debug, Type::Config, true, "验证脚本文件: {}", path);
logging!(debug, Type::Config, "验证脚本文件: {}", path);
// 使用boa引擎进行基本语法检查
use boa_engine::{Context, Source};
@@ -352,7 +362,7 @@ impl CoreManager {
match result {
Ok(_) => {
logging!(debug, Type::Config, true, "脚本语法验证通过: {}", path);
logging!(debug, Type::Config, "脚本语法验证通过: {}", path);
// 检查脚本是否包含main函数
if !content.contains("function main")
@@ -360,7 +370,7 @@ impl CoreManager {
&& !content.contains("let main")
{
let error_msg = "Script must contain a main function";
logging!(warn, Type::Config, true, "脚本缺少main函数: {}", path);
logging!(warn, Type::Config, "脚本缺少main函数: {}", path);
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
return Ok((false, error_msg.to_string()));
}
@@ -369,7 +379,7 @@ impl CoreManager {
}
Err(err) => {
let error_msg = format!("Script syntax error: {err}");
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
logging!(warn, Type::Config, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
Ok((false, error_msg))
}
@@ -379,30 +389,30 @@ impl CoreManager {
pub async fn update_config(&self) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过完整验证流程
if handle::Handle::global().is_exiting() {
logging!(info, Type::Config, true, "应用正在退出,跳过验证");
logging!(info, Type::Config, "应用正在退出,跳过验证");
return Ok((true, String::new()));
}
// 1. 先生成新的配置内容
logging!(info, Type::Config, true, "生成新的配置内容");
logging!(info, Type::Config, "生成新的配置内容");
Config::generate().await?;
// 2. 验证配置
match self.validate_config().await {
Ok((true, _)) => {
// 4. 验证通过后,生成正式的运行时配置
logging!(info, Type::Config, true, "配置验证通过, 生成运行时配置");
logging!(info, Type::Config, "配置验证通过, 生成运行时配置");
let run_path = Config::generate_file(ConfigType::Run).await?;
logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
logging_error!(Type::Config, self.put_configs_force(run_path).await);
Ok((true, "something".into()))
}
Ok((false, error_msg)) => {
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
logging!(warn, Type::Config, "配置验证失败: {}", error_msg);
Config::runtime().await.discard();
Ok((false, error_msg))
}
Err(e) => {
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
logging!(warn, Type::Config, "验证过程发生错误: {}", e);
Config::runtime().await.discard();
Err(e)
}
@@ -411,19 +421,23 @@ impl CoreManager {
pub async fn put_configs_force(&self, path_buf: PathBuf) -> Result<(), String> {
let run_path_str = dirs::path_to_str(&path_buf).map_err(|e| {
let msg = e.to_string();
logging_error!(Type::Core, true, "{}", msg);
logging_error!(Type::Core, "{}", msg);
msg
});
match IpcManager::global().put_configs_force(run_path_str?).await {
match handle::Handle::mihomo()
.await
.reload_config(true, run_path_str?)
.await
{
Ok(_) => {
Config::runtime().await.apply();
logging!(info, Type::Core, true, "Configuration updated successfully");
logging!(info, Type::Core, "Configuration updated successfully");
Ok(())
}
Err(e) => {
let msg = e.to_string();
Config::runtime().await.discard();
logging_error!(Type::Core, true, "Failed to update configuration: {}", msg);
logging_error!(Type::Core, "Failed to update configuration: {}", msg);
Err(msg)
}
}
@@ -433,7 +447,7 @@ impl CoreManager {
impl CoreManager {
/// 清理多余的 mihomo 进程
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
logging!(info, Type::Core, true, "开始清理多余的 mihomo 进程");
logging!(info, Type::Core, "开始清理多余的 mihomo 进程");
// 获取当前管理的进程 PID
let current_pid = {
@@ -464,12 +478,11 @@ impl CoreManager {
for pid in pids {
// 跳过当前管理的进程
if let Some(current) = current_pid
&& pid == current
&& Some(pid) == current
{
logging!(
debug,
Type::Core,
true,
"跳过当前管理的进程: {} (PID: {})",
process_name,
pid
@@ -480,13 +493,13 @@ impl CoreManager {
}
}
Err(e) => {
logging!(debug, Type::Core, true, "查找进程时发生错误: {}", e);
logging!(debug, Type::Core, "查找进程时发生错误: {}", e);
}
}
}
if pids_to_kill.is_empty() {
logging!(debug, Type::Core, true, "未发现多余的 mihomo 进程");
logging!(debug, Type::Core, "未发现多余的 mihomo 进程");
return Ok(());
}
@@ -503,7 +516,6 @@ impl CoreManager {
logging!(
info,
Type::Core,
true,
"清理完成,共终止了 {} 个多余的 mihomo 进程",
killed_count
);
@@ -612,7 +624,6 @@ impl CoreManager {
logging!(
info,
Type::Core,
true,
"尝试终止进程: {} (PID: {})",
process_name,
pid
@@ -659,7 +670,6 @@ impl CoreManager {
logging!(
warn,
Type::Core,
true,
"进程 {} (PID: {}) 终止命令成功但进程仍在运行",
process_name,
pid
@@ -669,7 +679,6 @@ impl CoreManager {
logging!(
info,
Type::Core,
true,
"成功终止进程: {} (PID: {})",
process_name,
pid
@@ -680,7 +689,6 @@ impl CoreManager {
logging!(
warn,
Type::Core,
true,
"无法终止进程: {} (PID: {})",
process_name,
pid
@@ -730,23 +738,13 @@ impl CoreManager {
}
async fn start_core_by_sidecar(&self) -> Result<()> {
logging!(info, Type::Core, true, "Running core by sidecar");
logging!(info, Type::Core, "Running core by sidecar");
let config_file = &Config::generate_file(ConfigType::Run).await?;
let app_handle = handle::Handle::global()
.app_handle()
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
let app_handle = handle::Handle::app_handle();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
create_dir_all(&service_log_dir)?;
let now = Local::now();
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
let log_path = service_log_dir.join(format!("sidecar_{timestamp}.log"));
let (mut rx, child) = app_handle
.shell()
.sidecar(&clash_core)?
@@ -759,30 +757,39 @@ impl CoreManager {
.spawn()?;
let pid = child.pid();
logging!(
trace,
Type::Core,
true,
"Started core by sidecar pid: {}",
pid
);
*self.child_sidecar.lock() = Some(child);
logging!(trace, Type::Core, "Started core by sidecar pid: {}", pid);
*self.child_sidecar.lock() = Some(CommandChildGuard::new(child));
self.set_running_mode(RunningMode::Sidecar);
let mut log_file = std::io::BufWriter::new(File::create(log_path)?);
let shared_writer: SharedWriter =
Arc::new(tokio::sync::Mutex::new(sidecar_writer().await?));
AsyncHandler::spawn(|| async move {
while let Some(event) = rx.recv().await {
let w = shared_writer.lock().await;
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line) => {
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
eprintln!("[Sidecar] write stdout failed: {e}");
}
let mut now = DeferredNow::default();
let message = String::from_utf8_lossy(&line).into_owned();
let message = write_sidecar_log(&*w, &mut now, log::Level::Error, message);
Logger::global().append_log(message);
}
tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
let _ = writeln!(log_file, "[stderr] {}", String::from_utf8_lossy(&line));
let mut now = DeferredNow::default();
let message = String::from_utf8_lossy(&line).into_owned();
let message = write_sidecar_log(&*w, &mut now, log::Level::Error, message);
Logger::global().append_log(message);
}
tauri_plugin_shell::process::CommandEvent::Terminated(term) => {
let _ = writeln!(log_file, "[terminated] {:?}", term);
let mut now = DeferredNow::default();
let message = if let Some(code) = term.code {
format!("Process terminated with code: {}", code)
} else if let Some(signal) = term.signal {
format!("Process terminated by signal: {}", signal)
} else {
"Process terminated".to_string()
};
write_sidecar_log(&*w, &mut now, log::Level::Info, message);
break;
}
_ => {}
@@ -793,18 +800,12 @@ impl CoreManager {
Ok(())
}
fn stop_core_by_sidecar(&self) -> Result<()> {
logging!(info, Type::Core, true, "Stopping core by sidecar");
logging!(info, Type::Core, "Stopping core by sidecar");
if let Some(child) = self.child_sidecar.lock().take() {
let pid = child.pid();
child.kill()?;
logging!(
trace,
Type::Core,
true,
"Stopped core by sidecar pid: {}",
pid
);
drop(child);
logging!(trace, Type::Core, "Stopped core by sidecar pid: {:?}", pid);
}
self.set_running_mode(RunningMode::NotRunning);
Ok(())
@@ -813,14 +814,14 @@ impl CoreManager {
impl CoreManager {
async fn start_core_by_service(&self) -> Result<()> {
logging!(info, Type::Core, true, "Running core by service");
logging!(info, Type::Core, "Running core by service");
let config_file = &Config::generate_file(ConfigType::Run).await?;
service::run_core_by_service(config_file).await?;
self.set_running_mode(RunningMode::Service);
Ok(())
}
async fn stop_core_by_service(&self) -> Result<()> {
logging!(info, Type::Core, true, "Stopping core by service");
logging!(info, Type::Core, "Stopping core by service");
service::stop_core_by_service().await?;
self.set_running_mode(RunningMode::NotRunning);
Ok(())
@@ -845,17 +846,16 @@ impl CoreManager {
logging!(
warn,
Type::Core,
true,
"应用初始化时清理多余 mihomo 进程失败: {}",
e
);
}
// 使用简化的启动流程
logging!(info, Type::Core, true, "开始核心初始化");
logging!(info, Type::Core, "开始核心初始化");
self.start_core().await?;
logging!(info, Type::Core, true, "核心初始化完成");
logging!(info, Type::Core, "核心初始化完成");
Ok(())
}
@@ -870,7 +870,6 @@ impl CoreManager {
}
pub async fn prestart_core(&self) -> Result<()> {
SERVICE_MANAGER.lock().await.refresh().await?;
match SERVICE_MANAGER.lock().await.current() {
ServiceStatus::Ready => {
self.set_running_mode(RunningMode::Service);
@@ -888,10 +887,10 @@ impl CoreManager {
match self.get_running_mode() {
RunningMode::Service => {
logging_error!(Type::Core, true, self.start_core_by_service().await);
logging_error!(Type::Core, self.start_core_by_service().await);
}
RunningMode::NotRunning | RunningMode::Sidecar => {
logging_error!(Type::Core, true, self.start_core_by_sidecar().await);
logging_error!(Type::Core, self.start_core_by_sidecar().await);
}
};
@@ -900,6 +899,7 @@ impl CoreManager {
/// 停止核心运行
pub async fn stop_core(&self) -> Result<()> {
Logger::global().clear_logs();
match self.get_running_mode() {
RunningMode::Service => self.stop_core_by_service().await,
RunningMode::Sidecar => self.stop_core_by_sidecar(),
@@ -909,8 +909,11 @@ impl CoreManager {
/// 重启内核
pub async fn restart_core(&self) -> Result<()> {
logging!(info, Type::Core, true, "Restarting core");
logging!(info, Type::Core, "Restarting core");
self.stop_core().await?;
if SERVICE_MANAGER.lock().await.init().await.is_ok() {
logging_error!(Type::Setup, SERVICE_MANAGER.lock().await.refresh().await);
}
self.start_core().await?;
Ok(())
}
@@ -919,17 +922,17 @@ impl CoreManager {
pub async fn change_core(&self, clash_core: Option<String>) -> Result<(), String> {
if clash_core.is_none() {
let error_message = "Clash core should not be Null";
logging!(error, Type::Core, true, "{}", error_message);
logging!(error, Type::Core, "{}", error_message);
return Err(error_message.to_string());
}
let core = clash_core.as_ref().ok_or_else(|| {
let msg = "Clash core should not be None";
logging!(error, Type::Core, true, "{}", msg);
logging!(error, Type::Core, "{}", msg);
msg.to_string()
})?;
if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) {
let error_message = format!("Clash core invalid name: {core}");
logging!(error, Type::Core, true, "{}", error_message);
logging!(error, Type::Core, "{}", error_message);
return Err(error_message);
}
@@ -938,11 +941,11 @@ impl CoreManager {
// 分离数据获取和异步调用避免Send问题
let verge_data = Config::verge().await.latest_ref().clone();
logging_error!(Type::Core, true, verge_data.save_file().await);
logging_error!(Type::Core, verge_data.save_file().await);
let run_path = Config::generate_file(ConfigType::Run).await.map_err(|e| {
let msg = e.to_string();
logging_error!(Type::Core, true, "{}", msg);
logging_error!(Type::Core, "{}", msg);
msg
})?;
@@ -5,7 +5,7 @@ use tokio::time::{Duration, sleep, timeout};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use crate::config::{Config, IVerge};
use crate::core::async_proxy_query::AsyncProxyQuery;
use crate::core::{async_proxy_query::AsyncProxyQuery, handle};
use crate::logging_error;
use crate::process::AsyncHandler;
use crate::utils::logging::Type;
@@ -85,6 +85,7 @@ struct ProxyConfig {
sys_enabled: bool,
pac_enabled: bool,
guard_enabled: bool,
guard_duration: u64,
}
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
@@ -184,16 +185,40 @@ impl EventDrivenProxyManager {
let mut event_stream = UnboundedReceiverStream::new(event_rx);
let mut query_stream = UnboundedReceiverStream::new(query_rx);
// 初始化定时器,用于周期性检查代理设置
let config = Self::get_proxy_config().await;
let mut guard_interval = tokio::time::interval(Duration::from_secs(config.guard_duration));
// 防止首次立即触发
guard_interval.tick().await;
loop {
tokio::select! {
Some(event) = event_stream.next() => {
log::debug!(target: "app", "处理代理事件: {event:?}");
let event_clone = event.clone(); // 保存一份副本用于后续检查
Self::handle_event(&state, event).await;
// 检查是否是配置变更事件,如果是,则可能需要更新定时器
if matches!(event_clone, ProxyEvent::ConfigChanged | ProxyEvent::AppStarted) {
let new_config = Self::get_proxy_config().await;
// 重新设置定时器间隔
guard_interval = tokio::time::interval(Duration::from_secs(new_config.guard_duration));
// 防止首次立即触发
guard_interval.tick().await;
}
}
Some(query) = query_stream.next() => {
let result = Self::handle_query(&state).await;
let _ = query.response_tx.send(result);
}
_ = guard_interval.tick() => {
// 定时检查代理设置
let config = Self::get_proxy_config().await;
if config.guard_enabled && config.sys_enabled {
log::debug!(target: "app", "定时检查代理设置");
Self::check_and_restore_proxy(&state).await;
}
}
else => {
// 两个通道都关闭时退出
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
@@ -225,6 +250,12 @@ impl EventDrivenProxyManager {
}
ProxyEvent::AppStopping => {
log::info!(target: "app", "清理代理状态");
Self::update_state_timestamp(state, |s| {
s.sys_enabled = false;
s.pac_enabled = false;
s.is_healthy = false;
})
.await;
}
}
}
@@ -276,6 +307,10 @@ impl EventDrivenProxyManager {
}
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过系统代理守卫检查");
return;
}
let (sys_enabled, pac_enabled) = {
let s = state.read().await;
(s.sys_enabled, s.pac_enabled)
@@ -295,6 +330,11 @@ impl EventDrivenProxyManager {
}
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过PAC代理恢复检查");
return;
}
let current = Self::get_auto_proxy_with_timeout().await;
let expected = Self::get_expected_pac_config().await;
@@ -321,6 +361,11 @@ impl EventDrivenProxyManager {
}
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过系统代理恢复检查");
return;
}
let current = Self::get_sys_proxy_with_timeout().await;
let expected = Self::get_expected_sys_proxy().await;
@@ -349,6 +394,11 @@ impl EventDrivenProxyManager {
}
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过启用系统代理");
return;
}
log::info!(target: "app", "启用系统代理");
let pac_enabled = state.read().await.pac_enabled;
@@ -376,17 +426,22 @@ impl EventDrivenProxyManager {
let disabled_sys = Sysproxy::default();
let disabled_auto = Autoproxy::default();
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
logging_error!(Type::System, disabled_auto.set_auto_proxy());
logging_error!(Type::System, disabled_sys.set_system_proxy());
}
}
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过代理模式切换");
return;
}
log::info!(target: "app", "切换到{}模式", if to_pac { "PAC" } else { "HTTP代理" });
if to_pac {
let disabled_sys = Sysproxy::default();
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
logging_error!(Type::System, disabled_sys.set_system_proxy());
let expected = Self::get_expected_pac_config().await;
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
@@ -394,7 +449,7 @@ impl EventDrivenProxyManager {
}
} else {
let disabled_auto = Autoproxy::default();
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
logging_error!(Type::System, disabled_auto.set_auto_proxy());
let expected = Self::get_expected_sys_proxy().await;
if let Err(e) = Self::restore_sys_proxy(&expected).await {
@@ -439,19 +494,21 @@ impl EventDrivenProxyManager {
}
async fn get_proxy_config() -> ProxyConfig {
let (sys_enabled, pac_enabled, guard_enabled) = {
let (sys_enabled, pac_enabled, guard_enabled, guard_duration) = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_ref();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
verge.enable_proxy_guard.unwrap_or(false),
verge.proxy_guard_duration.unwrap_or(30), // 默认30秒
)
};
ProxyConfig {
sys_enabled,
pac_enabled,
guard_enabled,
guard_duration,
}
}
@@ -518,6 +575,10 @@ impl EventDrivenProxyManager {
#[cfg(target_os = "windows")]
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过PAC代理恢复");
return Ok(());
}
Self::execute_sysproxy_command(&["pac", expected_url]).await
}
@@ -538,6 +599,10 @@ impl EventDrivenProxyManager {
#[cfg(target_os = "windows")]
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过系统代理恢复");
return Ok(());
}
let address = format!("{}:{}", expected.host, expected.port);
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await
}
@@ -555,6 +620,15 @@ impl EventDrivenProxyManager {
#[cfg(target_os = "windows")]
async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> {
if handle::Handle::global().is_exiting() {
log::debug!(
target: "app",
"应用正在退出,取消调用 sysproxy.exe,参数: {:?}",
args
);
return Ok(());
}
use crate::utils::dirs;
#[allow(unused_imports)] // creation_flags必须
use std::os::windows::process::CommandExt;
+33 -26
View File
@@ -1,4 +1,4 @@
use crate::singleton;
use crate::{APP_HANDLE, singleton};
use parking_lot::RwLock;
use std::{
sync::{
@@ -10,6 +10,8 @@ use std::{
time::{Duration, Instant},
};
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
use tauri_plugin_mihomo::{Mihomo, MihomoExt};
use tokio::sync::{RwLockReadGuard, RwLockWriteGuard};
use crate::{logging, utils::logging::Type};
@@ -107,7 +109,7 @@ impl NotificationSystem {
continue;
}
if let Some(window) = handle.get_window() {
if let Some(window) = Handle::get_window() {
*system.last_emit_time.write() = Instant::now();
let (event_name_str, payload_result) = match event {
@@ -249,7 +251,6 @@ impl NotificationSystem {
#[derive(Debug, Clone)]
pub struct Handle {
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
pub is_exiting: Arc<RwLock<bool>>,
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
startup_completed: Arc<RwLock<bool>>,
@@ -259,7 +260,6 @@ pub struct Handle {
impl Default for Handle {
fn default() -> Self {
Self {
app_handle: Arc::new(RwLock::new(None)),
is_exiting: Arc::new(RwLock::new(false)),
startup_errors: Arc::new(RwLock::new(Vec::new())),
startup_completed: Arc::new(RwLock::new(false)),
@@ -276,25 +276,41 @@ impl Handle {
Self::default()
}
pub fn init(&self, app_handle: AppHandle) {
{
let mut handle = self.app_handle.write();
*handle = Some(app_handle);
pub fn init(&self) {
// 如果正在退出,不要重新初始化
if self.is_exiting() {
log::debug!("Handle::init called while exiting, skipping initialization");
return;
}
let mut system_opt = self.notification_system.write();
if let Some(system) = system_opt.as_mut() {
system.start();
// 只在未运行时启动
if !system.is_running {
system.start();
} else {
log::debug!("NotificationSystem already running, skipping start");
}
}
}
/// 获取 AppHandle
pub fn app_handle(&self) -> Option<AppHandle> {
self.app_handle.read().clone()
#[allow(clippy::expect_used)]
pub fn app_handle() -> &'static AppHandle {
APP_HANDLE.get().expect("failed to get global app handle")
}
pub fn get_window(&self) -> Option<WebviewWindow> {
let app_handle = self.app_handle()?;
pub async fn mihomo() -> RwLockReadGuard<'static, Mihomo> {
Self::app_handle().mihomo().read().await
}
#[allow(unused)]
pub async fn mihomo_mut() -> RwLockWriteGuard<'static, Mihomo> {
Self::app_handle().mihomo().write().await
}
pub fn get_window() -> Option<WebviewWindow> {
let app_handle = Self::app_handle();
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
if window.is_none() {
log::debug!(target:"app", "main window not found");
@@ -402,7 +418,6 @@ impl Handle {
logging!(
info,
Type::Frontend,
true,
"启动过程中发现错误,加入消息队列: {} - {}",
status_str,
msg_str
@@ -452,7 +467,6 @@ impl Handle {
logging!(
info,
Type::Frontend,
true,
"发送{}条启动时累积的错误消息: {:?}",
errors.len(),
errors
@@ -509,14 +523,10 @@ impl Handle {
#[cfg(target_os = "macos")]
impl Handle {
pub fn set_activation_policy(&self, policy: tauri::ActivationPolicy) -> Result<(), String> {
let app_handle = self.app_handle();
if let Some(app_handle) = app_handle.as_ref() {
app_handle
.set_activation_policy(policy)
.map_err(|e| e.to_string())
} else {
Err("AppHandle not initialized".to_string())
}
let app_handle = Self::app_handle();
app_handle
.set_activation_policy(policy)
.map_err(|e| e.to_string())
}
pub fn set_activation_policy_regular(&self) {
@@ -524,7 +534,6 @@ impl Handle {
logging!(
warn,
Type::Setup,
true,
"Failed to set regular activation policy: {}",
e
);
@@ -536,7 +545,6 @@ impl Handle {
logging!(
warn,
Type::Setup,
true,
"Failed to set accessory activation policy: {}",
e
);
@@ -549,7 +557,6 @@ impl Handle {
logging!(
warn,
Type::Setup,
true,
"Failed to set prohibited activation policy: {}",
e
);
+4 -26
View File
@@ -200,9 +200,7 @@ impl Hotkey {
hotkey: &str,
function: HotkeyFunction,
) -> Result<()> {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
let app_handle = handle::Handle::app_handle();
let manager = app_handle.global_shortcut();
logging!(
@@ -296,7 +294,6 @@ impl Hotkey {
logging!(
debug,
Type::Hotkey,
true,
"Initializing global hotkeys: {}",
enable_global_hotkey
);
@@ -312,7 +309,6 @@ impl Hotkey {
logging!(
debug,
Type::Hotkey,
true,
"Has {} hotkeys need to register",
hotkeys.len()
);
@@ -327,7 +323,6 @@ impl Hotkey {
logging!(
debug,
Type::Hotkey,
true,
"Registering hotkey: {} -> {}",
key,
func
@@ -336,7 +331,6 @@ impl Hotkey {
logging!(
error,
Type::Hotkey,
true,
"Failed to register hotkey {} -> {}: {:?}",
key,
func,
@@ -358,7 +352,6 @@ impl Hotkey {
logging!(
error,
Type::Hotkey,
true,
"Invalid hotkey configuration: `{}`:`{}`",
key,
func
@@ -375,9 +368,7 @@ impl Hotkey {
}
pub fn reset(&self) -> Result<()> {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
let app_handle = handle::Handle::app_handle();
let manager = app_handle.global_shortcut();
manager.unregister_all()?;
Ok(())
@@ -390,9 +381,7 @@ impl Hotkey {
}
pub fn unregister(&self, hotkey: &str) -> Result<()> {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
let app_handle = handle::Handle::app_handle();
let manager = app_handle.global_shortcut();
manager.unregister(hotkey)?;
logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey);
@@ -468,22 +457,11 @@ impl Hotkey {
impl Drop for Hotkey {
fn drop(&mut self) {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
logging!(
error,
Type::Hotkey,
"Failed to get app handle during hotkey cleanup"
);
return;
}
};
let app_handle = handle::Handle::app_handle();
if let Err(e) = app_handle.global_shortcut().unregister_all() {
logging!(
error,
Type::Hotkey,
true,
"Error unregistering all hotkeys: {:?}",
e
);
@@ -0,0 +1,37 @@
use std::{collections::VecDeque, sync::Arc};
use once_cell::sync::OnceCell;
use parking_lot::{RwLock, RwLockReadGuard};
const LOGS_QUEUE_LEN: usize = 100;
pub struct Logger {
logs: Arc<RwLock<VecDeque<String>>>,
}
impl Logger {
pub fn global() -> &'static Logger {
static LOGGER: OnceCell<Logger> = OnceCell::new();
LOGGER.get_or_init(|| Logger {
logs: Arc::new(RwLock::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
})
}
pub fn get_logs(&self) -> RwLockReadGuard<'_, VecDeque<String>> {
self.logs.read()
}
pub fn append_log(&self, text: String) {
let mut logs = self.logs.write();
if logs.len() > LOGS_QUEUE_LEN {
logs.pop_front();
}
logs.push_back(text);
}
pub fn clear_logs(&self) {
let mut logs = self.logs.write();
logs.clear();
}
}
+1 -1
View File
@@ -5,8 +5,8 @@ mod core;
pub mod event_driven_proxy;
pub mod handle;
pub mod hotkey;
pub mod logger;
pub mod service;
pub mod service_ipc;
pub mod sysopt;
pub mod timer;
pub mod tray;
+100 -150
View File
@@ -1,17 +1,19 @@
use crate::{
cache::{CacheService, SHORT_TERM_TTL},
config::Config,
core::service_ipc::{IpcCommand, send_ipc_request},
logging, logging_error,
utils::{dirs, logging::Type},
utils::{dirs, init::service_writer_config, logging::Type},
};
use anyhow::{Context, Result, bail};
use clash_verge_service_ipc::CoreConfig;
use once_cell::sync::Lazy;
use std::{env::current_exe, path::PathBuf, process::Command as StdCommand};
use std::{
env::current_exe,
path::{Path, PathBuf},
process::Command as StdCommand,
time::Duration,
};
use tokio::sync::Mutex;
const REQUIRED_SERVICE_VERSION: &str = "1.1.2"; // 定义所需的服务版本号
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceStatus {
Ready,
@@ -29,14 +31,14 @@ pub struct ServiceManager(ServiceStatus);
#[allow(clippy::unused_async)]
#[cfg(target_os = "windows")]
async fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, true, "uninstall service");
logging!(info, Type::Service, "uninstall service");
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
let uninstall_path = binary_path.with_file_name("clash-verge-service-uninstall.exe");
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
@@ -64,14 +66,14 @@ async fn uninstall_service() -> Result<()> {
#[allow(clippy::unused_async)]
#[cfg(target_os = "windows")]
async fn install_service() -> Result<()> {
logging!(info, Type::Service, true, "install service");
logging!(info, Type::Service, "install service");
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service.exe");
let install_path = binary_path.with_file_name("clash-verge-service-install.exe");
if !install_path.exists() {
bail!(format!("installer not found: {install_path:?}"));
@@ -98,17 +100,11 @@ async fn install_service() -> Result<()> {
#[cfg(target_os = "windows")]
async fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "reinstall service");
logging!(info, Type::Service, "reinstall service");
// 先卸载服务
if let Err(err) = uninstall_service().await {
logging!(
warn,
Type::Service,
true,
"failed to uninstall service: {}",
err
);
logging!(warn, Type::Service, "failed to uninstall service: {}", err);
}
// 再安装服务
@@ -123,10 +119,11 @@ async fn reinstall_service() -> Result<()> {
#[allow(clippy::unused_async)]
#[cfg(target_os = "linux")]
async fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, true, "uninstall service");
logging!(info, Type::Service, "uninstall service");
use users::get_effective_uid;
let uninstall_path = tauri::utils::platform::current_exe()?.with_file_name("uninstall-service");
let uninstall_path =
tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall");
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
@@ -146,7 +143,6 @@ async fn uninstall_service() -> Result<()> {
logging!(
info,
Type::Service,
true,
"uninstall status code:{}",
status.code().unwrap_or(-1)
);
@@ -164,10 +160,11 @@ async fn uninstall_service() -> Result<()> {
#[cfg(target_os = "linux")]
#[allow(clippy::unused_async)]
async fn install_service() -> Result<()> {
logging!(info, Type::Service, true, "install service");
logging!(info, Type::Service, "install service");
use users::get_effective_uid;
let install_path = tauri::utils::platform::current_exe()?.with_file_name("install-service");
let install_path =
tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-install");
if !install_path.exists() {
bail!(format!("installer not found: {install_path:?}"));
@@ -187,7 +184,6 @@ async fn install_service() -> Result<()> {
logging!(
info,
Type::Service,
true,
"install status code:{}",
status.code().unwrap_or(-1)
);
@@ -204,17 +200,11 @@ async fn install_service() -> Result<()> {
#[cfg(target_os = "linux")]
async fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "reinstall service");
logging!(info, Type::Service, "reinstall service");
// 先卸载服务
if let Err(err) = uninstall_service().await {
logging!(
warn,
Type::Service,
true,
"failed to uninstall service: {}",
err
);
logging!(warn, Type::Service, "failed to uninstall service: {}", err);
}
// 再安装服务
@@ -230,10 +220,10 @@ async fn reinstall_service() -> Result<()> {
async fn uninstall_service() -> Result<()> {
use crate::utils::i18n::t;
logging!(info, Type::Service, true, "uninstall service");
logging!(info, Type::Service, "uninstall service");
let binary_path = dirs::service_path()?;
let uninstall_path = binary_path.with_file_name("uninstall-service");
let uninstall_path = binary_path.with_file_name("clash-verge-service-uninstall");
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
@@ -246,7 +236,7 @@ async fn uninstall_service() -> Result<()> {
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
);
// logging!(debug, Type::Service, true, "uninstall command: {}", command);
// logging!(debug, Type::Service, "uninstall command: {}", command);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
@@ -266,10 +256,10 @@ async fn uninstall_service() -> Result<()> {
async fn install_service() -> Result<()> {
use crate::utils::i18n::t;
logging!(info, Type::Service, true, "install service");
logging!(info, Type::Service, "install service");
let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service");
let install_path = binary_path.with_file_name("clash-verge-service-install");
if !install_path.exists() {
bail!(format!("installer not found: {install_path:?}"));
@@ -282,7 +272,7 @@ async fn install_service() -> Result<()> {
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
);
// logging!(debug, Type::Service, true, "install command: {}", command);
// logging!(debug, Type::Service, "install command: {}", command);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
@@ -300,17 +290,11 @@ async fn install_service() -> Result<()> {
#[cfg(target_os = "macos")]
async fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "reinstall service");
logging!(info, Type::Service, "reinstall service");
// 先卸载服务
if let Err(err) = uninstall_service().await {
logging!(
warn,
Type::Service,
true,
"failed to uninstall service: {}",
err
);
logging!(warn, Type::Service, "failed to uninstall service: {}", err);
}
// 再安装服务
@@ -324,37 +308,29 @@ async fn reinstall_service() -> Result<()> {
/// 强制重装服务(UI修复按钮)
pub async fn force_reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "用户请求强制重装服务");
logging!(info, Type::Service, "用户请求强制重装服务");
reinstall_service().await.map_err(|err| {
logging!(error, Type::Service, true, "强制重装服务失败: {}", err);
logging!(error, Type::Service, "强制重装服务失败: {}", err);
err
})
}
/// 检查服务版本 - 使用IPC通信
async fn check_service_version() -> Result<String> {
let cache = CacheService::global();
let key = CacheService::make_key("service", "version");
let version_arc = cache
.get_or_fetch(key, SHORT_TERM_TTL, || async {
logging!(info, Type::Service, true, "开始检查服务版本 (IPC)");
let payload = serde_json::json!({});
let response = send_ipc_request(IpcCommand::GetVersion, payload).await?;
let version_arc: Result<String> = {
logging!(info, Type::Service, "开始检查服务版本 (IPC)");
let response = clash_verge_service_ipc::get_version()
.await
.context("无法连接到Clash Verge Service")?;
if response.code > 0 {
let err_msg = response.message;
logging!(error, Type::Service, "获取服务版本失败: {}", err_msg);
return Err(anyhow::anyhow!(err_msg));
}
let data = response
.data
.ok_or_else(|| anyhow::anyhow!("服务版本响应中没有数据"))?;
if let Some(nested_data) = data.get("data")
&& let Some(version) = nested_data.get("version").and_then(|v| v.as_str())
{
// logging!(info, Type::Service, true, "获取到服务版本: {}", version);
return Ok(version.to_string());
}
Ok("unknown".to_string())
})
.await;
let version = response.data.unwrap_or("unknown".to_string());
Ok(version)
};
match version_arc.as_ref() {
Ok(v) => Ok(v.clone()),
@@ -365,14 +341,14 @@ async fn check_service_version() -> Result<String> {
/// 检查服务是否需要重装
pub async fn check_service_needs_reinstall() -> bool {
match check_service_version().await {
Ok(version) => version != REQUIRED_SERVICE_VERSION,
Ok(version) => version != clash_verge_service_ipc::VERSION,
Err(_) => false,
}
}
/// 尝试使用服务启动core
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
logging!(info, Type::Service, "尝试使用现有服务启动核心");
let verge_config = Config::verge().await;
let clash_core = verge_config.latest_ref().get_valid_clash_core();
@@ -381,113 +357,97 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let bin_path = current_exe()?.with_file_name(format!("{clash_core}{bin_ext}"));
let payload = serde_json::json!({
"core_type": clash_core,
"bin_path": dirs::path_to_str(&bin_path)?,
"config_dir": dirs::path_to_str(&dirs::app_home_dir()?)?,
"config_file": dirs::path_to_str(config_file)?,
"log_file": dirs::path_to_str(&dirs::service_log_file()?)?,
});
let payload = clash_verge_service_ipc::ClashConfig {
core_config: CoreConfig {
config_path: dirs::path_to_str(config_file)?.to_string(),
core_path: dirs::path_to_str(&bin_path)?.to_string(),
config_dir: dirs::path_to_str(&dirs::app_home_dir()?)?.to_string(),
},
log_config: service_writer_config().await?,
};
let response = send_ipc_request(IpcCommand::StartClash, payload)
let response = clash_verge_service_ipc::start_clash(&payload)
.await
.context("无法连接到Clash Verge Service")?;
if !response.success {
let err_msg = response.error.unwrap_or_else(|| "启动核心失败".to_string());
if response.code > 0 {
let err_msg = response.message;
logging!(error, Type::Service, "启动核心失败: {}", err_msg);
bail!(err_msg);
}
if let Some(data) = &response.data
&& let Some(code) = data.get("code").and_then(|c| c.as_u64())
&& code != 0
{
let msg = data
.get("msg")
.and_then(|m| m.as_str())
.unwrap_or("未知错误");
bail!("启动核心失败: {}", msg);
}
logging!(info, Type::Service, true, "服务成功启动核心");
logging!(info, Type::Service, "服务成功启动核心");
Ok(())
}
// 以服务启动core
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
logging!(info, Type::Service, true, "正在尝试通过服务启动核心");
logging!(info, Type::Service, "正在尝试通过服务启动核心");
if check_service_needs_reinstall().await {
reinstall_service().await?;
}
logging!(info, Type::Service, true, "服务已运行且版本匹配,直接使用");
logging!(info, Type::Service, "服务已运行且版本匹配,直接使用");
start_with_existing_service(config_file).await
}
/// 通过服务停止core
pub(super) async fn stop_core_by_service() -> Result<()> {
logging!(info, Type::Service, true, "通过服务停止核心 (IPC)");
logging!(info, Type::Service, "通过服务停止核心 (IPC)");
let payload = serde_json::json!({});
let response = send_ipc_request(IpcCommand::StopClash, payload)
let response = clash_verge_service_ipc::stop_clash()
.await
.context("无法连接到Clash Verge Service")?;
if !response.success {
let err_msg = response.error.unwrap_or_else(|| "停止核心失败".to_string());
logging!(error, Type::Service, true, "停止核心失败: {}", err_msg);
if response.code > 0 {
let err_msg = response.message;
logging!(error, Type::Service, "停止核心失败: {}", err_msg);
bail!(err_msg);
}
if let Some(data) = &response.data
&& let Some(code) = data.get("code")
{
let code_value = code.as_u64().unwrap_or(1);
let msg = data
.get("msg")
.and_then(|m| m.as_str())
.unwrap_or("未知错误");
if code_value != 0 {
logging!(
error,
Type::Service,
true,
"停止核心返回错误: code={}, msg={}",
code_value,
msg
);
bail!("停止核心失败: {}", msg);
}
}
logging!(info, Type::Service, true, "服务成功停止核心");
logging!(info, Type::Service, "服务成功停止核心");
Ok(())
}
/// 检查服务是否正在运行
pub async fn is_service_available() -> Result<()> {
check_service_version().await?;
clash_verge_service_ipc::connect().await?;
Ok(())
}
pub fn is_service_ipc_path_exists() -> bool {
Path::new(clash_verge_service_ipc::IPC_PATH).exists()
}
impl ServiceManager {
pub fn default() -> Self {
Self(ServiceStatus::Unavailable("Need Checks".into()))
}
pub fn config() -> Option<clash_verge_service_ipc::IpcConfig> {
Some(clash_verge_service_ipc::IpcConfig {
default_timeout: Duration::from_millis(30),
retry_delay: Duration::from_millis(250),
max_retries: 6,
})
}
pub async fn init(&mut self) -> Result<()> {
if let Err(e) = clash_verge_service_ipc::connect().await {
self.0 = ServiceStatus::Unavailable("服务连接失败: {e}".to_string());
return Err(e);
}
Ok(())
}
pub fn current(&self) -> ServiceStatus {
self.0.clone()
}
pub async fn refresh(&mut self) -> Result<()> {
let status = self.check_service_comprehensive().await;
logging_error!(
Type::Service,
true,
self.handle_service_status(&status).await
);
logging_error!(Type::Service, self.handle_service_status(&status).await);
self.0 = status;
Ok(())
}
@@ -496,16 +456,16 @@ impl ServiceManager {
pub async fn check_service_comprehensive(&self) -> ServiceStatus {
match is_service_available().await {
Ok(_) => {
logging!(info, Type::Service, true, "服务当前可用,检查是否需要重装");
logging!(info, Type::Service, "服务当前可用,检查是否需要重装");
if check_service_needs_reinstall().await {
logging!(info, Type::Service, true, "服务需要重装且允许重装");
logging!(info, Type::Service, "服务需要重装且允许重装");
ServiceStatus::NeedsReinstall
} else {
ServiceStatus::Ready
}
}
Err(err) => {
logging!(warn, Type::Service, true, "服务不可用,检查安装状态");
logging!(warn, Type::Service, "服务不可用,检查安装状态");
ServiceStatus::Unavailable(err.to_string())
}
}
@@ -515,50 +475,40 @@ impl ServiceManager {
pub async fn handle_service_status(&mut self, status: &ServiceStatus) -> Result<()> {
match status {
ServiceStatus::Ready => {
logging!(info, Type::Service, true, "服务就绪,直接启动");
Ok(())
logging!(info, Type::Service, "服务就绪,直接启动");
}
ServiceStatus::NeedsReinstall | ServiceStatus::ReinstallRequired => {
logging!(info, Type::Service, true, "服务需要重装,执行重装流程");
logging!(info, Type::Service, "服务需要重装,执行重装流程");
reinstall_service().await?;
self.0 = ServiceStatus::Ready;
Ok(())
}
ServiceStatus::ForceReinstallRequired => {
logging!(
info,
Type::Service,
true,
"服务需要强制重装,执行强制重装流程"
);
logging!(info, Type::Service, "服务需要强制重装,执行强制重装流程");
force_reinstall_service().await?;
self.0 = ServiceStatus::Ready;
Ok(())
}
ServiceStatus::InstallRequired => {
logging!(info, Type::Service, true, "需要安装服务,执行安装流程");
logging!(info, Type::Service, "需要安装服务,执行安装流程");
install_service().await?;
self.0 = ServiceStatus::Ready;
Ok(())
}
ServiceStatus::UninstallRequired => {
logging!(info, Type::Service, true, "服务需要卸载,执行卸载流程");
logging!(info, Type::Service, "服务需要卸载,执行卸载流程");
uninstall_service().await?;
self.0 = ServiceStatus::Unavailable("Service Uninstalled".into());
Ok(())
}
ServiceStatus::Unavailable(reason) => {
logging!(
info,
Type::Service,
true,
"服务不可用: {},将使用Sidecar模式",
reason
);
self.0 = ServiceStatus::Unavailable(reason.clone());
Err(anyhow::anyhow!("服务不可用: {}", reason))
return Err(anyhow::anyhow!("服务不可用: {}", reason));
}
}
Ok(())
}
}
@@ -1,349 +0,0 @@
use crate::{logging, utils::logging::Type};
use anyhow::{Context, Result, bail};
use backoff::{Error as BackoffError, ExponentialBackoff};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::windows::named_pipe::ClientOptions;
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
r"\\.\pipe\clash-verge-service"
} else {
"/tmp/clash-verge-service.sock"
};
// 定义命令类型
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum IpcCommand {
GetClash,
GetVersion,
StartClash,
StopClash,
}
// IPC消息格式
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpcRequest {
pub id: String,
pub timestamp: u64,
pub command: IpcCommand,
pub payload: serde_json::Value,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpcResponse {
pub id: String,
pub success: bool,
pub data: Option<serde_json::Value>,
pub error: Option<String>,
pub signature: String,
}
// 密钥派生函数
fn derive_secret_key() -> Vec<u8> {
// to do
// 从系统安全存储中获取或从程序安装时生成的密钥文件中读取
let unique_app_id = "clash-verge-app-secret-fuck-me-until-daylight";
let mut hasher = Sha256::new();
hasher.update(unique_app_id.as_bytes());
hasher.finalize().to_vec()
}
// 创建带签名的请求
pub fn create_signed_request(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcRequest> {
let id = nanoid::nanoid!(32);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let unsigned_request = IpcRequest {
id: id.clone(),
timestamp,
command: command.clone(),
payload: payload.clone(),
signature: String::new(),
};
let unsigned_json = serde_json::to_string(&unsigned_request)?;
let signature = sign_message(&unsigned_json)?;
Ok(IpcRequest {
id,
timestamp,
command,
payload,
signature,
})
}
// 签名消息
fn sign_message(message: &str) -> Result<String> {
type HmacSha256 = Hmac<Sha256>;
let secret_key = derive_secret_key();
let mut mac = HmacSha256::new_from_slice(&secret_key).context("HMAC初始化失败")?;
mac.update(message.as_bytes());
let result = mac.finalize();
let signature = hex::encode(result.into_bytes());
Ok(signature)
}
// 验证响应签名
pub fn verify_response_signature(response: &IpcResponse) -> Result<bool> {
let verification_response = IpcResponse {
id: response.id.clone(),
success: response.success,
data: response.data.clone(),
error: response.error.clone(),
signature: String::new(),
};
let message = serde_json::to_string(&verification_response)?;
let expected_signature = sign_message(&message)?;
Ok(expected_signature == response.signature)
}
fn create_backoff_strategy() -> ExponentialBackoff {
ExponentialBackoff {
initial_interval: Duration::from_millis(50),
max_interval: Duration::from_secs(1),
max_elapsed_time: Some(Duration::from_secs(3)),
multiplier: 1.5,
..Default::default()
}
}
pub async fn send_ipc_request(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
let command_type = format!("{command:?}");
let operation = || async {
match send_ipc_request_internal(command.clone(), payload.clone()).await {
Ok(response) => Ok(response),
Err(e) => {
logging!(
warn,
Type::Service,
true,
"IPC请求失败,准备重试: 命令={}, 错误={}",
command_type,
e
);
Err(BackoffError::transient(e))
}
}
};
match backoff::future::retry(create_backoff_strategy(), operation).await {
Ok(response) => {
// logging!(
// info,
// Type::Service,
// true,
// "IPC请求成功: 命令={}, 成功={}",
// command_type,
// response.success
// );
Ok(response)
}
Err(e) => {
logging!(
error,
Type::Service,
true,
"IPC请求最终失败,重试已耗尽: 命令={}, 错误={}",
command_type,
e
);
Err(anyhow::anyhow!("IPC请求重试失败: {}", e))
}
}
}
// 内部IPC请求实现(不带重试)
async fn send_ipc_request_internal(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
#[cfg(target_os = "windows")]
{
send_ipc_request_windows(command, payload).await
}
#[cfg(target_family = "unix")]
{
send_ipc_request_unix(command, payload).await
}
}
// IPC连接管理-win
#[cfg(target_os = "windows")]
async fn send_ipc_request_windows(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
let request = create_signed_request(command, payload)?;
let request_json = serde_json::to_string(&request)?;
let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
let mut pipe = match ClientOptions::new().open(IPC_SOCKET_NAME) {
Ok(p) => p,
Err(e) => {
logging!(error, Type::Service, true, "连接到服务命名管道失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", e));
}
};
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
pipe.write_all(&len_bytes).await?;
pipe.write_all(request_bytes).await?;
pipe.flush().await?;
let mut response_len_bytes = [0u8; 4];
pipe.read_exact(&mut response_len_bytes).await?;
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len];
pipe.read_exact(&mut response_bytes).await?;
let response: IpcResponse = serde_json::from_slice(&response_bytes)
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
if !verify_response_signature(&response)? {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
Ok(response)
}
// IPC连接管理-unix
#[cfg(target_family = "unix")]
async fn send_ipc_request_unix(
command: IpcCommand,
payload: serde_json::Value,
) -> Result<IpcResponse> {
let request = create_signed_request(command, payload)?;
let request_json = serde_json::to_string(&request)?;
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME).await {
Ok(s) => s,
Err(e) => {
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
}
};
let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
stream.write_all(&len_bytes).await?;
stream.write_all(request_bytes).await?;
stream.flush().await?;
// 读取响应长度
let mut response_len_bytes = [0u8; 4];
stream.read_exact(&mut response_len_bytes).await?;
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len];
stream.read_exact(&mut response_bytes).await?;
let response: IpcResponse = serde_json::from_slice(&response_bytes)
.map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
if !verify_response_signature(&response)? {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_signed_request() {
let command = IpcCommand::GetVersion;
let payload = serde_json::json!({"test": "data"});
let result = create_signed_request(command, payload);
assert!(result.is_ok());
if let Ok(request) = result {
assert!(!request.id.is_empty());
assert!(!request.signature.is_empty());
assert_eq!(request.command, IpcCommand::GetVersion);
}
}
#[test]
fn test_sign_and_verify_message() {
let test_message = "test message for signing";
let signature_result = sign_message(test_message);
assert!(signature_result.is_ok());
if let Ok(signature) = signature_result {
assert!(!signature.is_empty());
// 测试相同消息产生相同签名
if let Ok(signature2) = sign_message(test_message) {
assert_eq!(signature, signature2);
}
}
}
#[test]
fn test_verify_response_signature() {
let response = IpcResponse {
id: "test-id".to_string(),
success: true,
data: Some(serde_json::json!({"result": "success"})),
error: None,
signature: String::new(),
};
// 创建正确的签名
let verification_response = IpcResponse {
id: response.id.clone(),
success: response.success,
data: response.data.clone(),
error: response.error.clone(),
signature: String::new(),
};
if let Ok(message) = serde_json::to_string(&verification_response)
&& let Ok(correct_signature) = sign_message(&message)
{
let signed_response = IpcResponse {
signature: correct_signature,
..response
};
let verification_result = verify_response_signature(&signed_response);
assert!(verification_result.is_ok());
if let Ok(is_valid) = verification_result {
assert!(is_valid);
}
}
}
}
+44 -75
View File
@@ -51,6 +51,35 @@ async fn get_bypass() -> String {
}
}
// Uses tokio Command with CREATE_NO_WINDOW flag to avoid DLL initialization issues during shutdown
#[cfg(target_os = "windows")]
async fn execute_sysproxy_command(args: Vec<String>) -> Result<()> {
use crate::utils::dirs;
use anyhow::bail;
#[allow(unused_imports)] // Required for .creation_flags() method
use std::os::windows::process::CommandExt;
use tokio::process::Command;
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
bail!("sysproxy.exe not found");
}
let output = Command::new(sysproxy_exe)
.args(&args)
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
.output()
.await?;
if !output.status.success() {
bail!("sysproxy exe run failed");
}
Ok(())
}
impl Default for Sysopt {
fn default() -> Self {
Sysopt {
@@ -148,49 +177,17 @@ impl Sysopt {
proxy_manager.notify_config_changed();
return result;
}
use crate::{core::handle::Handle, utils::dirs};
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
bail!("sysproxy.exe not found");
}
let shell = app_handle.shell();
let output = if pac_enable {
let args = if pac_enable {
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
let sysproxy_str = sysproxy_exe
.as_path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
shell
.command(sysproxy_str)
.args(["pac", address.as_str()])
.output()
.await?
vec!["pac".to_string(), address]
} else {
let address = format!("{proxy_host}:{port}");
let bypass = get_bypass().await;
let sysproxy_str = sysproxy_exe
.as_path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
shell
.command(sysproxy_str)
.args(["global", address.as_str(), bypass.as_ref()])
.output()
.await?
vec!["global".to_string(), address, bypass]
};
if !output.status.success() {
bail!("sysproxy exe run failed");
}
execute_sysproxy_command(args).await?;
}
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
@@ -223,35 +220,7 @@ impl Sysopt {
#[cfg(target_os = "windows")]
{
use crate::{core::handle::Handle, utils::dirs};
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
bail!("sysproxy.exe not found");
}
let shell = app_handle.shell();
let sysproxy_str = sysproxy_exe
.as_path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
let output = shell
.command(sysproxy_str)
.args(["set", "1"])
.output()
.await?;
if !output.status.success() {
bail!("sysproxy exe run failed");
}
execute_sysproxy_command(vec!["set".to_string(), "1".to_string()]).await?;
}
Ok(())
@@ -261,7 +230,12 @@ impl Sysopt {
pub async fn update_launch(&self) -> Result<()> {
let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch };
let is_enable = enable_auto_launch.unwrap_or(false);
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
logging!(
info,
Type::System,
"Setting auto-launch state to: {:?}",
is_enable
);
// 首先尝试使用快捷方式方法
#[cfg(target_os = "windows")]
@@ -293,16 +267,13 @@ impl Sysopt {
/// 尝试使用原来的自启动方法
fn try_original_autostart_method(&self, is_enable: bool) {
let Some(app_handle) = Handle::global().app_handle() else {
log::error!(target: "app", "App handle not available for autostart");
return;
};
let app_handle = Handle::app_handle();
let autostart_manager = app_handle.autolaunch();
if is_enable {
logging_error!(Type::System, true, "{:?}", autostart_manager.enable());
logging_error!(Type::System, "{:?}", autostart_manager.enable());
} else {
logging_error!(Type::System, true, "{:?}", autostart_manager.disable());
logging_error!(Type::System, "{:?}", autostart_manager.disable());
}
}
@@ -323,9 +294,7 @@ impl Sysopt {
}
// 回退到原来的方法
let app_handle = Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
let app_handle = Handle::app_handle();
let autostart_manager = app_handle.autolaunch();
match autostart_manager.is_enabled() {
+23 -2
View File
@@ -64,7 +64,7 @@ impl Timer {
if let Err(e) = self.refresh().await {
// Reset initialization flag on error
self.initialized.store(false, Ordering::SeqCst);
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
logging_error!(Type::Timer, "Failed to initialize timer: {}", e);
return Err(e);
}
@@ -139,6 +139,27 @@ impl Timer {
Ok(())
}
/// 每 3 秒更新系统托盘菜单,总共执行 3 次
pub fn add_update_tray_menu_task(&self) -> Result<()> {
let tid = self.timer_count.fetch_add(1, Ordering::SeqCst);
let delay_timer = self.delay_timer.write();
let task = TaskBuilder::default()
.set_task_id(tid)
.set_maximum_parallel_runnable_num(1)
.set_frequency_count_down_by_seconds(3, 3)
.spawn_async_routine(|| async move {
logging!(info, Type::Timer, "Updating tray menu");
crate::core::tray::Tray::global()
.update_tray_display()
.await
})
.context("failed to create update tray menu timer task")?;
delay_timer
.add_task(task)
.context("failed to add update tray menu timer task")?;
Ok(())
}
/// Refresh timer tasks with better error handling
pub async fn refresh(&self) -> Result<()> {
// Generate diff outside of lock to minimize lock contention
@@ -480,7 +501,7 @@ impl Timer {
}
},
Err(_) => {
logging_error!(Type::Timer, false, "Timer task timed out for uid: {}", uid);
logging_error!(Type::Timer, "Timer task timed out for uid: {}", uid);
}
}
+119 -99
View File
@@ -3,16 +3,13 @@ use tauri::Emitter;
use tauri::tray::TrayIconBuilder;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::ipc::Rate;
use crate::module::lightweight;
use crate::process::AsyncHandler;
use crate::utils::window_manager::WindowManager;
use crate::{
Type, cmd,
config::Config,
feat,
ipc::IpcManager,
logging,
feat, logging,
module::lightweight::is_in_lightweight_mode,
singleton_lazy,
utils::{dirs::find_target_icons, i18n::t},
@@ -34,6 +31,8 @@ use tauri::{
tray::{MouseButton, MouseButtonState, TrayIconEvent},
};
// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑)
#[derive(Clone)]
struct TrayState {}
@@ -54,7 +53,7 @@ fn should_handle_tray_click() -> bool {
*last_click = now;
true
} else {
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
now.duration_since(*last_click).as_millis());
false
}
@@ -189,18 +188,35 @@ singleton_lazy!(Tray, TRAY, Tray::default);
impl Tray {
pub async fn init(&self) -> Result<()> {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray initialization"))?;
self.create_tray_from_handle(&app_handle).await?;
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘初始化");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
match self.create_tray_from_handle(app_handle).await {
Ok(_) => {
log::info!(target: "app", "System tray created successfully");
}
Err(e) => {
// Don't return error, let application continue running without tray
log::warn!(target: "app", "System tray creation failed: {}, Application will continue running without tray icon", e);
}
}
// TODO: 初始化时,暂时使用此方法更新系统托盘菜单,有效避免代理节点菜单空白
crate::core::timer::Timer::global().add_update_tray_menu_task()?;
Ok(())
}
/// 更新托盘点击行为
pub async fn update_click_behavior(&self) -> Result<()> {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘点击行为更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
let tray_event = { Config::verge().await.latest_ref().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into());
let tray = app_handle
@@ -215,6 +231,10 @@ impl Tray {
/// 更新托盘菜单
pub async fn update_menu(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘菜单更新");
return Ok(());
}
// 调整最小更新间隔,确保状态及时刷新
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
@@ -240,18 +260,12 @@ impl Tray {
return Ok(());
}
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
return Ok(());
}
};
let app_handle = handle::Handle::app_handle();
// 设置更新状态
self.menu_updating.store(true, Ordering::Release);
let result = self.update_menu_internal(&app_handle).await;
let result = self.update_menu_internal(app_handle).await;
{
let mut last_update = self.last_menu_update.lock();
@@ -308,14 +322,13 @@ impl Tray {
/// 更新托盘图标
#[cfg(target_os = "macos")]
pub async fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
return Ok(());
}
};
pub async fn update_icon(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
@@ -345,14 +358,13 @@ impl Tray {
}
#[cfg(not(target_os = "macos"))]
pub async fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
return Ok(());
}
};
pub async fn update_icon(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘图标更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
@@ -379,9 +391,12 @@ impl Tray {
/// 更新托盘显示状态的函数
pub async fn update_tray_display(&self) -> Result<()> {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘显示状态更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
let _tray = app_handle
.tray_by_id("main")
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
@@ -394,13 +409,12 @@ impl Tray {
/// 更新托盘提示
pub async fn update_tooltip(&self) -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "更新托盘提示失败: app_handle不存在");
return Ok(());
}
};
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘提示更新");
return Ok(());
}
let app_handle = handle::Handle::app_handle();
let verge = Config::verge().await.latest_ref().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
@@ -432,17 +446,24 @@ impl Tray {
let tun_text = t("TUN").await;
let profile_text = t("Profile").await;
let version = env!("CARGO_PKG_VERSION");
let v = env!("CARGO_PKG_VERSION");
let reassembled_version = v.split_once('+').map_or(v.to_string(), |(main, rest)| {
format!("{main}+{}", rest.split('.').next().unwrap_or(""))
});
let tooltip = format!(
"Clash Verge {}\n{}: {}\n{}: {}\n{}: {}",
reassembled_version,
sys_proxy_text,
switch_map[system_proxy],
tun_text,
switch_map[tun_mode],
profile_text,
current_profile_name
);
if let Some(tray) = app_handle.tray_by_id("main") {
let _ = tray.set_tooltip(Some(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
sys_proxy_text,
switch_map[system_proxy],
tun_text,
switch_map[tun_mode],
profile_text,
current_profile_name
)));
let _ = tray.set_tooltip(Some(&tooltip));
} else {
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
}
@@ -451,15 +472,24 @@ impl Tray {
}
pub async fn update_part(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘局部更新");
return Ok(());
}
// self.update_menu().await?;
// 更新轻量模式显示状态
self.update_tray_display().await?;
self.update_icon(None).await?;
self.update_icon().await?;
self.update_tooltip().await?;
Ok(())
}
pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘创建");
return Ok(());
}
log::info!(target: "app", "正在从AppHandle创建系统托盘");
// 获取图标
@@ -537,10 +567,15 @@ impl Tray {
// 托盘统一的状态更新函数
pub async fn update_all_states(&self) -> Result<()> {
if handle::Handle::global().is_exiting() {
log::debug!(target: "app", "应用正在退出,跳过托盘状态更新");
return Ok(());
}
// 确保所有状态更新完成
self.update_tray_display().await?;
// self.update_menu().await?;
self.update_icon(None).await?;
self.update_icon().await?;
self.update_tooltip().await?;
Ok(())
@@ -568,14 +603,7 @@ async fn create_tray_menu(
.unwrap_or_default()
};
let proxy_nodes_data = cmd::get_proxies().await.unwrap_or_else(|e| {
logging!(
error,
Type::Cmd,
"Failed to fetch proxies for tray menu: {e}"
);
serde_json::Value::Object(serde_json::Map::new())
});
let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await;
let version = env!("CARGO_PKG_VERSION");
@@ -628,46 +656,43 @@ async fn create_tray_menu(
let mut submenus = Vec::new();
let mut group_name_submenus_hash = HashMap::new();
if let Some(proxies) = proxy_nodes_data.get("proxies").and_then(|v| v.as_object()) {
for (group_name, group_data) in proxies.iter() {
// TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息
if let Ok(proxy_nodes_data) = proxy_nodes_data {
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
// Filter groups based on mode
let should_show = match mode {
"global" => group_name == "GLOBAL",
_ => group_name != "GLOBAL",
} &&
// Check if the group is hidden
!group_data.get("hidden").and_then(|v| v.as_bool()).unwrap_or(false);
!group_data.hidden.unwrap_or_default();
if !should_show {
continue;
}
let Some(all_proxies) = group_data.get("all").and_then(|v| v.as_array()) else {
let Some(all_proxies) = group_data.all.as_ref() else {
continue;
};
let now_proxy = group_data.get("now").and_then(|v| v.as_str()).unwrap_or("");
let now_proxy = group_data.now.as_deref().unwrap_or_default();
// Create proxy items
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
.iter()
.filter_map(|proxy_name| proxy_name.as_str())
.filter_map(|proxy_str| {
let is_selected = proxy_str == now_proxy;
let is_selected = *proxy_str == now_proxy;
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
// Get delay for display
let delay_text = proxies
let delay_text = proxy_nodes_data
.proxies
.get(proxy_str)
.and_then(|p| p.get("history"))
.and_then(|h| h.as_array())
.and_then(|h| h.last())
.and_then(|r| r.get("delay"))
.and_then(|d| d.as_i64())
.map(|delay| match delay {
-1 => "-ms".to_string(),
.and_then(|h| h.history.last())
.map(|h| match h.delay {
0 => "-ms".to_string(),
delay if delay >= 10000 => "-ms".to_string(),
_ => format!("{}ms", delay),
_ => format!("{}ms", h.delay),
})
.unwrap_or_else(|| "-ms".to_string());
@@ -994,13 +1019,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
match event.id.as_ref() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5]; // Removing the "_mode" suffix
logging!(
info,
Type::ProxyMode,
true,
"Switch Proxy Mode To: {}",
mode
);
logging!(info, Type::ProxyMode, "Switch Proxy Mode To: {}", mode);
feat::change_clash_mode(mode.into()).await;
}
"open_window" => {
@@ -1056,29 +1075,30 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
let group_name = parts[1];
let proxy_name = parts[2];
match cmd::proxy::update_proxy_and_sync(
group_name.to_string(),
proxy_name.to_string(),
)
.await
match handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await
{
Ok(_) => {
log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name);
let _ = handle::Handle::app_handle()
.emit("verge://refresh-proxy-config", ());
}
Err(e) => {
log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e);
// Fallback to IPC update
if (IpcManager::global()
.update_proxy(group_name, proxy_name)
if (handle::Handle::mihomo()
.await
.select_node_for_group(group_name, proxy_name)
.await)
.is_ok()
{
log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name);
if let Some(app_handle) = handle::Handle::global().app_handle() {
let _ = app_handle.emit("verge://force-refresh-proxies", ());
}
let app_handle = handle::Handle::app_handle();
let _ = app_handle.emit("verge://force-refresh-proxies", ());
}
}
}
@@ -29,6 +29,26 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
});
if enable {
#[cfg(target_os = "linux")]
{
let stack_key = Value::from("stack");
let should_override = match tun_val.get(&stack_key) {
Some(value) => value
.as_str()
.map(|stack| stack.eq_ignore_ascii_case("gvisor"))
.unwrap_or(false),
None => true,
};
if should_override {
revise!(tun_val, "stack", "mixed");
log::warn!(
target: "app",
"gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility"
);
}
}
// 读取DNS配置
let dns_key = Value::from("dns");
let dns_val = config.get(&dns_key);
+2 -1
View File
@@ -20,6 +20,8 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
.await
{
log::error!(target: "app", "Failed to upload to WebDAV: {err:#?}");
// 上传失败时重置客户端缓存
backup::WebDavClient::global().reset();
return Err(err);
}
@@ -73,7 +75,6 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
zip.extract(app_home_dir()?)?;
logging_error!(
Type::Backup,
true,
super::patch_verge(
IVerge {
webdav_url,
+23 -29
View File
@@ -1,10 +1,9 @@
use crate::{
config::Config,
core::{CoreManager, handle, tray},
ipc::IpcManager,
logging_error,
process::AsyncHandler,
utils::{logging::Type, resolve},
utils::{self, logging::Type, resolve},
};
use serde_yaml_ng::{Mapping, Value};
@@ -24,33 +23,28 @@ pub async fn restart_clash_core() {
/// Restart the application
pub async fn restart_app() {
// logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
resolve::resolve_reset_async().await;
utils::server::shutdown_embedded_server();
if let Err(err) = resolve::resolve_reset_async().await {
handle::Handle::notice_message(
"restart_app::error",
format!("Failed to cleanup resources: {err}"),
);
log::error!(target:"app", "Restart failed during cleanup: {err}");
return;
}
handle::Handle::global()
.app_handle()
.map(|app_handle| {
app_handle.restart();
})
.unwrap_or_else(|| {
logging_error!(
Type::System,
false,
"{}",
"Failed to get app handle for restart"
);
});
let app_handle = handle::Handle::app_handle();
app_handle.restart();
}
fn after_change_clash_mode() {
AsyncHandler::spawn(move || async {
match IpcManager::global().get_connections().await {
let mihomo = handle::Handle::mihomo().await;
match mihomo.get_connections().await {
Ok(connections) => {
if let Some(connections_array) = connections["connections"].as_array() {
if let Some(connections_array) = connections.connections {
for connection in connections_array {
if let Some(id) = connection["id"].as_str() {
let _ = IpcManager::global().delete_connection(id).await;
}
let _ = mihomo.close_connection(&connection.id).await;
}
}
}
@@ -70,7 +64,11 @@ pub async fn change_clash_mode(mode: String) {
"mode": mode
});
log::debug!(target: "app", "change clash mode to {mode}");
match IpcManager::global().patch_configs(json_value).await {
match handle::Handle::mihomo()
.await
.patch_base_config(&json_value)
.await
{
Ok(_) => {
// 更新订阅
Config::clash().await.data_mut().patch_config(mapping);
@@ -79,12 +77,8 @@ pub async fn change_clash_mode(mode: String) {
let clash_data = Config::clash().await.data_mut().clone();
if clash_data.save_config().await.is_ok() {
handle::Handle::refresh_clash();
logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
logging_error!(
Type::Tray,
true,
tray::Tray::global().update_icon(None).await
);
logging_error!(Type::Tray, tray::Tray::global().update_menu().await);
logging_error!(Type::Tray, tray::Tray::global().update_icon().await);
}
let is_auto_close_connection = Config::verge()
+3 -7
View File
@@ -22,12 +22,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
CoreManager::global().restart_core().await?;
} else {
if patch.get("mode").is_some() {
logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
logging_error!(
Type::Tray,
true,
tray::Tray::global().update_icon(None).await
);
logging_error!(Type::Tray, tray::Tray::global().update_menu().await);
logging_error!(Type::Tray, tray::Tray::global().update_icon().await);
}
Config::runtime().await.draft_mut().patch_config(patch);
CoreManager::global().update_config().await?;
@@ -211,7 +207,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
tray::Tray::global().update_menu().await?;
}
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
tray::Tray::global().update_icon(None).await?;
tray::Tray::global().update_icon().await?;
}
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
tray::Tray::global().update_tooltip().await?;
+14 -14
View File
@@ -13,7 +13,7 @@ pub async fn toggle_proxy_profile(profile_index: String) {
Ok(_) => {
let result = tray::Tray::global().update_menu().await;
if let Err(err) = result {
logging!(error, Type::Tray, true, "更新菜单失败: {}", err);
logging!(error, Type::Tray, "更新菜单失败: {}", err);
}
}
Err(err) => {
@@ -30,7 +30,7 @@ pub async fn update_profile(
option: Option<PrfOption>,
auto_refresh: Option<bool>,
) -> Result<()> {
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
let url_opt = {
@@ -138,23 +138,23 @@ pub async fn update_profile(
};
if should_update {
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
logging!(info, Type::Config, "[订阅更新] 更新内核配置");
match CoreManager::global().update_config().await {
Ok(_) => {
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
logging!(info, Type::Config, "[订阅更新] 更新成功");
handle::Handle::refresh_clash();
if let Err(err) = cmd::proxy::force_refresh_proxies().await {
logging!(
error,
Type::Config,
true,
"[订阅更新] 代理组刷新失败: {}",
err
);
}
// if let Err(err) = cmd::proxy::force_refresh_proxies().await {
// logging!(
// error,
// Type::Config,
// true,
// "[订阅更新] 代理组刷新失败: {}",
// err
// );
// }
}
Err(err) => {
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
logging!(error, Type::Config, "[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("update_failed", format!("{err}"));
log::error!(target: "app", "{err}");
}
+2 -12
View File
@@ -1,9 +1,6 @@
use crate::{
config::{Config, IVerge},
core::handle,
ipc::IpcManager,
logging,
utils::logging::Type,
};
use std::env;
use tauri_plugin_clipboard_manager::ClipboardExt;
@@ -26,7 +23,7 @@ pub async fn toggle_system_proxy() {
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
if enable
&& auto_close_connection
&& let Err(err) = IpcManager::global().close_all_connections().await
&& let Err(err) = handle::Handle::mihomo().await.close_all_connections().await
{
log::error!(target: "app", "Failed to close all connections: {err}");
}
@@ -78,14 +75,7 @@ pub async fn copy_clash_env() {
.unwrap_or_else(|| "127.0.0.1".to_string()),
};
let Some(app_handle) = handle::Handle::global().app_handle() else {
logging!(
error,
Type::System,
"Failed to get app handle for proxy operation"
);
return;
};
let app_handle = handle::Handle::app_handle();
let port = {
Config::verge()
.await
+148 -52
View File
@@ -1,14 +1,11 @@
use crate::config::Config;
use crate::core::event_driven_proxy::EventDrivenProxyManager;
use crate::core::{CoreManager, handle, sysopt};
use crate::utils;
use crate::utils::window_manager::WindowManager;
use crate::{
config::Config,
core::{CoreManager, handle, sysopt},
ipc::IpcManager,
logging,
module::lightweight,
utils::logging::Type,
};
use crate::{logging, module::lightweight, utils::logging::Type};
/// Open or close the dashboard window
/// Public API: open or close the dashboard
pub async fn open_or_close_dashboard() {
open_or_close_dashboard_internal().await
}
@@ -20,35 +17,27 @@ async fn open_or_close_dashboard_internal() {
log::info!(target: "app", "Window toggle result: {result:?}");
}
/// 异步优化的应用退出函数
pub async fn quit() {
logging!(debug, Type::System, true, "启动退出流程");
logging!(debug, Type::System, "启动退出流程");
utils::server::shutdown_embedded_server();
// 获取应用句柄并设置退出标志
let Some(app_handle) = handle::Handle::global().app_handle() else {
logging!(
error,
Type::System,
"Failed to get app handle for quit operation"
);
return;
};
let app_handle = handle::Handle::app_handle();
handle::Handle::global().set_is_exiting();
EventDrivenProxyManager::global().notify_app_stopping();
// 优先关闭窗口,提供立即反馈
if let Some(window) = handle::Handle::global().get_window() {
if let Some(window) = handle::Handle::get_window() {
let _ = window.hide();
log::info!(target: "app", "窗口已隐藏");
}
// 使用异步任务处理资源清理,避免阻塞
logging!(info, Type::System, true, "开始异步清理资源");
logging!(info, Type::System, "开始异步清理资源");
let cleanup_result = clean_async().await;
logging!(
info,
Type::System,
true,
"资源清理完成,退出代码: {}",
if cleanup_result { 0 } else { 1 }
);
@@ -58,7 +47,7 @@ pub async fn quit() {
async fn clean_async() -> bool {
use tokio::time::{Duration, timeout};
logging!(info, Type::System, true, "开始执行异步清理操作...");
logging!(info, Type::System, "开始执行异步清理操作...");
// 1. 处理TUN模式
let tun_success = if Config::verge()
@@ -68,9 +57,16 @@ async fn clean_async() -> bool {
.unwrap_or(false)
{
let disable_tun = serde_json::json!({"tun": {"enable": false}});
#[cfg(target_os = "windows")]
let tun_timeout = Duration::from_secs(2);
#[cfg(not(target_os = "windows"))]
let tun_timeout = Duration::from_secs(2);
match timeout(
Duration::from_secs(3),
IpcManager::global().patch_configs(disable_tun),
tun_timeout,
handle::Handle::mihomo()
.await
.patch_base_config(&disable_tun),
)
.await
{
@@ -81,11 +77,12 @@ async fn clean_async() -> bool {
}
Ok(Err(e)) => {
log::warn!(target: "app", "禁用TUN模式失败: {e}");
false
// 超时不阻塞退出
true
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时");
false
log::warn!(target: "app", "禁用TUN模式超时(可能系统正在关机),继续退出流程");
true
}
}
} else {
@@ -94,33 +91,130 @@ async fn clean_async() -> bool {
// 2. 系统代理重置
let proxy_task = async {
match timeout(
Duration::from_secs(3),
sysopt::Sysopt::global().reset_sysproxy(),
)
.await
#[cfg(target_os = "windows")]
{
Ok(_) => {
log::info!(target: "app", "系统代理已重置");
true
use sysproxy::{Autoproxy, Sysproxy};
use winapi::um::winuser::{GetSystemMetrics, SM_SHUTTINGDOWN};
// 检查系统代理是否开启
let sys_proxy_enabled = Config::verge()
.await
.latest_ref()
.enable_system_proxy
.unwrap_or(false);
if !sys_proxy_enabled {
log::info!(target: "app", "系统代理未启用,跳过重置");
return true;
}
Err(_) => {
log::warn!(target: "app", "重置系统代理超时");
false
// 检查是否正在关机
let is_shutting_down = unsafe { GetSystemMetrics(SM_SHUTTINGDOWN) != 0 };
if is_shutting_down {
// sysproxy-rs 操作注册表(避免.exe的dll错误)
log::info!(target: "app", "检测到正在关机,syspro-rs操作注册表关闭系统代理");
match Sysproxy::get_system_proxy() {
Ok(mut sysproxy) => {
sysproxy.enable = false;
if let Err(e) = sysproxy.set_system_proxy() {
log::warn!(target: "app", "关机时关闭系统代理失败: {e}");
} else {
log::info!(target: "app", "系统代理已关闭(通过注册表)");
}
}
Err(e) => {
log::warn!(target: "app", "关机时获取代理设置失败: {e}");
}
}
// 关闭自动代理配置
if let Ok(mut autoproxy) = Autoproxy::get_auto_proxy() {
autoproxy.enable = false;
let _ = autoproxy.set_auto_proxy();
}
return true;
}
// 正常退出:使用 sysproxy.exe 重置代理
log::info!(target: "app", "sysproxy.exe重置系统代理");
match timeout(
Duration::from_secs(2),
sysopt::Sysopt::global().reset_sysproxy(),
)
.await
{
Ok(Ok(_)) => {
log::info!(target: "app", "系统代理已重置");
true
}
Ok(Err(e)) => {
log::warn!(target: "app", "重置系统代理失败: {e}");
true
}
Err(_) => {
log::warn!(target: "app", "重置系统代理超时,继续退出流程");
true
}
}
}
// 非 Windows 平台:正常重置代理
#[cfg(not(target_os = "windows"))]
{
let sys_proxy_enabled = Config::verge()
.await
.latest_ref()
.enable_system_proxy
.unwrap_or(false);
if !sys_proxy_enabled {
log::info!(target: "app", "系统代理未启用,跳过重置");
return true;
}
log::info!(target: "app", "开始重置系统代理...");
match timeout(
Duration::from_millis(1500),
sysopt::Sysopt::global().reset_sysproxy(),
)
.await
{
Ok(Ok(_)) => {
log::info!(target: "app", "系统代理已重置");
true
}
Ok(Err(e)) => {
log::warn!(target: "app", "重置系统代理失败: {e}");
true
}
Err(_) => {
log::warn!(target: "app", "重置系统代理超时,继续退出");
true
}
}
}
};
// 3. 核心服务停止
let core_task = async {
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
#[cfg(target_os = "windows")]
let stop_timeout = Duration::from_secs(2);
#[cfg(not(target_os = "windows"))]
let stop_timeout = Duration::from_secs(3);
match timeout(stop_timeout, CoreManager::global().stop_core()).await {
Ok(_) => {
log::info!(target: "app", "核心服务已停止");
log::info!(target: "app", "core已停止");
true
}
Err(_) => {
log::warn!(target: "app", "停止核心服务超时");
false
log::warn!(target: "app", "停止core超时(可能系统正在关机),继续退出");
true
}
}
};
@@ -158,7 +252,6 @@ async fn clean_async() -> bool {
logging!(
info,
Type::System,
true,
"异步关闭操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
tun_success,
proxy_success,
@@ -176,26 +269,29 @@ pub fn clean() -> bool {
let (tx, rx) = std::sync::mpsc::channel();
AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "开始执行关闭操作...");
logging!(info, Type::System, "开始执行关闭操作...");
// 使用已有的异步清理函数
let cleanup_result = clean_async().await;
// 发送结果
let _ = tx.send(cleanup_result);
});
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
#[cfg(target_os = "windows")]
let total_timeout = std::time::Duration::from_secs(5);
#[cfg(not(target_os = "windows"))]
let total_timeout = std::time::Duration::from_secs(8);
match rx.recv_timeout(total_timeout) {
Ok(result) => {
logging!(info, Type::System, true, "关闭操作完成,结果: {}", result);
logging!(info, Type::System, "关闭操作完成,结果: {}", result);
result
}
Err(_) => {
logging!(
warn,
Type::System,
true,
"清理操作超时,返回成功状态避免阻塞"
"清理操作超时(可能正在关机),返回成功避免阻塞"
);
true
}
@@ -216,7 +312,7 @@ pub async fn hide() {
add_light_weight_timer().await;
}
if let Some(window) = handle::Handle::global().get_window()
if let Some(window) = handle::Handle::get_window()
&& window.is_visible().unwrap_or(false)
{
let _ = window.hide();
@@ -1,376 +0,0 @@
use std::time::Duration;
use kode_bridge::{
ClientConfig, IpcHttpClient, LegacyResponse,
errors::{AnyError, AnyResult},
};
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use crate::{
logging, singleton_with_logging,
utils::{dirs::ipc_path, logging::Type},
};
// 定义用于URL路径的编码集合,只编码真正必要的字符
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ') // 空格
.add(b'/') // 斜杠
.add(b'?') // 问号
.add(b'#') // 井号
.add(b'&') // 和号
.add(b'%'); // 百分号
// Helper function to create AnyError from string
fn create_error(msg: impl Into<String>) -> AnyError {
Box::new(std::io::Error::other(msg.into()))
}
pub struct IpcManager {
client: IpcHttpClient,
}
impl IpcManager {
pub fn new() -> Self {
logging!(info, Type::Ipc, true, "Creating new IpcManager instance");
let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
});
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
let config = ClientConfig {
default_timeout: Duration::from_secs(5),
enable_pooling: false,
max_retries: 4,
retry_delay: Duration::from_millis(125),
max_concurrent_requests: 16,
max_requests_per_second: Some(64.0),
..Default::default()
};
#[allow(clippy::unwrap_used)]
let client = IpcHttpClient::with_config(ipc_path, config).unwrap();
Self { client }
}
}
impl IpcManager {
pub async fn request(
&self,
method: &str,
path: &str,
body: Option<&serde_json::Value>,
) -> AnyResult<LegacyResponse> {
self.client.request(method, path, body).await
}
}
impl IpcManager {
pub async fn send_request(
&self,
method: &str,
path: &str,
body: Option<&serde_json::Value>,
) -> AnyResult<serde_json::Value> {
let response = IpcManager::global().request(method, path, body).await?;
match method {
"GET" => Ok(response.json()?),
"PATCH" => {
if response.status == 204 {
Ok(serde_json::json!({"code": 204}))
} else {
Ok(response.json()?)
}
}
"PUT" | "DELETE" => {
if response.status == 204 {
Ok(serde_json::json!({"code": 204}))
} else {
match response.json() {
Ok(json) => Ok(json),
Err(_) => Ok(serde_json::json!({
"code": response.status,
"message": response.body,
"error": "failed to parse response as JSON"
})),
}
}
}
_ => match response.json() {
Ok(json) => Ok(json),
Err(_) => Ok(serde_json::json!({
"code": response.status,
"message": response.body,
"error": "failed to parse response as JSON"
})),
},
}
}
// 基础代理信息获取
pub async fn get_proxies(&self) -> AnyResult<serde_json::Value> {
let url = "/proxies";
self.send_request("GET", url, None).await
}
// 代理提供者信息获取
pub async fn get_providers_proxies(&self) -> AnyResult<serde_json::Value> {
let url = "/providers/proxies";
self.send_request("GET", url, None).await
}
// 连接管理
pub async fn get_connections(&self) -> AnyResult<serde_json::Value> {
let url = "/connections";
self.send_request("GET", url, None).await
}
pub async fn delete_connection(&self, id: &str) -> AnyResult<()> {
let encoded_id = utf8_percent_encode(id, URL_PATH_ENCODE_SET).to_string();
let url = format!("/connections/{encoded_id}");
let response = self.send_request("DELETE", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"].as_str().unwrap_or("unknown error"),
))
}
}
pub async fn close_all_connections(&self) -> AnyResult<()> {
let url = "/connections";
let response = self.send_request("DELETE", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_owned(),
))
}
}
}
impl IpcManager {
#[allow(dead_code)]
pub async fn is_mihomo_running(&self) -> AnyResult<()> {
let url = "/version";
let _response = self.send_request("GET", url, None).await?;
Ok(())
}
pub async fn put_configs_force(&self, clash_config_path: &str) -> AnyResult<()> {
let url = "/configs?force=true";
let payload = serde_json::json!({
"path": clash_config_path,
});
let _response = self.send_request("PUT", url, Some(&payload)).await?;
Ok(())
}
pub async fn patch_configs(&self, config: serde_json::Value) -> AnyResult<()> {
let url = "/configs";
let response = self.send_request("PATCH", url, Some(&config)).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_owned(),
))
}
}
pub async fn test_proxy_delay(
&self,
name: &str,
test_url: Option<String>,
timeout: i32,
) -> AnyResult<serde_json::Value> {
let test_url =
test_url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
// 测速URL不再编码,直接传递
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
self.send_request("GET", &url, None).await
}
// 版本和配置相关
pub async fn get_version(&self) -> AnyResult<serde_json::Value> {
let url = "/version";
self.send_request("GET", url, None).await
}
pub async fn get_config(&self) -> AnyResult<serde_json::Value> {
let url = "/configs";
self.send_request("GET", url, None).await
}
pub async fn update_geo_data(&self) -> AnyResult<()> {
let url = "/configs/geo";
let response = self.send_request("POST", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
pub async fn upgrade_core(&self) -> AnyResult<()> {
let url = "/upgrade";
let response = self.send_request("POST", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 规则相关
pub async fn get_rules(&self) -> AnyResult<serde_json::Value> {
let url = "/rules";
self.send_request("GET", url, None).await
}
pub async fn get_rule_providers(&self) -> AnyResult<serde_json::Value> {
let url = "/providers/rules";
self.send_request("GET", url, None).await
}
pub async fn update_rule_provider(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/rules/{encoded_name}");
let response = self.send_request("PUT", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 代理相关
pub async fn update_proxy(&self, group: &str, proxy: &str) -> AnyResult<()> {
// 使用 percent-encoding 进行正确的 URL 编码
let encoded_group = utf8_percent_encode(group, URL_PATH_ENCODE_SET).to_string();
let url = format!("/proxies/{encoded_group}");
let payload = serde_json::json!({
"name": proxy
});
// println!("group: {}, proxy: {}", group, proxy);
match self.send_request("PUT", &url, Some(&payload)).await {
Ok(_) => {
// println!("updateProxy response: {:?}", response);
Ok(())
}
Err(e) => {
// println!("updateProxy encountered error: {}", e);
logging!(
error,
crate::utils::logging::Type::Ipc,
true,
"IPC: updateProxy encountered error: {} (ignored, always returning true)",
e
);
Ok(())
}
}
}
pub async fn proxy_provider_health_check(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/proxies/{encoded_name}/healthcheck");
let response = self.send_request("GET", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
pub async fn update_proxy_provider(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/proxies/{encoded_name}");
let response = self.send_request("PUT", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 延迟测试相关
pub async fn get_group_proxy_delays(
&self,
group_name: &str,
url: Option<String>,
timeout: i32,
) -> AnyResult<serde_json::Value> {
let test_url = url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
let encoded_group_name = utf8_percent_encode(group_name, URL_PATH_ENCODE_SET).to_string();
// 测速URL不再编码,直接传递
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
self.send_request("GET", &url, None).await
}
// 调试相关
pub async fn is_debug_enabled(&self) -> AnyResult<bool> {
let url = "/debug/pprof";
match self.send_request("GET", url, None).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
pub async fn gc(&self) -> AnyResult<()> {
let url = "/debug/gc";
let response = self.send_request("PUT", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理
}
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
-330
View File
@@ -1,330 +0,0 @@
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, sync::Arc, time::Instant};
use tauri::async_runtime::JoinHandle;
use tokio::{sync::RwLock, time::Duration};
use crate::{
ipc::monitor::MonitorData,
logging,
process::AsyncHandler,
singleton_with_logging,
utils::{dirs::ipc_path, logging::Type},
};
const MAX_LOGS: usize = 1000; // Maximum number of logs to keep in memory
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LogData {
#[serde(rename = "type")]
pub log_type: String,
pub payload: String,
}
#[derive(Debug, Clone)]
pub struct LogItem {
pub log_type: String,
pub payload: String,
pub time: String,
}
impl LogItem {
fn new(log_type: String, payload: String) -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs();
// Simple time formatting (HH:MM:SS)
let hours = (now / 3600) % 24;
let minutes = (now / 60) % 60;
let seconds = now % 60;
let time_str = format!("{hours:02}:{minutes:02}:{seconds:02}");
Self {
log_type,
payload,
time: time_str,
}
}
}
#[derive(Debug, Clone)]
pub struct CurrentLogs {
pub logs: VecDeque<LogItem>,
// pub level: String,
pub last_updated: Instant,
}
impl Default for CurrentLogs {
fn default() -> Self {
Self {
logs: VecDeque::with_capacity(MAX_LOGS),
// level: "info".to_string(),
last_updated: Instant::now(),
}
}
}
impl MonitorData for CurrentLogs {
fn mark_fresh(&mut self) {
self.last_updated = Instant::now();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.last_updated.elapsed() < duration
}
}
// Logs monitor with streaming support
pub struct LogsMonitor {
current: Arc<RwLock<CurrentLogs>>,
task_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
current_monitoring_level: Arc<RwLock<Option<String>>>,
}
// Use singleton_with_logging macro
singleton_with_logging!(LogsMonitor, INSTANCE, "LogsMonitor");
impl LogsMonitor {
fn new() -> Self {
let current = Arc::new(RwLock::new(CurrentLogs::default()));
Self {
current,
task_handle: Arc::new(RwLock::new(None)),
current_monitoring_level: Arc::new(RwLock::new(None)),
}
}
pub async fn start_monitoring(&self, level: Option<String>) {
let filter_level = level.clone().unwrap_or_else(|| "info".to_string());
// Check if we're already monitoring the same level
// let level_changed = {
// let current_level = self.current_monitoring_level.read().await;
// if let Some(existing_level) = current_level.as_ref() {
// if existing_level == &filter_level {
// logging!(
// info,
// Type::Ipc,
// true,
// "LogsMonitor: Already monitoring level '{}', skipping duplicate request",
// filter_level
// );
// return;
// }
// true // Level changed
// } else {
// true // First time or was stopped
// }
// };
// Stop existing monitoring task if level changed or first time
{
let mut handle = self.task_handle.write().await;
if let Some(task) = handle.take() {
task.abort();
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Stopped previous monitoring task (level changed)"
);
}
}
// We want to keep the logs cache even if the level changes,
// so we don't clear it here. The cache will be cleared only when the level changes
// and a new task is started. This allows us to keep logs from previous levels
// even if the level changes during monitoring.
// Clear logs cache when level changes to ensure fresh data
// if level_changed {
// let mut current = self.current.write().await;
// current.logs.clear();
// current.level = filter_level.clone();
// current.mark_fresh();
// logging!(
// info,
// Type::Ipc,
// true,
// "LogsMonitor: Cleared logs cache due to level change to '{}'",
// filter_level
// );
// }
// Update current monitoring level
{
let mut current_level = self.current_monitoring_level.write().await;
*current_level = Some(filter_level.clone());
}
let monitor_current = Arc::clone(&self.current);
let task = AsyncHandler::spawn(move || async move {
loop {
// Get fresh IPC path and client for each connection attempt
let (_ipc_path_buf, client) = match Self::create_ipc_client() {
Ok((path, client)) => (path, client),
Err(e) => {
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
}
};
let url = if filter_level == "all" {
"/logs".to_string()
} else {
format!("/logs?level={filter_level}")
};
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Starting stream for {}",
url
);
let _ = client
.get(&url)
.timeout(Duration::from_secs(30))
.process_lines(|line| {
Self::process_log_line(line, Arc::clone(&monitor_current))
})
.await;
// Wait before retrying
tokio::time::sleep(Duration::from_secs(2)).await;
}
});
// Store the task handle
{
let mut handle = self.task_handle.write().await;
*handle = Some(task);
}
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Started new monitoring task for level: {:?}",
level
);
}
pub async fn stop_monitoring(&self) {
// Stop monitoring task but keep logs
{
let mut handle = self.task_handle.write().await;
if let Some(task) = handle.take() {
task.abort();
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Stopped monitoring task"
);
}
}
// Reset monitoring level
{
let mut monitoring_level = self.current_monitoring_level.write().await;
*monitoring_level = None;
}
}
fn create_ipc_client() -> Result<
(std::path::PathBuf, kode_bridge::IpcStreamClient),
Box<dyn std::error::Error + Send + Sync>,
> {
use kode_bridge::IpcStreamClient;
let ipc_path_buf = ipc_path()?;
let ipc_path = ipc_path_buf.to_str().ok_or("Invalid IPC path")?;
let client = IpcStreamClient::new(ipc_path)?;
Ok((ipc_path_buf, client))
}
fn process_log_line(
line: &str,
current: Arc<RwLock<CurrentLogs>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(log_data) = serde_json::from_str::<LogData>(line.trim()) {
// Server-side filtering via query parameters handles the level filtering
// We only need to accept all logs since filtering is done at the endpoint level
let log_item = LogItem::new(log_data.log_type, log_data.payload);
AsyncHandler::spawn(move || async move {
let mut logs = current.write().await;
// Add new log
logs.logs.push_back(log_item);
// Keep only the last 1000 logs
if logs.logs.len() > 1000 {
logs.logs.pop_front();
}
logs.mark_fresh();
});
}
Ok(())
}
pub async fn current(&self) -> CurrentLogs {
self.current.read().await.clone()
}
pub async fn clear_logs(&self) {
let mut current = self.current.write().await;
current.logs.clear();
current.mark_fresh();
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Cleared frontend logs (monitoring continues)"
);
}
pub async fn get_logs_as_json(&self) -> serde_json::Value {
let current = self.current().await;
// Simply return all cached logs since filtering is handled by start_monitoring
// and the cache is cleared when level changes
let logs: Vec<serde_json::Value> = current
.logs
.iter()
.map(|log| {
serde_json::json!({
"type": log.log_type,
"payload": log.payload,
"time": log.time
})
})
.collect();
serde_json::Value::Array(logs)
}
}
pub async fn start_logs_monitoring(level: Option<String>) {
LogsMonitor::global().start_monitoring(level).await;
}
pub async fn stop_logs_monitoring() {
LogsMonitor::global().stop_monitoring().await;
}
pub async fn clear_logs() {
LogsMonitor::global().clear_logs().await;
}
pub async fn get_logs_json() -> serde_json::Value {
LogsMonitor::global().get_logs_as_json().await
}
-119
View File
@@ -1,119 +0,0 @@
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Instant};
use tokio::{sync::RwLock, time::Duration};
use crate::{
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
process::AsyncHandler,
singleton_lazy_with_logging,
utils::format::fmt_bytes,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MemoryData {
pub inuse: u64,
pub oslimit: u64,
}
#[derive(Debug, Clone)]
pub struct CurrentMemory {
pub inuse: u64,
pub oslimit: u64,
pub last_updated: Instant,
}
impl Default for CurrentMemory {
fn default() -> Self {
Self {
inuse: 0,
oslimit: 0,
last_updated: Instant::now(),
}
}
}
impl MonitorData for CurrentMemory {
fn mark_fresh(&mut self) {
self.last_updated = Instant::now();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.last_updated.elapsed() < duration
}
}
impl StreamingParser for CurrentMemory {
fn parse_and_update(
line: &str,
current: Arc<RwLock<Self>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(memory) = serde_json::from_str::<MemoryData>(line.trim()) {
AsyncHandler::spawn(move || async move {
let mut current_guard = current.write().await;
current_guard.inuse = memory.inuse;
current_guard.oslimit = memory.oslimit;
current_guard.mark_fresh();
});
}
Ok(())
}
}
// Minimal memory monitor using the new architecture
pub struct MemoryMonitor {
monitor: IpcStreamMonitor<CurrentMemory>,
}
impl Default for MemoryMonitor {
fn default() -> Self {
MemoryMonitor {
monitor: IpcStreamMonitor::new(
"/memory".to_string(),
Duration::from_secs(10),
Duration::from_secs(2),
Duration::from_secs(10),
),
}
}
}
// Use simplified singleton_lazy_with_logging macro
singleton_lazy_with_logging!(
MemoryMonitor,
INSTANCE,
"MemoryMonitor",
MemoryMonitor::default
);
impl MemoryMonitor {
pub async fn current(&self) -> CurrentMemory {
self.monitor.current().await
}
pub async fn is_fresh(&self) -> bool {
self.monitor.is_fresh().await
}
}
pub async fn get_current_memory() -> CurrentMemory {
MemoryMonitor::global().current().await
}
pub async fn get_formatted_memory() -> (String, String, f64, bool) {
let monitor = MemoryMonitor::global();
let memory = monitor.current().await;
let is_fresh = monitor.is_fresh().await;
let usage_percent = if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
};
(
fmt_bytes(memory.inuse),
fmt_bytes(memory.oslimit),
usage_percent,
is_fresh,
)
}
-15
View File
@@ -1,15 +0,0 @@
pub mod general;
pub mod logs;
pub mod memory;
pub mod monitor;
pub mod traffic;
pub use general::IpcManager;
pub use logs::{clear_logs, get_logs_json, start_logs_monitoring, stop_logs_monitoring};
pub use memory::{get_current_memory, get_formatted_memory};
pub use traffic::{get_current_traffic, get_formatted_traffic};
pub struct Rate {
// pub up: usize,
// pub down: usize,
}
@@ -1,120 +0,0 @@
use kode_bridge::IpcStreamClient;
use std::sync::Arc;
use tokio::{sync::RwLock, time::Duration};
use crate::{
logging,
process::AsyncHandler,
utils::{dirs::ipc_path, logging::Type},
};
/// Generic base structure for IPC monitoring data with freshness tracking
pub trait MonitorData: Clone + Send + Sync + 'static {
/// Update the last_updated timestamp to now
fn mark_fresh(&mut self);
/// Check if data is fresh based on the given duration
fn is_fresh_within(&self, duration: Duration) -> bool;
}
/// Trait for parsing streaming data and updating monitor state
pub trait StreamingParser: MonitorData {
/// Parse a line of streaming data and update the current state
fn parse_and_update(
line: &str,
current: Arc<RwLock<Self>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
/// Generic IPC stream monitor that handles the common streaming pattern
pub struct IpcStreamMonitor<T>
where
T: MonitorData + StreamingParser + Default,
{
current: Arc<RwLock<T>>,
#[allow(dead_code)]
endpoint: String,
#[allow(dead_code)]
timeout: Duration,
#[allow(dead_code)]
retry_interval: Duration,
freshness_duration: Duration,
}
impl<T> IpcStreamMonitor<T>
where
T: MonitorData + StreamingParser + Default,
{
pub fn new(
endpoint: String,
timeout: Duration,
retry_interval: Duration,
freshness_duration: Duration,
) -> Self {
let current = Arc::new(RwLock::new(T::default()));
let monitor_current = Arc::clone(&current);
let endpoint_clone = endpoint.clone();
// Start the monitoring task
AsyncHandler::spawn(move || async move {
Self::streaming_task(monitor_current, endpoint_clone, timeout, retry_interval).await;
});
Self {
current,
endpoint,
timeout,
retry_interval,
freshness_duration,
}
}
pub async fn current(&self) -> T {
self.current.read().await.clone()
}
pub async fn is_fresh(&self) -> bool {
self.current
.read()
.await
.is_fresh_within(self.freshness_duration)
}
/// The core streaming task that can be specialized per monitor type
async fn streaming_task(
current: Arc<RwLock<T>>,
endpoint: String,
timeout: Duration,
retry_interval: Duration,
) {
loop {
let ipc_path_buf = match ipc_path() {
Ok(path) => path,
Err(e) => {
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
tokio::time::sleep(retry_interval).await;
continue;
}
};
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
let client = match IpcStreamClient::new(ipc_path) {
Ok(client) => client,
Err(e) => {
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
tokio::time::sleep(retry_interval).await;
continue;
}
};
let _ = client
.get(&endpoint)
.timeout(timeout)
.process_lines(|line| T::parse_and_update(line, Arc::clone(&current)))
.await;
tokio::time::sleep(retry_interval).await;
}
}
}
@@ -1,153 +0,0 @@
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Instant};
use tokio::{sync::RwLock, time::Duration};
use crate::{
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
process::AsyncHandler,
singleton_lazy_with_logging,
utils::format::fmt_bytes,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TrafficData {
pub up: u64,
pub down: u64,
}
#[derive(Debug, Clone)]
pub struct CurrentTraffic {
pub up_rate: u64,
pub down_rate: u64,
pub total_up: u64,
pub total_down: u64,
pub last_updated: Instant,
}
impl Default for CurrentTraffic {
fn default() -> Self {
Self {
up_rate: 0,
down_rate: 0,
total_up: 0,
total_down: 0,
last_updated: Instant::now(),
}
}
}
impl MonitorData for CurrentTraffic {
fn mark_fresh(&mut self) {
self.last_updated = Instant::now();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.last_updated.elapsed() < duration
}
}
// Traffic monitoring state for calculating rates
#[derive(Debug, Clone, Default)]
pub struct TrafficMonitorState {
pub current: CurrentTraffic,
pub last_traffic: Option<TrafficData>,
}
impl MonitorData for TrafficMonitorState {
fn mark_fresh(&mut self) {
self.current.mark_fresh();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.current.is_fresh_within(duration)
}
}
impl StreamingParser for TrafficMonitorState {
fn parse_and_update(
line: &str,
current: Arc<RwLock<Self>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(traffic) = serde_json::from_str::<TrafficData>(line.trim()) {
AsyncHandler::spawn(move || async move {
let mut state_guard = current.write().await;
let (up_rate, down_rate) = state_guard
.last_traffic
.as_ref()
.map(|l| {
(
traffic.up.saturating_sub(l.up),
traffic.down.saturating_sub(l.down),
)
})
.unwrap_or((0, 0));
state_guard.current = CurrentTraffic {
up_rate,
down_rate,
total_up: traffic.up,
total_down: traffic.down,
last_updated: Instant::now(),
};
state_guard.last_traffic = Some(traffic);
});
}
Ok(())
}
}
// Minimal traffic monitor using the new architecture
pub struct TrafficMonitor {
monitor: IpcStreamMonitor<TrafficMonitorState>,
}
impl Default for TrafficMonitor {
fn default() -> Self {
TrafficMonitor {
monitor: IpcStreamMonitor::new(
"/traffic".to_string(),
Duration::from_secs(10),
Duration::from_secs(1),
Duration::from_secs(5),
),
}
}
}
// Use simplified singleton_lazy_with_logging macro
singleton_lazy_with_logging!(
TrafficMonitor,
INSTANCE,
"TrafficMonitor",
TrafficMonitor::default
);
impl TrafficMonitor {
pub async fn current(&self) -> CurrentTraffic {
self.monitor.current().await.current
}
pub async fn is_fresh(&self) -> bool {
self.monitor.is_fresh().await
}
}
pub async fn get_current_traffic() -> CurrentTraffic {
TrafficMonitor::global().current().await
}
pub async fn get_formatted_traffic() -> (String, String, String, String, bool) {
let monitor = TrafficMonitor::global();
let traffic = monitor.current().await;
let is_fresh = monitor.is_fresh().await;
(
fmt_bytes(traffic.up_rate),
fmt_bytes(traffic.down_rate),
fmt_bytes(traffic.total_up),
fmt_bytes(traffic.total_down),
is_fresh,
)
}
+168 -128
View File
@@ -1,34 +1,32 @@
#![allow(non_snake_case)]
#![recursion_limit = "512"]
mod cache;
mod cmd;
pub mod config;
mod core;
mod enhance;
mod feat;
mod ipc;
mod module;
mod process;
mod utils;
#[cfg(target_os = "macos")]
use crate::utils::window_manager::WindowManager;
use crate::{
core::handle,
core::hotkey,
core::{EventDrivenProxyManager, handle, hotkey},
process::AsyncHandler,
utils::{resolve, server},
};
use config::Config;
use tauri::AppHandle;
#[cfg(target_os = "macos")]
use tauri::Manager;
use once_cell::sync::OnceCell;
use tauri::{AppHandle, Manager};
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{Duration, timeout};
use utils::logging::Type;
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
/// Application initialization helper functions
mod app_init {
use super::*;
@@ -36,27 +34,22 @@ mod app_init {
/// Initialize singleton monitoring for other instances
pub fn init_singleton_check() {
AsyncHandler::spawn_blocking(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
logging!(info, Type::Setup, "开始检查单例实例...");
match timeout(Duration::from_millis(500), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if let Some(app_handle) = handle::Handle::global().app_handle() {
logging!(info, Type::Setup, "检测到已有应用实例运行");
if let Some(app_handle) = APP_HANDLE.get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
logging!(info, Type::Setup, "未检测到其他应用实例");
}
}
Err(_) => {
logging!(
warn,
Type::Setup,
true,
"单例检查超时,假定没有其他实例运行"
);
logging!(warn, Type::Setup, "单例检查超时,假定没有其他实例运行");
}
}
});
@@ -75,7 +68,13 @@ mod app_init {
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_http::init());
.plugin(tauri_plugin_http::init())
.plugin(
tauri_plugin_mihomo::Builder::new()
.protocol(tauri_plugin_mihomo::models::Protocol::LocalSocket)
.socket_path(crate::config::IClashTemp::guard_external_controller_ipc())
.build(),
);
// Devtools plugin only in debug mode with feature tauri-dev
// to avoid duplicated registering of logger since the devtools plugin also registers a logger
@@ -90,7 +89,7 @@ mod app_init {
pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
logging!(info, Type::Setup, true, "注册深层链接...");
logging!(info, Type::Setup, "注册深层链接...");
app.deep_link().register_all()?;
}
@@ -99,7 +98,7 @@ mod app_init {
if let Some(url) = url {
AsyncHandler::spawn(|| async {
if let Err(e) = resolve::resolve_scheme(url).await {
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
logging!(error, Type::Setup, "Failed to resolve scheme: {}", e);
}
});
}
@@ -127,7 +126,7 @@ mod app_init {
/// Setup window state management
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
logging!(info, Type::Setup, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
@@ -184,46 +183,13 @@ mod app_init {
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::get_proxies,
cmd::force_refresh_proxies,
cmd::get_providers_proxies,
cmd::sync_tray_proxy_selection,
cmd::update_proxy_and_sync,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::check_dns_config_exists,
cmd::get_dns_config_content,
cmd::validate_dns_config,
cmd::get_clash_version,
cmd::get_clash_config,
cmd::force_refresh_clash_config,
cmd::update_geo_data,
cmd::upgrade_clash_core,
cmd::get_clash_rules,
cmd::update_proxy_choice,
cmd::get_proxy_providers,
cmd::get_rule_providers,
cmd::proxy_provider_health_check,
cmd::update_proxy_provider,
cmd::update_rule_provider,
cmd::get_clash_connections,
cmd::delete_clash_connection,
cmd::close_all_clash_connections,
cmd::get_group_proxy_delays,
cmd::is_clash_debug_enabled,
cmd::clash_gc,
// Logging and monitoring
cmd::get_clash_logs,
cmd::start_logs_monitoring,
cmd::stop_logs_monitoring,
cmd::clear_logs,
cmd::get_traffic_data,
cmd::get_memory_data,
cmd::get_formatted_traffic_data,
cmd::get_formatted_memory_data,
cmd::get_system_monitor_overview,
cmd::start_traffic_service,
cmd::stop_traffic_service,
// Verge configuration
cmd::get_verge_config,
cmd::patch_verge_config,
@@ -251,8 +217,6 @@ mod app_init {
// Script validation
cmd::script_validate_notice,
cmd::validate_script_file,
// Clash API
cmd::clash_api_get_proxy_delay,
// Backup and WebDAV
cmd::create_webdav_backup,
cmd::save_webdav_config,
@@ -278,15 +242,76 @@ pub fn run() {
// Set Linux environment variable
#[cfg(target_os = "linux")]
{
unsafe {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.to_uppercase();
let session_desktop = std::env::var("XDG_SESSION_DESKTOP")
.unwrap_or_default()
.to_uppercase();
let desktop_session = std::env::var("DESKTOP_SESSION")
.unwrap_or_default()
.to_uppercase();
let is_kde_desktop = desktop_env.contains("KDE");
let is_plasma_desktop = desktop_env.contains("PLASMA");
let is_hyprland_desktop = desktop_env.contains("HYPR")
|| session_desktop.contains("HYPR")
|| desktop_session.contains("HYPR");
let is_wayland_session = std::env::var("XDG_SESSION_TYPE")
.map(|value| value.eq_ignore_ascii_case("wayland"))
.unwrap_or(false)
|| std::env::var("WAYLAND_DISPLAY").is_ok();
let prefer_native_wayland =
is_wayland_session && (is_kde_desktop || is_plasma_desktop || is_hyprland_desktop);
let dmabuf_override = std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER");
if prefer_native_wayland {
let compositor_label = if is_hyprland_desktop {
"Hyprland"
} else if is_plasma_desktop {
"KDE Plasma"
} else {
"KDE"
};
if matches!(dmabuf_override.as_deref(), Ok("1")) {
unsafe {
std::env::remove_var("WEBKIT_DISABLE_DMABUF_RENDERER");
}
logging!(
info,
Type::Setup,
"Wayland + {} detected: Re-enabled WebKit DMABUF renderer to avoid Cairo surface failures.",
compositor_label
);
} else {
logging!(
info,
Type::Setup,
"Wayland + {} detected: Using native Wayland backend for reliable rendering.",
compositor_label
);
}
} else {
if dmabuf_override.is_err() {
unsafe {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
}
// Force X11 backend for tray icon compatibility on Wayland
if is_wayland_session {
unsafe {
std::env::set_var("GDK_BACKEND", "x11");
std::env::remove_var("WAYLAND_DISPLAY");
}
logging!(
info,
Type::Setup,
"Wayland detected: Forcing X11 backend for tray icon compatibility"
);
}
}
if is_kde_desktop || is_plasma_desktop {
unsafe {
@@ -295,7 +320,6 @@ pub fn run() {
logging!(
info,
Type::Setup,
true,
"KDE detected: Disabled GTK CSD for better titlebar stability."
);
}
@@ -304,44 +328,35 @@ pub fn run() {
// Create and configure the Tauri builder
let builder = app_init::setup_plugins(tauri::Builder::default())
.setup(|app| {
logging!(info, Type::Setup, true, "开始应用初始化...");
logging!(info, Type::Setup, "开始应用初始化...");
#[allow(clippy::expect_used)]
APP_HANDLE
.set(app.app_handle().clone())
.expect("failed to set global app handle");
// Setup autostart plugin
if let Err(e) = app_init::setup_autostart(app) {
logging!(error, Type::Setup, true, "Failed to setup autostart: {}", e);
logging!(error, Type::Setup, "Failed to setup autostart: {}", e);
}
// Setup deep links
if let Err(e) = app_init::setup_deep_links(app) {
logging!(
error,
Type::Setup,
true,
"Failed to setup deep links: {}",
e
);
logging!(error, Type::Setup, "Failed to setup deep links: {}", e);
}
// Setup window state management
if let Err(e) = app_init::setup_window_state(app) {
logging!(
error,
Type::Setup,
true,
"Failed to setup window state: {}",
e
);
logging!(error, Type::Setup, "Failed to setup window state: {}", e);
}
let app_handle = app.handle().clone();
logging!(info, Type::Setup, "执行主要设置操作...");
logging!(info, Type::Setup, true, "执行主要设置操作...");
resolve::resolve_setup_handle(app_handle);
resolve::resolve_setup_handle();
resolve::resolve_setup_async();
resolve::resolve_setup_sync();
logging!(info, Type::Setup, true, "初始化完成,继续执行");
logging!(info, Type::Setup, "初始化完成,继续执行");
Ok(())
})
.invoke_handler(app_init::generate_handlers());
@@ -353,14 +368,24 @@ pub fn run() {
use super::*;
/// Handle application ready/resumed events
pub fn handle_ready_resumed(app_handle: &AppHandle) {
logging!(info, Type::System, true, "应用就绪或恢复");
handle::Handle::global().init(app_handle.clone());
pub fn handle_ready_resumed(_app_handle: &AppHandle) {
// 双重检查:确保不在退出状态
if handle::Handle::global().is_exiting() {
logging!(
debug,
Type::System,
"handle_ready_resumed: 应用正在退出,跳过处理"
);
return;
}
logging!(info, Type::System, "应用就绪或恢复");
handle::Handle::global().init();
#[cfg(target_os = "macos")]
{
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "设置macOS窗口标题");
if let Some(window) = _app_handle.get_webview_window("main") {
logging!(info, Type::Window, "设置macOS窗口标题");
let _ = window.set_title("Clash Verge");
}
}
@@ -368,33 +393,26 @@ pub fn run() {
/// Handle application reopen events (macOS)
#[cfg(target_os = "macos")]
pub async fn handle_reopen(app_handle: &AppHandle, has_visible_windows: bool) {
pub async fn handle_reopen(has_visible_windows: bool) {
logging!(
info,
Type::System,
true,
"处理 macOS 应用重新打开事件: has_visible_windows={}",
has_visible_windows
);
handle::Handle::global().init(app_handle.clone());
handle::Handle::global().init();
if !has_visible_windows {
// 当没有可见窗口时,设置为 regular 模式并显示主窗口
handle::Handle::global().set_activation_policy_regular();
logging!(info, Type::System, true, "没有可见窗口,尝试显示主窗口");
logging!(info, Type::System, "没有可见窗口,尝试显示主窗口");
let result = WindowManager::show_main_window().await;
logging!(
info,
Type::System,
true,
"窗口显示操作完成,结果: {:?}",
result
);
logging!(info, Type::System, "窗口显示操作完成,结果: {:?}", result);
} else {
logging!(info, Type::System, true, "已有可见窗口,无需额外操作");
logging!(info, Type::System, "已有可见窗口,无需额外操作");
}
}
@@ -410,10 +428,10 @@ pub fn run() {
log::info!(target: "app", "closing window...");
if let tauri::WindowEvent::CloseRequested { api, .. } = api {
api.prevent_close();
if let Some(window) = core::handle::Handle::global().get_window() {
if let Some(window) = core::handle::Handle::get_window() {
let _ = window.hide();
} else {
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
logging!(warn, Type::Window, "尝试隐藏窗口但窗口不存在");
}
}
}
@@ -435,20 +453,20 @@ pub fn run() {
.register_system_hotkey(SystemHotkey::CmdQ)
.await
{
logging!(error, Type::Hotkey, true, "Failed to register CMD+Q: {}", e);
logging!(error, Type::Hotkey, "Failed to register CMD+Q: {}", e);
}
if let Err(e) = hotkey::Hotkey::global()
.register_system_hotkey(SystemHotkey::CmdW)
.await
{
logging!(error, Type::Hotkey, true, "Failed to register CMD+W: {}", e);
logging!(error, Type::Hotkey, "Failed to register CMD+W: {}", e);
}
}
if !is_enable_global_hotkey
&& let Err(e) = hotkey::Hotkey::global().init().await
{
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
logging!(error, Type::Hotkey, "Failed to init hotkeys: {}", e);
}
return;
}
@@ -460,29 +478,17 @@ pub fn run() {
if let Err(e) =
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
{
logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+Q: {}",
e
);
logging!(error, Type::Hotkey, "Failed to unregister CMD+Q: {}", e);
}
if let Err(e) =
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
{
logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+W: {}",
e
);
logging!(error, Type::Hotkey, "Failed to unregister CMD+W: {}", e);
}
}
if !is_enable_global_hotkey && let Err(e) = hotkey::Hotkey::global().reset() {
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
logging!(error, Type::Hotkey, "Failed to reset hotkeys: {}", e);
}
});
}
@@ -498,7 +504,6 @@ pub fn run() {
logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+Q on destroy: {}",
e
);
@@ -509,7 +514,6 @@ pub fn run() {
logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+W on destroy: {}",
e
);
@@ -525,7 +529,6 @@ pub fn run() {
logging!(
error,
Type::Setup,
true,
"Failed to build Tauri application: {}",
e
);
@@ -535,6 +538,11 @@ pub fn run() {
app.run(|app_handle, e| {
match e {
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
// 如果正在退出,忽略 Ready/Resumed 事件
if core::handle::Handle::global().is_exiting() {
logging!(debug, Type::System, "忽略 Ready/Resumed 事件,应用正在退出");
return;
}
event_handlers::handle_ready_resumed(app_handle);
}
#[cfg(target_os = "macos")]
@@ -542,22 +550,54 @@ pub fn run() {
has_visible_windows,
..
} => {
let app_handle = app_handle.clone();
// 如果正在退出,忽略 Reopen 事件
if core::handle::Handle::global().is_exiting() {
logging!(debug, Type::System, "忽略 Reopen 事件,应用正在退出");
return;
}
AsyncHandler::spawn(move || async move {
event_handlers::handle_reopen(&app_handle, has_visible_windows).await;
event_handlers::handle_reopen(has_visible_windows).await;
});
}
tauri::RunEvent::ExitRequested { api, code, .. } => {
tauri::async_runtime::block_on(async {
let _ = handle::Handle::mihomo()
.await
.clear_all_ws_connections()
.await;
});
// 如果已经在退出流程中,不要阻止退出
if core::handle::Handle::global().is_exiting() {
logging!(
info,
Type::System,
"应用正在退出,允许 ExitRequested (code: {:?})",
code
);
return;
}
// 只阻止外部的无退出码请求(如用户取消系统关机)
if code.is_none() {
logging!(debug, Type::System, "阻止外部退出请求");
api.prevent_exit();
}
}
tauri::RunEvent::Exit => {
// Avoid duplicate cleanup
if core::handle::Handle::global().is_exiting() {
return;
let handle = core::handle::Handle::global();
if handle.is_exiting() {
logging!(
debug,
Type::System,
"Exit事件触发,但退出流程已执行,跳过重复清理"
);
} else {
logging!(debug, Type::System, "Exit事件触发,执行清理流程");
handle.set_is_exiting();
EventDrivenProxyManager::global().notify_app_stopping();
feat::clean();
}
feat::clean();
}
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
+9
View File
@@ -2,5 +2,14 @@
fn main() {
#[cfg(feature = "tokio-trace")]
console_subscriber::init();
// Check for --no-tray command line argument
let args: Vec<String> = std::env::args().collect();
if args.contains(&"--no-tray".to_string()) {
unsafe {
std::env::set_var("CLASH_VERGE_DISABLE_TRAY", "1");
}
}
app_lib::run();
}
@@ -1,5 +1,4 @@
use crate::{
cache::CacheProxy,
config::Config,
core::{handle, timer::Timer, tray::Tray},
log_err, logging,
@@ -51,13 +50,13 @@ fn set_state(new: LightweightState) {
LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release);
match new {
LightweightState::Normal => {
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
logging!(info, Type::Lightweight, "轻量模式已关闭");
}
LightweightState::In => {
logging!(info, Type::Lightweight, true, "轻量模式已开启");
logging!(info, Type::Lightweight, "轻量模式已开启");
}
LightweightState::Exiting => {
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
logging!(info, Type::Lightweight, "正在退出轻量模式");
}
}
}
@@ -101,7 +100,6 @@ pub async fn run_once_auto_lightweight() {
logging!(
info,
Type::Lightweight,
true,
"不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式"
);
return;
@@ -126,7 +124,6 @@ pub async fn auto_lightweight_mode_init() -> Result<()> {
logging!(
info,
Type::Lightweight,
true,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_state(LightweightState::Normal);
@@ -141,13 +138,13 @@ pub async fn enable_auto_light_weight_mode() {
logging!(error, Type::Lightweight, "Failed to initialize timer: {e}");
return;
}
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
logging!(info, Type::Lightweight, "开启自动轻量模式");
setup_window_close_listener();
setup_webview_focus_listener();
}
pub fn disable_auto_light_weight_mode() {
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
logging!(info, Type::Lightweight, "关闭自动轻量模式");
let _ = cancel_light_weight_timer();
cancel_window_close_listener();
cancel_webview_focus_listener();
@@ -164,7 +161,7 @@ pub async fn entry_lightweight_mode() -> bool {
)
.is_err()
{
logging!(info, Type::Lightweight, true, "无需进入轻量模式,跳过调用");
logging!(info, Type::Lightweight, "无需进入轻量模式,跳过调用");
return false;
}
@@ -176,7 +173,6 @@ pub async fn entry_lightweight_mode() -> bool {
// 回到 In
set_state(LightweightState::In);
CacheProxy::global().clean_default_keys();
true
}
@@ -195,7 +191,6 @@ pub async fn exit_lightweight_mode() -> bool {
logging!(
info,
Type::Lightweight,
true,
"轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
);
return false;
@@ -209,7 +204,7 @@ pub async fn exit_lightweight_mode() -> bool {
// 回到 Normal
set_state(LightweightState::Normal);
logging!(info, Type::Lightweight, true, "轻量模式退出完成");
logging!(info, Type::Lightweight, "轻量模式退出完成");
true
}
@@ -219,19 +214,14 @@ pub async fn add_light_weight_timer() {
}
fn setup_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() {
if let Some(window) = handle::Handle::get_window() {
let handler = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async {
if let Err(e) = setup_light_weight_timer().await {
log::warn!("Failed to setup light weight timer: {e}");
}
}));
logging!(
info,
Type::Lightweight,
true,
"监听到关闭请求,开始轻量模式计时"
);
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
});
WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release);
@@ -239,17 +229,17 @@ fn setup_window_close_listener() {
}
fn cancel_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() {
if let Some(window) = handle::Handle::get_window() {
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
logging!(info, Type::Lightweight, "取消了窗口关闭监听");
}
}
}
fn setup_webview_focus_listener() {
if let Some(window) = handle::Handle::global().get_window() {
if let Some(window) = handle::Handle::get_window() {
let handler = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer());
logging!(
@@ -264,11 +254,11 @@ fn setup_webview_focus_listener() {
}
fn cancel_webview_focus_listener() {
if let Some(window) = handle::Handle::global().get_window() {
if let Some(window) = handle::Handle::get_window() {
let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, true, "取消了窗口焦点监听");
logging!(info, Type::Lightweight, "取消了窗口焦点监听");
}
}
}
@@ -294,7 +284,7 @@ async fn setup_light_weight_timer() -> Result<()> {
.set_maximum_parallel_runnable_num(1)
.set_frequency_once_by_minutes(once_by_minutes)
.spawn_async_routine(move || async move {
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
logging!(info, Type::Timer, "计时器到期,开始进入轻量模式");
entry_lightweight_mode().await;
})
.context("failed to create timer task")?;
@@ -321,7 +311,6 @@ async fn setup_light_weight_timer() -> Result<()> {
logging!(
info,
Type::Timer,
true,
"计时器已设置,{} 分钟后将自动进入轻量模式",
once_by_minutes
);
@@ -337,7 +326,7 @@ fn cancel_light_weight_timer() -> Result<()> {
delay_timer
.remove_task(task.task_id)
.context("failed to remove timer task")?;
logging!(info, Type::Timer, true, "计时器已取消");
logging!(info, Type::Timer, "计时器已取消");
}
Ok(())
@@ -38,17 +38,7 @@ impl PlatformSpecification {
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
let system_arch = System::cpu_arch();
let Some(handler) = handle::Handle::global().app_handle() else {
return Self {
system_name,
system_version,
system_kernel_version,
system_arch,
verge_version: "unknown".into(),
running_mode: "NotRunning".to_string(),
is_admin: false,
};
};
let handler = handle::Handle::app_handle();
let verge_version = handler.package_info().version.to_string();
// 使用默认值避免在同步上下文中执行异步操作
@@ -0,0 +1,30 @@
use anyhow::Result;
use tauri_plugin_shell::process::CommandChild;
#[derive(Debug)]
pub struct CommandChildGuard(Option<CommandChild>);
impl Drop for CommandChildGuard {
fn drop(&mut self) {
if let Err(err) = self.kill() {
log::error!(target: "app", "Failed to kill child process: {}", err);
}
}
}
impl CommandChildGuard {
pub fn new(child: CommandChild) -> Self {
Self(Some(child))
}
pub fn kill(&mut self) -> Result<()> {
if let Some(child) = self.0.take() {
let _ = child.kill();
}
Ok(())
}
pub fn pid(&self) -> Option<u32> {
self.0.as_ref().map(|c| c.pid())
}
}
@@ -1,2 +1,4 @@
mod async_handler;
pub use async_handler::AsyncHandler;
mod guard;
pub use guard::CommandChildGuard;
+13 -70
View File
@@ -51,53 +51,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
}
// 避免在Handle未初始化时崩溃
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "app_handle not initialized, using default path");
// 使用可执行文件目录作为备用
let exe_path = tauri::utils::platform::current_exe()?;
let exe_dir = exe_path
.parent()
.ok_or(anyhow::anyhow!("failed to get executable directory"))?;
// 使用系统临时目录 + 应用ID
#[cfg(target_os = "windows")]
{
if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") {
let path = PathBuf::from(local_app_data).join(APP_ID);
return Ok(path);
}
}
#[cfg(target_os = "macos")]
{
if let Some(home) = std::env::var_os("HOME") {
let path = PathBuf::from(home)
.join("Library")
.join("Application Support")
.join(APP_ID);
return Ok(path);
}
}
#[cfg(target_os = "linux")]
{
if let Some(home) = std::env::var_os("HOME") {
let path = PathBuf::from(home)
.join(".local")
.join("share")
.join(APP_ID);
return Ok(path);
}
}
// 如果无法获取系统目录,则回退到可执行文件目录
let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID);
log::warn!(target: "app", "Using fallback data directory: {fallback_dir:?}");
return Ok(fallback_dir);
}
};
let app_handle = handle::Handle::app_handle();
match app_handle.path().data_dir() {
Ok(dir) => Ok(dir.join(APP_ID)),
@@ -111,18 +65,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
/// get the resources dir
pub fn app_resources_dir() -> Result<PathBuf> {
// 避免在Handle未初始化时崩溃
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "app_handle not initialized in app_resources_dir, using fallback");
// 使用可执行文件目录作为备用
let exe_dir = tauri::utils::platform::current_exe()?
.parent()
.ok_or(anyhow::anyhow!("failed to get executable directory"))?
.to_path_buf();
return Ok(exe_dir.join("resources"));
}
};
let app_handle = handle::Handle::app_handle();
match app_handle.path().resource_dir() {
Ok(dir) => Ok(dir.join("resources")),
@@ -201,18 +144,18 @@ pub fn service_path() -> Result<PathBuf> {
Ok(res_dir.join("clash-verge-service.exe"))
}
pub fn service_log_file() -> Result<PathBuf> {
use chrono::Local;
let log_dir = app_logs_dir()?.join("service");
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
let log_file = format!("{local_time}.log");
let log_file = log_dir.join(log_file);
pub fn sidecar_log_dir() -> Result<PathBuf> {
let log_dir = app_logs_dir()?.join("sidecar");
let _ = std::fs::create_dir_all(&log_dir);
Ok(log_file)
Ok(log_dir)
}
pub fn service_log_dir() -> Result<PathBuf> {
let log_dir = app_logs_dir()?.join("service");
let _ = std::fs::create_dir_all(&log_dir);
Ok(log_dir)
}
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
@@ -249,7 +192,7 @@ pub fn get_encryption_key() -> Result<Vec<u8>> {
#[cfg(unix)]
pub fn ensure_mihomo_safe_dir() -> Option<PathBuf> {
["/var/tmp", "/tmp"]
["/tmp"]
.iter()
.map(PathBuf::from)
.find(|path| path.exists())
@@ -1,4 +1,5 @@
/// Format bytes into human readable string (B, KB, MB, GB)
#[allow(unused)]
pub fn fmt_bytes(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let (mut val, mut unit) = (bytes as f64, 0);
+1 -1
View File
@@ -42,7 +42,7 @@ pub async fn read_mapping(path: &PathBuf) -> Result<Mapping> {
}
Err(err) => {
let error_msg = format!("YAML syntax error in {}: {}", path.display(), err);
logging!(error, Type::Config, true, "{}", error_msg);
logging!(error, Type::Config, "{}", error_msg);
crate::core::handle::Handle::notice_message(
"config_validate::yaml_syntax_error",
+77 -77
View File
@@ -1,19 +1,23 @@
cfg_if::cfg_if! {
if #[cfg(not(feature = "tauri-dev"))] {
use crate::utils::logging::{console_colored_format, file_format, NoExternModule};
use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, LogSpecification, Logger};
}
}
#[cfg(not(feature = "tauri-dev"))]
use crate::utils::logging::NoModuleFilter;
use crate::{
config::*,
core::handle,
logging,
process::AsyncHandler,
utils::{dirs, help, logging::Type},
utils::{
dirs::{self, service_log_dir, sidecar_log_dir},
help,
logging::Type,
},
};
use anyhow::Result;
use chrono::{Local, TimeZone};
use clash_verge_service_ipc::WriterConfig;
use flexi_logger::writers::FileLogWriter;
use flexi_logger::{Cleanup, Criterion, FileSpec};
#[cfg(not(feature = "tauri-dev"))]
use flexi_logger::{Duplicate, LogSpecBuilder, Logger};
use std::{path::PathBuf, str::FromStr};
use tauri_plugin_shell::ShellExt;
use tokio::fs;
@@ -34,11 +38,13 @@ pub async fn init_logger() -> Result<()> {
};
let log_dir = dirs::app_logs_dir()?;
let logger = Logger::with(LogSpecification::from(log_level))
let spec = LogSpecBuilder::new().default(log_level).build();
let logger = Logger::with(spec)
.log_to_file(FileSpec::default().directory(log_dir).basename(""))
.duplicate_to_stdout(Duplicate::Debug)
.format(console_colored_format)
.format_for_files(file_format)
.format(clash_verge_logger::console_format)
.format_for_files(clash_verge_logger::file_format_with_level)
.rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
@@ -47,7 +53,7 @@ pub async fn init_logger() -> Result<()> {
},
Cleanup::KeepLogFiles(log_max_count),
)
.filter(Box::new(NoExternModule));
.filter(Box::new(NoModuleFilter(&["wry", "tauri"])));
let _handle = logger.start()?;
@@ -59,6 +65,52 @@ pub async fn init_logger() -> Result<()> {
Ok(())
}
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_ref();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
)
};
let sidecar_log_dir = sidecar_log_dir()?;
Ok(FileLogWriter::builder(
FileSpec::default()
.directory(sidecar_log_dir)
.basename("sidecar")
.suppress_timestamp(),
)
.format(clash_verge_logger::file_format_without_level)
.rotate(
Criterion::Size(log_max_size * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
Cleanup::KeepLogFiles(log_max_count),
)
.try_build()?)
}
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_ref();
(
verge.app_log_max_size.unwrap_or(128),
verge.app_log_max_count.unwrap_or(8),
)
};
let service_log_dir = dirs::path_to_str(&service_log_dir()?)?.to_string();
Ok(WriterConfig {
directory: service_log_dir,
max_log_size: log_max_size * 1024,
max_log_files: log_max_count,
})
}
// TODO flexi_logger 提供了最大保留天数,或许我们应该用内置删除log文件
/// 删除log文件
pub async fn delete_log() -> Result<()> {
@@ -82,13 +134,7 @@ pub async fn delete_log() -> Result<()> {
_ => return Ok(()),
};
logging!(
info,
Type::Setup,
true,
"try to delete log files, day: {}",
day
);
logging!(info, Type::Setup, "try to delete log files, day: {}", day);
// %Y-%m-%d to NaiveDateTime
let parse_time_str = |s: &str| {
@@ -123,7 +169,7 @@ pub async fn delete_log() -> Result<()> {
if duration.num_days() > day {
let file_path = file.path();
let _ = fs::remove_file(file_path).await;
logging!(info, Type::Setup, true, "delete log file: {}", file_name);
logging!(info, Type::Setup, "delete log file: {}", file_name);
}
}
Ok(())
@@ -250,7 +296,7 @@ async fn init_dns_config() -> Result<()> {
let dns_path = app_dir.join("dns_config.yaml");
if !dns_path.exists() {
logging!(info, Type::Setup, true, "Creating default DNS config file");
logging!(info, Type::Setup, "Creating default DNS config file");
help::save_yaml(
&dns_path,
&default_dns_config,
@@ -275,14 +321,7 @@ async fn ensure_directories() -> Result<()> {
fs::create_dir_all(&dir).await.map_err(|e| {
anyhow::anyhow!("Failed to create {} directory {:?}: {}", name, dir, e)
})?;
logging!(
info,
Type::Setup,
true,
"Created {} directory: {:?}",
name,
dir
);
logging!(info, Type::Setup, "Created {} directory: {:?}", name, dir);
}
}
@@ -298,13 +337,7 @@ async fn initialize_config_files() -> Result<()> {
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create clash config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created clash config at {:?}",
path
);
logging!(info, Type::Setup, "Created clash config at {:?}", path);
}
if let Ok(path) = dirs::verge_path()
@@ -314,13 +347,7 @@ async fn initialize_config_files() -> Result<()> {
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create verge config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created verge config at {:?}",
path
);
logging!(info, Type::Setup, "Created verge config at {:?}", path);
}
if let Ok(path) = dirs::profiles_path()
@@ -330,13 +357,7 @@ async fn initialize_config_files() -> Result<()> {
help::save_yaml(&path, &template, Some("# Clash Verge"))
.await
.map_err(|e| anyhow::anyhow!("Failed to create profiles config: {}", e))?;
logging!(
info,
Type::Setup,
true,
"Created profiles config at {:?}",
path
);
logging!(info, Type::Setup, "Created profiles config at {:?}", path);
}
// 验证并修正verge配置
@@ -364,19 +385,13 @@ pub async fn init_config() -> Result<()> {
AsyncHandler::spawn(|| async {
if let Err(e) = delete_log().await {
logging!(warn, Type::Setup, true, "Failed to clean old logs: {}", e);
logging!(warn, Type::Setup, "Failed to clean old logs: {}", e);
}
logging!(info, Type::Setup, true, "后台日志清理任务完成");
logging!(info, Type::Setup, "后台日志清理任务完成");
});
if let Err(e) = init_dns_config().await {
logging!(
warn,
Type::Setup,
true,
"DNS config initialization failed: {}",
e
);
logging!(warn, Type::Setup, "DNS config initialization failed: {}", e);
}
Ok(())
@@ -406,13 +421,12 @@ pub async fn init_resources() -> Result<()> {
let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
match fs::copy(&src, &dest).await {
Ok(_) => {
logging!(debug, Type::Setup, true, "resources copied '{}'", file);
logging!(debug, Type::Setup, "resources copied '{}'", file);
}
Err(err) => {
logging!(
error,
Type::Setup,
true,
"failed to copy resources '{}' to '{:?}', {}",
file,
dest,
@@ -437,13 +451,7 @@ pub async fn init_resources() -> Result<()> {
}
}
_ => {
logging!(
debug,
Type::Setup,
true,
"failed to get modified '{}'",
file
);
logging!(debug, Type::Setup, "failed to get modified '{}'", file);
handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
}
};
@@ -494,15 +502,7 @@ pub fn init_scheme() -> Result<()> {
}
pub async fn startup_script() -> Result<()> {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
return Err(anyhow::anyhow!(
"app_handle not available for startup script execution"
));
}
};
let app_handle = handle::Handle::app_handle();
let script_path = {
let verge = Config::verge().await;
let verge = verge.latest_ref();
+35 -112
View File
@@ -1,15 +1,12 @@
cfg_if::cfg_if! {
if #[cfg(feature = "tauri-dev")] {
use std::fmt;
} else {
#[cfg(feature = "verge-dev")]
use nu_ansi_term::Color;
use std::{fmt, io::Write, thread};
use flexi_logger::DeferredNow;
use log::{LevelFilter, Record};
use flexi_logger::filter::LogLineFilter;
}
}
use flexi_logger::writers::FileLogWriter;
#[cfg(not(feature = "tauri-dev"))]
use flexi_logger::{DeferredNow, filter::LogLineFilter};
#[cfg(not(feature = "tauri-dev"))]
use log::Record;
use std::{fmt, sync::Arc};
use tokio::sync::Mutex;
pub type SharedWriter = Arc<Mutex<FileLogWriter>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type {
@@ -28,7 +25,6 @@ pub enum Type {
Lightweight,
Network,
ProxyMode,
Ipc,
// Cache,
ClashVergeRev,
}
@@ -51,7 +47,6 @@ impl fmt::Display for Type {
Type::Lightweight => write!(f, "[Lightweight]"),
Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"),
Type::Ipc => write!(f, "[IPC]"),
// Type::Cache => write!(f, "[Cache]"),
Type::ClashVergeRev => write!(f, "[ClashVergeRev]"),
}
@@ -118,18 +113,6 @@ macro_rules! wrap_err {
#[macro_export]
macro_rules! logging {
// 带 println 的版本(支持格式化参数)
($level:ident, $type:expr, true, $($arg:tt)*) => {
// We dont need println here anymore
// println!("{} {}", $type, format_args!($($arg)*));
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
};
// 带 println 的版本(使用 false 明确不打印)
($level:ident, $type:expr, false, $($arg:tt)*) => {
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
};
// 不带 print 参数的版本(默认不打印)
($level:ident, $type:expr, $($arg:tt)*) => {
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
@@ -138,110 +121,50 @@ macro_rules! logging {
#[macro_export]
macro_rules! logging_error {
// 1. 处理 Result<T, E>,带打印控制
($type:expr, $print:expr, $expr:expr) => {
match $expr {
Ok(_) => {},
Err(err) => {
if $print {
println!("[{}] Error: {}", $type, err);
}
log::error!(target: "app", "[{}] {}", $type, err);
}
}
};
// 2. 处理 Result<T, E>,默认不打印
// Handle Result<T, E>
($type:expr, $expr:expr) => {
if let Err(err) = $expr {
log::error!(target: "app", "[{}] {}", $type, err);
}
};
// 3. 处理格式化字符串,带打印控制
($type:expr, $print:expr, $fmt:literal $(, $arg:expr)*) => {
if $print {
println!("[{}] {}", $type, format_args!($fmt $(, $arg)*));
}
log::error!(target: "app", "[{}] {}", $type, format_args!($fmt $(, $arg)*));
};
// 4. 处理格式化字符串,不带 bool 时,默认 `false`
// Handle formatted message: always print to stdout and log as error
($type:expr, $fmt:literal $(, $arg:expr)*) => {
logging_error!($type, false, $fmt $(, $arg)*);
log::error!(target: "app", "[{}] {}", $type, format_args!($fmt $(, $arg)*));
};
}
#[cfg(not(feature = "tauri-dev"))]
static IGNORE_MODULES: &[&str] = &["tauri", "wry"];
pub struct NoModuleFilter<'a>(pub &'a [&'a str]);
#[cfg(not(feature = "tauri-dev"))]
pub struct NoExternModule;
impl<'a> NoModuleFilter<'a> {
#[inline]
pub fn filter(&self, record: &Record) -> bool {
if let Some(module) = record.module_path() {
for blocked in self.0 {
if module.len() >= blocked.len()
&& module.as_bytes()[..blocked.len()] == blocked.as_bytes()[..]
{
return false;
}
}
}
true
}
}
#[cfg(not(feature = "tauri-dev"))]
impl LogLineFilter for NoExternModule {
impl<'a> LogLineFilter for NoModuleFilter<'a> {
fn write(
&self,
now: &mut DeferredNow,
record: &Record,
log_line_writer: &dyn flexi_logger::filter::LogLineWriter,
writer: &dyn flexi_logger::filter::LogLineWriter,
) -> std::io::Result<()> {
let module_path = record.module_path().unwrap_or_default();
if IGNORE_MODULES.iter().any(|m| module_path.starts_with(m)) {
Ok(())
} else {
log_line_writer.write(now, record)
if !self.filter(record) {
return Ok(());
}
writer.write(now, record)
}
}
#[cfg(not(feature = "tauri-dev"))]
pub fn get_log_level(log_level: &LevelFilter) -> String {
#[cfg(feature = "verge-dev")]
match log_level {
LevelFilter::Off => Color::Fixed(8).paint("OFF").to_string(),
LevelFilter::Error => Color::Red.paint("ERROR").to_string(),
LevelFilter::Warn => Color::Yellow.paint("WARN ").to_string(),
LevelFilter::Info => Color::Green.paint("INFO ").to_string(),
LevelFilter::Debug => Color::Blue.paint("DEBUG").to_string(),
LevelFilter::Trace => Color::Purple.paint("TRACE").to_string(),
}
#[cfg(not(feature = "verge-dev"))]
log_level.to_string()
}
#[cfg(not(feature = "tauri-dev"))]
pub fn console_colored_format(
w: &mut dyn Write,
now: &mut DeferredNow,
record: &log::Record,
) -> std::io::Result<()> {
let current_thread = thread::current();
let thread_name = current_thread.name().unwrap_or("unnamed");
let level = get_log_level(&record.level().to_level_filter());
let line = record.line().unwrap_or(0);
write!(
w,
"[{}] {} [{}:{}] T[{}] {}",
now.format("%H:%M:%S%.3f"),
level,
record.module_path().unwrap_or("<unnamed>"),
line,
thread_name,
record.args(),
)
}
#[cfg(not(feature = "tauri-dev"))]
pub fn file_format(
w: &mut dyn Write,
now: &mut DeferredNow,
record: &Record,
) -> std::io::Result<()> {
write!(
w,
"[{}] {} {}",
now.format("%Y-%m-%d %H:%M:%S%.3f"),
record.level(),
record.args(),
)
}
@@ -2,13 +2,7 @@
pub async fn set_public_dns(dns_server: String) {
use crate::{core::handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::error!(target: "app", "app_handle not available for DNS configuration");
return;
}
};
let app_handle = handle::Handle::app_handle();
log::info!(target: "app", "try to set system dns");
let resource_dir = match dirs::app_resources_dir() {
@@ -50,13 +44,7 @@ pub async fn set_public_dns(dns_server: String) {
pub async fn restore_public_dns() {
use crate::{core::handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::error!(target: "app", "app_handle not available for DNS restoration");
return;
}
};
let app_handle = handle::Handle::app_handle();
log::info!(target: "app", "try to unset system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
@@ -1,10 +1,13 @@
use anyhow::Result;
use tauri::AppHandle;
use crate::{
config::Config,
core::{
CoreManager, Timer, handle, hotkey::Hotkey, service::SERVICE_MANAGER, sysopt, tray::Tray,
CoreManager, Timer, handle,
hotkey::Hotkey,
service::{SERVICE_MANAGER, ServiceManager, is_service_ipc_path_exists},
sysopt,
tray::Tray,
},
logging, logging_error,
module::lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight},
@@ -18,8 +21,8 @@ pub mod ui;
pub mod window;
pub mod window_script;
pub fn resolve_setup_handle(app_handle: AppHandle) {
init_handle(app_handle);
pub fn resolve_setup_handle() {
init_handle();
}
pub fn resolve_setup_sync() {
@@ -34,7 +37,6 @@ pub fn resolve_setup_async() {
logging!(
info,
Type::Setup,
true,
"开始执行异步设置任务... 线程ID: {:?}",
std::thread::current().id()
);
@@ -45,11 +47,10 @@ pub fn resolve_setup_async() {
logging!(
info,
Type::ClashVergeRev,
true,
"Version: {}",
env!("CARGO_PKG_VERSION")
);
init_service_manager().await;
futures::join!(init_service_manager());
futures::join!(
init_work_config(),
@@ -62,7 +63,12 @@ pub fn resolve_setup_async() {
init_once_auto_lightweight().await;
init_auto_lightweight_mode().await;
// 确保配置完全初始化后再启动核心管理器
init_verge_config().await;
// 添加配置验证,确保运行时配置已正确生成
Config::verify_config_initialization().await;
init_core_manager().await;
init_system_proxy().await;
@@ -78,187 +84,154 @@ pub fn resolve_setup_async() {
});
let elapsed = start_time.elapsed();
logging!(
info,
Type::Setup,
true,
"异步设置任务完成,耗时: {:?}",
elapsed
);
logging!(info, Type::Setup, "异步设置任务完成,耗时: {:?}", elapsed);
if elapsed.as_secs() > 10 {
logging!(
warn,
Type::Setup,
true,
"异步设置任务耗时较长({:?})",
elapsed
);
logging!(warn, Type::Setup, "异步设置任务耗时较长({:?})", elapsed);
}
}
// 其它辅助函数不变
pub async fn resolve_reset_async() {
logging!(info, Type::Tray, true, "Resetting system proxy");
logging_error!(
Type::System,
true,
sysopt::Sysopt::global().reset_sysproxy().await
);
pub async fn resolve_reset_async() -> Result<(), anyhow::Error> {
logging!(info, Type::Tray, "Resetting system proxy");
sysopt::Sysopt::global().reset_sysproxy().await?;
logging!(info, Type::Core, true, "Stopping core service");
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
logging!(info, Type::Core, "Stopping core service");
CoreManager::global().stop_core().await?;
#[cfg(target_os = "macos")]
{
use dns::restore_public_dns;
logging!(info, Type::System, true, "Restoring system DNS settings");
logging!(info, Type::System, "Restoring system DNS settings");
restore_public_dns().await;
}
Ok(())
}
pub fn init_handle(app_handle: AppHandle) {
logging!(info, Type::Setup, true, "Initializing app handle...");
handle::Handle::global().init(app_handle);
pub fn init_handle() {
logging!(info, Type::Setup, "Initializing app handle...");
handle::Handle::global().init();
}
pub(super) fn init_scheme() {
logging!(info, Type::Setup, true, "Initializing custom URL scheme");
logging_error!(Type::Setup, true, init::init_scheme());
logging!(info, Type::Setup, "Initializing custom URL scheme");
logging_error!(Type::Setup, init::init_scheme());
}
#[cfg(not(feature = "tauri-dev"))]
pub(super) async fn resolve_setup_logger() {
logging!(info, Type::Setup, true, "Initializing global logger...");
logging_error!(Type::Setup, true, init::init_logger().await);
logging!(info, Type::Setup, "Initializing global logger...");
logging_error!(Type::Setup, init::init_logger().await);
}
pub async fn resolve_scheme(param: String) -> Result<()> {
logging!(
info,
Type::Setup,
true,
"Resolving scheme for param: {}",
param
);
logging_error!(Type::Setup, true, scheme::resolve_scheme(param).await);
logging!(info, Type::Setup, "Resolving scheme for param: {}", param);
logging_error!(Type::Setup, scheme::resolve_scheme(param).await);
Ok(())
}
pub(super) fn init_embed_server() {
logging!(info, Type::Setup, true, "Initializing embedded server...");
logging!(info, Type::Setup, "Initializing embedded server...");
server::embed_server();
}
pub(super) async fn init_resources() {
logging!(info, Type::Setup, true, "Initializing resources...");
logging_error!(Type::Setup, true, init::init_resources().await);
logging!(info, Type::Setup, "Initializing resources...");
logging_error!(Type::Setup, init::init_resources().await);
}
pub(super) async fn init_startup_script() {
logging!(info, Type::Setup, true, "Initializing startup script");
logging_error!(Type::Setup, true, init::startup_script().await);
logging!(info, Type::Setup, "Initializing startup script");
logging_error!(Type::Setup, init::startup_script().await);
}
pub(super) async fn init_timer() {
logging!(info, Type::Setup, true, "Initializing timer...");
logging_error!(Type::Setup, true, Timer::global().init().await);
logging!(info, Type::Setup, "Initializing timer...");
logging_error!(Type::Setup, Timer::global().init().await);
}
pub(super) async fn init_hotkey() {
logging!(info, Type::Setup, true, "Initializing hotkey...");
logging_error!(Type::Setup, true, Hotkey::global().init().await);
logging!(info, Type::Setup, "Initializing hotkey...");
logging_error!(Type::Setup, Hotkey::global().init().await);
}
pub(super) async fn init_once_auto_lightweight() {
logging!(
info,
Type::Lightweight,
true,
"Running auto lightweight mode check..."
);
run_once_auto_lightweight().await;
}
pub(super) async fn init_auto_lightweight_mode() {
logging!(
info,
Type::Setup,
true,
"Initializing auto lightweight mode..."
);
logging_error!(Type::Setup, true, auto_lightweight_mode_init().await);
logging!(info, Type::Setup, "Initializing auto lightweight mode...");
logging_error!(Type::Setup, auto_lightweight_mode_init().await);
}
pub async fn init_work_config() {
logging!(
info,
Type::Setup,
true,
"Initializing work configuration..."
);
logging_error!(Type::Setup, true, init::init_config().await);
logging!(info, Type::Setup, "Initializing work configuration...");
logging_error!(Type::Setup, init::init_config().await);
}
pub(super) async fn init_tray() {
logging!(info, Type::Setup, true, "Initializing system tray...");
logging_error!(Type::Setup, true, Tray::global().init().await);
// Check if tray should be disabled via environment variable
if std::env::var("CLASH_VERGE_DISABLE_TRAY").unwrap_or_default() == "1" {
logging!(info, Type::Setup, "System tray disabled via --no-tray flag");
return;
}
logging!(info, Type::Setup, "Initializing system tray...");
logging_error!(Type::Setup, Tray::global().init().await);
}
pub(super) async fn init_verge_config() {
logging!(
info,
Type::Setup,
true,
"Initializing verge configuration..."
);
logging_error!(Type::Setup, true, Config::init_config().await);
logging!(info, Type::Setup, "Initializing verge configuration...");
logging_error!(Type::Setup, Config::init_config().await);
}
pub(super) async fn init_service_manager() {
logging!(info, Type::Setup, true, "Initializing service manager...");
logging_error!(
Type::Setup,
true,
SERVICE_MANAGER.lock().await.refresh().await
);
logging!(info, Type::Setup, "Initializing service manager...");
clash_verge_service_ipc::set_config(ServiceManager::config()).await;
if !is_service_ipc_path_exists() {
logging!(
warn,
Type::Setup,
"Service IPC path does not exist, service may be unavailable"
);
return;
}
if SERVICE_MANAGER.lock().await.init().await.is_ok() {
logging_error!(Type::Setup, SERVICE_MANAGER.lock().await.refresh().await);
}
}
pub(super) async fn init_core_manager() {
logging!(info, Type::Setup, true, "Initializing core manager...");
logging_error!(Type::Setup, true, CoreManager::global().init().await);
logging!(info, Type::Setup, "Initializing core manager...");
logging_error!(Type::Setup, CoreManager::global().init().await);
}
pub(super) async fn init_system_proxy() {
logging!(info, Type::Setup, true, "Initializing system proxy...");
logging!(info, Type::Setup, "Initializing system proxy...");
logging_error!(
Type::Setup,
true,
sysopt::Sysopt::global().update_sysproxy().await
);
}
pub(super) fn init_system_proxy_guard() {
logging!(
info,
Type::Setup,
true,
"Initializing system proxy guard..."
);
logging_error!(
Type::Setup,
true,
sysopt::Sysopt::global().init_guard_sysproxy()
);
logging!(info, Type::Setup, "Initializing system proxy guard...");
logging_error!(Type::Setup, sysopt::Sysopt::global().init_guard_sysproxy());
}
pub(super) async fn refresh_tray_menu() {
logging!(info, Type::Setup, true, "Refreshing tray menu...");
logging_error!(Type::Setup, true, Tray::global().update_part().await);
logging!(info, Type::Setup, "Refreshing tray menu...");
logging_error!(Type::Setup, Tray::global().update_part().await);
}
pub(super) async fn init_window() {
logging!(info, Type::Setup, true, "Initializing main window...");
logging!(info, Type::Setup, "Initializing main window...");
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")]
@@ -49,7 +49,7 @@ pub(super) async fn resolve_scheme(param: String) -> Result<()> {
let uid = match item.uid.clone() {
Some(uid) => uid,
None => {
logging!(error, Type::Config, true, "Profile item missing UID");
logging!(error, Type::Config, "Profile item missing UID");
handle::Handle::notice_message(
"import_sub_url::error",
"Profile item missing UID".to_string(),
@@ -66,7 +66,7 @@ pub fn update_ui_ready_stage(stage: UiReadyStage) {
// 标记UI已准备就绪
pub fn mark_ui_ready() {
get_ui_ready().store(true, Ordering::Release);
logging!(info, Type::Window, true, "UI已标记为完全就绪");
logging!(info, Type::Window, "UI已标记为完全就绪");
// 通知所有等待的任务
get_ui_ready_notify().notify_waiters();
@@ -2,7 +2,7 @@ use tauri::WebviewWindow;
use crate::{
core::handle,
logging, logging_error,
logging_error,
utils::{
logging::Type,
resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
@@ -18,24 +18,17 @@ const MINIMAL_HEIGHT: f64 = 520.0;
/// 构建新的 WebView 窗口
pub fn build_new_window() -> Result<WebviewWindow, String> {
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
logging!(
error,
Type::Window,
true,
"无法获取app_handle,窗口创建失败"
);
"无法获取app_handle".to_string()
})?;
let app_handle = handle::Handle::app_handle();
match tauri::WebviewWindowBuilder::new(
&app_handle,
app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.center()
.decorations(true)
// Using WindowManager::prefer_system_titlebar to control if show system built-in titlebar
// .decorations(true)
.fullscreen(false)
.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
.min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT)
@@ -44,7 +37,7 @@ pub fn build_new_window() -> Result<WebviewWindow, String> {
.build()
{
Ok(window) => {
logging_error!(Type::Window, true, window.eval(INITIAL_LOADING_OVERLAY));
logging_error!(Type::Window, window.eval(INITIAL_LOADING_OVERLAY));
Ok(window)
}
Err(e) => Err(e.to_string()),
+29 -2
View File
@@ -6,7 +6,10 @@ use crate::{
utils::logging::Type,
};
use anyhow::{Result, bail};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use port_scanner::local_port_available;
use tokio::sync::oneshot;
use warp::Filter;
#[derive(serde::Deserialize, Debug)]
@@ -14,6 +17,9 @@ struct QueryParam {
param: String,
}
// 关闭 embedded server 的信号发送端
static SHUTDOWN_SENDER: OnceCell<Mutex<Option<oneshot::Sender<()>>>> = OnceCell::new();
/// check whether there is already exists
pub async fn check_singleton() -> Result<()> {
let port = IVerge::get_singleton_port();
@@ -42,6 +48,11 @@ pub async fn check_singleton() -> Result<()> {
/// The embed server only be used to implement singleton process
/// maybe it can be used as pac server later
pub fn embed_server() {
let (shutdown_tx, shutdown_rx) = oneshot::channel();
#[allow(clippy::expect_used)]
SHUTDOWN_SENDER
.set(Mutex::new(Some(shutdown_tx)))
.expect("failed to set shutdown signal for embedded server");
let port = IVerge::get_singleton_port();
AsyncHandler::spawn(move || async move {
@@ -84,12 +95,28 @@ pub fn embed_server() {
// Spawn async work in a fire-and-forget manner
let param = query.param.clone();
tokio::task::spawn_local(async move {
logging_error!(Type::Setup, true, resolve::resolve_scheme(param).await);
logging_error!(Type::Setup, resolve::resolve_scheme(param).await);
});
warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK)
});
let commands = visible.or(scheme).or(pac);
warp::serve(commands).run(([127, 0, 0, 1], port)).await;
warp::serve(commands)
.bind(([127, 0, 0, 1], port))
.await
.graceful(async {
shutdown_rx.await.ok();
})
.run()
.await;
});
}
pub fn shutdown_embedded_server() {
log::info!("shutting down embedded server");
if let Some(sender) = SHUTDOWN_SENDER.get()
&& let Some(sender) = sender.lock().take()
{
sender.send(()).ok();
}
}
@@ -50,7 +50,6 @@ macro_rules! singleton_with_logging {
$crate::logging!(
info,
$crate::utils::logging::Type::Setup,
true,
concat!($struct_name_str, " initialized")
);
instance
@@ -88,7 +87,6 @@ macro_rules! singleton_lazy_with_logging {
$crate::logging!(
info,
$crate::utils::logging::Type::Setup,
true,
concat!($struct_name_str, " initialized")
);
instance
@@ -67,7 +67,7 @@ fn should_handle_window_operation() -> bool {
let now = Instant::now();
let elapsed = now.duration_since(*last_operation);
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
@@ -76,7 +76,7 @@ fn should_handle_window_operation() -> bool {
log::info!(target: "app", "[防抖] 窗口操作被允许执行");
true
} else {
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
false
}
@@ -117,9 +117,8 @@ impl WindowManager {
/// 获取主窗口实例
pub fn get_main_window() -> Option<WebviewWindow<Wry>> {
handle::Handle::global()
.app_handle()
.and_then(|app| app.get_webview_window("main"))
let app_handle = handle::Handle::app_handle();
app_handle.get_webview_window("main")
}
/// 智能显示主窗口
@@ -132,43 +131,32 @@ impl WindowManager {
finish_window_operation();
});
logging!(info, Type::Window, true, "开始智能显示主窗口");
logging!(
debug,
Type::Window,
true,
"{}",
Self::get_window_status_info()
);
logging!(info, Type::Window, "开始智能显示主窗口");
logging!(debug, Type::Window, "{}", Self::get_window_status_info());
let current_state = Self::get_main_window_state();
match current_state {
WindowState::NotExist => {
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
logging!(info, Type::Window, "窗口不存在,创建新窗口");
if Self::create_window(true).await {
logging!(info, Type::Window, true, "窗口创建成功");
logging!(info, Type::Window, "窗口创建成功");
std::thread::sleep(std::time::Duration::from_millis(100));
WindowOperationResult::Created
} else {
logging!(warn, Type::Window, true, "窗口创建失败");
logging!(warn, Type::Window, "窗口创建失败");
WindowOperationResult::Failed
}
}
WindowState::VisibleFocused => {
logging!(info, Type::Window, true, "窗口已经可见且有焦点,无需操作");
logging!(info, Type::Window, "窗口已经可见且有焦点,无需操作");
WindowOperationResult::NoAction
}
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
if let Some(window) = Self::get_main_window() {
let state_after_check = Self::get_main_window_state();
if state_after_check == WindowState::VisibleFocused {
logging!(
info,
Type::Window,
true,
"窗口在检查期间已变为可见和有焦点状态"
);
logging!(info, Type::Window, "窗口在检查期间已变为可见和有焦点状态");
return WindowOperationResult::NoAction;
}
Self::activate_window(&window)
@@ -189,13 +177,12 @@ impl WindowManager {
finish_window_operation();
});
logging!(info, Type::Window, true, "开始切换主窗口显示状态");
logging!(info, Type::Window, "开始切换主窗口显示状态");
let current_state = Self::get_main_window_state();
logging!(
info,
Type::Window,
true,
"当前窗口状态: {:?} | 详细状态: {}",
current_state,
Self::get_window_status_info()
@@ -204,7 +191,7 @@ impl WindowManager {
match current_state {
WindowState::NotExist => {
// 窗口不存在,创建新窗口
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
logging!(info, Type::Window, "窗口不存在,将创建新窗口");
// 由于已经有防抖保护,直接调用内部方法
if Self::create_window(true).await {
WindowOperationResult::Created
@@ -216,7 +203,6 @@ impl WindowManager {
logging!(
info,
Type::Window,
true,
"窗口可见(焦点状态: {}),将隐藏窗口",
if current_state == WindowState::VisibleFocused {
"有焦点"
@@ -227,30 +213,25 @@ impl WindowManager {
if let Some(window) = Self::get_main_window() {
match window.hide() {
Ok(_) => {
logging!(info, Type::Window, true, "窗口已成功隐藏");
logging!(info, Type::Window, "窗口已成功隐藏");
WindowOperationResult::Hidden
}
Err(e) => {
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
logging!(warn, Type::Window, "隐藏窗口失败: {}", e);
WindowOperationResult::Failed
}
}
} else {
logging!(warn, Type::Window, true, "无法获取窗口实例");
logging!(warn, Type::Window, "无法获取窗口实例");
WindowOperationResult::Failed
}
}
WindowState::Minimized | WindowState::Hidden => {
logging!(
info,
Type::Window,
true,
"窗口存在但被隐藏或最小化,将激活窗口"
);
logging!(info, Type::Window, "窗口存在但被隐藏或最小化,将激活窗口");
if let Some(window) = Self::get_main_window() {
Self::activate_window(&window)
} else {
logging!(warn, Type::Window, true, "无法获取窗口实例");
logging!(warn, Type::Window, "无法获取窗口实例");
WindowOperationResult::Failed
}
}
@@ -259,35 +240,35 @@ impl WindowManager {
/// 激活窗口(取消最小化、显示、设置焦点)
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult {
logging!(info, Type::Window, true, "开始激活窗口");
logging!(info, Type::Window, "开始激活窗口");
let mut operations_successful = true;
// 1. 如果窗口最小化,先取消最小化
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
logging!(info, Type::Window, "窗口已最小化,正在取消最小化");
if let Err(e) = window.unminimize() {
logging!(warn, Type::Window, true, "取消最小化失败: {}", e);
logging!(warn, Type::Window, "取消最小化失败: {}", e);
operations_successful = false;
}
}
// 2. 显示窗口
if let Err(e) = window.show() {
logging!(warn, Type::Window, true, "显示窗口失败: {}", e);
logging!(warn, Type::Window, "显示窗口失败: {}", e);
operations_successful = false;
}
// 3. 设置焦点
if let Err(e) = window.set_focus() {
logging!(warn, Type::Window, true, "设置窗口焦点失败: {}", e);
logging!(warn, Type::Window, "设置窗口焦点失败: {}", e);
operations_successful = false;
}
// 4. 平台特定的激活策略
#[cfg(target_os = "macos")]
{
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
logging!(info, Type::Window, "应用 macOS 特定的激活策略");
handle::Handle::global().set_activation_policy_regular();
}
@@ -295,31 +276,19 @@ impl WindowManager {
{
// Windows 尝试额外的激活方法
if let Err(e) = window.set_always_on_top(true) {
logging!(
debug,
Type::Window,
true,
"设置置顶失败(非关键错误): {}",
e
);
logging!(debug, Type::Window, "设置置顶失败(非关键错误): {}", e);
}
// 立即取消置顶
if let Err(e) = window.set_always_on_top(false) {
logging!(
debug,
Type::Window,
true,
"取消置顶失败(非关键错误): {}",
e
);
logging!(debug, Type::Window, "取消置顶失败(非关键错误): {}", e);
}
}
if operations_successful {
logging!(info, Type::Window, true, "窗口激活成功");
logging!(info, Type::Window, "窗口激活成功");
WindowOperationResult::Shown
} else {
logging!(warn, Type::Window, true, "窗口激活部分失败");
logging!(warn, Type::Window, "窗口激活部分失败");
WindowOperationResult::Failed
}
}
@@ -351,7 +320,6 @@ impl WindowManager {
logging!(
info,
Type::Window,
true,
"开始创建/显示主窗口, is_show={}",
is_show
);
@@ -362,15 +330,15 @@ impl WindowManager {
match build_new_window() {
Ok(_) => {
logging!(info, Type::Window, true, "新窗口创建成功");
logging!(info, Type::Window, "新窗口创建成功");
}
Err(e) => {
logging!(error, Type::Window, true, "新窗口创建失败: {}", e);
logging!(error, Type::Window, "新窗口创建失败: {}", e);
return false;
}
}
if WindowOperationResult::Failed != Self::show_main_window().await {
if WindowOperationResult::Failed == Self::show_main_window().await {
return false;
}
@@ -384,15 +352,15 @@ impl WindowManager {
pub fn destroy_main_window() -> WindowOperationResult {
if let Some(window) = Self::get_main_window() {
let _ = window.destroy();
logging!(info, Type::Window, true, "窗口已摧毁");
logging!(info, Type::Window, "窗口已摧毁");
#[cfg(target_os = "macos")]
{
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
logging!(info, Type::Window, "应用 macOS 特定的激活策略");
handle::Handle::global().set_activation_policy_accessory();
}
return WindowOperationResult::Destroyed;
}
logging!(warn, Type::Window, true, "窗口摧毁失败");
logging!(warn, Type::Window, "窗口摧毁失败");
WindowOperationResult::Failed
}
+5 -2
View File
@@ -49,8 +49,11 @@
"security": {
"capabilities": ["desktop-capability", "migrated"],
"assetProtocol": {
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
"enable": true
"enable": true,
"scope": {
"allow": ["**"],
"requireLiteralLeadingDot": false
}
},
"csp": null
}
@@ -5,7 +5,7 @@
"targets": ["deb", "rpm"],
"linux": {
"deb": {
"depends": ["openssl", "pkexec"],
"depends": ["openssl"],
"desktopTemplate": "./packages/linux/clash-verge.desktop",
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
@@ -14,7 +14,7 @@
"preRemoveScript": "./packages/linux/pre-remove.sh"
},
"rpm": {
"depends": ["openssl", "pkexec"],
"depends": ["openssl"],
"desktopTemplate": "./packages/linux/clash-verge.desktop",
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
@@ -25,8 +25,8 @@
},
"externalBin": [
"./resources/clash-verge-service",
"./resources/install-service",
"./resources/uninstall-service",
"./resources/clash-verge-service-install",
"./resources/clash-verge-service-uninstall",
"./sidecar/verge-mihomo",
"./sidecar/verge-mihomo-alpha"
]
+103 -102
View File
@@ -2,118 +2,103 @@
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
&__left {
flex: 1 0 200px;
.layout-content {
/* New container for the flex layout */
display: flex;
height: 100%;
width: 100%;
// max-width: 225px;
// min-width: 225px;
// padding: 16px 0 8px;
padding: 0px 0px 8px;
// position: relative;
flex-direction: column;
align-self: stretch;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
flex: 1; /* Take remaining height */
overflow: hidden;
border-right: 1px solid var(--divider-color);
// background-color: var(--background-color-alpha);
// $maxLogo: 100px;
.the-logo {
position: relative;
flex: 1 0 58px;
// width: 100%;
&__left {
flex: 1 0 200px;
display: flex;
height: 100%;
padding: 0px 20px;
width: 100%;
padding: 0 0 8px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
align-self: stretch;
// border-bottom: 1px solid var(--divider-color);
// max-width: $maxLogo + 32px;
// max-height: $maxLogo;
// margin: 0 auto;
// padding: 0 auto;
// text-align: center;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
overflow: hidden;
border-right: 1px solid var(--divider-color);
img,
svg {
width: 100%;
.the-logo {
position: relative;
flex: 1 0 58px;
display: flex;
height: 100%;
pointer-events: none;
// fill: var(--primary-main);
padding: 0 20px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
align-self: stretch;
box-sizing: border-box;
// #bg {
// fill: var(--background-color);
// }
img,
svg {
width: 100%;
height: 100%;
pointer-events: none;
}
.the-newbtn {
position: absolute;
right: 10px;
top: 15px;
border-radius: 8px;
padding: 2px 4px;
transform: scale(0.8);
}
}
.the-newbtn {
.the-menu {
flex: 1 1 80%;
overflow-y: auto;
margin-bottom: 0px;
padding-top: 4px;
}
.the-traffic {
flex: 0 0 60px;
> div {
margin: 0 auto;
padding: 0 20px;
}
}
}
&__right {
position: relative;
flex: 1 1 100%;
height: 100%;
.the-bar {
height: 36px;
display: flex;
justify-content: end;
box-sizing: border-box;
z-index: 2;
.the-dragbar {
margin-top: 5px;
app-region: drag;
}
}
.the-content {
position: absolute;
right: 10px;
top: 15px;
border-radius: 8px;
padding: 2px 4px;
transform: scale(0.8);
top: 0;
left: 0;
right: 1px;
bottom: 0px;
}
}
.the-menu {
flex: 1 1 80%;
overflow-y: auto;
margin-bottom: 0px;
padding-top: 4px;
}
.the-traffic {
flex: 0 0 60px;
> div {
margin: 0 auto;
padding: 0px 20px;
}
}
}
&__right {
position: relative;
flex: 1 1 100%;
height: 100%;
// background-color: var(--background-color-alpha);
.the-bar {
// position: absolute;
// top: 0px;
// right: 0px;
height: 36px;
display: flex;
// align-items: center;
justify-content: end;
box-sizing: border-box;
z-index: 2;
.the-dragbar {
margin-top: 5px;
app-region: drag;
}
}
.the-content {
position: absolute;
top: 0;
left: 0;
right: 1px;
bottom: 0px;
}
}
}
@@ -121,11 +106,17 @@
.windows,
.unknown {
&.layout {
//.layout__left {
// padding-top: 24px;
//}
.the_titlebar {
width: 100%;
display: flex;
justify-content: flex-end;
padding: 10px;
box-sizing: border-box;
height: 36px;
border-bottom: 1px solid var(--divider-color);
}
.layout__left .the-logo {
.layout-content__left .the-logo {
flex: 1 0 58px;
margin-top: 10px;
margin-left: 10px;
@@ -135,7 +126,7 @@
padding-bottom: 16px;
}
.layout__right .the-content {
.layout-content__right .the-content {
top: 5px;
}
}
@@ -143,15 +134,25 @@
.macos {
&.layout {
.layout__left {
.the_titlebar {
width: 100%;
display: flex;
justify-content: flex-start;
padding: 10px;
box-sizing: border-box;
height: 36px;
border-bottom: 1px solid var(--divider-color);
}
.layout-content__left {
padding-top: 5px;
}
.layout__right .the-content {
.layout-content__right .the-content {
top: 5px;
}
.layout__left .the-newbtn {
.layout-content__left .the-newbtn {
right: 9px;
top: 2px;
}
@@ -3,8 +3,8 @@ import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { t } from "i18next";
import { useImperativeHandle, useState, type Ref } from "react";
import { closeConnections } from "tauri-plugin-mihomo-api";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
export interface ConnectionDetailRef {
@@ -97,7 +97,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
];
const onDelete = useLockFn(async () => deleteConnection(data.id));
const onDelete = useLockFn(async () => closeConnections(data.id));
return (
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>
@@ -9,8 +9,8 @@ import {
} from "@mui/material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { closeConnections } from "tauri-plugin-mihomo-api";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
const Tag = styled("span")(({ theme }) => ({
@@ -34,7 +34,7 @@ export const ConnectionItem = (props: Props) => {
const { id, metadata, chains, start, curUpload, curDownload } = value;
const onDelete = useLockFn(async () => deleteConnection(id));
const onDelete = useLockFn(async () => closeConnections(id));
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
return (
@@ -0,0 +1,114 @@
import { Close, CropSquare, FilterNone, Minimize } from "@mui/icons-material";
import { IconButton } from "@mui/material";
import { forwardRef, useImperativeHandle } from "react";
import { useWindowControls } from "@/hooks/use-window";
import getSystem from "@/utils/get-system";
export const WindowControls = forwardRef(function WindowControls(props, ref) {
const OS = getSystem();
const {
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
} = useWindowControls();
useImperativeHandle(
ref,
() => ({
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
}),
[
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
],
);
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准
return (
<div style={{ display: "flex", gap: 4 }}>
{OS === "macos" && (
<>
{/* macOS 风格:关闭 → 最小化 → 全屏 */}
<IconButton size="small" sx={{ fontSize: 14 }} onClick={close}>
<Close fontSize="inherit" color="inherit" />
</IconButton>
<IconButton size="small" sx={{ fontSize: 14 }} onClick={minimize}>
<Minimize fontSize="inherit" color="inherit" />
</IconButton>
<IconButton
size="small"
sx={{ fontSize: 14 }}
onClick={toggleMaximize}
>
{maximized ? (
<FilterNone fontSize="inherit" color="inherit" />
) : (
<CropSquare fontSize="inherit" color="inherit" />
)}
</IconButton>
</>
)}
{OS === "windows" && (
<>
{/* Windows 风格:最小化 → 最大化 → 关闭 */}
<IconButton size="small" sx={{ fontSize: 14 }} onClick={minimize}>
<Minimize fontSize="small" color="inherit" />
</IconButton>
<IconButton
size="small"
sx={{ fontSize: 14 }}
onClick={toggleMaximize}
>
{maximized ? (
<FilterNone fontSize="small" color="inherit" />
) : (
<CropSquare fontSize="small" color="inherit" />
)}
</IconButton>
<IconButton size="small" sx={{ fontSize: 14 }} onClick={close}>
<Close fontSize="small" color="inherit" />
</IconButton>
</>
)}
{OS === "linux" && (
<>
{/* Linux 桌面常见布局(GNOME/KDE 多为:最小化 → 最大化 → 关闭) */}
<IconButton size="small" sx={{ fontSize: 14 }} onClick={minimize}>
<Minimize fontSize="small" color="inherit" />
</IconButton>
<IconButton
size="small"
sx={{ fontSize: 14 }}
onClick={toggleMaximize}
>
{maximized ? (
<FilterNone fontSize="small" color="inherit" />
) : (
<CropSquare fontSize="small" color="inherit" />
)}
</IconButton>
<IconButton size="small" sx={{ fontSize: 14 }} onClick={close}>
<Close fontSize="small" color="inherit" />
</IconButton>
</>
)}
</div>
);
});
@@ -53,7 +53,7 @@ export const ClashInfoCard = () => {
{t("Mixed Port")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{clashConfig["mixed-port"] || "-"}
{clashConfig.mixedPort || "-"}
</Typography>
</Stack>
<Divider />
@@ -7,10 +7,11 @@ import { Box, Paper, Stack, Typography } from "@mui/material";
import { useLockFn } from "ahooks";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { closeAllConnections, patchClashMode } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
export const ClashModeCard = () => {
const { t } = useTranslation();
@@ -1,6 +1,7 @@
import {
AccessTimeRounded,
ChevronRight,
NetworkCheckRounded,
WifiOff as SignalError,
SignalWifi3Bar as SignalGood,
SignalWifi2Bar as SignalMedium,
@@ -25,12 +26,16 @@ import {
alpha,
useTheme,
} from "@mui/material";
import { useLockFn } from "ahooks";
import React from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
import { EnhancedCard } from "@/components/home/enhanced-card";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import delayManager from "@/services/delay";
@@ -47,7 +52,9 @@ interface ProxyOption {
// 排序类型: 默认 | 按延迟 | 按字母
type ProxySortType = 0 | 1 | 2;
function convertDelayColor(delayValue: number) {
function convertDelayColor(
delayValue: number,
): "success" | "warning" | "error" | "primary" | "default" {
const colorStr = delayManager.formatDelayColor(delayValue);
if (!colorStr) return "default";
@@ -67,7 +74,11 @@ function convertDelayColor(delayValue: number) {
}
}
function getSignalIcon(delay: number) {
function getSignalIcon(delay: number): {
icon: React.ReactElement;
text: string;
color: string;
} {
if (delay < 0)
return { icon: <SignalNone />, text: "未测试", color: "text.secondary" };
if (delay >= 10000)
@@ -81,20 +92,12 @@ function getSignalIcon(delay: number) {
return { icon: <SignalStrong />, text: "延迟极佳", color: "success.main" };
}
// 简单的防抖函数
function debounce(fn: Function, ms = 100) {
let timeoutId: ReturnType<typeof setTimeout>;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms);
};
}
export const CurrentProxyCard = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
const { proxies, clashConfig, refreshProxy } = useAppData();
const { verge } = useVerge();
// 统一代理选择器
const { handleSelectChange } = useProxySelection({
@@ -172,6 +175,7 @@ export const CurrentProxyCard = () => {
// 根据模式确定初始组
if (isGlobalMode) {
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setState((prev) => ({
...prev,
selection: {
@@ -180,6 +184,7 @@ export const CurrentProxyCard = () => {
},
}));
} else if (isDirectMode) {
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setState((prev) => ({
...prev,
selection: {
@@ -189,6 +194,7 @@ export const CurrentProxyCard = () => {
}));
} else {
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setState((prev) => ({
...prev,
selection: {
@@ -203,6 +209,7 @@ export const CurrentProxyCard = () => {
useEffect(() => {
if (!proxies) return;
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setState((prev) => {
// 只保留 Selector 类型的组用于选择
const filteredGroups = proxies.groups
@@ -270,16 +277,23 @@ export const CurrentProxyCard = () => {
}, [proxies, isGlobalMode, isDirectMode]);
// 使用防抖包装状态更新
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const debouncedSetState = useCallback(
debounce((updateFn: (prev: ProxyState) => ProxyState) => {
setState(updateFn);
}, 300),
[],
(updateFn: (prev: ProxyState) => ProxyState) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setState(updateFn);
}, 300);
},
[setState],
);
// 处理代理组变更
const handleGroupChange = useCallback(
(event: SelectChangeEvent) => {
(event: SelectChangeEvent<string>) => {
if (isGlobalMode || isDirectMode) return;
const newGroup = event.target.value;
@@ -314,7 +328,7 @@ export const CurrentProxyCard = () => {
// 处理代理节点变更
const handleProxyChange = useCallback(
(event: SelectChangeEvent) => {
(event: SelectChangeEvent<string>) => {
if (isDirectMode) return;
const newProxy = event.target.value;
@@ -399,6 +413,85 @@ export const CurrentProxyCard = () => {
localStorage.setItem(STORAGE_KEY_SORT_TYPE, newSortType.toString());
}, [sortType]);
// 延迟测试
const handleCheckDelay = useLockFn(async () => {
const groupName = state.selection.group;
if (!groupName || isDirectMode) return;
console.log(`[CurrentProxyCard] 开始测试所有延迟,组: ${groupName}`);
const timeout = verge?.default_latency_timeout || 10000;
// 获取当前组的所有代理
const proxyNames: string[] = [];
const providers: Set<string> = new Set();
if (isGlobalMode && proxies?.global) {
// 全局模式
const allProxies = proxies.global.all
.filter((p: any) => {
const name = typeof p === "string" ? p : p.name;
return name !== "DIRECT" && name !== "REJECT";
})
.map((p: any) => (typeof p === "string" ? p : p.name));
allProxies.forEach((name: string) => {
const proxy = state.proxyData.records[name];
if (proxy?.provider) {
providers.add(proxy.provider);
} else {
proxyNames.push(name);
}
});
} else {
// 规则模式
const group = state.proxyData.groups.find((g) => g.name === groupName);
if (group) {
group.all.forEach((name: string) => {
const proxy = state.proxyData.records[name];
if (proxy?.provider) {
providers.add(proxy.provider);
} else {
proxyNames.push(name);
}
});
}
}
console.log(
`[CurrentProxyCard] 找到代理数量: ${proxyNames.length}, 提供者数量: ${providers.size}`,
);
// 测试提供者的节点
if (providers.size > 0) {
console.log(`[CurrentProxyCard] 开始测试提供者节点`);
await Promise.allSettled(
[...providers].map((p) => healthcheckProxyProvider(p)),
);
}
// 测试非提供者的节点
if (proxyNames.length > 0) {
const url = delayManager.getUrl(groupName);
console.log(`[CurrentProxyCard] 测试URL: ${url}, 超时: ${timeout}ms`);
try {
await Promise.race([
delayManager.checkListDelay(proxyNames, groupName, timeout),
delayGroup(groupName, url, timeout),
]);
console.log(`[CurrentProxyCard] 延迟测试完成,组: ${groupName}`);
} catch (error) {
console.error(
`[CurrentProxyCard] 延迟测试出错,组: ${groupName}`,
error,
);
}
}
refreshProxy();
});
// 排序代理函数(增加非空校验)
const sortProxies = useCallback(
(proxies: ProxyOption[]) => {
@@ -474,7 +567,7 @@ export const CurrentProxyCard = () => {
]);
// 获取排序图标
const getSortIcon = () => {
const getSortIcon = (): React.ReactElement => {
switch (sortType) {
case 1:
return <AccessTimeRounded fontSize="small" />;
@@ -486,7 +579,7 @@ export const CurrentProxyCard = () => {
};
// 获取排序提示文本
const getSortTooltip = () => {
const getSortTooltip = (): string => {
switch (sortType) {
case 0:
return t("Sort by default");
@@ -517,13 +610,24 @@ export const CurrentProxyCard = () => {
}
iconColor={currentProxy ? "primary" : undefined}
action={
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Tooltip title={t("Delay check")}>
<span>
<IconButton
size="small"
color="inherit"
onClick={handleCheckDelay}
disabled={isDirectMode}
>
<NetworkCheckRounded />
</IconButton>
</span>
</Tooltip>
<Tooltip title={getSortTooltip()}>
<IconButton
size="small"
color="inherit"
onClick={handleSortTypeChange}
sx={{ mr: 1 }}
>
{getSortIcon()}
</IconButton>
@@ -657,7 +761,7 @@ export const CurrentProxyCard = () => {
>
{isDirectMode
? null
: proxyOptions.map((proxy, index) => {
: proxyOptions.map((proxy) => {
const delayValue =
state.proxyData.records[proxy.name] &&
state.selection.group
@@ -668,7 +772,7 @@ export const CurrentProxyCard = () => {
: -1;
return (
<MenuItem
key={`${proxy.name}-${index}`}
key={proxy.name}
value={proxy.name}
sx={{
display: "flex",
@@ -92,7 +92,7 @@ export const EnhancedCanvasTrafficGraph = memo(
const { t } = useTranslation();
// 使用增强版全局流量数据管理
const { dataPoints, getDataForTimeRange, isDataFresh, samplerStats } =
const { dataPoints, getDataForTimeRange, samplerStats } =
useTrafficGraphDataEnhanced();
// 基础状态
@@ -865,6 +865,7 @@ export const EnhancedCanvasTrafficGraph = memo(
}}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={toggleStyle}
/>
{/* 控制层覆盖 */}
@@ -962,8 +963,8 @@ export const EnhancedCanvasTrafficGraph = memo(
lineHeight: 1.2,
}}
>
Points: {displayData.length} | Fresh: {isDataFresh ? "✓" : "✗"} |
Compressed: {samplerStats.compressedBufferSize}
Points: {displayData.length} | Compressed:{" "}
{samplerStats.compressedBufferSize}
</Box>
{/* 悬浮提示框 */}
@@ -988,6 +989,7 @@ export const EnhancedCanvasTrafficGraph = memo(
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
backdropFilter: "none",
opacity: 1,
whiteSpace: "nowrap",
}}
>
<Box color="text.secondary" mb={0.2}>
@@ -7,7 +7,6 @@ import {
MemoryRounded,
} from "@mui/icons-material";
import {
Box,
Grid,
PaletteColor,
Paper,
@@ -15,16 +14,16 @@ import {
alpha,
useTheme,
} from "@mui/material";
import { ReactNode, memo, useCallback, useMemo, useRef } from "react";
import { useRef, memo, useMemo } from "react";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { useConnectionData } from "@/hooks/use-connection-data";
import { useMemoryData } from "@/hooks/use-memory-data";
import { useTrafficData } from "@/hooks/use-traffic-data";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-context";
import { gc, isDebugEnabled } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import {
@@ -148,51 +147,33 @@ export const EnhancedTrafficStats = () => {
const trafficRef = useRef<EnhancedCanvasTrafficGraphRef>(null);
const pageVisible = useVisibility();
// 使用AppDataProvider
const { connections } = useAppData();
const {
response: { data: traffic },
} = useTrafficData();
// 使用增强版的统一流量数据Hook
const { traffic, memory, isLoading, isDataFresh, hasValidData } =
useTrafficDataEnhanced();
const {
response: { data: memory },
} = useMemoryData();
const {
response: { data: connections },
} = useConnectionData();
// 是否显示流量图表
const trafficGraph = verge?.traffic_graph ?? true;
// 检查是否支持调试
// TODO: merge this hook with layout-traffic.tsx
const { data: isDebug } = useSWR(
`clash-verge-rev-internal://isDebugEnabled`,
() => isDebugEnabled(),
{
// default value before is fetched
fallbackData: false,
},
);
// Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点
// 执行垃圾回收
const handleGarbageCollection = useCallback(async () => {
if (isDebug) {
try {
await gc();
console.log("[Debug] 垃圾回收已执行");
} catch (err) {
console.error("[Debug] 垃圾回收失败:", err);
}
}
}, [isDebug]);
// 使用useMemo计算解析后的流量数据
const parsedData = useMemo(() => {
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
const [up, upUnit] = parseTraffic(traffic?.up || 0);
const [down, downUnit] = parseTraffic(traffic?.down || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
const [uploadTotal, uploadTotalUnit] = parseTraffic(
connections.uploadTotal,
connections?.uploadTotal,
);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
connections.downloadTotal,
connections?.downloadTotal,
);
return {
@@ -206,7 +187,7 @@ export const EnhancedTrafficStats = () => {
uploadTotalUnit,
downloadTotal,
downloadTotalUnit,
connectionsCount: connections.count,
connectionsCount: connections?.connections.length,
};
}, [traffic, memory, connections]);
@@ -228,33 +209,10 @@ export const EnhancedTrafficStats = () => {
>
<div style={{ height: "100%", position: "relative" }}>
<EnhancedCanvasTrafficGraph ref={trafficRef} />
{isDebug && (
<div
style={{
position: "absolute",
top: "2px",
left: "2px",
zIndex: 10,
backgroundColor: "rgba(0,0,0,0.5)",
color: "white",
fontSize: "8px",
padding: "2px 4px",
borderRadius: "4px",
}}
>
DEBUG: {trafficRef.current ? "图表已初始化" : "图表未初始化"}
<br />
: {isDataFresh ? "active" : "inactive"}
<br />
: {traffic?.is_fresh ? "Fresh" : "Stale"}
<br />
{new Date().toISOString().slice(11, 19)}
</div>
)}
</div>
</Paper>
);
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
}, [trafficGraph, pageVisible, theme.palette.divider]);
// 使用useMemo计算统计卡片配置
const statCards = useMemo(
@@ -300,10 +258,10 @@ export const EnhancedTrafficStats = () => {
value: parsedData.inuse,
unit: parsedData.inuseUnit,
color: "error" as const,
onClick: isDebug ? handleGarbageCollection : undefined,
onClick: undefined,
},
],
[t, parsedData, isDebug, handleGarbageCollection],
[t, parsedData],
);
return (
@@ -320,28 +278,11 @@ export const EnhancedTrafficStats = () => {
</Grid>
)}
{/* 统计卡片区域 */}
{statCards.map((card, index) => (
<Grid key={index} size={4}>
<CompactStatCard {...card} />
{statCards.map((card, _index) => (
<Grid key={card.title} size={4}>
<CompactStatCard {...(card as StatCardProps)} />
</Grid>
))}
{/* 数据状态指示器(调试用)*/}
{isDebug && (
<Grid size={12}>
<Box
sx={{
p: 1,
bgcolor: "action.hover",
borderRadius: 1,
fontSize: "0.75rem",
}}
>
: {isDataFresh ? "新鲜" : "过期"} | :{" "}
{hasValidData ? "是" : "否"} | : {isLoading ? "是" : "否"}
</Box>
</Grid>
)}
</Grid>
</TrafficErrorBoundary>
);
@@ -6,34 +6,19 @@ import {
import { Box, Typography } from "@mui/material";
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import { useClashInfo } from "@/hooks/use-clash";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { useMemoryData } from "@/hooks/use-memory-data";
import { useTrafficData } from "@/hooks/use-traffic-data";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
// setup the traffic
export const LayoutTraffic = () => {
const { data: isDebug } = useSWR(
"clash-verge-rev-internal://isDebugEnabled",
() => isDebugEnabled(),
{
// default value before is fetched
fallbackData: false,
},
);
if (isDebug) {
console.debug("[Traffic][LayoutTraffic] 组件正在渲染");
}
const { t } = useTranslation();
const { clashInfo } = useClashInfo();
const { verge } = useVerge();
// whether hide traffic graph
@@ -42,31 +27,19 @@ export const LayoutTraffic = () => {
const trafficRef = useRef<TrafficRef>(null);
const pageVisible = useVisibility();
// 使用增强版的统一流量数据Hook
const { traffic, memory } = useTrafficDataEnhanced();
// 启动流量服务
useEffect(() => {
console.log(
"[Traffic][LayoutTraffic] useEffect 触发,clashInfo:",
clashInfo,
"pageVisible:",
pageVisible,
);
// 简化条件,只要组件挂载就尝试启动服务
console.log("[Traffic][LayoutTraffic] 开始启动流量服务");
startTrafficService().catch((error) => {
console.error("[Traffic][LayoutTraffic] 启动流量服务失败:", error);
});
}, []); // 移除依赖,只在组件挂载时启动一次
const {
response: { data: traffic },
} = useTrafficData();
const {
response: { data: memory },
} = useMemoryData();
// 监听数据变化,为图表添加数据点
useEffect(() => {
if (traffic?.raw && trafficRef.current) {
if (trafficRef.current) {
trafficRef.current.appendData({
up: traffic.raw.up_rate || 0,
down: traffic.raw.down_rate || 0,
up: traffic?.up || 0,
down: traffic?.down || 0,
});
}
}, [traffic]);
@@ -75,9 +48,9 @@ export const LayoutTraffic = () => {
const displayMemory = verge?.enable_memory_usage ?? true;
// 使用parseTraffic统一处理转换,保持与首页一致的显示格式
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
const [up, upUnit] = parseTraffic(traffic?.up || 0);
const [down, downUnit] = parseTraffic(traffic?.down || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
const boxStyle: any = {
display: "flex",
@@ -114,18 +87,16 @@ export const LayoutTraffic = () => {
<Box display="flex" flexDirection="column" gap={0.75}>
<Box
title={`${t("Upload Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
title={`${t("Upload Speed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
opacity: traffic?.is_fresh ? 1 : 0.6,
// opacity: traffic?.is_fresh ? 1 : 0.6,
}}
>
<ArrowUpwardRounded
{...iconStyle}
color={
(traffic?.raw?.up_rate || 0) > 0 ? "secondary" : "disabled"
}
color={(traffic?.up || 0) > 0 ? "secondary" : "disabled"}
/>
<Typography {...valStyle} color="secondary">
{up}
@@ -134,18 +105,16 @@ export const LayoutTraffic = () => {
</Box>
<Box
title={`${t("Download Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
title={`${t("Download Speed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
opacity: traffic?.is_fresh ? 1 : 0.6,
// opacity: traffic?.is_fresh ? 1 : 0.6,
}}
>
<ArrowDownwardRounded
{...iconStyle}
color={
(traffic?.raw?.down_rate || 0) > 0 ? "primary" : "disabled"
}
color={(traffic?.down || 0) > 0 ? "primary" : "disabled"}
/>
<Typography {...valStyle} color="primary">
{down}
@@ -155,15 +124,15 @@ export const LayoutTraffic = () => {
{displayMemory && (
<Box
title={`${t(isDebug ? "Memory Cleanup" : "Memory Usage")} ${memory?.is_fresh ? "" : "(Stale)"} ${"usage_percent" in (memory?.formatted || {}) && memory.formatted.usage_percent ? `(${memory.formatted.usage_percent.toFixed(1)}%)` : ""}`}
title={`${t("Memory Usage")} `}
{...boxStyle}
sx={{
cursor: isDebug ? "pointer" : "auto",
opacity: memory?.is_fresh ? 1 : 0.6,
cursor: "auto",
// opacity: memory?.is_fresh ? 1 : 0.6,
}}
color={isDebug ? "success.main" : "disabled"}
color={"disabled"}
onClick={async () => {
isDebug && (await gc());
// isDebug && (await gc());
}}
>
<MemoryRounded {...iconStyle} />
@@ -53,6 +53,13 @@ export const useCustomTheme = () => {
return;
}
if (
typeof window !== "undefined" &&
typeof window.matchMedia === "function"
) {
return;
}
let isMounted = true;
const timerId = setTimeout(() => {
@@ -90,6 +97,44 @@ export const useCustomTheme = () => {
};
}, [theme_mode, appWindow, setMode]);
useEffect(() => {
if (theme_mode !== "system") {
return;
}
if (
typeof window === "undefined" ||
typeof window.matchMedia !== "function"
) {
return;
}
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const syncMode = (isDark: boolean) => setMode(isDark ? "dark" : "light");
const handleChange = (event: MediaQueryListEvent) =>
syncMode(event.matches);
syncMode(mediaQuery.matches);
if (typeof mediaQuery.addEventListener === "function") {
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}
type MediaQueryListLegacy = MediaQueryList & {
addListener?: (
listener: (this: MediaQueryList, event: MediaQueryListEvent) => void,
) => void;
removeListener?: (
listener: (this: MediaQueryList, event: MediaQueryListEvent) => void,
) => void;
};
const legacyQuery = mediaQuery as MediaQueryListLegacy;
legacyQuery.addListener?.(handleChange);
return () => legacyQuery.removeListener?.(handleChange);
}, [theme_mode, setMode]);
useEffect(() => {
if (theme_mode === undefined) {
return;
@@ -1,6 +1,11 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { RefreshRounded, DragIndicatorRounded } from "@mui/icons-material";
import {
RefreshRounded,
DragIndicatorRounded,
CheckBoxRounded,
CheckBoxOutlineBlankRounded,
} from "@mui/icons-material";
import {
Box,
Typography,
@@ -49,11 +54,24 @@ interface Props {
onEdit: () => void;
onSave?: (prev?: string, curr?: string) => void;
onDelete: () => void;
batchMode?: boolean;
isSelected?: boolean;
onSelectionChange?: () => void;
}
export const ProfileItem = (props: Props) => {
const { selected, activating, itemData, onSelect, onEdit, onSave, onDelete } =
props;
const {
selected,
activating,
itemData,
onSelect,
onEdit,
onSave,
onDelete,
batchMode,
isSelected,
onSelectionChange,
} = props;
const {
attributes,
listeners,
@@ -363,7 +381,12 @@ export const ProfileItem = (props: Props) => {
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
if (batchMode) {
// If in batch mode, just toggle selection instead of showing delete confirmation
onSelectionChange && onSelectionChange();
} else {
setConfirmOpen(true);
}
},
disabled: false,
},
@@ -402,7 +425,12 @@ export const ProfileItem = (props: Props) => {
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
if (batchMode) {
// If in batch mode, just toggle selection instead of showing delete confirmation
onSelectionChange && onSelectionChange();
} else {
setConfirmOpen(true);
}
},
disabled: false,
},
@@ -510,9 +538,29 @@ export const ProfileItem = (props: Props) => {
)}
<Box position="relative">
<Box sx={{ display: "flex", justifyContent: "start" }}>
{batchMode && (
<IconButton
size="small"
sx={{ padding: "2px", marginRight: "4px", marginLeft: "-8px" }}
onClick={(e) => {
e.stopPropagation();
onSelectionChange && onSelectionChange();
}}
>
{isSelected ? (
<CheckBoxRounded color="primary" />
) : (
<CheckBoxOutlineBlankRounded />
)}
</IconButton>
)}
<Box
ref={setNodeRef}
sx={{ display: "flex", margin: "auto 0" }}
sx={{
display: "flex",
margin: "auto 0",
...(batchMode && { marginLeft: "-4px" }),
}}
{...attributes}
{...listeners}
>
@@ -527,7 +575,7 @@ export const ProfileItem = (props: Props) => {
</Box>
<Typography
width="calc(100% - 36px)"
width={batchMode ? "calc(100% - 56px)" : "calc(100% - 36px)"}
sx={{ fontSize: "18px", fontWeight: "600", lineHeight: "26px" }}
variant="h6"
component="h2"
@@ -20,26 +20,12 @@ import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { updateProxyProvider } from "tauri-plugin-mihomo-api";
import { useAppData } from "@/providers/app-data-context";
import { proxyProviderUpdate } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import parseTraffic from "@/utils/parse-traffic";
// 定义代理提供者类型
interface ProxyProviderItem {
name?: string;
proxies: any[];
updatedAt: number;
vehicleType: string;
subscriptionInfo?: {
Upload: number;
Download: number;
Total: number;
Expire: number;
};
}
// 样式化组件 - 类型框
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
display: "inline-block",
@@ -74,7 +60,7 @@ export const ProviderButton = () => {
// 设置更新状态
setUpdating((prev) => ({ ...prev, [name]: true }));
await proxyProviderUpdate(name);
await updateProxyProvider(name);
// 刷新数据
await refreshProxy();
@@ -115,7 +101,7 @@ export const ProviderButton = () => {
// 改为串行逐个更新所有provider
for (const name of allProviders) {
try {
await proxyProviderUpdate(name);
await updateProxyProvider(name);
// 每个更新完成后更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) {
@@ -177,161 +163,164 @@ export const ProviderButton = () => {
<DialogContent>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(proxyProviders || {}).map(([key, item]) => {
const provider = item as ProxyProviderItem;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
{Object.entries(proxyProviders || {})
.sort()
.map(([key, item]) => {
const provider = item;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
// 订阅信息
const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub;
const upload = sub?.Upload || 0;
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
// 订阅信息
const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub;
const upload = sub?.Upload || 0;
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
// 流量使用进度
const progress =
total > 0
? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
// 流量使用进度
const progress =
total > 0
? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor =
mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
},
};
},
]}
>
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.proxies.length}
</TypeBox>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<>
{/* 订阅信息 */}
{hasSubInfo && (
<>
<Box
sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title={t("Expire Time") as string}>
{parseExpire(expire)}
</span>
</Box>
{/* 进度条 */}
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 6,
borderRadius: 3,
opacity: total > 0 ? 1 : 0,
}}
/>
</>
)}
</>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
},
};
},
]}
>
<IconButton
size="small"
color="primary"
onClick={() => {
updateProvider(key);
}}
disabled={isUpdating}
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.proxies.length}
</TypeBox>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<>
{/* 订阅信息 */}
{hasSubInfo && (
<>
<Box
sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title={t("Expire Time") as string}>
{parseExpire(expire)}
</span>
</Box>
{/* 进度条 */}
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 6,
borderRadius: 3,
opacity: total > 0 ? 1 : 0,
}}
/>
</>
)}
</>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
<IconButton
size="small"
color="primary"
onClick={() => {
updateProvider(key);
}}
disabled={isUpdating}
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
</List>
</DialogContent>
@@ -34,14 +34,13 @@ import {
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { useAppData } from "@/providers/app-data-context";
import {
closeAllConnections,
getProxies,
updateProxyAndSync,
updateProxyChainConfigInRuntime,
} from "@/services/cmds";
selectNodeForGroup,
} from "tauri-plugin-mihomo-api";
import { useAppData } from "@/providers/app-data-context";
import { calcuProxies, updateProxyChainConfigInRuntime } from "@/services/cmds";
interface ProxyChainItem {
id: string;
@@ -204,7 +203,7 @@ export const ProxyChain = ({
// 获取当前代理信息以检查连接状态
const { data: currentProxies, mutate: mutateProxies } = useSWR(
"getProxies",
getProxies,
calcuProxies,
{
revalidateOnFocus: true,
revalidateIfStale: true,
@@ -367,7 +366,7 @@ export const ProxyChain = ({
const targetGroup = mode === "global" ? "GLOBAL" : selectedGroup;
await updateProxyAndSync(targetGroup || "GLOBAL", lastNode.name);
await selectNodeForGroup(targetGroup || "GLOBAL", lastNode.name);
localStorage.setItem("proxy-chain-group", targetGroup || "GLOBAL");
localStorage.setItem("proxy-chain-exit-node", lastNode.name);
@@ -14,14 +14,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import useSWR from "swr";
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import {
getGroupProxyDelays,
getRuntimeConfig,
providerHealthCheck,
updateProxyChainConfigInRuntime,
} from "@/services/cmds";
import delayManager from "@/services/delay";
@@ -153,15 +152,14 @@ export const ProxyGroups = (props: Props) => {
// 添加和清理滚动事件监听器
useEffect(() => {
const currentScroller = scrollerRef.current;
if (currentScroller) {
currentScroller.addEventListener("scroll", handleScroll, {
passive: true,
});
return () => {
currentScroller.removeEventListener("scroll", handleScroll);
};
}
if (!scrollerRef.current) return;
scrollerRef.current.addEventListener("scroll", handleScroll, {
passive: true,
});
return () => {
scrollerRef.current?.removeEventListener("scroll", handleScroll);
};
}, [handleScroll]);
// 滚动到顶部
@@ -215,6 +213,7 @@ export const ProxyGroups = (props: Props) => {
const currentGroup = getCurrentGroup();
const availableGroups = getAvailableGroups();
// TODO: 频繁点击切换代理节点,导致应用卡死
const handleChangeProxy = useCallback(
(group: IProxyGroupItem, proxy: IProxyItem) => {
if (isChainMode) {
@@ -273,7 +272,7 @@ export const ProxyGroups = (props: Props) => {
if (providers.size) {
console.log(`[ProxyGroups] 发现提供者,数量: ${providers.size}`);
Promise.allSettled(
[...providers].map((p) => providerHealthCheck(p)),
[...providers].map((p) => healthcheckProxyProvider(p)),
).then(() => {
console.log(`[ProxyGroups] 提供者健康检查完成`);
onProxies();
@@ -289,7 +288,7 @@ export const ProxyGroups = (props: Props) => {
try {
await Promise.race([
delayManager.checkListDelay(names, groupName, timeout),
getGroupProxyDelays(groupName, url, timeout).then((result) => {
delayGroup(groupName, url, timeout).then((result) => {
console.log(
`[ProxyGroups] getGroupProxyDelays返回结果数量:`,
Object.keys(result || {}).length,
@@ -518,7 +517,7 @@ export const ProxyGroups = (props: Props) => {
},
}}
>
{availableGroups.map((group: any, index: number) => (
{availableGroups.map((group: any, _index: number) => (
<MenuItem
key={group.name}
onClick={() => handleGroupSelect(group.name)}
@@ -52,11 +52,13 @@ export const ProxyHead = ({
}, []);
const { verge } = useVerge();
const default_latency_test = verge!.default_latency_test!;
const defaultLatencyUrl =
verge?.default_latency_test?.trim() ||
"https://cp.cloudflare.com/generate_204";
useEffect(() => {
delayManager.setUrl(groupName, testUrl || url || default_latency_test);
}, [groupName, testUrl, default_latency_test, url]);
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl);
}, [groupName, testUrl, defaultLatencyUrl, url]);
return (
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, ...sx }}>

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