mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Sun Oct 12 20:34:11 CEST 2025
This commit is contained in:
@@ -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
|
||||
|
||||
Generated
+22
-22
@@ -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",
|
||||
|
||||
Generated
+37
-28
@@ -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
@@ -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 }}"
|
||||
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -9,6 +9,9 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
cargo-check:
|
||||
# Treat all Rust compiler warnings as errors
|
||||
|
||||
+1
@@ -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
@@ -7,6 +7,9 @@ name: Check Formatting
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -3,6 +3,8 @@ name: Clippy Lint
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -2,6 +2,9 @@ name: Updater CI
|
||||
|
||||
on: workflow_dispatch
|
||||
permissions: write-all
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
release-update:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,3 +6,5 @@ pnpm-lock.yaml
|
||||
|
||||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
|
||||
target
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"endOfLine": "lf",
|
||||
"endOfLine": "auto",
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+511
-281
File diff suppressed because it is too large
Load Diff
@@ -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}`,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Generated
+514
-340
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存客户端
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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(¤t);
|
||||
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(¤t)))
|
||||
.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,
|
||||
)
|
||||
}
|
||||
@@ -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" {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user