Compare commits

...
107 Commits
Author SHA1 Message Date
Tunglies 45fce7db73 feat: enhance autobuild workflow with pre-release tagging and promotion steps 2025-09-20 16:12:48 +08:00
Tunglies 1cf8e2384e fix: Cargo.toml dependencies option (#4805)
* chore: update cargo lock

* refactor: streamline dependency declarations in Cargo.toml

* refactor: reorder import statements and comment out Cargo.toml version update

* fix: enable Cargo version update in release script and update tauri-plugin-devtools dependency
2025-09-20 15:15:24 +08:00
Tunglies d9a5c11d6a refactor: improve code readability and consistency in proxy-chain and uri-parser utilities
refactor: add keys to icons in routers for improved rendering and performance
refactor: optimize RegExp polyfill by using Object.prototype.hasOwnProperty.call
refactor: reorder imports in chain-proxy-provider for consistency
refactor: remove unused "obfs-opts" property from IProxySnellConfig interface

refactor: reorganize imports and enhance refresh logic in app data provider

refactor: re-enable prop-types linting for better type safety in BaseDialog component

refactor: update dependencies in effect hooks for improved stability and performance
2025-09-20 11:19:36 +08:00
Tunglies 7811714f89 refactor: enhance logging system and add new development commands (#4803)
* refactor: enhance logging system and add new development commands

* refactor: add cfg-if dependency and improve logging configuration
2025-09-20 00:04:46 +08:00
Tunglies e869da8d4c refactor: remove unused HTTP-specific structs to streamline service code 2025-09-19 00:04:51 +08:00
Tunglies e414b49879 Refactor imports across multiple components for consistency and clarity
- Reorganized import statements in various components to ensure consistent ordering and grouping.
- Removed unnecessary imports and added missing ones where applicable.
- Improved readability and maintainability of the codebase by standardizing import styles.
2025-09-19 00:01:04 +08:00
Tunglies 627119bb22 Refactor imports and improve code organization across multiple components and hooks
- Consolidated and reordered imports in various files for better readability and maintainability.
- Removed unused imports and ensured consistent import styles.
- Enhanced the structure of components by grouping related imports together.
- Updated the layout and organization of hooks to streamline functionality.
- Improved the overall code quality by following best practices in import management.
2025-09-18 23:34:38 +08:00
Tunglies 74ade3ee41 refactor: update ESLint configuration and improve lint command with cache 2025-09-18 23:13:13 +08:00
Tunglies 324628dd3d refactor: replace 'let' with 'const' for better variable scoping and immutability 2025-09-18 23:07:18 +08:00
Tunglies 9d96ac0f6a feat: Integrate HTTP plugin and update IP detection to use fetch API #4712 2025-09-18 19:35:14 +08:00
Tunglies 409571f54b refactor: remove unused notification permission hook and related code 2025-09-18 19:13:23 +08:00
TianHua Liu a995a13163 chore: use jsx-runtime presets of eslint-plugin-react (#4794) 2025-09-18 19:04:12 +08:00
Tunglies 7848d6b1de refactor: window handle usage (#4788)
* refactor: Remove unused UI reset function and streamline window creation logic

* refactor: Remove debug print statements and streamline lightweight mode initialization

* fix: Ensure tray status refresh during silent startup and lightweight mode entry is independent of window creation

* refactor: Simplify window creation process and remove debug print statements
2025-09-18 10:22:43 +08:00
Tunglies 5d2e114b4d fix: Update tray menu event handling to toggle lightweight mode based on current state #4785 2025-09-18 00:46:49 +08:00
Tunglies c207516b47 refactor: clash-verge-service management (#4674)
* refactor: clash-verge-service management

* fix: correct service state checks in ProxyControlSwitches component
refactor: improve logging in service state update functions

* fix: add missing async handler for Windows and adjust logging import for macOS

* fix: streamline logging imports and add missing async handler for Windows

* refactor: remove unused useServiceStateSync hook and update imports in _layout

* refactor: remove unused useServiceStateSync import and clean up code in ProxyControlSwitches and _layout

* refactor: simplify service status checks and reduce wait time in useServiceInstaller hook

* refactor: remove unnecessary logging statements in service checks and IPC connection

* refactor: extract SwitchRow component for better code organization and readability

* refactor: enhance service state management and update related mutations in layout

* refactor: streamline core stopping logic and improve IPC connection logging

* refactor: consolidate service uninstallation logic and improve error handling

* fix: simplify conditional statements in CoreManager and service functions

* feat: add backoff dependency and implement retry strategy for IPC requests

* refactor: remove redundant Windows conditional and improve error handling in IPC tests

* test: improve error handling in IPC tests for message signing and verification

* fix: adjust IPC backoff retry parameters

* refactor: Remove service state tracking and related logic from service management

* feat: Enhance service status handling with logging and running mode updates

* fix: Improve service status handling with enhanced error logging

* fix: Ensure proper handling of service operations with error propagation

* refactor: Simplify service operation execution and enhance service status handling

* fix: Improve error message formatting in service operation execution and simplify service status retrieval

* refactor: Replace Cache with CacheProxy in multiple modules and update CacheEntry to be generic

* fix: Remove unnecessary success message from config validation

* refactor: Comment out logging statements in service version check and IPC request handling
2025-09-17 22:59:02 +08:00
Tunglies 6724f1ae35 feat: Implement caching mechanism with Cache struct and update related commands 2025-09-17 19:37:42 +08:00
Tunglies 1787d5372e fix: Update OS_PLATFORM definition to reflect the current platform 2025-09-17 16:10:22 +08:00
Tunglies 4c41144dd0 fix: Update error message to include details of accumulated startup errors 2025-09-17 16:10:01 +08:00
Tunglies 27636c848f fix: update changelog to reflect removal of hidden groups in tray node switching #4765 2025-09-17 13:39:38 +08:00
Tunglies 8060d699f0 fix: enhance prebuild script to support shorthand for force update #4777 2025-09-17 13:36:31 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f36f31a636 chore(deps): update npm dependencies (#4686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 12:45:57 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> d300fac3d9 chore(deps): update cargo dependencies (#4687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 12:45:50 +08:00
Thomas 8c2262dd95 stop showing hidden groups in the tray menu (#4765)
托盘菜单不显示隐藏代理组
2025-09-17 12:45:35 +08:00
wonfen c438e916ca perf: remove system-level unlock test timeout notice 2025-09-15 22:16:13 +08:00
ZShab NibaandTunglies 0855bd4896 fix: Fix icon to RGBA/sRGB Colorspace (#4753)
* fix: Fixed icon pixel size

* fix: fix icon to RGBA/sRGB Colorspace

* chore: update UPDATELOG

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-09-15 12:29:30 +08:00
Junkai W. f2073a2f83 Add Func 链式代理 (#4624)
* 添加链式代理gui和语言支持
在Iruntime中添跟新链式代理配置方法
同时添加了cmd

* 修复读取运行时代理链配置文件bug

* t

* 完成链式代理配置构造

* 修复获取链式代理运行时配置的bug

* 完整的链式代理功能
2025-09-15 07:44:54 +08:00
Tunglies a1f468202f Revert "fix: Fixed icon pixel size (#4698)"
revert due to runtime error [[Setup]] Error: failed to process image: Format error decoding Ico: The PNG is not in RGBA format!

This reverts commit a24bf4042c.
2025-09-15 00:56:59 +08:00
ZShab Niba a24bf4042c fix: Fixed icon pixel size (#4698) 2025-09-13 10:56:07 +08:00
❤是纱雾酱哟~ 15d22b4bf6 chore(issue template): disable blank issue template in GitHub config (#4731)
- Prevent users from creating issue without a template
- Always use a template for guiding users to provide necessary information for us

Signed-off-by: Dragon1573 <49941141+Dragon1573@users.noreply.github.com>
2025-09-13 10:11:40 +08:00
Tunglies 231d264652 fix: update alpha version description in README 2025-09-12 18:45:40 +08:00
Tunglies 67ac353fd5 chore: update cron schedule for auto build workflow 2025-09-12 18:43:13 +08:00
Tunglies 1c5534ad36 fix: remove deprecated updater for alpha endpoints in tauri configuration 2025-09-11 17:14:57 +08:00
Tunglies 251678493c edition 2024 (#4702)
* feat: update Cargo.toml for 2024 edition and optimize release profiles

* feat: refactor environment variable settings for Linux and improve code organization

* Refactor conditional statements to use `&&` for improved readability

- Updated multiple files to combine nested `if let` statements using `&&` for better clarity and conciseness.
- This change enhances the readability of the code by reducing indentation levels and making the conditions more straightforward.
- Affected files include: media_unlock_checker.rs, profile.rs, clash.rs, profiles.rs, async_proxy_query.rs, core.rs, handle.rs, hotkey.rs, service.rs, timer.rs, tray/mod.rs, merge.rs, seq.rs, config.rs, proxy.rs, window.rs, general.rs, dirs.rs, i18n.rs, init.rs, network.rs, and window.rs in the resolve module.

* refactor: streamline conditional checks using `&&` for improved readability

* fix: update release profile settings for panic behavior and optimization

* fix: adjust optimization level in Cargo.toml and reorder imports in lightweight.rs
2025-09-10 09:49:06 +08:00
Tunglies ccbffa14f0 fix: replace toggle with show for main window in lightweight mode exit #4697 2025-09-09 21:25:38 +08:00
Tunglies dfc1f736af fix: resolve from lightweight cause crash (#4682)
* refactor: streamline lightweight mode handling and improve window management

* refactor: replace mutex-based window creation lock with atomic operations for improved performance

* refactor: remove startup completed event handling and simplify initialization logic

* refactor: remove conditional compilation for emit_update_event function

* refactor: simplify return statements and clean up commented code in lightweight and window manager modules

* refactor: streamline lightweight mode handling by consolidating window management calls

* refactor: prevent unnecessary window toggle when exiting lightweight mode

* refactor: reorder imports for consistency in lightweight module

* refactor: move macOS specific logging_error import for clarity
2025-09-09 18:50:24 +08:00
Tunglies c54d89a465 feat: add support for Windows ARM64 in development workflow 2025-09-09 16:55:46 +08:00
Tunglies 55b95a1985 Revert "feat: update Cargo.toml for 2024 edition and optimize release profiles (#4681)"
This reverts commit 31e3104c7f.
2025-09-08 21:48:09 +08:00
Tunglies 31e3104c7f feat: update Cargo.toml for 2024 edition and optimize release profiles (#4681)
* feat: update Cargo.toml for 2024 edition and optimize release profiles

* feat: refactor environment variable settings for Linux and improve code organization

* Refactor conditional statements to use `&&` for improved readability

- Updated multiple files to combine nested `if let` statements using `&&` for better clarity and conciseness.
- This change enhances the readability of the code by reducing indentation levels and making the conditions more straightforward.
- Affected files include: media_unlock_checker.rs, profile.rs, clash.rs, profiles.rs, async_proxy_query.rs, core.rs, handle.rs, hotkey.rs, service.rs, timer.rs, tray/mod.rs, merge.rs, seq.rs, config.rs, proxy.rs, window.rs, general.rs, dirs.rs, i18n.rs, init.rs, network.rs, and window.rs in the resolve module.

* refactor: streamline conditional checks using `&&` for improved readability
2025-09-08 13:57:32 +08:00
wonfen 58a0089b19 fix: workflow file name 2025-09-07 15:24:14 +08:00
Tunglies 043ed4cb31 feat: add known issues section and update lightweight mode handling 2025-09-07 13:00:51 +08:00
Tunglies f64c01044c feat: bump version to 2.4.3 and update changelog for macOS intel Mihomo compatibility 2025-09-07 12:06:33 +08:00
Tunglies 5dca724017 feat: update Vite configuration for improved chunking and build options 2025-09-06 21:24:46 +08:00
Tunglies 579f9bd1f8 feat: add path and process dependencies; remove unused SCSS preprocessor options 2025-09-06 21:04:58 +08:00
Tunglies 7c9104a5b9 feat: optimize home page loading with lazy loading and improve card rendering logic 2025-09-06 20:38:33 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 14d1531469 chore(deps): update npm dependencies (#4567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 14:11:30 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 74e1e92607 chore(deps): update rust crate zip to v5 (#4653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 14:11:09 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f7a56c0eb3 chore(deps): update cargo dependencies (#4586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 14:10:35 +08:00
Tunglies e2fa76332a fix: remove unused ProxyRequestCache import and improve proxy update error handling 2025-09-06 14:10:09 +08:00
Tunglies 0daa8720cd feat: implement i18n lazy loading optimization
🚀 Performance improvements:
- Replace static language imports with dynamic imports
- Load only current language on startup instead of all 13 languages
- Implement on-demand loading when switching languages

📦 Bundle optimization:
- Reduce initial bundle size by avoiding preloading all language files
- Add resource caching to prevent reloading same language
- Support all 13 languages: en, ru, zh, fa, tt, id, ar, ko, tr, de, es, jp, zhtw

🔧 Technical changes:
- Convert i18n.ts to use dynamic import() for language resources
- Add async initializeLanguage() for app startup
- Create useI18n hook for language management with loading states
- Update main.tsx for async language initialization
- Fix language display labels in settings dropdown
- Maintain backward compatibility with existing language system

✅ Fixed issues:
- Resolve infinite loop in React components
- Fix missing language labels in settings UI
- Prevent circular dependencies in language loading
- Add proper error handling and fallback mechanisms
2025-09-06 14:05:36 +08:00
Tunglies f70b8b1213 Revert "fix: auto-detect KDE/Plasma and switch to X11 as backend to fix titlebar button freeze (#4523)"
This reverts commit d58c0a7df5.
2025-09-06 11:59:33 +08:00
wonfen feb3dfbe86 chore: use UTF+8 time to build 2025-09-05 11:21:34 +08:00
Tunglies f38e4a6cac fix: refactor proxy fetching to use command methods and improve error handling 2025-09-04 15:32:46 +08:00
Tunglies 893188d693 fix: add basic authorization header support for URL parsing in NetworkManager #4618 2025-09-03 01:07:45 +08:00
Tunglies b989aeb7b0 refactor: clean up imports and remove unused initialization in NetworkManager 2025-09-02 23:48:27 +08:00
Tunglies 40f87c834d fix: update ClientConfig settings for improved connection management 2025-09-02 23:21:05 +08:00
Tunglies 0bb9cb5097 fix: enhance startup speed and fix connection issues during initialization 2025-09-02 23:10:02 +08:00
Tunglies b51797e238 fix: update logging types and clean up ProxyRequestCache usage 2025-09-02 22:19:22 +08:00
Tunglies 926c095409 perf: update ProxyRequestCache to use boxed CacheEntry for improved memory management 2025-09-02 19:09:44 +08:00
Tunglies 0c65f8ebad fix: remove macOS specific conditional compilation for logging_error import 2025-09-02 18:36:13 +08:00
Tunglies 63f4295063 fix: update required service version to 1.1.2 2025-09-02 16:09:21 +08:00
Tunglies d2b38a8a3c fix: optimize async handler usage in singleton checks and resource initialization #4576, #4590, #4609 2025-09-02 13:37:14 +08:00
Tunglies 45ddb15d56 fix: remove redundant service stop call in CoreManager and clean up unused Mutex import 2025-09-02 12:12:19 +08:00
Tunglies 7aef9d2a5a fix: resolve lightweight mode state detection issues and improve logging #3814 2025-09-02 08:00:53 +08:00
wonfen 45fdebeaca style: simplify and improve proxy settings UI 2025-09-01 13:57:04 +08:00
wonfen 0ea875f7f7 fix: unify homepage node selection 2025-09-01 11:30:27 +08:00
Guanghui Qinandhiaoxui 1b54c9fc1b fix rpm/deb package naming issues (#4582)
* fix arm64 deb package name

* fix rpm package naming issue

* fix rpm package naming issue

---------

Co-authored-by: hiaoxui <hiaoxui@gmail.com>
2025-09-01 01:12:40 +08:00
Tunglies 89f3adcbef fix: add redirect policy to HTTP client builder 2025-09-01 00:45:39 +08:00
Tunglies b13fef5ad9 fix: add missing allow(dead_code) attribute to block_on method 2025-08-31 16:22:35 +08:00
Tunglies 9110955b63 fix: streamline service availability checks and improve logging for core startup 2025-08-31 16:19:31 +08:00
Tunglies 4508d062f1 chore: bump version to 2.4.2 2025-08-31 15:53:50 +08:00
Junkai W. bea0dde074 Win 下添加代理节点的系统托盘 (#4562)
* add proxy memu in tray

* 添加win下系统托盘 节点
代理->代理组->nodes
同时添加了对应gui同步

* 添加win 系统托盘显示代理节点
且gui和托盘刷新机制

* rust format

* 添加 win下系统托盘节点延迟

* Squashed commit of the following:

commit 44caaa62c5
Merge: 1916e539 3939741a
Author: Junkai W. <129588175+Be-Forever223@users.noreply.github.com>
Date:   Sat Aug 30 02:37:07 2025 +0800

    Merge branch 'dev' into dev

commit 3939741a06
Author: Tunglies <tunglies.dev@outlook.com>
Date:   Sat Aug 30 02:24:47 2025 +0800

    refactor: migrate from serde_yaml to serde_yaml_ng for improved YAML handling (#4568)

    * refactor: migrate from serde_yaml to serde_yaml_ng for improved YAML handling

    * refactor: format code for better readability in DNS configuration

commit f86a1816e0
Author: Tunglies <tunglies.dev@outlook.com>
Date:   Sat Aug 30 02:15:34 2025 +0800

    chore(deps): update sysinfo to 0.37.0 and zip to 4.5.0 in Cargo.toml (#4564)

    * chore(deps): update sysinfo to 0.37.0 and zip to 4.5.0 in Cargo.toml

    * chore(deps): remove libnghttp2-sys dependency and update isahc features in Cargo.toml

    * chore(deps): remove sysinfo and zip from ignoreDeps in renovate.json

commit 9cbd8b4529
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Sat Aug 30 01:30:48 2025 +0800

    feat: add x86 OpenSSL installation step for macOS in workflows

commit 5dea73fc2a
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Sat Aug 30 01:21:53 2025 +0800

    chore(deps): update npm dependencies (#4542)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 01af1bea23
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Sat Aug 30 01:21:46 2025 +0800

    chore(deps): update rust crate reqwest_dav to 0.2.2 (#4554)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit 1227e86134
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Sat Aug 30 01:12:03 2025 +0800

    Remove unnecessary "rustls-tls" feature from reqwest dependency in Cargo.toml

commit c6a6ea48dd
Author: Tunglies <tunglies.dev@outlook.com>
Date:   Fri Aug 29 23:51:09 2025 +0800

    refactor: enhance async initialization and streamline setup process (#4560)

    * feat: Implement DNS management for macOS

    - Added `set_public_dns` and `restore_public_dns` functions in `dns.rs` to manage system DNS settings.
    - Introduced `resolve` module to encapsulate DNS and scheme resolution functionalities.
    - Implemented `resolve_scheme` function in `scheme.rs` to handle deep links and profile imports.
    - Created UI readiness management in `ui.rs` to track and update UI loading states.
    - Developed window management logic in `window.rs` to handle window creation and visibility.
    - Added initial loading overlay script in `window_script.rs` for better user experience during startup.
    - Updated server handling in `server.rs` to integrate new resolve functionalities.
    - Refactored window creation calls in `window_manager.rs` to use the new window management logic.

    * refactor: streamline asynchronous handling in config and resolve setup

    * Revert "refactor: streamline asynchronous handling in config and resolve setup"

    This reverts commit 23d7dc86d5.

    * fix: optimize asynchronous memory handling

    * fix: enhance task logging by adding size check for special cases

    * refactor: enhance async initialization and streamline setup process

    * refactor: optimize async setup by consolidating initialization tasks

    * chore: update changelog for Mihomo(Meta) kernel upgrade to v1.19.13

    * fix: improve startup phase initialization performance

    * refactor: optimize file read/write performance to reduce application wait time

    * refactor: simplify app instance exit logic and adjust system proxy guard initialization

    * refactor: change resolve_setup_async to synchronous execution for improved performance

    * refactor: update resolve_setup_async to accept AppHandle for improved initialization flow

    * refactor: remove unnecessary initialization of portable flag in run function

    * refactor: consolidate async initialization tasks into a single blocking call for improved execution flow

    * refactor: optimize resolve_setup_async by restructuring async tasks for improved concurrency

    * refactor: streamline resolve_setup_async and embed_server for improved async handling

    * refactor: separate synchronous and asynchronous setup functions for improved clarity

    * refactor: simplify async notification handling and remove redundant network manager initialization

    * refactor: enhance async handling in proxy request cache and window creation logic

    * refactor: improve code formatting and readability in ProxyRequestCache

    * refactor: adjust singleton check timeout and optimize trace size conditions

    * refactor: update TRACE_SPECIAL_SIZE to include additional size condition

    * refactor: update kode-bridge dependency to version 0.2.1-rc2

    * refactor: replace RwLock with AtomicBool for UI readiness and implement event-driven monitoring

    * refactor: convert async functions to synchronous for window management

    * Update src-tauri/src/utils/resolve/window.rs

    * fix: handle missing app_handle in create_window function

    * Update src-tauri/src/module/lightweight.rs

* format
2025-08-31 14:20:57 +08:00
wonfen 3e674b186f fix: refine release workflow 2025-08-31 08:49:30 +08:00
Tunglies 92d9c94e87 fix: resolve crashes when exiting lightweight mode by ensuring async window operations 2025-08-30 20:04:21 +08:00
Tunglies c09066c0a3 refactor: restructure async initialization and standardize logging system
### Major Improvements

- **Async initialization refactoring**: Complete async migration of init_config, improving app startup performance and stability
  - Change init_work_config from blocking to async execution
  - Optimize error handling for directory creation and config file initialization
  - Enhance structure and robustness of initialization process

- **Logging system standardization**: Unify usage of project's built-in logging! macro
  - Replace all log::info!/warn!/error!/debug! with logging!(level, Type::Setup, true, ...) format
  - Maintain consistency in log categorization and formatting
  - Improve convenience for log tracking and debugging

### Technical Optimizations

- **Error handling improvements**: Remove crate::log_err! macro, use standard Result error propagation
- **Directory management optimization**: Refactor ensure_directories function with clearer directory creation logic
- **Config initialization enhancement**: Separate initialize_config_files function for better code maintainability
- **Async task management**: Use AsyncHandler::spawn to optimize background log cleanup tasks

### Bug Fixes

- Fix potential race conditions in async config initialization
- Improve error feedback and logging during app startup
- Enhance error handling for DNS config and resource file initialization

### Updates

- Update wording in UPDATELOG.md issue descriptions
2025-08-30 17:58:26 +08:00
Tunglies 3a7be3dfb7 refactor: streamline resolve_scheme function calls and visibility in utils 2025-08-30 17:22:52 +08:00
Tunglies 09f14c23e4 fix: update required service version to 1.1.1 2025-08-30 11:02:58 +08:00
Tunglies eaaae3b393 fix: improve stability during profile switching to prevent crashes 2025-08-30 07:40:31 +08:00
Tunglies 3939741a06 refactor: migrate from serde_yaml to serde_yaml_ng for improved YAML handling (#4568)
* refactor: migrate from serde_yaml to serde_yaml_ng for improved YAML handling

* refactor: format code for better readability in DNS configuration
2025-08-30 02:24:47 +08:00
Tunglies f86a1816e0 chore(deps): update sysinfo to 0.37.0 and zip to 4.5.0 in Cargo.toml (#4564)
* chore(deps): update sysinfo to 0.37.0 and zip to 4.5.0 in Cargo.toml

* chore(deps): remove libnghttp2-sys dependency and update isahc features in Cargo.toml

* chore(deps): remove sysinfo and zip from ignoreDeps in renovate.json
2025-08-30 02:15:34 +08:00
Tunglies 9cbd8b4529 feat: add x86 OpenSSL installation step for macOS in workflows 2025-08-30 01:31:02 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 5dea73fc2a chore(deps): update npm dependencies (#4542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-30 01:21:53 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 01af1bea23 chore(deps): update rust crate reqwest_dav to 0.2.2 (#4554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-30 01:21:46 +08:00
Tunglies 1227e86134 Remove unnecessary "rustls-tls" feature from reqwest dependency in Cargo.toml 2025-08-30 01:12:03 +08:00
Tunglies c6a6ea48dd refactor: enhance async initialization and streamline setup process (#4560)
* feat: Implement DNS management for macOS

- Added `set_public_dns` and `restore_public_dns` functions in `dns.rs` to manage system DNS settings.
- Introduced `resolve` module to encapsulate DNS and scheme resolution functionalities.
- Implemented `resolve_scheme` function in `scheme.rs` to handle deep links and profile imports.
- Created UI readiness management in `ui.rs` to track and update UI loading states.
- Developed window management logic in `window.rs` to handle window creation and visibility.
- Added initial loading overlay script in `window_script.rs` for better user experience during startup.
- Updated server handling in `server.rs` to integrate new resolve functionalities.
- Refactored window creation calls in `window_manager.rs` to use the new window management logic.

* refactor: streamline asynchronous handling in config and resolve setup

* Revert "refactor: streamline asynchronous handling in config and resolve setup"

This reverts commit 23d7dc86d5.

* fix: optimize asynchronous memory handling

* fix: enhance task logging by adding size check for special cases

* refactor: enhance async initialization and streamline setup process

* refactor: optimize async setup by consolidating initialization tasks

* chore: update changelog for Mihomo(Meta) kernel upgrade to v1.19.13

* fix: improve startup phase initialization performance

* refactor: optimize file read/write performance to reduce application wait time

* refactor: simplify app instance exit logic and adjust system proxy guard initialization

* refactor: change resolve_setup_async to synchronous execution for improved performance

* refactor: update resolve_setup_async to accept AppHandle for improved initialization flow

* refactor: remove unnecessary initialization of portable flag in run function

* refactor: consolidate async initialization tasks into a single blocking call for improved execution flow

* refactor: optimize resolve_setup_async by restructuring async tasks for improved concurrency

* refactor: streamline resolve_setup_async and embed_server for improved async handling

* refactor: separate synchronous and asynchronous setup functions for improved clarity

* refactor: simplify async notification handling and remove redundant network manager initialization

* refactor: enhance async handling in proxy request cache and window creation logic

* refactor: improve code formatting and readability in ProxyRequestCache

* refactor: adjust singleton check timeout and optimize trace size conditions

* refactor: update TRACE_SPECIAL_SIZE to include additional size condition

* refactor: update kode-bridge dependency to version 0.2.1-rc2

* refactor: replace RwLock with AtomicBool for UI readiness and implement event-driven monitoring

* refactor: convert async functions to synchronous for window management

* Update src-tauri/src/utils/resolve/window.rs

* fix: handle missing app_handle in create_window function

* Update src-tauri/src/module/lightweight.rs
2025-08-29 23:57:42 +08:00
wonfen 2080dbdc0f refactor: proxy control component and system settings UI
fix: handle tun toggle state after service uninstall
2025-08-29 20:46:45 +08:00
Tunglies 6eecd70bd5 fix(subscription): resolve issues causing import failures in some cases #4534, #4436, #4552, #4519, #4517, #4503, #4336, #4301 (#4553)
* fix(subscription): resolve issues causing import failures in some cases #4534, #4436, #4552, #4519, #4517, #4503, #4336, #4301

* fix(profile): update profile creation to include file data handling

* fix(app): improve singleton instance exit handling

* fix: remove unsued handle method
2025-08-29 17:46:46 +08:00
Tunglies a9951e4eca refactor: replace AppHandleManager with handle::Handle for macOS activation policy management 2025-08-28 17:27:54 +08:00
Tunglies 53688f332f fix: replace tokio::runtime::Handle with tauri::async_runtime::handle 2025-08-28 04:58:24 +08:00
Tunglies d23d1d9a1d fix: remove auto clean up profiles behavior in resolve process 2025-08-28 04:41:12 +08:00
Tunglies 51ff9e1851 fix: resolve issue with application not restoring after tray restart 2025-08-27 22:20:04 +08:00
Tunglies 824814da56 fix: unexpected restart behavior #4438 2025-08-27 22:04:44 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 040fcd059f chore(deps): update npm dependencies (#4467)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 21:19:22 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f2339620a5 chore(deps): update cargo dependencies (#4468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 21:19:13 +08:00
Sergey Kharenko d58c0a7df5 fix: auto-detect KDE/Plasma and switch to X11 as backend to fix titlebar button freeze (#4523) 2025-08-26 01:50:44 +08:00
Tunglies 355a18e5eb refactor(async): migrate from sync-blocking async execution to true async with unified AsyncHandler::spawn (#4502)
* feat: replace all tokio::spawn with unified AsyncHandler::spawn

- 🚀 Core Improvements:
  * Replace all tokio::spawn calls with AsyncHandler::spawn for unified Tauri async task management
  * Prioritize converting sync functions to async functions to reduce spawn usage
  * Use .await directly in async contexts instead of spawn

- 🔧 Major Changes:
  * core/hotkey.rs: Use AsyncHandler::spawn for hotkey callback functions
  * module/lightweight.rs: Async lightweight mode switching
  * feat/window.rs: Convert window operation functions to async, use .await internally
  * feat/proxy.rs, feat/clash.rs: Async proxy and mode switching functions
  * lib.rs: Window focus handling with AsyncHandler::spawn
  * core/tray/mod.rs: Complete async tray event handling

- ✨ Technical Advantages:
  * Unified task tracking and debugging capabilities (via tokio-trace feature)
  * Better error handling and task management
  * Consistency with Tauri runtime
  * Reduced async boundaries for better performance

- 🧪 Verification:
  * Compilation successful with 0 errors, 0 warnings
  * Maintains complete original functionality
  * Optimized async execution flow

* feat: complete tokio fs migration and replace tokio::spawn with AsyncHandler

🚀 Major achievements:
- Migrate 8 core modules from std::fs to tokio::fs
- Create 6 Send-safe wrapper functions using spawn_blocking pattern
- Replace all tokio::spawn calls with AsyncHandler::spawn for unified async task management
- Solve all 19 Send trait compilation errors through innovative spawn_blocking architecture

🔧 Core changes:
- config/profiles.rs: Add profiles_*_safe functions to handle Send trait constraints
- cmd/profile.rs: Update all Tauri commands to use Send-safe operations
- config/prfitem.rs: Replace append_item calls with profiles_append_item_safe
- utils/help.rs: Convert YAML operations to async (read_yaml, save_yaml)
- Multiple modules: Replace tokio::task::spawn_blocking with AsyncHandler::spawn_blocking

✅ Technical innovations:
- spawn_blocking wrapper pattern resolves parking_lot RwLock Send trait conflicts
- Maintain parking_lot performance while achieving Tauri async command compatibility
- Preserve backwards compatibility with gradual migration strategy

🎯 Results:
- Zero compilation errors
- Zero warnings
- All async file operations working correctly
- Complete Send trait compliance for Tauri commands

* feat: refactor app handle and command functions to use async/await for improved performance

* feat: update async handling in profiles and logging functions for improved error handling and performance

* fix: update TRACE_MINI_SIZE constant to improve task logging threshold

* fix(windows): convert service management functions to async for improved performance

* fix: convert service management functions to async for improved responsiveness

* fix(ubuntu): convert install and reinstall service functions to async for improved performance

* fix(linux): convert uninstall_service function to async for improved performance

* fix: convert uninstall_service call to async for improved performance

* fix: convert file and directory creation calls to async for improved performance

* fix: convert hotkey functions to async for improved responsiveness

* chore: update UPDATELOG.md for v2.4.1 with major improvements and performance optimizations
2025-08-26 01:49:51 +08:00
Tunglies 4598c805eb refactor: remove Oxlint workflow and update linting to use ESLint 2025-08-24 16:40:05 +08:00
Tunglies aa204649fa fix: add web asset build step before running Clippy 2025-08-24 16:38:17 +08:00
Tunglies fbaff3e90c fix: remove logLevel parameter from fetchLogsViaIPCPeriodically for consistency 2025-08-23 00:28:39 +08:00
Tunglies 0d070fb934 refactor: update AppHandle usage to use Arc<AppHandle> for improved memory management (#4491)
* refactor: update AppHandle usage to use Arc<AppHandle> for improved memory management

* fix: clippy ci

* fix: ensure default_latency_test is safely accessed with non-null assertion
2025-08-23 00:20:58 +08:00
Tunglies c416bd5755 fix: reorder pnpm installation step in Clippy workflow 2025-08-23 00:15:25 +08:00
Tunglies 90406ae883 fix: clippy ci 2025-08-23 00:11:44 +08:00
wonfen 600b3dfbac fix: release workflow & refine telegram notify format 2025-08-22 20:47:02 +08:00
Tunglies 72e4491dc4 Implement code changes to enhance functionality and improve performance 2025-08-22 19:56:51 +08:00
Tunglies 76c3695567 feat: add Clippy and Oxlint workflows for enhanced linting 2025-08-22 19:20:35 +08:00
Tunglies 475a09bb54 feat: comprehensive oxlint cleanup - remove unused code
🧹 Cleanup Summary:
- Fixed 83 oxlint warnings across 50+ files
- Removed unused imports, variables, and functions
- Maintained all functional code and error handling
- Improved bundle size and code maintainability

📝 Key Changes:
- Cleaned unused React hooks (useState, useEffect, useClashInfo)
- Removed unused Material-UI imports (useTheme, styled components)
- Deleted unused interfaces and type definitions
- Fixed spread operator usage and boolean casting
- Simplified catch parameters where appropriate

🎯 Files Modified:
- React components: home.tsx, settings, profiles, etc.
- Custom hooks: use-*.ts files
- Utility functions and type definitions
- Configuration files

✅ Result: 0 oxlint warnings (from 83 warnings)
🔧 All functionality preserved
📦 Reduced bundle size through dead code elimination
2025-08-22 18:48:56 +08:00
Tunglies 6a1fce69e0 refactor: comment out includeUpdaterJson in autobuild jobs for clarity 2025-08-22 17:41:33 +08:00
Tunglies 485fd0169b chore: bump version to 2.4.1 2025-08-22 17:31:32 +08:00
245 changed files with 12640 additions and 9396 deletions
+1
View File
@@ -1,3 +1,4 @@
blank_issues_enabled: false
contact_links: contact_links:
- name: 讨论交流 / Communication - name: 讨论交流 / Communication
url: https://t.me/clash_verge_rev url: https://t.me/clash_verge_rev
+9
View File
@@ -294,6 +294,15 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install x86 OpenSSL (macOS only)
if: matrix.target == 'x86_64-apple-darwin'
run: |
arch -x86_64 brew install openssl@3
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
+75 -23
View File
@@ -3,12 +3,14 @@ name: Auto Build
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
# UTC+8 0,6,12,18 # UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
- cron: "0 16,22,4,10 * * *" - cron: "0 4,10 * * *"
permissions: write-all permissions: write-all
env: env:
TAG_NAME: autobuild TAG_NAME: autobuild
TAG_CHANNEL: AutoBuild TAG_CHANNEL: AutoBuild
PRE_TAG_NAME: pre-autobuild
PRE_TAG_CHANNEL: PreAutoBuild
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short RUST_BACKTRACE: short
concurrency: concurrency:
@@ -71,7 +73,7 @@ jobs:
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
VERSION=$(jq -r .version package.json) VERSION=$(jq -r .version package.json)
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/${{ env.PRE_TAG_NAME }}" >> $GITHUB_ENV
shell: bash shell: bash
- run: | - run: |
@@ -99,7 +101,7 @@ jobs:
### Linux ### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装 #### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装 #### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
@@ -113,11 +115,11 @@ jobs:
Created at ${{ env.BUILDTIME }}. Created at ${{ env.BUILDTIME }}.
EOF EOF
- name: Upload Release - name: Upload Pre-Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
body_path: release.txt body_path: release.txt
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@@ -130,7 +132,7 @@ jobs:
uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev
with: with:
tag_name: autobuild tag_name: pre-autobuild
dry_run: false dry_run: false
autobuild-x86-windows-macos-linux: autobuild-x86-windows-macos-linux:
@@ -176,6 +178,15 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install x86 OpenSSL (macOS only)
if: matrix.target == 'x86_64-apple-darwin'
run: |
arch -x86_64 brew install openssl@3
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
name: Install pnpm name: Install pnpm
with: with:
@@ -192,7 +203,7 @@ jobs:
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Tauri build for Windows-macOS-Linux - name: Tauri build for Windows-macOS-Linux
@@ -209,14 +220,13 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with: with:
tagName: ${{ env.TAG_NAME }} tagName: ${{ env.PRE_TAG_NAME }}
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" releaseName: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
releaseBody: "More new features are now supported." releaseBody: "More new features are now supported."
releaseDraft: false releaseDraft: false
prerelease: true prerelease: true
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
includeUpdaterJson: true
autobuild-arm-linux: autobuild-arm-linux:
name: Autobuild ARM Linux name: Autobuild ARM Linux
@@ -267,7 +277,7 @@ jobs:
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Setup for linux - name: Setup for linux
@@ -342,11 +352,11 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release - name: Upload Pre-Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: | files: |
@@ -399,7 +409,7 @@ jobs:
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.PRE_TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Download WebView2 Runtime - name: Download WebView2 Runtime
@@ -420,7 +430,6 @@ jobs:
with: with:
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
includeUpdaterJson: true
- name: Rename - name: Rename
run: | run: |
@@ -442,20 +451,62 @@ jobs:
Rename-Item $file.FullName $newName Rename-Item $file.FullName $newName
} }
- name: Upload Release - name: Upload Pre-Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.PRE_TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}" name: "Clash Verge Rev ${{ env.PRE_TAG_CHANNEL }}"
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
- name: Portable Bundle - name: Portable Bundle
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }} run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.PRE_TAG_NAME }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
promote-autobuild:
name: Promote PreAutoBuild to AutoBuild
runs-on: ubuntu-latest
needs:
[
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
]
if: >
${{
needs.autobuild-x86-windows-macos-linux.result == 'success' &&
needs.autobuild-arm-linux.result == 'success' &&
needs.autobuild-x86-arm-windows_webview2.result == 'success'
}}
steps:
- name: Install gh CLI
run: |
sudo apt-get update
sudo apt-get install -y gh jq
- name: Download PreAutoBuild Release Assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir pre-assets
release_id=$(gh api repos/${{ github.repository }}/releases/tags/${{ env.PRE_TAG_NAME }} | jq .id)
gh release download ${{ env.PRE_TAG_NAME }} --dir pre-assets
body=$(gh api repos/${{ github.repository }}/releases/$release_id | jq -r .body)
echo "$body" > release.txt
- name: Publish to autobuild tag
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: pre-assets/*
notify-telegram: notify-telegram:
name: Notify Telegram name: Notify Telegram
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -465,6 +516,7 @@ jobs:
autobuild-x86-windows-macos-linux, autobuild-x86-windows-macos-linux,
autobuild-arm-linux, autobuild-arm-linux,
autobuild-x86-arm-windows_webview2, autobuild-x86-arm-windows_webview2,
promote-autobuild,
] ]
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -538,7 +590,7 @@ jobs:
### Linux ### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装 #### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装 #### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
@@ -104,8 +104,8 @@ jobs:
# Generate autobuild version using autobuild-latest format # Generate autobuild version using autobuild-latest format
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g') CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
MONTH=$(date +%m) MONTH=$(TZ=Asia/Shanghai date +%m)
DAY=$(date +%d) DAY=$(TZ=Asia/Shanghai date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}" AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Autobuild version: $AUTOBUILD_VERSION" echo "🏷️ Autobuild version: $AUTOBUILD_VERSION"
+2 -2
View File
@@ -88,8 +88,8 @@ jobs:
# Generate autobuild version for consistency # Generate autobuild version for consistency
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g') CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
MONTH=$(date +%m) MONTH=$(TZ=Asia/Shanghai date +%m)
DAY=$(date +%d) DAY=$(TZ=Asia/Shanghai date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}" AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION" echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION"
+10
View File
@@ -13,6 +13,11 @@ on:
required: false required: false
type: boolean type: boolean
default: true default: true
run_windows_arm64:
description: "运行 Windows ARM64"
required: false
type: boolean
default: true
permissions: write-all permissions: write-all
env: env:
@@ -40,6 +45,11 @@ jobs:
bundle: dmg bundle: dmg
id: macos-aarch64 id: macos-aarch64
input: run_macos_aarch64 input: run_macos_aarch64
- os: windows-latest
target: aarch64-pc-windows-msvc
bundle: nsis
id: windows-arm64
input: run_windows_arm64
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@@ -42,21 +42,25 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
run_install: false run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
# This workflow runs linting using cargo clippy.
# Note: If the web build step is skipped,
# cargo clippy will fail to run due to missing web dist in the Tauri environment.
- name: Build Web Assets - name: Build Web Assets
run: pnpm run web:build run: pnpm run web:build
env: env:
+23 -9
View File
@@ -122,10 +122,10 @@ jobs:
### Linux ### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装 #### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装 #### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
### FAQ ### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html) - [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -141,6 +141,7 @@ jobs:
tag_name: ${{ env.TAG_NAME }} tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_NAME }}" name: "Clash Verge Rev ${{ env.TAG_NAME }}"
body_path: release.txt body_path: release.txt
draft: false
prerelease: false prerelease: false
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
# generate_release_notes: true # generate_release_notes: true
@@ -186,6 +187,15 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install x86 OpenSSL (macOS only)
if: matrix.target == 'x86_64-apple-darwin'
run: |
arch -x86_64 brew install openssl@3
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@@ -224,8 +234,6 @@ jobs:
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
includeUpdaterJson: true includeUpdaterJson: true
release-for-linux-arm: release-for-linux-arm:
name: Release Build for Linux ARM name: Release Build for Linux ARM
needs: [check_tag_version] needs: [check_tag_version]
@@ -447,7 +455,7 @@ jobs:
release-update: release-update:
name: Release Update name: Release Update
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [release, release-for-linux-arm] needs: [update_tag]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -472,7 +480,7 @@ jobs:
release-update-for-fixed-webview2: release-update-for-fixed-webview2:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [release-for-fixed-webview2] needs: [update_tag]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -521,7 +529,13 @@ jobs:
notify-telegram: notify-telegram:
name: Notify Telegram name: Notify Telegram
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [update_tag, release-update, release-update-for-fixed-webview2, submit-to-winget] needs:
[
update_tag,
release-update,
release-update-for-fixed-webview2,
submit-to-winget,
]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -591,10 +605,10 @@ jobs:
### Linux ### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装 #### DEB包(Debian系) 使用 apt ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
#### RPM包(Redhat系) 使用 dnf ./路径 安装 #### RPM包(Redhat系) 使用 dnf ./路径 安装
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm) - [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
### FAQ ### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html) - [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
+1
View File
@@ -10,3 +10,4 @@ scripts/_env.sh
.tool-versions .tool-versions
.idea .idea
.old .old
.eslintcache
+5 -2
View File
@@ -59,8 +59,9 @@ You have two options for downloading the clash binary:
```shell ```shell
pnpm run prebuild pnpm run prebuild
# Use '--force' to force update to the latest version # Use '--force' or '-f' to update both the Mihomo core version
# pnpm run prebuild --force # and the Clash Verge Rev service version to the latest available.
pnpm run prebuild --force
``` ```
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin). - Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
@@ -73,6 +74,8 @@ To run the development server, use the following command:
pnpm dev pnpm dev
# If an app instance already exists, use a different command # If an app instance already exists, use a different command
pnpm dev:diff pnpm dev:diff
# To using tauri built-in dev tool
pnpm dev:tauri
``` ```
### Build the Project ### Build the Project
+5 -5
View File
@@ -23,11 +23,11 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
#### 我应当怎样选择发行版 #### 我应当怎样选择发行版
| 版本 | 特征 | 链接 | | 版本 | 特征 | 链接 |
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- | | :---------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) | | Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) | | Alpha(废弃) | 测试发布流程。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) | | AutoBuild | 滚动更新版,适合测试反馈,可能存在缺陷。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看 #### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
+87
View File
@@ -1,3 +1,90 @@
## v2.4.3
### 🚀 优化改进
- 重构并简化服务模式启动检测流程,消除重复检测
- 重构并简化窗口创建流程
- 重构日志系统,单个日志默认最大 10 MB
- 优化前端资源占用
### 🐞 修复问题
- 优化服务模式重装逻辑,避免不必要的重复检查
- 修复轻量模式退出无响应的问题
- 修复托盘轻量模式支持退出/进入
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
- macOS Tun/系统代理 模式下图标大小不统一
- 托盘节点切换不再显示隐藏组
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
## v2.4.2
### ✨ 新增功能
- 增加托盘节点选择
### 🚀 性能优化
- 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存
- 优化后端内存占用
- 优化后端启动速度
### 🐞 修复问题
- 修复首页节点切换失效的问题
- 修复和优化服务检查流程
- 修复2.4.1引入的订阅地址重定向报错问题
- 修复 rpm/deb 包名称问题
- 修复托盘轻量模式状态检测异常
- 修复通过 scheme 导入订阅崩溃
- 修复单例检测实效
- 修复启动阶段可能导致的无法连接内核
- 修复导入订阅无法 Auth Basic
### 👙 界面样式
- 简化和改进代理设置样式
## v2.4.1
### 🏆 重大改进
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.13**
### 🚀 性能优化
- 优化热键响应速度,提升快捷键操作体验
- 改进服务管理响应性,减少系统服务操作等待时间
- 提升文件和配置处理性能
- 优化任务管理和日志记录效率
- 优化异步内存管理,减少内存占用并提升多任务处理效率
- 优化启动阶段初始化性能
### 🐞 修复问题
- 修复应用在某些操作中可能出现的响应延迟问题
- 修复任务管理中的潜在并发问题
- 修复通过托盘重启应用无法恢复
- 修复订阅在某些情况下无法导入
- 修复无法新建订阅时使用远程链接
- 修复卸载服务后的 tun 开关状态问题
- 修复页面快速切换订阅时导致崩溃
- 修复丢失工作目录时无法恢复环境
- 修复从轻量模式恢复导致崩溃
### 👙 界面样式
- 统一代理设置样式
### 🗑️ 移除内容
- 移除启动阶段自动清理过期订阅
## v2.4.0 ## v2.4.0
**发行代号:融** **发行代号:融**
+106
View File
@@ -0,0 +1,106 @@
import js from "@eslint/js";
import configPrettier from "eslint-config-prettier";
import pluginImport from "eslint-plugin-import";
import pluginPrettier from "eslint-plugin-prettier";
import pluginReact from "eslint-plugin-react";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReactRefresh from "eslint-plugin-react-refresh";
import pluginUnusedImports from "eslint-plugin-unused-imports";
import { defineConfig } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
plugins: {
js,
react: pluginReact,
"react-hooks": pluginReactHooks,
import: pluginImport,
"react-refresh": pluginReactRefresh,
"unused-imports": pluginUnusedImports,
prettier: pluginPrettier,
},
extends: [
"js/recommended",
tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
pluginReact.configs.flat["jsx-runtime"],
configPrettier,
],
languageOptions: {
globals: globals.browser,
},
settings: {
react: {
version: "detect",
},
"import/resolver": {
typescript: {
project: "./tsconfig.json",
},
},
},
rules: {
// React
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
// TypeScript
"@typescript-eslint/no-explicit-any": "off",
// unused-imports 代替 no-unused-vars
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
vars: "all",
varsIgnorePattern: "^_+$",
args: "after-used",
argsIgnorePattern: "^_+$",
},
],
// Import
"import/no-unresolved": "error",
"import/order": [
"warn",
{
groups: [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
],
"newlines-between": "always",
alphabetize: {
order: "asc",
caseInsensitive: true,
},
},
],
// 其他常见
"prefer-const": "warn",
"no-case-declarations": "error",
"no-fallthrough": "error",
"no-empty": ["warn", { allowEmptyCatch: true }],
// Prettier 格式化问题
"prettier/prettier": "warn",
},
},
]);
+46 -39
View File
@@ -1,11 +1,12 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "2.4.0", "version": "2.4.3",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "dev:diff": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=1 RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace", "dev:trace": "cross-env RUST_BACKTRACE=full RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
"dev:tauri": "cross-env RUST_BACKTRACE=full tauri dev -f tauri-dev",
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build", "build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release", "build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
"tauri": "tauri", "tauri": "tauri",
@@ -24,6 +25,7 @@
"publish-version": "node scripts/publish-version.mjs", "publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml", "fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml", "clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint -c eslint.config.ts --cache src",
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check ." "format:check": "prettier --check ."
}, },
@@ -34,76 +36,81 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.3.1", "@mui/icons-material": "^7.3.2",
"@mui/lab": "7.0.0-beta.16", "@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.1", "@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.10.2", "@mui/x-data-grid": "^8.11.2",
"@tauri-apps/api": "2.8.0", "@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.3.3", "@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-global-shortcut": "^2.3.0", "@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-notification": "^2.3.0",
"@tauri-apps/plugin-process": "^2.3.0", "@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.0", "@tauri-apps/plugin-shell": "2.3.1",
"@tauri-apps/plugin-updater": "2.9.0", "@tauri-apps/plugin-updater": "2.9.0",
"@tauri-apps/plugin-window-state": "^2.4.0",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.9.3", "ahooks": "^3.9.5",
"axios": "^1.11.0", "axios": "^1.12.2",
"chart.js": "^4.5.0", "dayjs": "1.11.18",
"cli-color": "^2.0.4",
"dayjs": "1.11.13",
"foxact": "^0.2.49", "foxact": "^0.2.49",
"glob": "^11.0.3", "i18next": "^25.5.2",
"i18next": "^25.4.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.53.0",
"monaco-yaml": "^5.4.0", "monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"react": "19.1.1", "react": "19.1.1",
"react-beautiful-dnd": "^13.1.1",
"react-chartjs-2": "^5.3.0",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"react-error-boundary": "6.0.0", "react-error-boundary": "6.0.0",
"react-hook-form": "^7.62.0", "react-hook-form": "^7.62.0",
"react-i18next": "15.7.0", "react-i18next": "15.7.3",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0", "react-monaco-editor": "0.59.0",
"react-router-dom": "7.8.1", "react-router-dom": "7.9.1",
"react-virtuoso": "^4.14.0", "react-virtuoso": "^4.14.0",
"sockette": "^2.0.6",
"swr": "^2.3.6", "swr": "^2.3.6",
"tar": "^7.4.3",
"types-pac": "^1.0.3", "types-pac": "^1.0.3",
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
"@tauri-apps/cli": "2.8.1", "@eslint/js": "^9.35.0",
"@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/react": "19.1.10", "@types/react": "19.1.13",
"@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "19.1.9",
"@types/react-dom": "19.1.7",
"@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.0.1", "@vitejs/plugin-react": "5.0.2",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"commander": "^14.0.0", "cli-color": "^2.0.4",
"commander": "^14.0.1",
"cross-env": "^10.0.0", "cross-env": "^10.0.0",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-unused-imports": "^4.2.0",
"glob": "^11.0.3",
"globals": "^16.4.0",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"meta-json-schema": "^1.19.12", "jiti": "^2.5.1",
"meta-json-schema": "^1.19.13",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.2.0", "sass": "^1.92.1",
"sass": "^1.90.0", "tar": "^7.4.3",
"terser": "^5.43.1", "terser": "^5.44.0",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"vite": "^7.1.3", "typescript-eslint": "^8.44.0",
"vite": "^7.1.5",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.0" "vite-plugin-svgr": "^4.5.0"
}, },
"type": "module", "type": "module",
"packageManager": "pnpm@9.13.2" "packageManager": "pnpm@9.13.2"
+2709 -516
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -38,5 +38,5 @@
} }
], ],
"postUpdateOptions": ["pnpmDedupe"], "postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["serde_yaml", "sysinfo", "zip", "criterion"] "ignoreDeps": ["criterion"]
} }
+4 -3
View File
@@ -12,7 +12,7 @@ import { glob } from "glob";
const cwd = process.cwd(); const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge"); const TEMP_DIR = path.join(cwd, "node_modules/.verge");
const FORCE = process.argv.includes("--force"); const FORCE = process.argv.includes("--force") || process.argv.includes("-f");
const PLATFORM_MAP = { const PLATFORM_MAP = {
"x86_64-pc-windows-msvc": "win32", "x86_64-pc-windows-msvc": "win32",
@@ -43,7 +43,8 @@ const ARCH_MAP = {
const arg1 = process.argv.slice(2)[0]; const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1]; const arg2 = process.argv.slice(2)[1];
const target = arg1 === "--force" ? arg2 : arg1; let target;
target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
const { platform, arch } = target const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] } ? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process; : process;
@@ -64,7 +65,7 @@ const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64-v2", "win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386", "win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64", "win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-v2", "darwin-x64": "mihomo-darwin-amd64-v1",
"darwin-arm64": "mihomo-darwin-arm64", "darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-v2", "linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386", "linux-ia32": "mihomo-linux-386",
+1 -1
View File
@@ -54,7 +54,7 @@ async function run() {
execSync(`git tag ${tag}`, { stdio: "inherit" }); execSync(`git tag ${tag}`, { stdio: "inherit" });
execSync(`git push origin ${tag}`, { stdio: "inherit" }); execSync(`git push origin ${tag}`, { stdio: "inherit" });
console.log(`[INFO]: Git tag ${tag} created and pushed.`); console.log(`[INFO]: Git tag ${tag} created and pushed.`);
} catch (e) { } catch {
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`); console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
process.exit(1); process.exit(1);
} }
+17 -7
View File
@@ -29,10 +29,10 @@
* Errors are logged and the process exits with code 1 on failure. * Errors are logged and the process exits with code 1 on failure.
*/ */
import { execSync } from "child_process";
import { program } from "commander";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
import { program } from "commander";
import { execSync } from "child_process";
/** /**
* 获取当前 git 短 commit hash * 获取当前 git 短 commit hash
@@ -41,7 +41,7 @@ import { execSync } from "child_process";
function getGitShortCommit() { function getGitShortCommit() {
try { try {
return execSync("git rev-parse --short HEAD").toString().trim(); return execSync("git rev-parse --short HEAD").toString().trim();
} catch (e) { } catch {
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'"); console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
return "nogit"; return "nogit";
} }
@@ -59,7 +59,7 @@ function getLatestTauriCommit() {
.toString() .toString()
.trim(); .trim();
return execSync(`git rev-parse --short ${fullHash}`).toString().trim(); return execSync(`git rev-parse --short ${fullHash}`).toString().trim();
} catch (e) { } catch {
console.warn( console.warn(
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit", "[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
); );
@@ -68,14 +68,24 @@ function getLatestTauriCommit() {
} }
/** /**
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27) * 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
* 使用 Asia/Shanghai 时区
* @param {boolean} withCommit 是否带 commit * @param {boolean} withCommit 是否带 commit
* @returns {string} * @returns {string}
*/ */
function generateShortTimestamp(withCommit = false) { function generateShortTimestamp(withCommit = false) {
const now = new Date(); const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0"); const formatter = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Shanghai",
month: "2-digit",
day: "2-digit",
});
const parts = formatter.formatToParts(now);
const month = parts.find((part) => part.type === "month").value;
const day = parts.find((part) => part.type === "day").value;
if (withCommit) { if (withCommit) {
const gitShort = getGitShortCommit(); const gitShort = getGitShortCommit();
return `${month}${day}.${gitShort}`; return `${month}${day}.${gitShort}`;
+10 -5
View File
@@ -36,7 +36,7 @@ async function sendTelegramNotification() {
releaseContent = readFileSync("release.txt", "utf-8"); releaseContent = readFileSync("release.txt", "utf-8");
log_info("成功读取 release.txt 文件"); log_info("成功读取 release.txt 文件");
} catch (error) { } catch (error) {
log_error("无法读取 release.txt,使用默认发布说明"); log_error("无法读取 release.txt,使用默认发布说明", error);
releaseContent = "更多新功能现已支持,详细更新日志请查看发布页面。"; releaseContent = "更多新功能现已支持,详细更新日志请查看发布页面。";
} }
@@ -56,7 +56,10 @@ async function sendTelegramNotification() {
} else { } else {
let processedLine = line.replace( let processedLine = line.replace(
/\[([^\]]+)\]\(([^)]+)\)/g, /\[([^\]]+)\]\(([^)]+)\)/g,
'<a href="$2">$1</a>', (match, text, url) => {
const encodedUrl = encodeURI(url);
return `<a href="${encodedUrl}">${text}</a>`;
},
); );
processedLine = processedLine.replace( processedLine = processedLine.replace(
/\*\*([^*]+)\*\*/g, /\*\*([^*]+)\*\*/g,
@@ -71,7 +74,8 @@ async function sendTelegramNotification() {
const formattedContent = convertMarkdownToTelegramHTML(releaseContent); const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布"; const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${version}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`; const encodedVersion = encodeURIComponent(version);
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
// 发送到 Telegram // 发送到 Telegram
try { try {
@@ -82,7 +86,7 @@ async function sendTelegramNotification() {
text: content, text: content,
link_preview_options: { link_preview_options: {
is_disabled: false, is_disabled: false,
url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${version}`, url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}`,
prefer_large_media: true, prefer_large_media: true,
}, },
parse_mode: "HTML", parse_mode: "HTML",
@@ -93,6 +97,7 @@ async function sendTelegramNotification() {
log_error( log_error(
`❌ Telegram 通知发送失败到 ${chatId}:`, `❌ Telegram 通知发送失败到 ${chatId}:`,
error.response?.data || error.message, error.response?.data || error.message,
error,
); );
process.exit(1); process.exit(1);
} }
@@ -100,6 +105,6 @@ async function sendTelegramNotification() {
// 执行函数 // 执行函数
sendTelegramNotification().catch((error) => { sendTelegramNotification().catch((error) => {
log_error("脚本执行失败:", error.message); log_error("脚本执行失败:", error);
process.exit(1); process.exit(1);
}); });
+2 -2
View File
@@ -8,7 +8,7 @@ const UPDATE_LOG = "UPDATELOG.md";
export async function resolveUpdateLog(tag) { export async function resolveUpdateLog(tag) {
const cwd = process.cwd(); const cwd = process.cwd();
const reTitle = /^## v[\d\.]+/; const reTitle = /^## v[\d.]+/;
const reEnd = /^---/; const reEnd = /^---/;
const file = path.join(cwd, UPDATE_LOG); const file = path.join(cwd, UPDATE_LOG);
@@ -54,7 +54,7 @@ export async function resolveUpdateLogDefault() {
const data = await fsp.readFile(file, "utf-8"); const data = await fsp.readFile(file, "utf-8");
const reTitle = /^## v[\d\.]+/; const reTitle = /^## v[\d.]+/;
const reEnd = /^---/; const reEnd = /^---/;
let isCapturing = false; let isCapturing = false;
+1052 -825
View File
File diff suppressed because it is too large Load Diff
+53 -37
View File
@@ -1,50 +1,49 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "2.4.0" version = "2.4.3"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"] authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git" repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge" default-run = "clash-verge"
edition = "2021" edition = "2024"
build = "build.rs" build = "build.rs"
[package.metadata.bundle] [package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev" identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.4.0", features = [] } tauri-build = { version = "2.4.1", features = [] }
[dependencies] [dependencies]
warp = { version = "0.4.2", features = ["server"] } warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.99" anyhow = "1.0.99"
dirs = "6.0" dirs = "6.0"
open = "5.3.2" open = "5.3.2"
log = "0.4.27" log = "0.4.28"
dunce = "1.0.5" dunce = "1.0.5"
log4rs = "1.3.0"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4.41" chrono = "0.4.42"
sysinfo = { version = "=0.35.2", features = ["network", "system"] } sysinfo = { version = "0.37.0", features = ["network", "system"] }
boa_engine = "0.20.0" boa_engine = "0.20.0"
serde_json = "1.0.143" serde_json = "1.0.145"
serde_yaml = "0.9.34" serde_yaml_ng = "0.10.0"
once_cell = "1.21.3" once_cell = "1.21.3"
port_scanner = "0.1.5" port_scanner = "0.1.5"
delay_timer = "0.11.6" delay_timer = "0.11.6"
parking_lot = "0.12.4" parking_lot = "0.12.4"
percent-encoding = "2.3.1" percent-encoding = "2.3.2"
tokio = { version = "1.47.1", features = [ tokio = { version = "1.47.1", features = [
"rt-multi-thread", "rt-multi-thread",
"macros", "macros",
"time", "time",
"sync", "sync",
] } ] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.225", features = ["derive"] }
reqwest = { version = "0.12.23", features = ["json", "rustls-tls", "cookies"] } reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.1" regex = "1.11.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
tauri = { version = "2.8.2", features = [ tauri = { version = "2.8.5", features = [
"protocol-asset", "protocol-asset",
"devtools", "devtools",
"tray-icon", "tray-icon",
@@ -52,16 +51,15 @@ tauri = { version = "2.8.2", features = [
"image-png", "image-png",
] } ] }
network-interface = { version = "2.0.3", features = ["serde"] } network-interface = { version = "2.0.3", features = ["serde"] }
tauri-plugin-shell = "2.3.0" tauri-plugin-shell = "2.3.1"
tauri-plugin-dialog = "2.3.3" tauri-plugin-dialog = "2.4.0"
tauri-plugin-fs = "2.4.2" tauri-plugin-fs = "2.4.2"
tauri-plugin-process = "2.3.0" tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0" tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.1" tauri-plugin-deep-link = "2.4.3"
tauri-plugin-devtools = "2.0.1"
tauri-plugin-window-state = "2.4.0" tauri-plugin-window-state = "2.4.0"
zip = "=4.2.0" zip = "5.1.1"
reqwest_dav = "0.2.1" reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] } aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1" base64 = "0.22.1"
getrandom = "0.3.3" getrandom = "0.3.3"
@@ -73,11 +71,22 @@ hmac = "0.12.1"
sha2 = "0.10.9" sha2 = "0.10.9"
hex = "0.4.3" hex = "0.4.3"
scopeguard = "1.2.0" scopeguard = "1.2.0"
kode-bridge = "0.2.1-rc1" kode-bridge = "0.2.1"
dashmap = "6.1.0" dashmap = "6.1.0"
tauri-plugin-notification = "2.3.0" tauri-plugin-notification = "2.3.1"
console-subscriber = { version = "0.4.1", optional = true }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
"parking_lot",
] }
backoff = { version = "0.4.0", features = ["tokio"] }
tauri-plugin-http = "2"
flexi_logger = "0.31.2"
cfg-if = "1.0.3"
nu-ansi-term = { version = "0.50.1", optional = true }
console-subscriber = { version = "0.4.1", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"
@@ -108,31 +117,38 @@ tauri-plugin-updater = "2.9.0"
[features] [features]
default = ["custom-protocol"] default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
verge-dev = [] verge-dev = ["nu-ansi-term"]
tauri-dev = []
tokio-trace = ["console-subscriber"] tokio-trace = ["console-subscriber"]
[profile.release] [profile.release]
panic = "abort" panic = "abort"
codegen-units = 1 codegen-units = 16
lto = true lto = true
opt-level = "s" opt-level = 2
debug = false
strip = true strip = true
overflow-checks = false
rpath = false
[profile.dev] [profile.dev]
incremental = true incremental = true
codegen-units = 256 # 增加编译单元,提升编译速度 codegen-units = 64
opt-level = 0 # 禁用优化,进一步提升编译速度 opt-level = 0
debug = true # 保留调试信息 debug = true
strip = false # 不剥离符号,保留调试信息 strip = "none"
overflow-checks = true
lto = false
rpath = false
[profile.fast-release] [profile.fast-release]
inherits = "release" # 继承 release 的配置 inherits = "release"
panic = "abort" # 与 release 相同 incremental = true
codegen-units = 256 # 增加编译单元,提升编译速度 codegen-units = 64
lto = false # 禁用 LTO,提升编译速度 lto = false
opt-level = 0 # 禁用优化,大幅提升编译速度 opt-level = 0
debug = true # 保留调试信息 debug = true
strip = false # 不剥离符号,保留调试信息 strip = false
[lib] [lib]
name = "app_lib" name = "app_lib"
+1 -1
View File
@@ -1,4 +1,4 @@
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{Criterion, criterion_group, criterion_main};
use std::hint::black_box; use std::hint::black_box;
// 业务模型 & Draft // 业务模型 & Draft
+7 -1
View File
@@ -18,6 +18,12 @@
"autostart:allow-disable", "autostart:allow-disable",
"autostart:allow-is-enabled", "autostart:allow-is-enabled",
"core:window:allow-set-theme", "core:window:allow-set-theme",
"notification:default" "notification:default",
"http:default",
"http:allow-fetch",
{
"identifier": "http:default",
"allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }]
}
] ]
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+111
View File
@@ -0,0 +1,111 @@
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);
+11 -19
View File
@@ -4,18 +4,18 @@ use crate::{
utils::{dirs, logging::Type}, utils::{dirs, logging::Type},
wrap_err, wrap_err,
}; };
use tauri::Manager; use tauri::{AppHandle, Manager};
/// 打开应用程序所在目录 /// 打开应用程序所在目录
#[tauri::command] #[tauri::command]
pub fn open_app_dir() -> CmdResult<()> { pub async fn open_app_dir() -> CmdResult<()> {
let app_dir = wrap_err!(dirs::app_home_dir())?; let app_dir = wrap_err!(dirs::app_home_dir())?;
wrap_err!(open::that(app_dir)) wrap_err!(open::that(app_dir))
} }
/// 打开核心所在目录 /// 打开核心所在目录
#[tauri::command] #[tauri::command]
pub fn open_core_dir() -> CmdResult<()> { pub async fn open_core_dir() -> CmdResult<()> {
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?; let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
let core_dir = core_dir.parent().ok_or("failed to get core dir")?; let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
wrap_err!(open::that(core_dir)) wrap_err!(open::that(core_dir))
@@ -23,7 +23,7 @@ pub fn open_core_dir() -> CmdResult<()> {
/// 打开日志目录 /// 打开日志目录
#[tauri::command] #[tauri::command]
pub fn open_logs_dir() -> CmdResult<()> { pub async fn open_logs_dir() -> CmdResult<()> {
let log_dir = wrap_err!(dirs::app_logs_dir())?; let log_dir = wrap_err!(dirs::app_logs_dir())?;
wrap_err!(open::that(log_dir)) wrap_err!(open::that(log_dir))
} }
@@ -36,7 +36,7 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
/// 打开/关闭开发者工具 /// 打开/关闭开发者工具
#[tauri::command] #[tauri::command]
pub fn open_devtools(app_handle: tauri::AppHandle) { pub fn open_devtools(app_handle: AppHandle) {
if let Some(window) = app_handle.get_webview_window("main") { if let Some(window) = app_handle.get_webview_window("main") {
if !window.is_devtools_open() { if !window.is_devtools_open() {
window.open_devtools(); window.open_devtools();
@@ -48,14 +48,14 @@ pub fn open_devtools(app_handle: tauri::AppHandle) {
/// 退出应用 /// 退出应用
#[tauri::command] #[tauri::command]
pub fn exit_app() { pub async fn exit_app() {
feat::quit(); feat::quit().await;
} }
/// 重启应用 /// 重启应用
#[tauri::command] #[tauri::command]
pub async fn restart_app() -> CmdResult<()> { pub async fn restart_app() -> CmdResult<()> {
feat::restart_app(); feat::restart_app().await;
Ok(()) Ok(())
} }
@@ -209,7 +209,7 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
#[tauri::command] #[tauri::command]
pub fn notify_ui_ready() -> CmdResult<()> { pub fn notify_ui_ready() -> CmdResult<()> {
log::info!(target: "app", "前端UI已准备就绪"); log::info!(target: "app", "前端UI已准备就绪");
crate::utils::resolve::mark_ui_ready(); crate::utils::resolve::ui::mark_ui_ready();
Ok(()) Ok(())
} }
@@ -218,7 +218,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
pub fn update_ui_stage(stage: String) -> CmdResult<()> { pub fn update_ui_stage(stage: String) -> CmdResult<()> {
log::info!(target: "app", "UI加载阶段更新: {stage}"); log::info!(target: "app", "UI加载阶段更新: {stage}");
use crate::utils::resolve::UiReadyStage; use crate::utils::resolve::ui::UiReadyStage;
let stage_enum = match stage.as_str() { let stage_enum = match stage.as_str() {
"NotStarted" => UiReadyStage::NotStarted, "NotStarted" => UiReadyStage::NotStarted,
@@ -232,14 +232,6 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
} }
}; };
crate::utils::resolve::update_ui_ready_stage(stage_enum); crate::utils::resolve::ui::update_ui_ready_stage(stage_enum);
Ok(())
}
/// 重置UI就绪状态
#[tauri::command]
pub fn reset_ui_ready_state() -> CmdResult<()> {
log::info!(target: "app", "重置UI就绪状态");
crate::utils::resolve::reset_ui_ready();
Ok(()) Ok(())
} }
+113 -119
View File
@@ -1,31 +1,33 @@
use super::CmdResult; use super::CmdResult;
use crate::{
cache::CacheProxy,
config::Config,
core::{CoreManager, handle},
};
use crate::{ use crate::{
config::*, config::*,
core::*,
feat, feat,
ipc::{self, IpcManager}, ipc::{self, IpcManager},
logging, logging,
process::AsyncHandler,
state::proxy::ProxyRequestCache,
utils::logging::Type, utils::logging::Type,
wrap_err, wrap_err,
}; };
use serde_yaml::Mapping; 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环境变量 /// 复制Clash环境变量
#[tauri::command] #[tauri::command]
pub fn copy_clash_env() -> CmdResult { pub async fn copy_clash_env() -> CmdResult {
feat::copy_clash_env(); feat::copy_clash_env().await;
Ok(()) Ok(())
} }
/// 获取Clash信息 /// 获取Clash信息
#[tauri::command] #[tauri::command]
pub fn get_clash_info() -> CmdResult<ClashInfo> { pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().latest_ref().get_client_info()) Ok(Config::clash().await.latest_ref().get_client_info())
} }
/// 修改Clash配置 /// 修改Clash配置
@@ -37,7 +39,7 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
/// 修改Clash模式 /// 修改Clash模式
#[tauri::command] #[tauri::command]
pub async fn patch_clash_mode(payload: String) -> CmdResult { pub async fn patch_clash_mode(payload: String) -> CmdResult {
feat::change_clash_mode(payload); feat::change_clash_mode(payload).await;
Ok(()) Ok(())
} }
@@ -127,15 +129,22 @@ pub async fn clash_api_get_proxy_delay(
/// 测试URL延迟 /// 测试URL延迟
#[tauri::command] #[tauri::command]
pub async fn test_delay(url: String) -> CmdResult<u32> { pub async fn test_delay(url: String) -> CmdResult<u32> {
Ok(feat::test_delay(url).await.unwrap_or(10000u32)) let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
log::error!(target: "app", "{}", e);
10000u32
}
};
Ok(result)
} }
/// 保存DNS配置到单独文件 /// 保存DNS配置到单独文件
#[tauri::command] #[tauri::command]
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
use crate::utils::dirs; use crate::utils::dirs;
use serde_yaml; use serde_yaml_ng;
use std::fs; use tokio::fs;
// 获取DNS配置文件路径 // 获取DNS配置文件路径
let dns_path = dirs::app_home_dir() let dns_path = dirs::app_home_dir()
@@ -143,8 +152,10 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
.join("dns_config.yaml"); .join("dns_config.yaml");
// 保存DNS配置到文件 // 保存DNS配置到文件
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?; let yaml_str = serde_yaml_ng::to_string(&dns_config).map_err(|e| e.to_string())?;
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?; fs::write(&dns_path, yaml_str)
.await
.map_err(|e| e.to_string())?;
logging!(info, Type::Config, "DNS config saved to {dns_path:?}"); logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
Ok(()) Ok(())
@@ -152,111 +163,92 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
/// 应用或撤销DNS配置 /// 应用或撤销DNS配置
#[tauri::command] #[tauri::command]
pub fn apply_dns_config(apply: bool) -> CmdResult { pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{ use crate::{
config::Config, config::Config,
core::{handle, CoreManager}, core::{CoreManager, handle},
utils::dirs, utils::dirs,
}; };
// 使用spawn来处理异步操作 if apply {
AsyncHandler::spawn(move || async move { // 读取DNS配置文件
if apply { let dns_path = dirs::app_home_dir()
// 读取DNS配置文件 .map_err(|e| e.to_string())?
let dns_path = match dirs::app_home_dir() { .join("dns_config.yaml");
Ok(path) => path.join("dns_config.yaml"),
Err(e) => {
logging!(error, Type::Config, "Failed to get home dir: {e}");
return;
}
};
if !dns_path.exists() { if !dns_path.exists() {
logging!(warn, Type::Config, "DNS config file not found"); logging!(warn, Type::Config, "DNS config file not found");
return; return Err("DNS config file not found".into());
}
let dns_yaml = match std::fs::read_to_string(&dns_path) {
Ok(content) => content,
Err(e) => {
logging!(error, Type::Config, "Failed to read DNS config: {e}");
return;
}
};
// 解析DNS配置并创建patch
let patch_config = match serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
Ok(config) => {
let mut patch = serde_yaml::Mapping::new();
patch.insert("dns".into(), config.into());
patch
}
Err(e) => {
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
return;
}
};
logging!(info, Type::Config, "Applying DNS config from file");
// 重新生成配置,确保DNS配置被正确应用
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
Config::runtime()
.draft_mut()
.patch_config(patch_config.clone());
// 首先重新生成配置
if let Err(err) = Config::generate() {
logging!(
error,
Type::Config,
"Failed to regenerate config with DNS: {err}"
);
return;
}
// 然后应用新配置
if let Err(err) = CoreManager::global().update_config().await {
logging!(
error,
Type::Config,
"Failed to apply config with DNS: {err}"
);
} else {
logging!(info, Type::Config, "DNS config successfully applied");
handle::Handle::refresh_clash();
}
} else {
// 当关闭DNS设置时,不需要对配置进行任何修改
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
logging!(
info,
Type::Config,
"DNS settings disabled, regenerating config"
);
// 重新生成配置
if let Err(err) = Config::generate() {
logging!(error, Type::Config, "Failed to regenerate config: {err}");
return;
}
// 应用新配置
match CoreManager::global().update_config().await {
Ok(_) => {
logging!(info, Type::Config, "Config regenerated successfully");
handle::Handle::refresh_clash();
}
Err(err) => {
logging!(
error,
Type::Config,
"Failed to apply regenerated config: {err}"
);
}
}
} }
});
let dns_yaml = tokio::fs::read_to_string(&dns_path).await.map_err(|e| {
logging!(error, Type::Config, "Failed to read DNS config: {e}");
e.to_string()
})?;
// 解析DNS配置
let patch_config =
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).map_err(|e| {
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
e.to_string()
})?;
logging!(info, Type::Config, "Applying DNS config from file");
// 创建包含DNS配置的patch
let mut patch = serde_yaml_ng::Mapping::new();
patch.insert("dns".into(), patch_config.into());
// 应用DNS配置到运行时配置
Config::runtime().await.draft_mut().patch_config(patch);
// 重新生成配置
Config::generate().await.map_err(|err| {
logging!(
error,
Type::Config,
"Failed to regenerate config with DNS: {err}"
);
"Failed to regenerate config with DNS".to_string()
})?;
// 应用新配置
CoreManager::global().update_config().await.map_err(|err| {
logging!(
error,
Type::Config,
"Failed to apply config with DNS: {err}"
);
"Failed to apply config with DNS".to_string()
})?;
logging!(info, Type::Config, "DNS config successfully applied");
handle::Handle::refresh_clash();
} else {
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
logging!(
info,
Type::Config,
"DNS settings disabled, regenerating config"
);
Config::generate().await.map_err(|err| {
logging!(error, Type::Config, "Failed to regenerate config: {err}");
"Failed to regenerate config".to_string()
})?;
CoreManager::global().update_config().await.map_err(|err| {
logging!(
error,
Type::Config,
"Failed to apply regenerated config: {err}"
);
"Failed to apply regenerated config".to_string()
})?;
logging!(info, Type::Config, "Config regenerated successfully");
handle::Handle::refresh_clash();
}
Ok(()) Ok(())
} }
@@ -277,17 +269,19 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
#[tauri::command] #[tauri::command]
pub async fn get_dns_config_content() -> CmdResult<String> { pub async fn get_dns_config_content() -> CmdResult<String> {
use crate::utils::dirs; use crate::utils::dirs;
use std::fs; use tokio::fs;
let dns_path = dirs::app_home_dir() let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
.join("dns_config.yaml"); .join("dns_config.yaml");
if !dns_path.exists() { if !fs::try_exists(&dns_path).await.map_err(|e| e.to_string())? {
return Err("DNS config file not found".into()); return Err("DNS config file not found".into());
} }
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?; let content = fs::read_to_string(&dns_path)
.await
.map_err(|e| e.to_string())?;
Ok(content) Ok(content)
} }
@@ -323,8 +317,8 @@ pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
#[tauri::command] #[tauri::command]
pub async fn get_clash_config() -> CmdResult<serde_json::Value> { pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global(); let manager = IpcManager::global();
let cache = ProxyRequestCache::global(); let cache = CacheProxy::global();
let key = ProxyRequestCache::make_key("clash_config", "default"); let key = CacheProxy::make_key("clash_config", "default");
let value = cache let value = cache
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async { .get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
manager.get_config().await.unwrap_or_else(|e| { manager.get_config().await.unwrap_or_else(|e| {
@@ -339,8 +333,8 @@ pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
/// 强制刷新Clash配置缓存 /// 强制刷新Clash配置缓存
#[tauri::command] #[tauri::command]
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> { pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global(); let cache = CacheProxy::global();
let key = ProxyRequestCache::make_key("clash_config", "default"); let key = CacheProxy::make_key("clash_config", "default");
cache.map.remove(&key); cache.map.remove(&key);
get_clash_config().await get_clash_config().await
} }
+2 -2
View File
@@ -4,12 +4,12 @@ use super::CmdResult;
#[tauri::command] #[tauri::command]
pub async fn entry_lightweight_mode() -> CmdResult { pub async fn entry_lightweight_mode() -> CmdResult {
lightweight::entry_lightweight_mode(); lightweight::entry_lightweight_mode().await;
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn exit_lightweight_mode() -> CmdResult { pub async fn exit_lightweight_mode() -> CmdResult {
lightweight::exit_lightweight_mode(); lightweight::exit_lightweight_mode().await;
Ok(()) Ok(())
} }
+26 -30
View File
@@ -618,20 +618,20 @@ async fn check_netflix(client: &Client) -> UnlockItem {
{ {
Ok(response) => { Ok(response) => {
// 检查重定向位置 // 检查重定向位置
if let Some(location) = response.headers().get("location") { if let Some(location) = response.headers().get("location")
if let Ok(location_str) = location.to_str() { && let Ok(location_str) = location.to_str()
// 解析位置获取区域 {
let parts: Vec<&str> = location_str.split('/').collect(); // 解析位置获取区域
if parts.len() >= 4 { let parts: Vec<&str> = location_str.split('/').collect();
let region_code = parts[3].split('-').next().unwrap_or("unknown"); if parts.len() >= 4 {
let emoji = country_code_to_emoji(region_code); let region_code = parts[3].split('-').next().unwrap_or("unknown");
return UnlockItem { let emoji = country_code_to_emoji(region_code);
name: "Netflix".to_string(), return UnlockItem {
status: "Yes".to_string(), name: "Netflix".to_string(),
region: Some(format!("{emoji}{region_code}")), status: "Yes".to_string(),
check_time: Some(get_local_date_string()), region: Some(format!("{emoji}{region_code}")),
}; check_time: Some(get_local_date_string()),
} };
} }
} }
// 如果没有重定向,假设是美国 // 如果没有重定向,假设是美国
@@ -691,22 +691,18 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
match response.json::<serde_json::Value>().await { match response.json::<serde_json::Value>().await {
Ok(data) => { Ok(data) => {
// 尝试从数据中提取区域信息 // 尝试从数据中提取区域信息
if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) { if let Some(targets) = data.get("targets").and_then(|t| t.as_array())
if !targets.is_empty() { && !targets.is_empty()
if let Some(location) = targets[0].get("location") { && let Some(location) = targets[0].get("location")
if let Some(country) = && let Some(country) = location.get("country").and_then(|c| c.as_str())
location.get("country").and_then(|c| c.as_str()) {
{ let emoji = country_code_to_emoji(country);
let emoji = country_code_to_emoji(country); return UnlockItem {
return UnlockItem { name: "Netflix".to_string(),
name: "Netflix".to_string(), status: "Yes".to_string(),
status: "Yes".to_string(), region: Some(format!("{emoji}{country}")),
region: Some(format!("{emoji}{country}")), check_time: Some(get_local_date_string()),
check_time: Some(get_local_date_string()), };
};
}
}
}
} }
// 如果无法解析区域信息 // 如果无法解析区域信息
+3 -3
View File
@@ -1,9 +1,9 @@
use super::CmdResult; use super::CmdResult;
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager}; use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
use crate::process::AsyncHandler; use crate::process::AsyncHandler;
use crate::wrap_err; use crate::wrap_err;
use network_interface::NetworkInterface; use network_interface::NetworkInterface;
use serde_yaml::Mapping; use serde_yaml_ng::Mapping;
/// get the system proxy /// get the system proxy
#[tauri::command] #[tauri::command]
@@ -31,7 +31,7 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
let proxy_manager = EventDrivenProxyManager::global(); let proxy_manager = EventDrivenProxyManager::global();
let current = proxy_manager.get_auto_proxy_cached(); let current = proxy_manager.get_auto_proxy_cached().await;
// 异步请求更新,立即返回缓存数据 // 异步请求更新,立即返回缓存数据
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
let _ = proxy_manager.get_auto_proxy_async().await; let _ = proxy_manager.get_auto_proxy_async().await;
+280 -337
View File
@@ -1,99 +1,76 @@
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
config::{Config, IProfiles, PrfItem, PrfOption}, config::{
core::{handle, timer::Timer, tray::Tray, CoreManager}, Config, IProfiles, PrfItem, PrfOption,
profiles::{
profiles_append_item_with_filedata_safe, profiles_delete_item_safe,
profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_safe,
},
profiles_append_item_safe,
},
core::{CoreManager, handle, timer::Timer, tray::Tray},
feat, logging, feat, logging,
process::AsyncHandler, process::AsyncHandler,
ret_err, ret_err,
utils::{dirs, help, logging::Type}, utils::{dirs, help, logging::Type},
wrap_err, wrap_err,
}; };
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration; use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
// 全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
// 全局请求序列号跟踪,用于避免队列化执行 // 全局请求序列号跟踪,用于避免队列化执行
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0); static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
static CURRENT_PROCESSING_PROFILE: RwLock<Option<String>> = RwLock::const_new(None); static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
/// 清理配置处理状态
async fn cleanup_processing_state(sequence: u64, reason: &str) {
*CURRENT_PROCESSING_PROFILE.write().await = None;
logging!(
info,
Type::Cmd,
true,
"{},清理状态,序列号: {}",
reason,
sequence
);
}
/// 获取配置文件避免锁竞争
#[tauri::command] #[tauri::command]
pub async fn get_profiles() -> CmdResult<IProfiles> { pub async fn get_profiles() -> CmdResult<IProfiles> {
// 策略1: 尝试快速获取latest数据 // 策略1: 尝试快速获取latest数据
let latest_result = tokio::time::timeout( let latest_result = tokio::time::timeout(Duration::from_millis(500), async {
Duration::from_millis(500), let profiles = Config::profiles().await;
AsyncHandler::spawn_blocking(move || { let latest = profiles.latest_ref();
let profiles = Config::profiles(); IProfiles {
let latest = profiles.latest_ref(); current: latest.current.clone(),
IProfiles { items: latest.items.clone(),
current: latest.current.clone(), }
items: latest.items.clone(), })
}
}),
)
.await; .await;
match latest_result { match latest_result {
Ok(Ok(profiles)) => { Ok(profiles) => {
logging!(info, Type::Cmd, false, "快速获取配置列表成功"); logging!(info, Type::Cmd, false, "快速获取配置列表成功");
return Ok(profiles); return Ok(profiles);
} }
Ok(Err(join_err)) => {
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
}
Err(_) => { Err(_) => {
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)"); logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
} }
} }
// 策略2: 如果快速获取失败,尝试获取data() // 策略2: 如果快速获取失败,尝试获取data()
let data_result = tokio::time::timeout( let data_result = tokio::time::timeout(Duration::from_secs(2), async {
Duration::from_secs(2), let profiles = Config::profiles().await;
AsyncHandler::spawn_blocking(move || { let data = profiles.latest_ref();
let profiles = Config::profiles(); IProfiles {
let data = profiles.latest_ref(); current: data.current.clone(),
IProfiles { items: data.items.clone(),
current: data.current.clone(), }
items: data.items.clone(), })
}
}),
)
.await; .await;
match data_result { match data_result {
Ok(Ok(profiles)) => { Ok(profiles) => {
logging!(info, Type::Cmd, false, "获取draft配置列表成功"); logging!(info, Type::Cmd, false, "获取draft配置列表成功");
return Ok(profiles); return Ok(profiles);
} }
Ok(Err(join_err)) => { Err(join_err) => {
logging!( logging!(
error, error,
Type::Cmd, Type::Cmd,
true, true,
"获取draft配置任务失败: {}", "获取draft配置任务失败或超时: {}",
join_err join_err
); );
} }
Err(_) => {
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
}
} }
// 策略3: fallback,尝试重新创建配置 // 策略3: fallback,尝试重新创建配置
@@ -104,26 +81,19 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
"所有获取配置策略都失败,尝试fallback" "所有获取配置策略都失败,尝试fallback"
); );
match AsyncHandler::spawn_blocking(IProfiles::new).await { Ok(IProfiles::new().await)
Ok(profiles) => {
logging!(info, Type::Cmd, true, "使用fallback配置成功");
Ok(profiles)
}
Err(err) => {
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
// 返回空配置避免崩溃
Ok(IProfiles {
current: None,
items: Some(vec![]),
})
}
}
} }
/// 增强配置文件 /// 增强配置文件
#[tauri::command] #[tauri::command]
pub async fn enhance_profiles() -> CmdResult { pub async fn enhance_profiles() -> CmdResult {
wrap_err!(feat::enhance_profiles().await)?; match feat::enhance_profiles().await {
Ok(_) => {}
Err(e) => {
log::error!(target: "app", "{}", e);
return Err(e.to_string());
}
}
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
Ok(()) Ok(())
} }
@@ -133,82 +103,76 @@ pub async fn enhance_profiles() -> CmdResult {
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult { pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url); logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url);
// 使用超时保护避免长时间阻塞 let import_result = tokio::time::timeout(Duration::from_secs(60), async {
let import_result = tokio::time::timeout( let item = PrfItem::from_url(&url, None, None, option).await?;
Duration::from_secs(60), // 60秒超时 logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置");
async {
let item = PrfItem::from_url(&url, None, None, option).await?;
logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置");
// 获取导入前的配置数量用于验证 let profiles = Config::profiles().await;
let pre_count = Config::profiles() let pre_count = profiles
.latest_ref() .latest_ref()
.items .items
.as_ref() .as_ref()
.map_or(0, |items| items.len()); .map_or(0, |items| items.len());
Config::profiles().data_mut().append_item(item.clone())?; let result = profiles_append_item_safe(item.clone()).await;
result?;
// 验证导入是否成功 let post_count = profiles
let post_count = Config::profiles() .latest_ref()
.latest_ref() .items
.items .as_ref()
.as_ref() .map_or(0, |items| items.len());
.map_or(0, |items| items.len()); if post_count <= pre_count {
if post_count <= pre_count { logging!(
logging!( error,
error, Type::Cmd,
Type::Cmd, true,
true, "[导入订阅] 配置未增加,导入可能失败"
"[导入订阅] 配置未增加,导入可能失败" );
); return Err(anyhow::anyhow!("配置导入后数量未增加"));
return Err(anyhow::anyhow!("配置导入后数量未增加")); }
}
logging!(
info,
Type::Cmd,
true,
"[导入订阅] 配置保存成功,数量: {} -> {}",
pre_count,
post_count
);
// 立即发送配置变更通知
if let Some(uid) = &item.uid {
logging!( logging!(
info, info,
Type::Cmd, Type::Cmd,
true, true,
"[导入订阅] 配置保存成功,数量: {} -> {}", "[导入订阅] 发送配置变更通知: {}",
pre_count, uid
post_count
); );
handle::Handle::notify_profile_changed(uid.clone());
}
// 立即发送配置变更通知 // 异步保存配置文件并发送全局通知
if let Some(uid) = &item.uid { let uid_clone = item.uid.clone();
logging!( crate::process::AsyncHandler::spawn(move || async move {
info, // 使用Send-safe helper函数
Type::Cmd, if let Err(e) = profiles_save_file_safe().await {
true, logging!(error, Type::Cmd, true, "[导入订阅] 保存配置文件失败: {}", e);
"[导入订阅] 发送配置变更通知: {}", } else {
uid logging!(info, Type::Cmd, true, "[导入订阅] 配置文件保存成功");
);
handle::Handle::notify_profile_changed(uid.clone());
}
// 异步保存配置文件并发送全局通知 // 发送全局配置更新通知
let uid_clone = item.uid.clone(); if let Some(uid) = uid_clone {
crate::process::AsyncHandler::spawn(move || async move { // 延迟发送,确保文件已完全写入
// 在异步块中重新获取锁,避免跨await问题 tokio::time::sleep(Duration::from_millis(100)).await;
let save_result = { Config::profiles().data_mut().save_file() }; handle::Handle::notify_profile_changed(uid);
if let Err(e) = save_result {
logging!(error, Type::Cmd, true, "[导入订阅] 保存配置文件失败: {}", e);
} else {
logging!(info, Type::Cmd, true, "[导入订阅] 配置文件保存成功");
// 发送全局配置更新通知
if let Some(uid) = uid_clone {
// 延迟发送,确保文件已完全写入
tokio::time::sleep(Duration::from_millis(100)).await;
handle::Handle::notify_profile_changed(uid);
}
} }
}); }
});
Ok(()) Ok(())
}, })
)
.await; .await;
match import_result { match import_result {
@@ -227,36 +191,62 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
} }
} }
/// 重新排序配置文件 /// 调整profile的顺序
#[tauri::command] #[tauri::command]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
wrap_err!(Config::profiles().data_mut().reorder(active_id, over_id)) match profiles_reorder_safe(active_id, over_id).await {
Ok(_) => {
log::info!(target: "app", "重新排序配置文件");
Ok(())
}
Err(err) => {
log::error!(target: "app", "重新排序配置文件失败: {}", err);
Err(format!("重新排序配置文件失败: {}", err))
}
}
} }
/// 创建配置文件 /// 创建新的profile
/// 创建一个新的配置文件
#[tauri::command] #[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult { pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
let item = wrap_err!(PrfItem::from(item, file_data).await)?; match profiles_append_item_with_filedata_safe(item, file_data).await {
wrap_err!(Config::profiles().data_mut().append_item(item)) Ok(_) => 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}")),
},
}
} }
/// 更新配置文件 /// 更新配置文件
#[tauri::command] #[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult { pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
wrap_err!(feat::update_profile(index, option, Some(true)).await) match feat::update_profile(index, option, Some(true)).await {
Ok(_) => Ok(()),
Err(e) => {
log::error!(target: "app", "{}", e);
Err(e.to_string())
}
}
} }
/// 删除配置文件 /// 删除配置文件
#[tauri::command] #[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult { pub async fn delete_profile(index: String) -> CmdResult {
let should_update = wrap_err!({ Config::profiles().data_mut().delete_item(index) })?; // 使用Send-safe helper函数
let should_update = wrap_err!(profiles_delete_item_safe(index).await)?;
// 删除后自动清理冗余文件
let _ = Config::profiles().latest_ref().auto_cleanup();
if should_update { if should_update {
wrap_err!(CoreManager::global().update_config().await)?; match CoreManager::global().update_config().await {
handle::Handle::refresh_clash(); Ok(_) => {
handle::Handle::refresh_clash();
}
Err(e) => {
log::error!(target: "app", "{}", e);
return Err(e.to_string());
}
}
} }
Ok(()) Ok(())
} }
@@ -264,6 +254,12 @@ pub async fn delete_profile(index: String) -> CmdResult {
/// 修改profiles的配置 /// 修改profiles的配置
#[tauri::command] #[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> { pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
logging!(info, Type::Cmd, true, "当前正在切换配置,放弃请求");
return Ok(false);
}
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
// 为当前请求分配序列号 // 为当前请求分配序列号
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1; let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
let target_profile = profiles.current.clone(); let target_profile = profiles.current.clone();
@@ -277,35 +273,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
target_profile target_profile
); );
let mutex_result =
tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await;
let _guard = match mutex_result {
Ok(guard) => guard,
Err(_) => {
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
true,
"检测到更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
return Ok(false);
}
logging!(
info,
Type::Cmd,
true,
"强制获取锁以处理最新请求: {}",
current_sequence
);
PROFILE_UPDATE_MUTEX.lock().await
}
};
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence { if current_sequence < latest_sequence {
logging!( logging!(
@@ -320,113 +287,110 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
} }
// 保存当前配置,以便在验证失败时恢复 // 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().latest_ref().current.clone(); let current_profile = Config::profiles().await.latest_ref().current.clone();
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile); logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误 // 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref() { if let Some(new_profile) = profiles.current.as_ref()
if current_profile.as_ref() != Some(new_profile) { && current_profile.as_ref() != Some(new_profile)
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile); {
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
// 获取目标配置文件路径 // 获取目标配置文件路径
let config_file_result = { let config_file_result = {
let profiles_config = Config::profiles(); let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_ref(); let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) { match profiles_data.get_item(new_profile) {
Ok(item) => { Ok(item) => {
if let Some(file) = &item.file { if let Some(file) = &item.file {
let path = dirs::app_profiles_dir().map(|dir| dir.join(file)); let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
path.ok() path.ok()
} else { } else {
None
}
}
Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
None None
} }
} }
}; Err(e) => {
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
// 如果获取到文件路径,检查YAML语法 None
if let Some(file_path) = config_file_result {
if !file_path.exists() {
logging!(
error,
Type::Cmd,
true,
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
"config_validate::file_not_found",
format!("{}", file_path.display()),
);
return Ok(false);
} }
}
};
// 超时保护 // 如果获取到文件路径,检查YAML语法
let file_read_result = tokio::time::timeout( if let Some(file_path) = config_file_result {
Duration::from_secs(5), if !file_path.exists() {
tokio::fs::read_to_string(&file_path), logging!(
) error,
.await; Type::Cmd,
true,
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
"config_validate::file_not_found",
format!("{}", file_path.display()),
);
return Ok(false);
}
match file_read_result { // 超时保护
Ok(Ok(content)) => { let file_read_result = tokio::time::timeout(
let yaml_parse_result = AsyncHandler::spawn_blocking(move || { Duration::from_secs(5),
serde_yaml::from_str::<serde_yaml::Value>(&content) tokio::fs::read_to_string(&file_path),
}) )
.await; .await;
match yaml_parse_result { match file_read_result {
Ok(Ok(_)) => { Ok(Ok(content)) => {
logging!(info, Type::Cmd, true, "目标配置文件语法正确"); let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
} serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
Ok(Err(err)) => { })
let error_msg = format!(" {err}"); .await;
logging!(
error, match yaml_parse_result {
Type::Cmd, Ok(Ok(_)) => {
true, logging!(info, Type::Cmd, true, "目标配置文件语法正确");
"目标配置文件存在YAML语法错误:{}", }
error_msg Ok(Err(err)) => {
); let error_msg = format!(" {err}");
handle::Handle::notice_message( logging!(
"config_validate::yaml_syntax_error", error,
&error_msg, Type::Cmd,
); true,
return Ok(false); "目标配置文件存在YAML语法错误:{}",
} error_msg
Err(join_err) => { );
let error_msg = format!("YAML解析任务失败: {join_err}"); handle::Handle::notice_message(
logging!(error, Type::Cmd, true, "{}", error_msg); "config_validate::yaml_syntax_error",
handle::Handle::notice_message( &error_msg,
"config_validate::yaml_parse_error", );
&error_msg, return Ok(false);
); }
return Ok(false); Err(join_err) => {
} let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
&error_msg,
);
return Ok(false);
} }
} }
Ok(Err(err)) => { }
let error_msg = format!("无法读取目标配置文件: {err}"); Ok(Err(err)) => {
logging!(error, Type::Cmd, true, "{}", error_msg); let error_msg = format!("无法读取目标配置文件: {err}");
handle::Handle::notice_message( logging!(error, Type::Cmd, true, "{}", error_msg);
"config_validate::file_read_error", handle::Handle::notice_message("config_validate::file_read_error", &error_msg);
&error_msg, return Ok(false);
); }
return Ok(false); Err(_) => {
} let error_msg = "读取配置文件超时(5秒)".to_string();
Err(_) => { logging!(error, Type::Cmd, true, "{}", error_msg);
let error_msg = "读取配置文件超时(5秒)".to_string(); handle::Handle::notice_message(
logging!(error, Type::Cmd, true, "{}", error_msg); "config_validate::file_read_timeout",
handle::Handle::notice_message( &error_msg,
"config_validate::file_read_timeout", );
&error_msg, return Ok(false);
);
return Ok(false);
}
} }
} }
} }
@@ -446,18 +410,6 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
return Ok(false); return Ok(false);
} }
if let Some(ref profile) = target_profile {
*CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone());
logging!(
info,
Type::Cmd,
true,
"设置当前处理profile: {}, 序列号: {}",
profile,
current_sequence
);
}
// 更新profiles配置 // 更新profiles配置
logging!( logging!(
info, info,
@@ -469,7 +421,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
let current_value = profiles.current.clone(); let current_value = profiles.current.clone();
let _ = Config::profiles().draft_mut().patch_config(profiles); let _ = Config::profiles().await.draft_mut().patch_config(profiles);
// 在调用内核前再次验证请求有效性 // 在调用内核前再次验证请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst); let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
@@ -482,7 +434,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
current_sequence, current_sequence,
latest_sequence latest_sequence
); );
Config::profiles().discard(); Config::profiles().await.discard();
return Ok(false); return Ok(false);
} }
@@ -514,7 +466,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
current_sequence, current_sequence,
latest_sequence latest_sequence
); );
Config::profiles().discard(); Config::profiles().await.discard();
return Ok(false); return Ok(false);
} }
@@ -525,7 +477,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
"配置更新成功,序列号: {}", "配置更新成功,序列号: {}",
current_sequence current_sequence
); );
Config::profiles().apply(); Config::profiles().await.apply();
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据 // 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
@@ -535,20 +487,18 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
} }
}); });
crate::process::AsyncHandler::spawn(|| async move { if let Err(e) = Tray::global().update_tooltip().await {
if let Err(e) = Tray::global().update_tooltip() { log::warn!(target: "app", "异步更新托盘提示失败: {e}");
log::warn!(target: "app", "异步更新托盘提示失败: {e}"); }
}
if let Err(e) = Tray::global().update_menu() { if let Err(e) = Tray::global().update_menu().await {
log::warn!(target: "app", "异步更新托盘菜单失败: {e}"); log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
} }
// 保存配置文件 // 保存配置文件
if let Err(e) = Config::profiles().data_mut().save_file() { if let Err(e) = profiles_save_file_safe().await {
log::warn!(target: "app", "异步保存配置文件失败: {e}"); log::warn!(target: "app", "异步保存配置文件失败: {e}");
} }
});
// 立即通知前端配置变更 // 立即通知前端配置变更
if let Some(current) = &current_value { if let Some(current) = &current_value {
@@ -563,13 +513,12 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
handle::Handle::notify_profile_changed(current.clone()); handle::Handle::notify_profile_changed(current.clone());
} }
cleanup_processing_state(current_sequence, "配置切换完成").await; CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(true) Ok(true)
} }
Ok(Ok((false, error_msg))) => { Ok(Ok((false, error_msg))) => {
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg); logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
Config::profiles().discard(); Config::profiles().await.discard();
// 如果验证失败,恢复到之前的配置 // 如果验证失败,恢复到之前的配置
if let Some(prev_profile) = current_profile { if let Some(prev_profile) = current_profile {
logging!( logging!(
@@ -586,13 +535,14 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 静默恢复,不触发验证 // 静默恢复,不触发验证
wrap_err!({ wrap_err!({
Config::profiles() Config::profiles()
.await
.draft_mut() .draft_mut()
.patch_config(restore_profiles) .patch_config(restore_profiles)
})?; })?;
Config::profiles().apply(); Config::profiles().await.apply();
crate::process::AsyncHandler::spawn(|| async move { crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Config::profiles().data_mut().save_file() { if let Err(e) = profiles_save_file_safe().await {
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}"); log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
} }
}); });
@@ -602,9 +552,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 发送验证错误通知 // 发送验证错误通知
handle::Handle::notice_message("config_validate::error", &error_msg); handle::Handle::notice_message("config_validate::error", &error_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
cleanup_processing_state(current_sequence, "配置验证失败").await;
Ok(false) Ok(false)
} }
Ok(Err(e)) => { Ok(Err(e)) => {
@@ -616,11 +564,10 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
e, e,
current_sequence current_sequence
); );
Config::profiles().discard(); Config::profiles().await.discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string()); handle::Handle::notice_message("config_validate::boot_error", e.to_string());
cleanup_processing_state(current_sequence, "更新过程错误").await; CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false) Ok(false)
} }
Err(_) => { Err(_) => {
@@ -634,7 +581,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
timeout_msg, timeout_msg,
current_sequence current_sequence
); );
Config::profiles().discard(); Config::profiles().await.discard();
if let Some(prev_profile) = current_profile { if let Some(prev_profile) = current_profile {
logging!( logging!(
@@ -651,16 +598,15 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}; };
wrap_err!({ wrap_err!({
Config::profiles() Config::profiles()
.await
.draft_mut() .draft_mut()
.patch_config(restore_profiles) .patch_config(restore_profiles)
})?; })?;
Config::profiles().apply(); Config::profiles().await.apply();
} }
handle::Handle::notice_message("config_validate::timeout", timeout_msg); handle::Handle::notice_message("config_validate::timeout", timeout_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
cleanup_processing_state(current_sequence, "配置更新超时").await;
Ok(false) Ok(false)
} }
} }
@@ -668,10 +614,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
/// 根据profile name修改profiles /// 根据profile name修改profiles
#[tauri::command] #[tauri::command]
pub async fn patch_profiles_config_by_profile_index( pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<bool> {
_app_handle: tauri::AppHandle,
profile_index: String,
) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index); logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
let profiles = IProfiles { let profiles = IProfiles {
@@ -683,28 +626,26 @@ pub async fn patch_profiles_config_by_profile_index(
/// 修改某个profile item的 /// 修改某个profile item的
#[tauri::command] #[tauri::command]
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult { pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
// 保存修改前检查是否有更新 update_interval // 保存修改前检查是否有更新 update_interval
let update_interval_changed = let profiles = Config::profiles().await;
if let Ok(old_profile) = Config::profiles().latest_ref().get_item(&index) { let update_interval_changed = if let Ok(old_profile) = profiles.latest_ref().get_item(&index) {
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval); let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval); let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
old_interval != new_interval old_interval != new_interval
} else { } else {
false false
}; };
// 保存修改 // 保存修改
wrap_err!(Config::profiles() wrap_err!(profiles_patch_item_safe(index.clone(), profile).await)?;
.data_mut()
.patch_item(index.clone(), profile))?;
// 如果更新间隔变更,异步刷新定时器 // 如果更新间隔变更,异步刷新定时器
if update_interval_changed { if update_interval_changed {
let index_clone = index.clone(); let index_clone = index.clone();
crate::process::AsyncHandler::spawn(move || async move { crate::process::AsyncHandler::spawn(move || async move {
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器..."); logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
if let Err(e) = crate::core::Timer::global().refresh() { if let Err(e) = crate::core::Timer::global().refresh().await {
logging!(error, Type::Timer, "刷新定时器失败: {}", e); logging!(error, Type::Timer, "刷新定时器失败: {}", e);
} else { } else {
// 刷新成功后发送自定义事件,不触发配置重载 // 刷新成功后发送自定义事件,不触发配置重载
@@ -718,9 +659,11 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
/// 查看配置文件 /// 查看配置文件
#[tauri::command] #[tauri::command]
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult { pub async fn view_profile(index: String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let file = { let file = {
wrap_err!(Config::profiles().latest_ref().get_item(&index))? wrap_err!(profiles_ref.get_item(&index))?
.file .file
.clone() .clone()
.ok_or("the file field is null") .ok_or("the file field is null")
@@ -731,23 +674,23 @@ pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
ret_err!("the file not found"); ret_err!("the file not found");
} }
wrap_err!(help::open_file(app_handle, path)) wrap_err!(help::open_file(path))
} }
/// 读取配置文件内容 /// 读取配置文件内容
#[tauri::command] #[tauri::command]
pub fn read_profile_file(index: String) -> CmdResult<String> { pub async fn read_profile_file(index: String) -> CmdResult<String> {
let profiles = Config::profiles(); let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles_ref = profiles.latest_ref();
let item = wrap_err!(profiles.get_item(&index))?; let item = wrap_err!(profiles_ref.get_item(&index))?;
let data = wrap_err!(item.read_file())?; let data = wrap_err!(item.read_file())?;
Ok(data) Ok(data)
} }
/// 获取下一次更新时间 /// 获取下一次更新时间
#[tauri::command] #[tauri::command]
pub fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> { pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
let timer = Timer::global(); let timer = Timer::global();
let next_time = timer.get_next_update_time(&uid); let next_time = timer.get_next_update_time(&uid).await;
Ok(next_time) Ok(next_time)
} }
+85 -9
View File
@@ -1,5 +1,13 @@
use tauri::Emitter;
use super::CmdResult; use super::CmdResult;
use crate::{ipc::IpcManager, logging, state::proxy::ProxyRequestCache, utils::logging::Type}; use crate::{
cache::CacheProxy,
core::{handle::Handle, tray::Tray},
ipc::IpcManager,
logging,
utils::logging::Type,
};
use std::time::Duration; use std::time::Duration;
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60); const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
@@ -7,11 +15,11 @@ const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command] #[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> { pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global(); let cache = CacheProxy::global();
let cache = ProxyRequestCache::global(); let key = CacheProxy::make_key("proxies", "default");
let key = ProxyRequestCache::make_key("proxies", "default");
let value = cache let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async { .get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_proxies().await.unwrap_or_else(|e| { manager.get_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}"); logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
serde_json::Value::Object(serde_json::Map::new()) serde_json::Value::Object(serde_json::Map::new())
@@ -24,19 +32,19 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
/// 强制刷新代理缓存用于profile切换 /// 强制刷新代理缓存用于profile切换
#[tauri::command] #[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> { pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global(); let cache = CacheProxy::global();
let key = ProxyRequestCache::make_key("proxies", "default"); let key = CacheProxy::make_key("proxies", "default");
cache.map.remove(&key); cache.map.remove(&key);
get_proxies().await get_proxies().await
} }
#[tauri::command] #[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> { pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global(); let cache = CacheProxy::global();
let cache = ProxyRequestCache::global(); let key = CacheProxy::make_key("providers", "default");
let key = ProxyRequestCache::make_key("providers", "default");
let value = cache let value = cache
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async { .get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_providers_proxies().await.unwrap_or_else(|e| { manager.get_providers_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}"); logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
serde_json::Value::Object(serde_json::Map::new()) serde_json::Value::Object(serde_json::Map::new())
@@ -45,3 +53,71 @@ pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
.await; .await;
Ok((*value).clone()) Ok((*value).clone())
} }
/// 同步托盘和GUI的代理选择状态
#[tauri::command]
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
use crate::core::tray::Tray;
match Tray::global().update_menu().await {
Ok(_) => {
logging!(info, Type::Cmd, "Tray proxy selection synced successfully");
Ok(())
}
Err(e) => {
logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}");
Err(e.to_string())
}
}
}
/// 更新代理选择并同步托盘和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())
}
}
}
+104 -15
View File
@@ -1,36 +1,125 @@
use super::CmdResult; use super::CmdResult;
use crate::{config::*, wrap_err}; use crate::{config::*, core::CoreManager, log_err, wrap_err};
use anyhow::Context; use anyhow::Context;
use serde_yaml::Mapping; use serde_yaml_ng::Mapping;
use std::collections::HashMap; use std::collections::HashMap;
/// 获取运行时配置 /// 获取运行时配置
#[tauri::command] #[tauri::command]
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> { pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().latest_ref().config.clone()) Ok(Config::runtime().await.latest_ref().config.clone())
} }
/// 获取运行时YAML配置 /// 获取运行时YAML配置
#[tauri::command] #[tauri::command]
pub fn get_runtime_yaml() -> CmdResult<String> { pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime(); let runtime = Config::runtime().await;
let runtime = runtime.latest_ref(); let runtime = runtime.latest_ref();
let config = runtime.config.as_ref(); let config = runtime.config.as_ref();
wrap_err!(config wrap_err!(
.ok_or(anyhow::anyhow!("failed to parse config to yaml file")) config
.and_then( .ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|config| serde_yaml::to_string(config).context("failed to convert config to yaml") .and_then(|config| serde_yaml_ng::to_string(config)
)) .context("failed to convert config to yaml"))
)
} }
/// 获取运行时存在的键 /// 获取运行时存在的键
#[tauri::command] #[tauri::command]
pub fn get_runtime_exists() -> CmdResult<Vec<String>> { pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().latest_ref().exists_keys.clone()) Ok(Config::runtime().await.latest_ref().exists_keys.clone())
} }
/// 获取运行时日志 /// 获取运行时日志
#[tauri::command] #[tauri::command]
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> { pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().latest_ref().chain_logs.clone()) Ok(Config::runtime().await.latest_ref().chain_logs.clone())
}
/// 读取运行时链式代理配置
#[tauri::command]
pub async fn get_runtime_proxy_chain_config() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_ref();
let config = wrap_err!(
runtime
.config
.as_ref()
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
)?;
if let (
Some(serde_yaml_ng::Value::Sequence(proxies)),
Some(serde_yaml_ng::Value::Sequence(proxy_groups)),
) = (config.get("proxies"), config.get("proxy-groups"))
{
let mut proxy_name = None;
let mut proxies_chain = Vec::new();
let proxy_chain_groups = proxy_groups
.iter()
.filter_map(
|proxy_group| match proxy_group.get("name").and_then(|n| n.as_str()) {
Some("proxy_chain") => {
if let Some(serde_yaml_ng::Value::Sequence(ps)) = proxy_group.get("proxies")
&& let Some(x) = ps.first()
{
proxy_name = Some(x); //插入出口节点名字
}
Some(proxy_group.to_owned())
}
_ => None,
},
)
.collect::<Vec<serde_yaml_ng::Value>>();
while let Some(proxy) = proxies.iter().find(|proxy| {
if let serde_yaml_ng::Value::Mapping(proxy_map) = proxy {
proxy_map.get("name") == proxy_name && proxy_map.get("dialer-proxy").is_some()
} else {
false
}
}) {
proxies_chain.push(proxy.to_owned());
proxy_name = proxy.get("dialer-proxy");
}
if let Some(entry_proxy) = proxies.iter().find(|proxy| proxy.get("name") == proxy_name) {
proxies_chain.push(entry_proxy.to_owned());
}
proxies_chain.reverse();
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
config.insert("proxies".to_string(), proxies_chain);
config.insert("proxy-groups".to_string(), proxy_chain_groups);
wrap_err!(serde_yaml_ng::to_string(&config).context("YAML generation failed"))
} else {
wrap_err!(Err(anyhow::anyhow!(
"failed to get proxies or proxy-groups".to_string()
)))
}
}
/// 更新运行时链式代理配置
#[tauri::command]
pub async fn update_proxy_chain_config_in_runtime(
proxy_chain_config: Option<serde_yaml_ng::Value>,
) -> CmdResult<()> {
{
let runtime = Config::runtime().await;
let mut draft = runtime.draft_mut();
draft.update_proxy_chain_config(proxy_chain_config);
drop(draft);
runtime.apply();
}
// 生成新的运行配置文件并通知 Clash 核心重新加载
let run_path = wrap_err!(Config::generate_file(ConfigType::Run).await)?;
log_err!(CoreManager::global().put_configs_force(run_path).await);
Ok(())
} }
+7 -7
View File
@@ -6,7 +6,7 @@ use crate::{
utils::{dirs, logging::Type}, utils::{dirs, logging::Type},
wrap_err, wrap_err,
}; };
use std::fs; use tokio::fs;
/// 保存profiles的配置 /// 保存profiles的配置
#[tauri::command] #[tauri::command]
@@ -17,7 +17,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
// 在异步操作前完成所有文件操作 // 在异步操作前完成所有文件操作
let (file_path, original_content, is_merge_file) = { let (file_path, original_content, is_merge_file) = {
let profiles = Config::profiles(); let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_ref(); let profiles_guard = profiles.latest_ref();
let item = wrap_err!(profiles_guard.get_item(&index))?; let item = wrap_err!(profiles_guard.get_item(&index))?;
// 确定是否为merge类型文件 // 确定是否为merge类型文件
@@ -30,7 +30,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
// 保存新的配置文件 // 保存新的配置文件
let file_data = file_data.ok_or("file_data is None")?; let file_data = file_data.ok_or("file_data is None")?;
wrap_err!(fs::write(&file_path, &file_data))?; wrap_err!(fs::write(&file_path, &file_data).await)?;
let file_path_str = file_path.to_string_lossy().to_string(); let file_path_str = file_path.to_string_lossy().to_string();
logging!( logging!(
@@ -88,7 +88,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
error_msg error_msg
); );
// 恢复原始配置文件 // 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?; wrap_err!(fs::write(&file_path, original_content).await)?;
// 发送合并文件专用错误通知 // 发送合并文件专用错误通知
let result = (false, error_msg.clone()); let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件"); crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
@@ -103,7 +103,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
e e
); );
// 恢复原始配置文件 // 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?; wrap_err!(fs::write(&file_path, original_content).await)?;
return Err(e.to_string()); return Err(e.to_string());
} }
} }
@@ -127,7 +127,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
error_msg error_msg
); );
// 恢复原始配置文件 // 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?; wrap_err!(fs::write(&file_path, original_content).await)?;
// 智能判断错误类型 // 智能判断错误类型
let is_script_error = file_path_str.ends_with(".js") let is_script_error = file_path_str.ends_with(".js")
@@ -177,7 +177,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
e e
); );
// 恢复原始配置文件 // 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?; wrap_err!(fs::write(&file_path, original_content).await)?;
Err(e.to_string()) Err(e.to_string())
} }
} }
+19 -16
View File
@@ -1,44 +1,47 @@
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
core::{service, CoreManager}, core::{
CoreManager,
service::{self, SERVICE_MANAGER, ServiceStatus},
},
utils::i18n::t, utils::i18n::t,
}; };
use anyhow::Result;
async fn execute_service_operation_sync<F, E>(service_op: F, op_type: &str) -> CmdResult async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
where if let Err(e) = SERVICE_MANAGER
F: FnOnce() -> Result<(), E>, .lock()
E: ToString + std::fmt::Debug, .await
{ .handle_service_status(&status)
if let Err(e) = service_op() { .await
let emsg = format!("{} {} failed: {}", op_type, "Service", e.to_string()); {
return Err(t(emsg.as_str())); let emsg = format!("{} Service failed: {}", op_type, e);
return Err(t(emsg.as_str()).await);
} }
if CoreManager::global().restart_core().await.is_err() { if CoreManager::global().restart_core().await.is_err() {
let emsg = format!("{} {} failed", "Restart", "Core"); let emsg = "Restart Core failed";
return Err(t(emsg.as_str())); return Err(t(emsg).await);
} }
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn install_service() -> CmdResult { pub async fn install_service() -> CmdResult {
execute_service_operation_sync(service::install_service, "Install").await execute_service_operation_sync(ServiceStatus::InstallRequired, "Install").await
} }
#[tauri::command] #[tauri::command]
pub async fn uninstall_service() -> CmdResult { pub async fn uninstall_service() -> CmdResult {
execute_service_operation_sync(service::uninstall_service, "Uninstall").await execute_service_operation_sync(ServiceStatus::UninstallRequired, "Uninstall").await
} }
#[tauri::command] #[tauri::command]
pub async fn reinstall_service() -> CmdResult { pub async fn reinstall_service() -> CmdResult {
execute_service_operation_sync(service::reinstall_service, "Reinstall").await execute_service_operation_sync(ServiceStatus::ReinstallRequired, "Reinstall").await
} }
#[tauri::command] #[tauri::command]
pub async fn repair_service() -> CmdResult { pub async fn repair_service() -> CmdResult {
execute_service_operation_sync(service::force_reinstall_service, "Repair").await execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "Repair").await
} }
#[tauri::command] #[tauri::command]
+1 -1
View File
@@ -1,6 +1,6 @@
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
core::{handle, CoreManager}, core::{CoreManager, handle},
logging, logging,
module::sysinfo::PlatformSpecification, module::sysinfo::PlatformSpecification,
utils::logging::Type, utils::logging::Type,
+8 -4
View File
@@ -3,10 +3,14 @@ use crate::{config::*, feat, wrap_err};
/// 获取Verge配置 /// 获取Verge配置
#[tauri::command] #[tauri::command]
pub fn get_verge_config() -> CmdResult<IVergeResponse> { pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge(); let verge = Config::verge().await;
let verge_data = verge.latest_ref().clone(); let verge_data = {
Ok(IVergeResponse::from(*verge_data)) let ref_data = verge.latest_ref();
ref_data.clone()
};
let verge_response = IVergeResponse::from(*verge_data);
Ok(verge_response)
} }
/// 修改Verge配置 /// 修改Verge配置
+9 -3
View File
@@ -11,11 +11,17 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
webdav_password: Some(password), webdav_password: Some(password),
..IVerge::default() ..IVerge::default()
}; };
Config::verge().draft_mut().patch_config(patch.clone());
Config::verge().apply();
Config::verge() Config::verge()
.latest_ref() .await
.draft_mut()
.patch_config(patch.clone());
Config::verge().await.apply();
// 分离数据获取和异步调用
let verge_data = Config::verge().await.latest_ref().clone();
verge_data
.save_file() .save_file()
.await
.map_err(|err| err.to_string())?; .map_err(|err| err.to_string())?;
core::backup::WebDavClient::global().reset(); core::backup::WebDavClient::global().reset();
Ok(()) Ok(())
+22 -13
View File
@@ -3,7 +3,7 @@ use crate::utils::dirs::{ipc_path, path_to_str};
use crate::utils::{dirs, help}; use crate::utils::{dirs, help};
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value}; use serde_yaml_ng::{Mapping, Value};
use std::{ use std::{
net::{IpAddr, Ipv4Addr, SocketAddr}, net::{IpAddr, Ipv4Addr, SocketAddr},
str::FromStr, str::FromStr,
@@ -13,22 +13,29 @@ use std::{
pub struct IClashTemp(pub Mapping); pub struct IClashTemp(pub Mapping);
impl IClashTemp { impl IClashTemp {
pub fn new() -> Self { pub async fn new() -> Self {
let template = Self::template(); let template = Self::template();
match dirs::clash_path().and_then(|path| help::read_mapping(&path)) { let clash_path_result = dirs::clash_path();
let map_result = if let Ok(path) = clash_path_result {
help::read_mapping(&path).await
} else {
Err(anyhow::anyhow!("Failed to get clash path"))
};
match map_result {
Ok(mut map) => { Ok(mut map) => {
template.0.keys().for_each(|key| { template.0.keys().for_each(|key| {
if !map.contains_key(key) { if !map.contains_key(key)
if let Some(value) = template.0.get(key) { && let Some(value) = template.0.get(key)
map.insert(key.clone(), value.clone()); {
} map.insert(key.clone(), value.clone());
} }
}); });
// 确保 secret 字段存在且不为空 // 确保 secret 字段存在且不为空
if let Some(Value::String(s)) = map.get_mut("secret") { if let Some(Value::String(s)) = map.get_mut("secret")
if s.is_empty() { && s.is_empty()
*s = "set-your-secret".to_string(); {
} *s = "set-your-secret".to_string();
} }
Self(Self::guard(map)) Self(Self::guard(map))
} }
@@ -135,12 +142,13 @@ impl IClashTemp {
} }
} }
pub fn save_config(&self) -> Result<()> { pub async fn save_config(&self) -> Result<()> {
help::save_yaml( help::save_yaml(
&dirs::clash_path()?, &dirs::clash_path()?,
&self.0, &self.0,
Some("# Generated by Clash Verge"), Some("# Generated by Clash Verge"),
) )
.await
} }
pub fn get_mixed_port(&self) -> u16 { pub fn get_mixed_port(&self) -> u16 {
@@ -280,9 +288,10 @@ impl IClashTemp {
Self::guard_server_ctrl(config) Self::guard_server_ctrl(config)
} }
pub fn guard_external_controller_with_setting(config: &Mapping) -> String { pub async fn guard_external_controller_with_setting(config: &Mapping) -> String {
// 检查 enable_external_controller 设置,用于运行时配置生成 // 检查 enable_external_controller 设置,用于运行时配置生成
let enable_external_controller = Config::verge() let enable_external_controller = Config::verge()
.await
.latest_ref() .latest_ref()
.enable_external_controller .enable_external_controller
.unwrap_or(false); .unwrap_or(false);
+53 -47
View File
@@ -1,15 +1,14 @@
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge}; use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{ use crate::{
config::PrfItem, config::{PrfItem, profiles_append_item_safe},
core::{handle, CoreManager}, core::{CoreManager, handle},
enhance, logging, enhance, logging,
process::AsyncHandler,
utils::{dirs, help, logging::Type}, utils::{dirs, help, logging::Type},
}; };
use anyhow::{anyhow, Result}; use anyhow::{Result, anyhow};
use once_cell::sync::OnceCell;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::time::{sleep, Duration}; use tokio::sync::OnceCell;
use tokio::time::{Duration, sleep};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml"; pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
@@ -22,64 +21,65 @@ pub struct Config {
} }
impl Config { impl Config {
pub fn global() -> &'static Config { pub async fn global() -> &'static Config {
static CONFIG: OnceCell<Config> = OnceCell::new(); static CONFIG: OnceCell<Config> = OnceCell::const_new();
CONFIG
CONFIG.get_or_init(|| Config { .get_or_init(|| async {
clash_config: Draft::from(Box::new(IClashTemp::new())), Config {
verge_config: Draft::from(Box::new(IVerge::new())), clash_config: Draft::from(Box::new(IClashTemp::new().await)),
profiles_config: Draft::from(Box::new(IProfiles::new())), verge_config: Draft::from(Box::new(IVerge::new().await)),
runtime_config: Draft::from(Box::new(IRuntime::new())), profiles_config: Draft::from(Box::new(IProfiles::new().await)),
}) runtime_config: Draft::from(Box::new(IRuntime::new())),
}
})
.await
} }
pub fn clash() -> Draft<Box<IClashTemp>> { pub async fn clash() -> Draft<Box<IClashTemp>> {
Self::global().clash_config.clone() Self::global().await.clash_config.clone()
} }
pub fn verge() -> Draft<Box<IVerge>> { pub async fn verge() -> Draft<Box<IVerge>> {
Self::global().verge_config.clone() Self::global().await.verge_config.clone()
} }
pub fn profiles() -> Draft<Box<IProfiles>> { pub async fn profiles() -> Draft<Box<IProfiles>> {
Self::global().profiles_config.clone() Self::global().await.profiles_config.clone()
} }
pub fn runtime() -> Draft<Box<IRuntime>> { pub async fn runtime() -> Draft<Box<IRuntime>> {
Self::global().runtime_config.clone() Self::global().await.runtime_config.clone()
} }
/// 初始化订阅 /// 初始化订阅
pub async fn init_config() -> Result<()> { pub async fn init_config() -> Result<()> {
if Self::profiles() if Self::profiles()
.await
.latest_ref() .latest_ref()
.get_item(&"Merge".to_string()) .get_item(&"Merge".to_string())
.is_err() .is_err()
{ {
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?; let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
Self::profiles() profiles_append_item_safe(merge_item.clone()).await?;
.data_mut()
.append_item(merge_item.clone())?;
} }
if Self::profiles() if Self::profiles()
.await
.latest_ref() .latest_ref()
.get_item(&"Script".to_string()) .get_item(&"Script".to_string())
.is_err() .is_err()
{ {
let script_item = PrfItem::from_script(Some("Script".to_string()))?; let script_item = PrfItem::from_script(Some("Script".to_string()))?;
Self::profiles() profiles_append_item_safe(script_item.clone()).await?;
.data_mut()
.append_item(script_item.clone())?;
} }
// 生成运行时配置 // 生成运行时配置
if let Err(err) = Self::generate() { if let Err(err) = Self::generate().await {
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err); logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
} else { } else {
logging!(info, Type::Config, true, "生成运行时配置成功"); logging!(info, Type::Config, true, "生成运行时配置成功");
} }
// 生成运行时配置文件并验证 // 生成运行时配置文件并验证
let config_result = Self::generate_file(ConfigType::Run); let config_result = Self::generate_file(ConfigType::Run).await;
let validation_result = if config_result.is_ok() { let validation_result = if config_result.is_ok() {
// 验证配置文件 // 验证配置文件
@@ -96,60 +96,66 @@ impl Config {
error_msg error_msg
); );
CoreManager::global() CoreManager::global()
.use_default_config("config_validate::boot_error", &error_msg)?; .use_default_config("config_validate::boot_error", &error_msg)
.await?;
Some(("config_validate::boot_error", error_msg)) Some(("config_validate::boot_error", error_msg))
} else { } else {
logging!(info, Type::Config, true, "配置验证成功"); logging!(info, Type::Config, true, "配置验证成功");
Some(("config_validate::success", String::new())) // 前端没有必要知道验证成功的消息,也没有事件驱动
// Some(("config_validate::success", String::new()))
None
} }
} }
Err(err) => { Err(err) => {
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err); logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
CoreManager::global() CoreManager::global()
.use_default_config("config_validate::process_terminated", "")?; .use_default_config("config_validate::process_terminated", "")
.await?;
Some(("config_validate::process_terminated", String::new())) Some(("config_validate::process_terminated", String::new()))
} }
} }
} else { } else {
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置"); logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
CoreManager::global().use_default_config("config_validate::error", "")?; CoreManager::global()
.use_default_config("config_validate::error", "")
.await?;
Some(("config_validate::error", String::new())) Some(("config_validate::error", String::new()))
}; };
// 在单独的任务中发送通知 // 在单独的任务中发送通知
if let Some((msg_type, msg_content)) = validation_result { if let Some((msg_type, msg_content)) = validation_result {
AsyncHandler::spawn(move || async move { sleep(Duration::from_secs(2)).await;
sleep(Duration::from_secs(2)).await; handle::Handle::notice_message(msg_type, &msg_content);
handle::Handle::notice_message(msg_type, &msg_content);
});
} }
Ok(()) Ok(())
} }
/// 将订阅丢到对应的文件中 /// 将订阅丢到对应的文件中
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> { pub async fn generate_file(typ: ConfigType) -> Result<PathBuf> {
let path = match typ { let path = match typ {
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG), ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG), ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG),
}; };
let runtime = Config::runtime(); let runtime = Config::runtime().await;
let runtime = runtime.latest_ref();
let config = runtime let config = runtime
.latest_ref()
.config .config
.as_ref() .as_ref()
.ok_or(anyhow!("failed to get runtime config"))?; .ok_or(anyhow!("failed to get runtime config"))?
.clone();
drop(runtime); // 显式释放锁
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?; help::save_yaml(&path, &config, Some("# Generated by Clash Verge")).await?;
Ok(path) Ok(path)
} }
/// 生成订阅存好 /// 生成订阅存好
pub fn generate() -> Result<()> { pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance(); let (config, exists_keys, logs) = enhance::enhance().await;
*Config::runtime().draft_mut() = Box::new(IRuntime { *Config::runtime().await.draft_mut() = Box::new(IRuntime {
config: Some(config), config: Some(config),
exists_keys, exists_keys,
chain_logs: logs, chain_logs: logs,
+2 -2
View File
@@ -1,9 +1,9 @@
use crate::utils::dirs::get_encryption_key; use crate::utils::dirs::get_encryption_key;
use aes_gcm::{ use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Key, Aes256Gcm, Key,
aead::{Aead, KeyInit},
}; };
use base64::{engine::general_purpose::STANDARD, Engine}; use base64::{Engine, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
const NONCE_LENGTH: usize = 12; const NONCE_LENGTH: usize = 12;
+1 -1
View File
@@ -4,7 +4,7 @@ mod config;
mod draft; mod draft;
mod encrypt; mod encrypt;
mod prfitem; mod prfitem;
mod profiles; pub mod profiles;
mod runtime; mod runtime;
mod verge; mod verge;
+18 -41
View File
@@ -3,14 +3,11 @@ use crate::utils::{
network::{NetworkManager, ProxyType}, network::{NetworkManager, ProxyType},
tmpl, tmpl,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result, bail};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::Mapping; use serde_yaml_ng::Mapping;
use std::{fs, time::Duration}; use std::{fs, time::Duration};
use super::Config;
#[derive(Debug, Clone, Deserialize, Serialize, Default)] #[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PrfItem { pub struct PrfItem {
pub uid: Option<String>, pub uid: Option<String>,
@@ -163,7 +160,7 @@ impl PrfItem {
"local" => { "local" => {
let name = item.name.unwrap_or("Local File".into()); let name = item.name.unwrap_or("Local File".into());
let desc = item.desc.unwrap_or("".into()); let desc = item.desc.unwrap_or("".into());
PrfItem::from_local(name, desc, file_data, item.option) PrfItem::from_local(name, desc, file_data, item.option).await
} }
typ => bail!("invalid profile item type \"{typ}\""), typ => bail!("invalid profile item type \"{typ}\""),
} }
@@ -171,7 +168,7 @@ impl PrfItem {
/// ## Local type /// ## Local type
/// create a new item from name/desc /// create a new item from name/desc
pub fn from_local( pub async fn from_local(
name: String, name: String,
desc: String, desc: String,
file_data: Option<String>, file_data: Option<String>,
@@ -189,37 +186,27 @@ impl PrfItem {
if merge.is_none() { if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?; let merge_item = PrfItem::from_merge(None)?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
.data_mut()
.append_item(merge_item.clone())?;
merge = merge_item.uid; merge = merge_item.uid;
} }
if script.is_none() { if script.is_none() {
let script_item = PrfItem::from_script(None)?; let script_item = PrfItem::from_script(None)?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
.data_mut()
.append_item(script_item.clone())?;
script = script_item.uid; script = script_item.uid;
} }
if rules.is_none() { if rules.is_none() {
let rules_item = PrfItem::from_rules()?; let rules_item = PrfItem::from_rules()?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
.data_mut()
.append_item(rules_item.clone())?;
rules = rules_item.uid; rules = rules_item.uid;
} }
if proxies.is_none() { if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?; let proxies_item = PrfItem::from_proxies()?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
.data_mut()
.append_item(proxies_item.clone())?;
proxies = proxies_item.uid; proxies = proxies_item.uid;
} }
if groups.is_none() { if groups.is_none() {
let groups_item = PrfItem::from_groups()?; let groups_item = PrfItem::from_groups()?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
.data_mut()
.append_item(groups_item.clone())?;
groups = groups_item.uid; groups = groups_item.uid;
} }
Ok(PrfItem { Ok(PrfItem {
@@ -278,7 +265,7 @@ impl PrfItem {
}; };
// 使用网络管理器发送请求 // 使用网络管理器发送请求
let resp = match NetworkManager::global() let resp = match NetworkManager::new()
.get_with_interrupt( .get_with_interrupt(
url, url,
proxy_type, proxy_type,
@@ -296,7 +283,7 @@ impl PrfItem {
}; };
let status_code = resp.status(); let status_code = resp.status();
if !StatusCode::is_success(&status_code) { if !status_code.is_success() {
bail!("failed to fetch remote profile with status {status_code}") bail!("failed to fetch remote profile with status {status_code}")
} }
@@ -362,13 +349,13 @@ impl PrfItem {
let uid = help::get_uid("R"); let uid = help::get_uid("R");
let file = format!("{uid}.yaml"); let file = format!("{uid}.yaml");
let name = name.unwrap_or(filename.unwrap_or("Remote File".into())); let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
let data = resp.text_with_charset("utf-8").await?; let data = resp.text_with_charset()?;
// process the charset "UTF-8 with BOM" // process the charset "UTF-8 with BOM"
let data = data.trim_start_matches('\u{feff}'); let data = data.trim_start_matches('\u{feff}');
// check the data whether the valid yaml format // check the data whether the valid yaml format
let yaml = serde_yaml::from_str::<Mapping>(data) let yaml = serde_yaml_ng::from_str::<Mapping>(data)
.context("the remote profile data is invalid yaml")?; .context("the remote profile data is invalid yaml")?;
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") { if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
@@ -377,37 +364,27 @@ impl PrfItem {
if merge.is_none() { if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?; let merge_item = PrfItem::from_merge(None)?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
.data_mut()
.append_item(merge_item.clone())?;
merge = merge_item.uid; merge = merge_item.uid;
} }
if script.is_none() { if script.is_none() {
let script_item = PrfItem::from_script(None)?; let script_item = PrfItem::from_script(None)?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
.data_mut()
.append_item(script_item.clone())?;
script = script_item.uid; script = script_item.uid;
} }
if rules.is_none() { if rules.is_none() {
let rules_item = PrfItem::from_rules()?; let rules_item = PrfItem::from_rules()?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
.data_mut()
.append_item(rules_item.clone())?;
rules = rules_item.uid; rules = rules_item.uid;
} }
if proxies.is_none() { if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?; let proxies_item = PrfItem::from_proxies()?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
.data_mut()
.append_item(proxies_item.clone())?;
proxies = proxies_item.uid; proxies = proxies_item.uid;
} }
if groups.is_none() { if groups.is_none() {
let groups_item = PrfItem::from_groups()?; let groups_item = PrfItem::from_groups()?;
Config::profiles() crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
.data_mut()
.append_item(groups_item.clone())?;
groups = groups_item.uid; groups = groups_item.uid;
} }
+297 -167
View File
@@ -1,9 +1,14 @@
use super::{prfitem::PrfItem, PrfOption}; use super::{PrfOption, prfitem::PrfItem};
use crate::utils::{dirs, help}; use crate::{
use anyhow::{bail, Context, Result}; logging_error,
process::AsyncHandler,
utils::{dirs, help, logging::Type},
};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::Mapping; use serde_yaml_ng::Mapping;
use std::{collections::HashSet, fs, io::Write}; use std::collections::HashSet;
use tokio::fs;
/// Define the `profiles.yaml` schema /// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -32,22 +37,28 @@ macro_rules! patch {
} }
impl IProfiles { impl IProfiles {
pub fn new() -> Self { pub async fn new() -> Self {
match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) { match dirs::profiles_path() {
Ok(mut profiles) => { Ok(path) => match help::read_yaml::<Self>(&path).await {
if profiles.items.is_none() { Ok(mut profiles) => {
profiles.items = Some(vec![]); if profiles.items.is_none() {
} profiles.items = Some(vec![]);
// compatible with the old old old version }
if let Some(items) = profiles.items.as_mut() { // compatible with the old old old version
for item in items.iter_mut() { if let Some(items) = profiles.items.as_mut() {
if item.uid.is_none() { for item in items.iter_mut() {
item.uid = Some(help::get_uid("d")); if item.uid.is_none() {
item.uid = Some(help::get_uid("d"));
}
} }
} }
profiles
} }
profiles Err(err) => {
} log::error!(target: "app", "{err}");
Self::template()
}
},
Err(err) => { Err(err) => {
log::error!(target: "app", "{err}"); log::error!(target: "app", "{err}");
Self::template() Self::template()
@@ -62,12 +73,13 @@ impl IProfiles {
} }
} }
pub fn save_file(&self) -> Result<()> { pub async fn save_file(&self) -> Result<()> {
help::save_yaml( help::save_yaml(
&dirs::profiles_path()?, &dirs::profiles_path()?,
self, self,
Some("# Profiles Config for Clash Verge"), Some("# Profiles Config for Clash Verge"),
) )
.await
} }
/// 只修改current,valid和chain /// 只修改current,valid和chain
@@ -76,12 +88,12 @@ impl IProfiles {
self.items = Some(vec![]); self.items = Some(vec![]);
} }
if let Some(current) = patch.current { if let Some(current) = patch.current
if let Some(items) = self.items.as_ref() { && let Some(items) = self.items.as_ref()
let some_uid = Some(current); {
if items.iter().any(|e| e.uid == some_uid) { let some_uid = Some(current);
self.current = some_uid; if items.iter().any(|e| e.uid == some_uid) {
} self.current = some_uid;
} }
} }
@@ -115,7 +127,7 @@ impl IProfiles {
/// append new item /// append new item
/// if the file_data is some /// if the file_data is some
/// then should save the data to file /// then should save the data to file
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> { pub async fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
if item.uid.is_none() { if item.uid.is_none() {
bail!("the uid should not be null"); bail!("the uid should not be null");
} }
@@ -133,9 +145,8 @@ impl IProfiles {
})?; })?;
let path = dirs::app_profiles_dir()?.join(&file); let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path) fs::write(&path, file_data.as_bytes())
.with_context(|| format!("failed to create file \"{file}\""))? .await
.write(file_data.as_bytes())
.with_context(|| format!("failed to write to file \"{file}\""))?; .with_context(|| format!("failed to write to file \"{file}\""))?;
} }
@@ -153,11 +164,11 @@ impl IProfiles {
items.push(item) items.push(item)
} }
self.save_file() self.save_file().await
} }
/// reorder items /// reorder items
pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> { pub async fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
let mut items = self.items.take().unwrap_or_default(); let mut items = self.items.take().unwrap_or_default();
let mut old_index = None; let mut old_index = None;
let mut new_index = None; let mut new_index = None;
@@ -178,11 +189,11 @@ impl IProfiles {
let item = items.remove(old_idx); let item = items.remove(old_idx);
items.insert(new_idx, item); items.insert(new_idx, item);
self.items = Some(items); self.items = Some(items);
self.save_file() self.save_file().await
} }
/// update the item value /// update the item value
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { pub async fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or_default(); let mut items = self.items.take().unwrap_or_default();
for each in items.iter_mut() { for each in items.iter_mut() {
@@ -198,7 +209,7 @@ impl IProfiles {
patch!(each, item, option); patch!(each, item, option);
self.items = Some(items); self.items = Some(items);
return self.save_file(); return self.save_file().await;
} }
} }
@@ -208,7 +219,7 @@ impl IProfiles {
/// be used to update the remote item /// be used to update the remote item
/// only patch `updated` `extra` `file_data` /// only patch `updated` `extra` `file_data`
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> { pub async fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
if self.items.is_none() { if self.items.is_none() {
self.items = Some(vec![]); self.items = Some(vec![]);
} }
@@ -237,9 +248,8 @@ impl IProfiles {
let path = dirs::app_profiles_dir()?.join(&file); let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path) fs::write(&path, file_data.as_bytes())
.with_context(|| format!("failed to create file \"{file}\""))? .await
.write(file_data.as_bytes())
.with_context(|| format!("failed to write to file \"{file}\""))?; .with_context(|| format!("failed to write to file \"{file}\""))?;
} }
@@ -248,12 +258,12 @@ impl IProfiles {
} }
} }
self.save_file() self.save_file().await
} }
/// delete item /// delete item
/// if delete the current then return true /// if delete the current then return true
pub fn delete_item(&mut self, uid: String) -> Result<bool> { pub async fn delete_item(&mut self, uid: String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(&uid); let current = self.current.as_ref().unwrap_or(&uid);
let current = current.clone(); let current = current.clone();
let item = self.get_item(&uid)?; let item = self.get_item(&uid)?;
@@ -277,15 +287,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = index { if let Some(index) = index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(|path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let _ = fs::remove_file(path); if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
} }
}); }
} });
} }
// get the merge index // get the merge index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -294,15 +313,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = merge_index { if let Some(index) = merge_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(|path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let _ = fs::remove_file(path); if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
} }
}); }
} });
} }
// get the script index // get the script index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -311,15 +339,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = script_index { if let Some(index) = script_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(|path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let _ = fs::remove_file(path); if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
} }
}); }
} });
} }
// get the rules index // get the rules index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -328,15 +365,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = rules_index { if let Some(index) = rules_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(|path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let _ = fs::remove_file(path); if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
} }
}); }
} });
} }
// get the proxies index // get the proxies index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -345,15 +391,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = proxies_index { if let Some(index) = proxies_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(|path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let _ = fs::remove_file(path); if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
} }
}); }
} });
} }
// get the groups index // get the groups index
for (i, _) in items.iter().enumerate() { for (i, _) in items.iter().enumerate() {
@@ -362,15 +417,24 @@ impl IProfiles {
break; break;
} }
} }
if let Some(index) = groups_index { if let Some(index) = groups_index
if let Some(file) = items.remove(index).file { && let Some(file) = items.remove(index).file
let _ = dirs::app_profiles_dir().map(|path| { {
let path = path.join(file); let _ = dirs::app_profiles_dir().map(async move |path| {
if path.exists() { let path = path.join(file);
let _ = fs::remove_file(path); if path.exists() {
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
} }
}); }
} });
} }
// delete the original uid // delete the original uid
if current == uid { if current == uid {
@@ -386,12 +450,12 @@ impl IProfiles {
} }
self.items = Some(items); self.items = Some(items);
self.save_file()?; self.save_file().await?;
Ok(current == uid) Ok(current == uid)
} }
/// 获取current指向的订阅内容 /// 获取current指向的订阅内容
pub fn current_mapping(&self) -> Result<Mapping> { pub async fn current_mapping(&self) -> Result<Mapping> {
match (self.current.as_ref(), self.items.as_ref()) { match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => { (Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) { if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
@@ -399,7 +463,7 @@ impl IProfiles {
Some(file) => dirs::app_profiles_dir()?.join(file), Some(file) => dirs::app_profiles_dir()?.join(file),
None => bail!("failed to get the file field"), None => bail!("failed to get the file field"),
}; };
return help::read_mapping(&file_path); return help::read_mapping(&file_path).await;
} }
bail!("failed to find the current profile \"uid:{current}\""); bail!("failed to find the current profile \"uid:{current}\"");
} }
@@ -531,25 +595,25 @@ impl IProfiles {
total_files += 1; total_files += 1;
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
if Self::is_profile_file(file_name) { && Self::is_profile_file(file_name)
// 检查是否为全局扩展文件 {
if protected_files.contains(file_name) { // 检查是否为全局扩展文件
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}"); if protected_files.contains(file_name) {
continue; log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
} continue;
}
// 检查是否为活跃文件 // 检查是否为活跃文件
if !active_files.contains(file_name) { if !active_files.contains(file_name) {
match std::fs::remove_file(&path) { match std::fs::remove_file(&path) {
Ok(_) => { Ok(_) => {
deleted_files.push(file_name.to_string()); deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {file_name}"); log::info!(target: "app", "已清理冗余文件: {file_name}");
} }
Err(e) => { Err(e) => {
failed_deletions.push(format!("{file_name}: {e}")); failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "清理文件失败: {file_name} - {e}"); log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
} }
} }
} }
@@ -595,50 +659,44 @@ impl IProfiles {
} }
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件 // 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
if let Some(itype) = &item.itype { if let Some(itype) = &item.itype
if itype == "remote" || itype == "local" { && (itype == "remote" || itype == "local")
if let Some(option) = &item.option { && let Some(option) = &item.option
// 收集关联的扩展文件 {
if let Some(merge_uid) = &option.merge { // 收集关联的扩展文件
if let Ok(merge_item) = self.get_item(merge_uid) { if let Some(merge_uid) = &option.merge
if let Some(file) = &merge_item.file { && let Ok(merge_item) = self.get_item(merge_uid)
active_files.insert(file.clone()); && let Some(file) = &merge_item.file
} {
} active_files.insert(file.clone());
} }
if let Some(script_uid) = &option.script { if let Some(script_uid) = &option.script
if let Ok(script_item) = self.get_item(script_uid) { && let Ok(script_item) = self.get_item(script_uid)
if let Some(file) = &script_item.file { && let Some(file) = &script_item.file
active_files.insert(file.clone()); {
} active_files.insert(file.clone());
} }
}
if let Some(rules_uid) = &option.rules { if let Some(rules_uid) = &option.rules
if let Ok(rules_item) = self.get_item(rules_uid) { && let Ok(rules_item) = self.get_item(rules_uid)
if let Some(file) = &rules_item.file { && let Some(file) = &rules_item.file
active_files.insert(file.clone()); {
} active_files.insert(file.clone());
} }
}
if let Some(proxies_uid) = &option.proxies { if let Some(proxies_uid) = &option.proxies
if let Ok(proxies_item) = self.get_item(proxies_uid) { && let Ok(proxies_item) = self.get_item(proxies_uid)
if let Some(file) = &proxies_item.file { && let Some(file) = &proxies_item.file
active_files.insert(file.clone()); {
} active_files.insert(file.clone());
} }
}
if let Some(groups_uid) = &option.groups { if let Some(groups_uid) = &option.groups
if let Ok(groups_item) = self.get_item(groups_uid) { && let Ok(groups_item) = self.get_item(groups_uid)
if let Some(file) = &groups_item.file { && let Some(file) = &groups_item.file
active_files.insert(file.clone()); {
} active_files.insert(file.clone());
}
}
}
} }
} }
} }
@@ -671,23 +729,95 @@ impl IProfiles {
.unwrap_or(false) .unwrap_or(false)
}) })
} }
}
pub fn auto_cleanup(&self) -> Result<()> { // 特殊的Send-safe helper函数,完全避免跨await持有guard
match self.cleanup_orphaned_files() { use crate::config::Config;
Ok(result) => {
if !result.deleted_files.is_empty() { pub async fn profiles_append_item_with_filedata_safe(
log::info!( item: PrfItem,
target: "app", file_data: Option<String>,
"自动清理完成,删除了 {} 个冗余文件", ) -> Result<()> {
result.deleted_files.len() AsyncHandler::spawn_blocking(move || {
); AsyncHandler::handle().block_on(async {
} let item = PrfItem::from(item, file_data).await?;
Ok(()) let profiles = Config::profiles().await;
} let mut profiles_guard = profiles.data_mut();
Err(e) => { profiles_guard.append_item(item).await
log::warn!(target: "app", "自动清理失败: {e}"); })
Ok(()) })
} .await
} .map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
} }
pub async fn profiles_append_item_safe(item: PrfItem) -> Result<()> {
AsyncHandler::spawn_blocking(move || {
AsyncHandler::handle().block_on(async {
let profiles = Config::profiles().await;
let mut profiles_guard = profiles.data_mut();
profiles_guard.append_item(item).await
})
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
}
pub async fn profiles_patch_item_safe(index: String, item: PrfItem) -> Result<()> {
AsyncHandler::spawn_blocking(move || {
AsyncHandler::handle().block_on(async {
let profiles = Config::profiles().await;
let mut profiles_guard = profiles.data_mut();
profiles_guard.patch_item(index, item).await
})
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
}
pub async fn profiles_delete_item_safe(index: String) -> Result<bool> {
AsyncHandler::spawn_blocking(move || {
AsyncHandler::handle().block_on(async {
let profiles = Config::profiles().await;
let mut profiles_guard = profiles.data_mut();
profiles_guard.delete_item(index).await
})
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
}
pub async fn profiles_reorder_safe(active_id: String, over_id: String) -> Result<()> {
AsyncHandler::spawn_blocking(move || {
AsyncHandler::handle().block_on(async {
let profiles = Config::profiles().await;
let mut profiles_guard = profiles.data_mut();
profiles_guard.reorder(active_id, over_id).await
})
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
}
pub async fn profiles_save_file_safe() -> Result<()> {
AsyncHandler::spawn_blocking(move || {
AsyncHandler::handle().block_on(async {
let profiles = Config::profiles().await;
let profiles_guard = profiles.data_mut();
profiles_guard.save_file().await
})
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
}
pub async fn profiles_draft_update_item_safe(index: String, item: PrfItem) -> Result<()> {
AsyncHandler::spawn_blocking(move || {
AsyncHandler::handle().block_on(async {
let profiles = Config::profiles().await;
let mut profiles_guard = profiles.draft_mut();
profiles_guard.update_item(index, item).await
})
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))?
} }
+136 -1
View File
@@ -1,6 +1,6 @@
use crate::enhance::field::use_keys; use crate::enhance::field::use_keys;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value}; use serde_yaml_ng::{Mapping, Value};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IRuntime { pub struct IRuntime {
@@ -46,4 +46,139 @@ impl IRuntime {
} }
} }
} }
//跟新链式代理配置文件
/// {
/// "proxies":[
/// {
/// name : 入口节点,
/// type: xxx
/// server: xxx
/// port: xxx
/// ports: xxx
/// password: xxx
/// skip-cert-verify: xxx,
/// },
/// {
/// name : hop_node_1_xxxx,
/// type: xxx
/// server: xxx
/// port: xxx
/// ports: xxx
/// password: xxx
/// skip-cert-verify: xxx,
/// dialer-proxy : "入口节点"
/// },
/// {
/// name : 出口节点,
/// type: xxx
/// server: xxx
/// port: xxx
/// ports: xxx
/// password: xxx
/// skip-cert-verify: xxx,
/// dialer-proxy : "hop_node_1_xxxx"
/// }
/// ],
/// "proxy-groups" : [
/// {
/// name : "proxy_chain",
/// type: "select",
/// proxies ["出口节点"]
/// }
/// ]
/// }
///
/// 传入none 为删除
pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) {
if let Some(config) = self.config.as_mut() {
// 获取 默认第一的代理组的名字
let proxy_group_name =
if let Some(Value::Sequence(proxy_groups)) = config.get("proxy-groups") {
if let Some(Value::Mapping(proxy_group)) = proxy_groups.first() {
if let Some(Value::String(proxy_group_name)) = proxy_group.get("name") {
proxy_group_name.to_string()
} else {
"".to_string()
}
} else {
"".to_string()
}
} else {
"".to_string()
};
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
proxies.iter_mut().for_each(|proxy| {
if let Some(proxy) = proxy.as_mapping_mut()
&& proxy.get("dialer-proxy").is_some()
{
proxy.remove("dialer-proxy");
}
});
}
// 清除proxy_chain代理组
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") {
proxy_groups.retain(|proxy_group| {
!matches!(proxy_group.get("name").and_then(|n| n.as_str()), Some(name) if name== "proxy_chain")
});
}
// 清除rules
if let Some(Value::Sequence(rules)) = config.get_mut("rules") {
rules.retain(|rule| rule.as_str() != Some("MATCH,proxy_chain"));
rules.push(Value::String(format!("MATCH,{}", proxy_group_name)));
}
// some 则写入新配置
// 给proxy添加dialer-proxy字段
// 第一个proxy不添加dialer-proxy
// 然后第二个开始,dialer-proxy为上一个元素的name
if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config {
let mut proxy_chain_group = Mapping::new();
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
if let Some(Value::Mapping(proxy)) = proxies
.iter_mut()
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
{
if i != 0
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
{
proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
}
if i == dialer_proxies.len() - 1 {
// 添加proxy-groups
proxy_chain_group
.insert("name".into(), Value::String("proxy_chain".into()));
proxy_chain_group
.insert("type".into(), Value::String("select".into()));
proxy_chain_group.insert(
"proxies".into(),
Value::Sequence(vec![dialer_proxy.to_owned()]),
);
}
}
}
}
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") {
proxy_groups.push(Value::Mapping(proxy_chain_group));
}
// 添加rules
if let Some(Value::Sequence(rules)) = config.get_mut("rules")
&& let Ok(rule) = serde_yaml_ng::to_value("MATCH,proxy_chain")
{
rules.retain(|rule| {
rule.as_str() != Some(&format!("MATCH,{}", proxy_group_name))
});
rules.push(rule);
// *rules = vec![rule];
}
}
}
}
} }
+18 -19
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC}, config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
logging, logging,
utils::{dirs, help, i18n, logging::Type}, utils::{dirs, help, i18n, logging::Type},
}; };
@@ -203,9 +203,6 @@ pub struct IVerge {
/// 启用外部控制器 /// 启用外部控制器
pub enable_external_controller: Option<bool>, pub enable_external_controller: Option<bool>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -237,9 +234,9 @@ impl IVerge {
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"]; pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
/// 验证并修正配置文件中的clash_core值 /// 验证并修正配置文件中的clash_core值
pub fn validate_and_fix_config() -> Result<()> { pub async fn validate_and_fix_config() -> Result<()> {
let config_path = dirs::verge_path()?; let config_path = dirs::verge_path()?;
let mut config = match help::read_yaml::<IVerge>(&config_path) { let mut config = match help::read_yaml::<IVerge>(&config_path).await {
Ok(config) => config, Ok(config) => config,
Err(_) => Self::template(), Err(_) => Self::template(),
}; };
@@ -273,7 +270,7 @@ impl IVerge {
// 修正后保存配置 // 修正后保存配置
if needs_fix { if needs_fix {
logging!(info, Type::Config, true, "正在保存修正后的配置文件..."); logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?; help::save_yaml(&config_path, &config, Some("# Clash Verge Config")).await?;
logging!( logging!(
info, info,
Type::Config, Type::Config,
@@ -281,7 +278,7 @@ impl IVerge {
"配置文件修正完成,需要重新加载配置" "配置文件修正完成,需要重新加载配置"
); );
Self::reload_config_after_fix(config)?; Self::reload_config_after_fix(config).await?;
} else { } else {
logging!( logging!(
info, info,
@@ -296,10 +293,10 @@ impl IVerge {
} }
/// 配置修正后重新加载配置 /// 配置修正后重新加载配置
fn reload_config_after_fix(updated_config: IVerge) -> Result<()> { async fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
use crate::config::Config; use crate::config::Config;
let config_draft = Config::verge(); let config_draft = Config::verge().await;
*config_draft.draft_mut() = Box::new(updated_config.clone()); *config_draft.draft_mut() = Box::new(updated_config.clone());
config_draft.apply(); config_draft.apply();
@@ -335,9 +332,15 @@ impl IVerge {
} }
} }
pub fn new() -> Self { pub async fn new() -> Self {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) { match dirs::verge_path() {
Ok(config) => config, Ok(path) => match help::read_yaml::<IVerge>(&path).await {
Ok(config) => config,
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
}
},
Err(err) => { Err(err) => {
log::error!(target: "app", "{err}"); log::error!(target: "app", "{err}");
Self::template() Self::template()
@@ -401,15 +404,14 @@ impl IVerge {
auto_light_weight_minutes: Some(10), auto_light_weight_minutes: Some(10),
enable_dns_settings: Some(false), enable_dns_settings: Some(false),
home_cards: None, home_cards: None,
service_state: None,
enable_external_controller: Some(false), enable_external_controller: Some(false),
..Self::default() ..Self::default()
} }
} }
/// Save IVerge App Config /// Save IVerge App Config
pub fn save_file(&self) -> Result<()> { pub async fn save_file(&self) -> Result<()> {
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")) help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")).await
} }
/// patch verge config /// patch verge config
@@ -489,7 +491,6 @@ impl IVerge {
patch!(auto_light_weight_minutes); patch!(auto_light_weight_minutes);
patch!(enable_dns_settings); patch!(enable_dns_settings);
patch!(home_cards); patch!(home_cards);
patch!(service_state);
patch!(enable_external_controller); patch!(enable_external_controller);
} }
@@ -586,7 +587,6 @@ pub struct IVergeResponse {
pub home_cards: Option<serde_json::Value>, pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>, pub enable_hover_jump_navigator: Option<bool>,
pub enable_external_controller: Option<bool>, pub enable_external_controller: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>,
} }
impl From<IVerge> for IVergeResponse { impl From<IVerge> for IVergeResponse {
@@ -658,7 +658,6 @@ impl From<IVerge> for IVergeResponse {
home_cards: verge.home_cards, home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator, enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
enable_external_controller: verge.enable_external_controller, enable_external_controller: verge.enable_external_controller,
service_state: verge.service_state,
} }
} }
} }
+81 -80
View File
@@ -2,7 +2,7 @@
use crate::process::AsyncHandler; use crate::process::AsyncHandler;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::time::{timeout, Duration}; use tokio::time::{Duration, timeout};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use anyhow::anyhow; use anyhow::anyhow;
@@ -87,7 +87,7 @@ impl AsyncProxyQuery {
use std::ptr; use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY}; use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER}; use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
unsafe { unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
@@ -209,13 +209,13 @@ impl AsyncProxyQuery {
// Linux: 检查环境变量和GNOME设置 // Linux: 检查环境变量和GNOME设置
// 首先检查环境变量 // 首先检查环境变量
if let Ok(auto_proxy) = std::env::var("auto_proxy") { if let Ok(auto_proxy) = std::env::var("auto_proxy")
if !auto_proxy.is_empty() { && !auto_proxy.is_empty()
return Ok(AsyncAutoproxy { {
enable: true, return Ok(AsyncAutoproxy {
url: auto_proxy, enable: true,
}); url: auto_proxy,
} });
} }
// 尝试使用 gsettings 获取 GNOME 代理设置 // 尝试使用 gsettings 获取 GNOME 代理设置
@@ -224,31 +224,31 @@ impl AsyncProxyQuery {
.output() .output()
.await; .await;
if let Ok(output) = output { if let Ok(output) = output
if output.status.success() { && output.status.success()
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string(); {
if mode.contains("auto") { let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
// 获取 PAC URL if mode.contains("auto") {
let pac_output = Command::new("gsettings") // 获取 PAC URL
.args(["get", "org.gnome.system.proxy", "autoconfig-url"]) let pac_output = Command::new("gsettings")
.output() .args(["get", "org.gnome.system.proxy", "autoconfig-url"])
.await; .output()
.await;
if let Ok(pac_output) = pac_output { if let Ok(pac_output) = pac_output
if pac_output.status.success() { && pac_output.status.success()
let pac_url = String::from_utf8_lossy(&pac_output.stdout) {
.trim() let pac_url = String::from_utf8_lossy(&pac_output.stdout)
.trim_matches('\'') .trim()
.trim_matches('"') .trim_matches('\'')
.to_string(); .trim_matches('"')
.to_string();
if !pac_url.is_empty() { if !pac_url.is_empty() {
return Ok(AsyncAutoproxy { return Ok(AsyncAutoproxy {
enable: true, enable: true,
url: pac_url, url: pac_url,
}); });
}
}
} }
} }
} }
@@ -271,7 +271,7 @@ impl AsyncProxyQuery {
use std::ptr; use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY}; use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER}; use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
unsafe { unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
@@ -402,10 +402,10 @@ impl AsyncProxyQuery {
http_host = host_part.trim().to_string(); http_host = host_part.trim().to_string();
} }
} else if line.contains("HTTPPort") { } else if line.contains("HTTPPort") {
if let Some(port_part) = line.split(':').nth(1) { if let Some(port_part) = line.split(':').nth(1)
if let Ok(port) = port_part.trim().parse::<u16>() { && let Ok(port) = port_part.trim().parse::<u16>()
http_port = port; {
} http_port = port;
} }
} else if line.contains("ExceptionsList") { } else if line.contains("ExceptionsList") {
// 解析异常列表 // 解析异常列表
@@ -431,16 +431,16 @@ impl AsyncProxyQuery {
// Linux: 检查环境变量和桌面环境设置 // Linux: 检查环境变量和桌面环境设置
// 首先检查环境变量 // 首先检查环境变量
if let Ok(http_proxy) = std::env::var("http_proxy") { if let Ok(http_proxy) = std::env::var("http_proxy")
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) { && let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy)
return Ok(proxy_info); {
} return Ok(proxy_info);
} }
if let Ok(https_proxy) = std::env::var("https_proxy") { if let Ok(https_proxy) = std::env::var("https_proxy")
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) { && let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy)
return Ok(proxy_info); {
} return Ok(proxy_info);
} }
// 尝试使用 gsettings 获取 GNOME 代理设置 // 尝试使用 gsettings 获取 GNOME 代理设置
@@ -449,45 +449,46 @@ impl AsyncProxyQuery {
.output() .output()
.await; .await;
if let Ok(mode_output) = mode_output { if let Ok(mode_output) = mode_output
if mode_output.status.success() { && mode_output.status.success()
let mode = String::from_utf8_lossy(&mode_output.stdout) {
.trim() let mode = String::from_utf8_lossy(&mode_output.stdout)
.to_string(); .trim()
if mode.contains("manual") { .to_string();
// 获取HTTP代理设置 if mode.contains("manual") {
let host_result = Command::new("gsettings") // 获取HTTP代理设置
.args(["get", "org.gnome.system.proxy.http", "host"]) let host_result = Command::new("gsettings")
.output() .args(["get", "org.gnome.system.proxy.http", "host"])
.await; .output()
.await;
let port_result = Command::new("gsettings") let port_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "port"]) .args(["get", "org.gnome.system.proxy.http", "port"])
.output() .output()
.await; .await;
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) { if let (Ok(host_output), Ok(port_output)) = (host_result, port_result)
if host_output.status.success() && port_output.status.success() { && host_output.status.success()
let host = String::from_utf8_lossy(&host_output.stdout) && port_output.status.success()
.trim() {
.trim_matches('\'') let host = String::from_utf8_lossy(&host_output.stdout)
.trim_matches('"') .trim()
.to_string(); .trim_matches('\'')
.trim_matches('"')
.to_string();
let port = String::from_utf8_lossy(&port_output.stdout) let port = String::from_utf8_lossy(&port_output.stdout)
.trim() .trim()
.parse::<u16>() .parse::<u16>()
.unwrap_or(8080); .unwrap_or(8080);
if !host.is_empty() { if !host.is_empty() {
return Ok(AsyncSysproxy { return Ok(AsyncSysproxy {
enable: true, enable: true,
host, host,
port, port,
bypass: String::new(), bypass: String::new(),
}); });
}
}
} }
} }
} }
+11 -7
View File
@@ -74,11 +74,14 @@ impl WebDavClient {
// 获取或创建配置 // 获取或创建配置
let config = { let config = {
let mut lock = self.config.lock(); // 首先检查是否已有配置
if let Some(cfg) = lock.as_ref() { let existing_config = self.config.lock().as_ref().cloned();
cfg.clone()
if let Some(cfg) = existing_config {
cfg
} else { } else {
let verge = Config::verge().latest_ref().clone(); // 释放锁后获取异步配置
let verge = Config::verge().await.latest_ref().clone();
if verge.webdav_url.is_none() if verge.webdav_url.is_none()
|| verge.webdav_username.is_none() || verge.webdav_username.is_none()
|| verge.webdav_password.is_none() || verge.webdav_password.is_none()
@@ -97,7 +100,8 @@ impl WebDavClient {
password: verge.webdav_password.unwrap_or_default(), password: verge.webdav_password.unwrap_or_default(),
}; };
*lock = Some(config.clone()); // 重新获取锁并存储配置
*self.config.lock() = Some(config.clone());
config config
} }
}; };
@@ -260,14 +264,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?; zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
let mut verge_config: serde_json::Value = let mut verge_config: serde_json::Value =
serde_yaml::from_str(&fs::read_to_string(dirs::verge_path()?)?)?; serde_yaml_ng::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
if let Some(obj) = verge_config.as_object_mut() { if let Some(obj) = verge_config.as_object_mut() {
obj.remove("webdav_username"); obj.remove("webdav_username");
obj.remove("webdav_password"); obj.remove("webdav_password");
obj.remove("webdav_url"); obj.remove("webdav_url");
} }
zip.start_file(dirs::VERGE_CONFIG, options)?; zip.start_file(dirs::VERGE_CONFIG, options)?;
zip.write_all(serde_yaml::to_string(&verge_config)?.as_bytes())?; zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?;
zip.start_file(dirs::PROFILE_YAML, options)?; zip.start_file(dirs::PROFILE_YAML, options)?;
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?; zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
+99 -290
View File
@@ -1,13 +1,13 @@
#[cfg(target_os = "windows")]
use crate::AsyncHandler;
use crate::{ use crate::{
config::*, config::*,
core::{ core::{
handle, handle,
service::{self}, service::{self, SERVICE_MANAGER, ServiceStatus},
}, },
ipc::IpcManager, ipc::IpcManager,
logging, logging_error, logging, logging_error, singleton_lazy,
process::AsyncHandler,
singleton_lazy,
utils::{ utils::{
dirs, dirs,
help::{self}, help::{self},
@@ -19,12 +19,12 @@ use chrono::Local;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
fmt, fmt,
fs::{create_dir_all, File}, fs::{File, create_dir_all},
io::Write, io::Write,
path::PathBuf, path::PathBuf,
sync::Arc, sync::Arc,
}; };
use tauri_plugin_shell::{process::CommandChild, ShellExt}; use tauri_plugin_shell::{ShellExt, process::CommandChild};
#[derive(Debug)] #[derive(Debug)]
pub struct CoreManager { pub struct CoreManager {
@@ -137,25 +137,25 @@ impl CoreManager {
Ok(false) Ok(false)
} }
/// 使用默认配置 /// 使用默认配置
pub fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> { pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
*Config::runtime().draft_mut() = Box::new(IRuntime {
config: Some(Config::clash().latest_ref().0.clone()), // Extract clash config before async operations
let clash_config = Config::clash().await.latest_ref().0.clone();
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
config: Some(clash_config.clone()),
exists_keys: vec![], exists_keys: vec![],
chain_logs: Default::default(), chain_logs: Default::default(),
}); });
help::save_yaml( help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?;
&runtime_path,
&Config::clash().latest_ref().0,
Some("# Clash Verge Runtime"),
)?;
handle::Handle::notice_message(msg_type, msg_content); handle::Handle::notice_message(msg_type, msg_content);
Ok(()) Ok(())
} }
/// 验证运行时配置 /// 验证运行时配置
pub async fn validate_config(&self) -> Result<(bool, String)> { pub async fn validate_config(&self) -> Result<(bool, String)> {
logging!(info, Type::Config, true, "生成临时配置文件用于验证"); logging!(info, Type::Config, true, "生成临时配置文件用于验证");
let config_path = Config::generate_file(ConfigType::Check)?; let config_path = Config::generate_file(ConfigType::Check).await?;
let config_path = dirs::path_to_str(&config_path)?; let config_path = dirs::path_to_str(&config_path)?;
self.validate_config_internal(config_path).await self.validate_config_internal(config_path).await
} }
@@ -248,7 +248,7 @@ impl CoreManager {
config_path config_path
); );
let clash_core = Config::verge().latest_ref().get_valid_clash_core(); let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
logging!(info, Type::Config, true, "使用内核: {}", clash_core); logging!(info, Type::Config, true, "使用内核: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| { let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
@@ -317,7 +317,7 @@ impl CoreManager {
}; };
// 对YAML文件尝试解析,只检查语法正确性 // 对YAML文件尝试解析,只检查语法正确性
logging!(info, Type::Config, true, "进行YAML语法检查"); logging!(info, Type::Config, true, "进行YAML语法检查");
match serde_yaml::from_str::<serde_yaml::Value>(&content) { match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content) {
Ok(_) => { Ok(_) => {
logging!(info, Type::Config, true, "YAML语法检查通过"); logging!(info, Type::Config, true, "YAML语法检查通过");
Ok((true, String::new())) Ok((true, String::new()))
@@ -384,30 +384,27 @@ impl CoreManager {
return Ok((true, String::new())); return Ok((true, String::new()));
} }
logging!(info, Type::Config, true, "开始更新配置");
// 1. 先生成新的配置内容 // 1. 先生成新的配置内容
logging!(info, Type::Config, true, "生成新的配置内容"); logging!(info, Type::Config, true, "生成新的配置内容");
Config::generate()?; Config::generate().await?;
// 2. 验证配置 // 2. 验证配置
match self.validate_config().await { match self.validate_config().await {
Ok((true, _)) => { Ok((true, _)) => {
logging!(info, Type::Config, true, "配置验证通过");
// 4. 验证通过后,生成正式的运行时配置 // 4. 验证通过后,生成正式的运行时配置
logging!(info, Type::Config, true, "生成运行时配置"); logging!(info, Type::Config, true, "配置验证通过, 生成运行时配置");
let run_path = Config::generate_file(ConfigType::Run)?; 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, true, self.put_configs_force(run_path).await);
Ok((true, "something".into())) Ok((true, "something".into()))
} }
Ok((false, error_msg)) => { Ok((false, error_msg)) => {
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg); logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
Config::runtime().discard(); Config::runtime().await.discard();
Ok((false, error_msg)) Ok((false, error_msg))
} }
Err(e) => { Err(e) => {
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e); logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
Config::runtime().discard(); Config::runtime().await.discard();
Err(e) Err(e)
} }
} }
@@ -420,13 +417,13 @@ impl CoreManager {
}); });
match IpcManager::global().put_configs_force(run_path_str?).await { match IpcManager::global().put_configs_force(run_path_str?).await {
Ok(_) => { Ok(_) => {
Config::runtime().apply(); Config::runtime().await.apply();
logging!(info, Type::Core, true, "Configuration updated successfully"); logging!(info, Type::Core, true, "Configuration updated successfully");
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
let msg = e.to_string(); let msg = e.to_string();
Config::runtime().discard(); Config::runtime().await.discard();
logging_error!(Type::Core, true, "Failed to update configuration: {}", msg); logging_error!(Type::Core, true, "Failed to update configuration: {}", msg);
Err(msg) Err(msg)
} }
@@ -467,18 +464,18 @@ impl CoreManager {
Ok((pids, process_name)) => { Ok((pids, process_name)) => {
for pid in pids { for pid in pids {
// 跳过当前管理的进程 // 跳过当前管理的进程
if let Some(current) = current_pid { if let Some(current) = current_pid
if pid == current { && pid == current
logging!( {
debug, logging!(
Type::Core, debug,
true, Type::Core,
"跳过当前管理的进程: {} (PID: {})", true,
process_name, "跳过当前管理的进程: {} (PID: {})",
pid process_name,
); pid
continue; );
} continue;
} }
pids_to_kill.push((pid, process_name.clone())); pids_to_kill.push((pid, process_name.clone()));
} }
@@ -527,7 +524,7 @@ impl CoreManager {
use std::mem; use std::mem;
use winapi::um::handleapi::CloseHandle; use winapi::um::handleapi::CloseHandle;
use winapi::um::tlhelp32::{ use winapi::um::tlhelp32::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS, TH32CS_SNAPPROCESS,
}; };
use winapi::um::winnt::HANDLE; use winapi::um::winnt::HANDLE;
@@ -703,7 +700,7 @@ impl CoreManager {
use winapi::um::processthreadsapi::OpenProcess; use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION}; use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
let result = AsyncHandler::spawn_blocking(move || -> Result<bool> { AsyncHandler::spawn_blocking(move || -> Result<bool> {
unsafe { unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid); let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if process_handle.is_null() { if process_handle.is_null() {
@@ -719,9 +716,7 @@ impl CoreManager {
Ok(exit_code == 259) Ok(exit_code == 259)
} }
}) })
.await?; .await?
result
} }
#[cfg(not(windows))] #[cfg(not(windows))]
@@ -735,13 +730,14 @@ impl CoreManager {
} }
} }
fn start_core_by_sidecar(&self) -> Result<()> { async fn start_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Running core by sidecar"); logging!(info, Type::Core, true, "Running core by sidecar");
let config_file = &Config::generate_file(ConfigType::Run)?;
let config_file = &Config::generate_file(ConfigType::Run).await?;
let app_handle = handle::Handle::global() let app_handle = handle::Handle::global()
.app_handle() .app_handle()
.ok_or(anyhow::anyhow!("failed to get app handle"))?; .ok_or(anyhow::anyhow!("failed to get app handle"))?;
let clash_core = Config::verge().latest_ref().get_valid_clash_core(); let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?; let config_dir = dirs::app_home_dir()?;
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service"); let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
@@ -765,21 +761,19 @@ impl CoreManager {
]) ])
.spawn()?; .spawn()?;
AsyncHandler::spawn(move || async move { while let Some(event) = rx.recv().await {
while let Some(event) = rx.recv().await { if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event { && let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line))
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) { {
logging!( logging!(
error, error,
Type::Core, Type::Core,
true, true,
"[Sidecar] Failed to write stdout to file: {}", "[Sidecar] Failed to write stdout to file: {}",
e e
); );
}
}
} }
}); }
let pid = child.pid(); let pid = child.pid();
logging!( logging!(
@@ -794,7 +788,7 @@ impl CoreManager {
Ok(()) Ok(())
} }
fn stop_core_by_sidecar(&self) -> Result<()> { fn stop_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by sidecar"); logging!(info, Type::Core, true, "Stopping core by sidecar");
if let Some(child) = self.child_sidecar.lock().take() { if let Some(child) = self.child_sidecar.lock().take() {
let pid = child.pid(); let pid = child.pid();
@@ -814,14 +808,14 @@ impl CoreManager {
impl CoreManager { impl CoreManager {
async fn start_core_by_service(&self) -> Result<()> { async fn start_core_by_service(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Running core by service"); logging!(info, Type::Core, true, "Running core by service");
let config_file = &Config::generate_file(ConfigType::Run)?; let config_file = &Config::generate_file(ConfigType::Run).await?;
service::run_core_by_service(config_file).await?; service::run_core_by_service(config_file).await?;
self.set_running_mode(RunningMode::Service); self.set_running_mode(RunningMode::Service);
Ok(()) Ok(())
} }
async fn stop_core_by_service(&self) -> Result<()> { async fn stop_core_by_service(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by service"); logging!(info, Type::Core, true, "Stopping core by service");
service::stop_core_by_service().await?; service::stop_core_by_service().await?;
self.set_running_mode(RunningMode::NotRunning); self.set_running_mode(RunningMode::NotRunning);
Ok(()) Ok(())
@@ -837,58 +831,9 @@ impl Default for CoreManager {
} }
} }
// Use simplified singleton_lazy macro
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
impl CoreManager { impl CoreManager {
// 当服务安装失败时的回退逻辑
async fn attempt_service_init(&self) -> Result<()> {
if service::check_service_needs_reinstall().await {
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装");
if let Err(e) = service::reinstall_service() {
logging!(
warn,
Type::Core,
true,
"服务重装失败 during attempt_service_init: {}",
e
);
return Err(e);
}
// 如果重装成功,还需要尝试启动服务
logging!(info, Type::Core, true, "服务重装成功,尝试启动服务");
}
if let Err(e) = self.start_core_by_service().await {
logging!(
warn,
Type::Core,
true,
"通过服务启动核心失败 during attempt_service_init: {}",
e
);
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
let mut state = service::ServiceState::get();
if !state.prefer_sidecar {
state.prefer_sidecar = true;
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
if let Err(save_err) = state.save() {
logging!(
error,
Type::Core,
true,
"保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}",
save_err
);
}
}
return Err(e);
}
Ok(())
}
pub async fn init(&self) -> Result<()> { pub async fn init(&self) -> Result<()> {
logging!(trace, Type::Core, "Initializing core"); logging!(info, Type::Core, "Initializing core");
// 应用启动时先清理任何遗留的 mihomo 进程 // 应用启动时先清理任何遗留的 mihomo 进程
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await { if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
@@ -901,155 +846,11 @@ impl CoreManager {
); );
} }
let mut core_started_successfully = false; // 使用简化的启动流程
logging!(info, Type::Core, true, "开始核心初始化");
self.start_core().await?;
if service::is_service_available().await.is_ok() { logging!(info, Type::Core, true, "核心初始化完成");
logging!(
info,
Type::Core,
true,
"服务当前可用或看似可用,尝试通过服务模式启动/重装"
);
match self.attempt_service_init().await {
Ok(_) => {
logging!(info, Type::Core, true, "服务模式成功启动核心");
core_started_successfully = true;
}
Err(_err) => {
logging!(
warn,
Type::Core,
true,
"服务模式启动或重装失败。将尝试Sidecar模式回退。"
);
}
}
} else {
logging!(
info,
Type::Core,
true,
"服务初始不可用 (is_service_available 调用失败)"
);
}
if !core_started_successfully {
logging!(
info,
Type::Core,
true,
"核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑"
);
let service_state = service::ServiceState::get();
if service_state.prefer_sidecar {
logging!(
info,
Type::Core,
true,
"用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动"
);
self.start_core_by_sidecar()?;
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
// 后续的 Tray::global().subscribe_traffic().await 仍然会执行
} else {
let has_service_install_record = service_state.last_install_time > 0;
if !has_service_install_record {
logging!(
info,
Type::Core,
true,
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
);
match service::install_service() {
Ok(_) => {
logging!(info, Type::Core, true, "服务安装成功(首次尝试)");
let mut new_state = service::ServiceState::default();
new_state.record_install();
new_state.prefer_sidecar = false;
new_state.save()?;
if service::is_service_available().await.is_ok() {
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动");
if self.start_core_by_service().await.is_ok() {
logging!(info, Type::Core, true, "新安装的服务启动成功");
} else {
logging!(
warn,
Type::Core,
true,
"新安装的服务启动失败,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get();
final_state.prefer_sidecar = true;
final_state.last_error =
Some("Newly installed service failed to start".to_string());
final_state.save()?;
self.start_core_by_sidecar()?;
}
} else {
logging!(
warn,
Type::Core,
true,
"服务安装成功但未能连接/立即可用,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get();
final_state.prefer_sidecar = true;
final_state.last_error = Some(
"Newly installed service not immediately available/connectable"
.to_string(),
);
final_state.save()?;
self.start_core_by_sidecar()?;
}
}
Err(err) => {
logging!(warn, Type::Core, true, "服务首次安装失败: {}", err);
let new_state = service::ServiceState {
last_error: Some(err.to_string()),
prefer_sidecar: true,
..Default::default()
};
new_state.save()?;
self.start_core_by_sidecar()?;
}
}
} else {
// 有安装记录,服务未成功启动,且初始不偏好sidecar
// 这意味着服务之前可能可用,但 attempt_service_init 失败了(并应已设置 prefer_sidecar),
// 或者服务初始不可用,无偏好,有记录。应强制使用 sidecar。
logging!(
info,
Type::Core,
true,
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式"
);
let mut final_state = service::ServiceState::get();
if !final_state.prefer_sidecar {
logging!(
warn,
Type::Core,
true,
"prefer_sidecar 为 false,因服务启动失败或不可用而强制设置为 true"
);
final_state.prefer_sidecar = true;
final_state.last_error =
Some(final_state.last_error.unwrap_or_else(|| {
"Service startup failed or unavailable before sidecar fallback"
.to_string()
}));
final_state.save()?;
}
self.start_core_by_sidecar()?;
}
}
}
logging!(trace, Type::Core, "Initied core logic completed");
// #[cfg(target_os = "macos")]
// logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
Ok(()) Ok(())
} }
@@ -1063,33 +864,35 @@ impl CoreManager {
(*guard).clone() (*guard).clone()
} }
/// 启动核心 pub async fn prestart_core(&self) -> Result<()> {
pub async fn start_core(&self) -> Result<()> { SERVICE_MANAGER.lock().await.refresh().await?;
if service::is_service_available().await.is_ok() { match SERVICE_MANAGER.lock().await.current() {
if service::check_service_needs_reinstall().await { ServiceStatus::Ready => {
service::reinstall_service()?; self.set_running_mode(RunningMode::Service);
} }
logging!(info, Type::Core, true, "服务可用,使用服务模式启动"); _ => {
self.start_core_by_service().await?; self.set_running_mode(RunningMode::Sidecar);
} else {
// 服务不可用,检查用户偏好
let service_state = service::ServiceState::get();
if service_state.prefer_sidecar {
logging!(
info,
Type::Core,
true,
"服务不可用,根据用户偏好使用Sidecar模式"
);
self.start_core_by_sidecar()?;
} else {
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
self.start_core_by_sidecar()?;
} }
} }
Ok(()) Ok(())
} }
/// 启动核心
pub async fn start_core(&self) -> Result<()> {
self.prestart_core().await?;
match self.get_running_mode() {
RunningMode::Service => {
logging_error!(Type::Core, true, self.start_core_by_service().await);
}
RunningMode::NotRunning | RunningMode::Sidecar => {
logging_error!(Type::Core, true, self.start_core_by_sidecar().await);
}
};
Ok(())
}
/// 停止核心运行 /// 停止核心运行
pub async fn stop_core(&self) -> Result<()> { pub async fn stop_core(&self) -> Result<()> {
match self.get_running_mode() { match self.get_running_mode() {
@@ -1101,8 +904,8 @@ impl CoreManager {
/// 重启内核 /// 重启内核
pub async fn restart_core(&self) -> Result<()> { pub async fn restart_core(&self) -> Result<()> {
logging!(info, Type::Core, true, "Restarting core");
self.stop_core().await?; self.stop_core().await?;
self.start_core().await?; self.start_core().await?;
Ok(()) Ok(())
} }
@@ -1125,11 +928,14 @@ impl CoreManager {
return Err(error_message); return Err(error_message);
} }
Config::verge().draft_mut().clash_core = clash_core.clone(); Config::verge().await.draft_mut().clash_core = clash_core.clone();
Config::verge().apply(); Config::verge().await.apply();
logging_error!(Type::Core, true, Config::verge().latest_ref().save_file());
let run_path = Config::generate_file(ConfigType::Run).map_err(|e| { // 分离数据获取和异步调用避免Send问题
let verge_data = Config::verge().await.latest_ref().clone();
logging_error!(Type::Core, true, verge_data.save_file().await);
let run_path = Config::generate_file(ConfigType::Run).await.map_err(|e| {
let msg = e.to_string(); let msg = e.to_string();
logging_error!(Type::Core, true, "{}", msg); logging_error!(Type::Core, true, "{}", msg);
msg msg
@@ -1140,3 +946,6 @@ impl CoreManager {
Ok(()) Ok(())
} }
} }
// Use simplified singleton_lazy macro
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
+70 -67
View File
@@ -1,8 +1,8 @@
use parking_lot::RwLock;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use tokio::time::{sleep, timeout, Duration}; use tokio::time::{Duration, sleep, timeout};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use crate::config::{Config, IVerge}; use crate::config::{Config, IVerge};
use crate::core::async_proxy_query::AsyncProxyQuery; use crate::core::async_proxy_query::AsyncProxyQuery;
@@ -99,7 +99,8 @@ impl EventDrivenProxyManager {
let (event_tx, event_rx) = mpsc::unbounded_channel(); let (event_tx, event_rx) = mpsc::unbounded_channel();
let (query_tx, query_rx) = mpsc::unbounded_channel(); let (query_tx, query_rx) = mpsc::unbounded_channel();
Self::start_event_loop(Arc::clone(&state), event_rx, query_rx); let state_clone = Arc::clone(&state);
AsyncHandler::spawn(move || Self::start_event_loop(state_clone, event_rx, query_rx));
Self { Self {
state, state,
@@ -109,8 +110,8 @@ impl EventDrivenProxyManager {
} }
/// 获取自动代理配置(缓存) /// 获取自动代理配置(缓存)
pub fn get_auto_proxy_cached(&self) -> Autoproxy { pub async fn get_auto_proxy_cached(&self) -> Autoproxy {
self.state.read().auto_proxy.clone() self.state.read().await.auto_proxy.clone()
} }
/// 异步获取最新的自动代理配置 /// 异步获取最新的自动代理配置
@@ -120,14 +121,14 @@ impl EventDrivenProxyManager {
if self.query_sender.send(query).is_err() { if self.query_sender.send(query).is_err() {
log::error!(target: "app", "发送查询请求失败,返回缓存数据"); log::error!(target: "app", "发送查询请求失败,返回缓存数据");
return self.get_auto_proxy_cached(); return self.get_auto_proxy_cached().await;
} }
match timeout(Duration::from_secs(5), rx).await { match timeout(Duration::from_secs(5), rx).await {
Ok(Ok(result)) => result, Ok(Ok(result)) => result,
_ => { _ => {
log::warn!(target: "app", "查询超时,返回缓存数据"); log::warn!(target: "app", "查询超时,返回缓存数据");
self.get_auto_proxy_cached() self.get_auto_proxy_cached().await
} }
} }
} }
@@ -172,36 +173,34 @@ impl EventDrivenProxyManager {
} }
} }
pub fn start_event_loop( pub async fn start_event_loop(
state: Arc<RwLock<ProxyState>>, state: Arc<RwLock<ProxyState>>,
event_rx: mpsc::UnboundedReceiver<ProxyEvent>, event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
query_rx: mpsc::UnboundedReceiver<QueryRequest>, query_rx: mpsc::UnboundedReceiver<QueryRequest>,
) { ) {
AsyncHandler::spawn(move || async move { log::info!(target: "app", "事件驱动代理管理器启动");
log::info!(target: "app", "事件驱动代理管理器启动");
// 将 mpsc 接收器包装成 Stream,避免每次循环创建 future // 将 mpsc 接收器包装成 Stream,避免每次循环创建 future
let mut event_stream = UnboundedReceiverStream::new(event_rx); let mut event_stream = UnboundedReceiverStream::new(event_rx);
let mut query_stream = UnboundedReceiverStream::new(query_rx); let mut query_stream = UnboundedReceiverStream::new(query_rx);
loop { loop {
tokio::select! { tokio::select! {
Some(event) = event_stream.next() => { Some(event) = event_stream.next() => {
log::debug!(target: "app", "处理代理事件: {event:?}"); log::debug!(target: "app", "处理代理事件: {event:?}");
Self::handle_event(&state, event).await; Self::handle_event(&state, event).await;
} }
Some(query) = query_stream.next() => { Some(query) = query_stream.next() => {
let result = Self::handle_query(&state).await; let result = Self::handle_query(&state).await;
let _ = query.response_tx.send(result); let _ = query.response_tx.send(result);
} }
else => { else => {
// 两个通道都关闭时退出 // 两个通道都关闭时退出
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止"); log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
break; break;
}
} }
} }
}); }
} }
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) { async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
@@ -235,7 +234,8 @@ impl EventDrivenProxyManager {
Self::update_state_timestamp(state, |s| { Self::update_state_timestamp(state, |s| {
s.auto_proxy = auto_proxy.clone(); s.auto_proxy = auto_proxy.clone();
}); })
.await;
auto_proxy auto_proxy
} }
@@ -243,7 +243,7 @@ impl EventDrivenProxyManager {
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) { async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "初始化代理状态"); log::info!(target: "app", "初始化代理状态");
let config = Self::get_proxy_config(); let config = Self::get_proxy_config().await;
let auto_proxy = Self::get_auto_proxy_with_timeout().await; let auto_proxy = Self::get_auto_proxy_with_timeout().await;
let sys_proxy = Self::get_sys_proxy_with_timeout().await; let sys_proxy = Self::get_sys_proxy_with_timeout().await;
@@ -253,7 +253,8 @@ impl EventDrivenProxyManager {
s.auto_proxy = auto_proxy; s.auto_proxy = auto_proxy;
s.sys_proxy = sys_proxy; s.sys_proxy = sys_proxy;
s.is_healthy = true; s.is_healthy = true;
}); })
.await;
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled); log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
} }
@@ -261,12 +262,13 @@ impl EventDrivenProxyManager {
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) { async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
log::debug!(target: "app", "更新代理配置"); log::debug!(target: "app", "更新代理配置");
let config = Self::get_proxy_config(); let config = Self::get_proxy_config().await;
Self::update_state_timestamp(state, |s| { Self::update_state_timestamp(state, |s| {
s.sys_enabled = config.sys_enabled; s.sys_enabled = config.sys_enabled;
s.pac_enabled = config.pac_enabled; s.pac_enabled = config.pac_enabled;
}); })
.await;
if config.guard_enabled && config.sys_enabled { if config.guard_enabled && config.sys_enabled {
Self::check_and_restore_proxy(state).await; Self::check_and_restore_proxy(state).await;
@@ -275,7 +277,7 @@ impl EventDrivenProxyManager {
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) { async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
let (sys_enabled, pac_enabled) = { let (sys_enabled, pac_enabled) = {
let s = state.read(); let s = state.read().await;
(s.sys_enabled, s.pac_enabled) (s.sys_enabled, s.pac_enabled)
}; };
@@ -294,11 +296,12 @@ impl EventDrivenProxyManager {
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) { async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
let current = Self::get_auto_proxy_with_timeout().await; let current = Self::get_auto_proxy_with_timeout().await;
let expected = Self::get_expected_pac_config(); let expected = Self::get_expected_pac_config().await;
Self::update_state_timestamp(state, |s| { Self::update_state_timestamp(state, |s| {
s.auto_proxy = current.clone(); s.auto_proxy = current.clone();
}); })
.await;
if !current.enable || current.url != expected.url { if !current.enable || current.url != expected.url {
log::info!(target: "app", "PAC代理设置异常,正在恢复..."); log::info!(target: "app", "PAC代理设置异常,正在恢复...");
@@ -312,17 +315,19 @@ impl EventDrivenProxyManager {
Self::update_state_timestamp(state, |s| { Self::update_state_timestamp(state, |s| {
s.is_healthy = restored.enable && restored.url == expected.url; s.is_healthy = restored.enable && restored.url == expected.url;
s.auto_proxy = restored; s.auto_proxy = restored;
}); })
.await;
} }
} }
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) { async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
let current = Self::get_sys_proxy_with_timeout().await; let current = Self::get_sys_proxy_with_timeout().await;
let expected = Self::get_expected_sys_proxy(); let expected = Self::get_expected_sys_proxy().await;
Self::update_state_timestamp(state, |s| { Self::update_state_timestamp(state, |s| {
s.sys_proxy = current.clone(); s.sys_proxy = current.clone();
}); })
.await;
if !current.enable || current.host != expected.host || current.port != expected.port { if !current.enable || current.host != expected.host || current.port != expected.port {
log::info!(target: "app", "系统代理设置异常,正在恢复..."); log::info!(target: "app", "系统代理设置异常,正在恢复...");
@@ -338,22 +343,23 @@ impl EventDrivenProxyManager {
&& restored.host == expected.host && restored.host == expected.host
&& restored.port == expected.port; && restored.port == expected.port;
s.sys_proxy = restored; s.sys_proxy = restored;
}); })
.await;
} }
} }
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) { async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "启用系统代理"); log::info!(target: "app", "启用系统代理");
let pac_enabled = state.read().pac_enabled; let pac_enabled = state.read().await.pac_enabled;
if pac_enabled { if pac_enabled {
let expected = Self::get_expected_pac_config(); let expected = Self::get_expected_pac_config().await;
if let Err(e) = Self::restore_pac_proxy(&expected.url).await { if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
log::error!(target: "app", "启用PAC代理失败: {}", e); log::error!(target: "app", "启用PAC代理失败: {}", e);
} }
} else { } else {
let expected = Self::get_expected_sys_proxy(); let expected = Self::get_expected_sys_proxy().await;
if let Err(e) = Self::restore_sys_proxy(&expected).await { if let Err(e) = Self::restore_sys_proxy(&expected).await {
log::error!(target: "app", "启用系统代理失败: {}", e); log::error!(target: "app", "启用系统代理失败: {}", e);
} }
@@ -382,7 +388,7 @@ impl EventDrivenProxyManager {
let disabled_sys = Sysproxy::default(); let disabled_sys = Sysproxy::default();
logging_error!(Type::System, true, disabled_sys.set_system_proxy()); logging_error!(Type::System, true, disabled_sys.set_system_proxy());
let expected = Self::get_expected_pac_config(); let expected = Self::get_expected_pac_config().await;
if let Err(e) = Self::restore_pac_proxy(&expected.url).await { if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
log::error!(target: "app", "切换到PAC模式失败: {}", e); log::error!(target: "app", "切换到PAC模式失败: {}", e);
} }
@@ -390,13 +396,13 @@ impl EventDrivenProxyManager {
let disabled_auto = Autoproxy::default(); let disabled_auto = Autoproxy::default();
logging_error!(Type::System, true, disabled_auto.set_auto_proxy()); logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
let expected = Self::get_expected_sys_proxy(); let expected = Self::get_expected_sys_proxy().await;
if let Err(e) = Self::restore_sys_proxy(&expected).await { if let Err(e) = Self::restore_sys_proxy(&expected).await {
log::error!(target: "app", "切换到HTTP代理模式失败: {}", e); log::error!(target: "app", "切换到HTTP代理模式失败: {}", e);
} }
} }
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac); Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac).await;
Self::check_and_restore_proxy(state).await; Self::check_and_restore_proxy(state).await;
} }
@@ -423,18 +429,18 @@ impl EventDrivenProxyManager {
} }
// 统一的状态更新方法 // 统一的状态更新方法
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F) async fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
where where
F: FnOnce(&mut ProxyState), F: FnOnce(&mut ProxyState),
{ {
let mut state_guard = state.write(); let mut state_guard = state.write().await;
update_fn(&mut state_guard); update_fn(&mut state_guard);
state_guard.last_updated = std::time::Instant::now(); state_guard.last_updated = std::time::Instant::now();
} }
fn get_proxy_config() -> ProxyConfig { async fn get_proxy_config() -> ProxyConfig {
let (sys_enabled, pac_enabled, guard_enabled) = { let (sys_enabled, pac_enabled, guard_enabled) = {
let verge_config = Config::verge(); let verge_config = Config::verge().await;
let verge = verge_config.latest_ref(); let verge = verge_config.latest_ref();
( (
verge.enable_system_proxy.unwrap_or(false), verge.enable_system_proxy.unwrap_or(false),
@@ -449,9 +455,9 @@ impl EventDrivenProxyManager {
} }
} }
fn get_expected_pac_config() -> Autoproxy { async fn get_expected_pac_config() -> Autoproxy {
let proxy_host = { let proxy_host = {
let verge_config = Config::verge(); let verge_config = Config::verge().await;
let verge = verge_config.latest_ref(); let verge = verge_config.latest_ref();
verge verge
.proxy_host .proxy_host
@@ -465,28 +471,25 @@ impl EventDrivenProxyManager {
} }
} }
fn get_expected_sys_proxy() -> Sysproxy { async fn get_expected_sys_proxy() -> Sysproxy {
let verge_config = Config::verge(); let verge_config = Config::verge().await;
let verge = verge_config.latest_ref(); let verge_mixed_port = verge_config.latest_ref().verge_mixed_port;
let port = verge let proxy_host = verge_config.latest_ref().proxy_host.clone();
.verge_mixed_port
.unwrap_or(Config::clash().latest_ref().get_mixed_port()); let port = verge_mixed_port.unwrap_or(Config::clash().await.latest_ref().get_mixed_port());
let proxy_host = verge let proxy_host = proxy_host.unwrap_or_else(|| "127.0.0.1".to_string());
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".to_string());
Sysproxy { Sysproxy {
enable: true, enable: true,
host: proxy_host, host: proxy_host,
port, port,
bypass: Self::get_bypass_config(), bypass: Self::get_bypass_config().await,
} }
} }
fn get_bypass_config() -> String { async fn get_bypass_config() -> String {
let (use_default, custom_bypass) = { let (use_default, custom_bypass) = {
let verge_config = Config::verge(); let verge_config = Config::verge().await;
let verge = verge_config.latest_ref(); let verge = verge_config.latest_ref();
( (
verge.use_default_bypass.unwrap_or(true), verge.use_default_bypass.unwrap_or(true),
+74 -39
View File
@@ -2,8 +2,9 @@ use crate::singleton;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::{ use std::{
sync::{ sync::{
Arc,
atomic::{AtomicU64, Ordering}, atomic::{AtomicU64, Ordering},
mpsc, Arc, mpsc,
}, },
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
@@ -20,7 +21,6 @@ enum FrontendEvent {
NoticeMessage { status: String, message: String }, NoticeMessage { status: String, message: String },
ProfileChanged { current_profile_id: String }, ProfileChanged { current_profile_id: String },
TimerUpdated { profile_index: String }, TimerUpdated { profile_index: String },
StartupCompleted,
ProfileUpdateStarted { uid: String }, ProfileUpdateStarted { uid: String },
ProfileUpdateCompleted { uid: String }, ProfileUpdateCompleted { uid: String },
} }
@@ -98,16 +98,14 @@ impl NotificationSystem {
let is_emergency = *system.emergency_mode.read(); let is_emergency = *system.emergency_mode.read();
if is_emergency { if is_emergency
if let FrontendEvent::NoticeMessage { ref status, .. } = event { && let FrontendEvent::NoticeMessage { ref status, .. } = event
if status == "info" { && status == "info" {
log::warn!( log::warn!(
"Emergency mode active, skipping info message" "Emergency mode active, skipping info message"
); );
continue; continue;
} }
}
}
if let Some(window) = handle.get_window() { if let Some(window) = handle.get_window() {
*system.last_emit_time.write() = Instant::now(); *system.last_emit_time.write() = Instant::now();
@@ -134,9 +132,6 @@ impl NotificationSystem {
FrontendEvent::TimerUpdated { profile_index } => { FrontendEvent::TimerUpdated { profile_index } => {
("verge://timer-updated", Ok(serde_json::json!(profile_index))) ("verge://timer-updated", Ok(serde_json::json!(profile_index)))
} }
FrontendEvent::StartupCompleted => {
("verge://startup-completed", Ok(serde_json::json!(null)))
}
FrontendEvent::ProfileUpdateStarted { uid } => { FrontendEvent::ProfileUpdateStarted { uid } => {
("profile-update-started", Ok(serde_json::json!({ "uid": uid }))) ("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
} }
@@ -203,13 +198,12 @@ impl NotificationSystem {
/// 发送事件到队列 /// 发送事件到队列
fn send_event(&self, event: FrontendEvent) -> bool { fn send_event(&self, event: FrontendEvent) -> bool {
if *self.emergency_mode.read() { if *self.emergency_mode.read()
if let FrontendEvent::NoticeMessage { ref status, .. } = event { && let FrontendEvent::NoticeMessage { ref status, .. } = event
if status == "info" { && status == "info"
log::info!("Skipping info message in emergency mode"); {
return false; log::info!("Skipping info message in emergency mode");
} return false;
}
} }
if let Some(sender) = &self.sender { if let Some(sender) = &self.sender {
@@ -282,10 +276,10 @@ impl Handle {
Self::default() Self::default()
} }
pub fn init(&self, app_handle: &AppHandle) { pub fn init(&self, app_handle: AppHandle) {
{ {
let mut handle = self.app_handle.write(); let mut handle = self.app_handle.write();
*handle = Some(app_handle.clone()); *handle = Some(app_handle);
} }
let mut system_opt = self.notification_system.write(); let mut system_opt = self.notification_system.write();
@@ -294,6 +288,7 @@ impl Handle {
} }
} }
/// 获取 AppHandle
pub fn app_handle(&self) -> Option<AppHandle> { pub fn app_handle(&self) -> Option<AppHandle> {
self.app_handle.read().clone() self.app_handle.read().clone()
} }
@@ -365,22 +360,6 @@ impl Handle {
} }
} }
pub fn notify_startup_completed() {
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::StartupCompleted);
} else {
log::warn!(
"Notification system not initialized when trying to send StartupCompleted event."
);
}
}
pub fn notify_profile_update_started(uid: String) { pub fn notify_profile_update_started(uid: String) {
let handle = Self::global(); let handle = Self::global();
if handle.is_exiting() { if handle.is_exiting() {
@@ -391,7 +370,9 @@ impl Handle {
if let Some(system) = system_opt.as_ref() { if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::ProfileUpdateStarted { uid }); system.send_event(FrontendEvent::ProfileUpdateStarted { uid });
} else { } else {
log::warn!("Notification system not initialized when trying to send ProfileUpdateStarted event."); log::warn!(
"Notification system not initialized when trying to send ProfileUpdateStarted event."
);
} }
} }
@@ -405,7 +386,9 @@ impl Handle {
if let Some(system) = system_opt.as_ref() { if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::ProfileUpdateCompleted { uid }); system.send_event(FrontendEvent::ProfileUpdateCompleted { uid });
} else { } else {
log::warn!("Notification system not initialized when trying to send ProfileUpdateCompleted event."); log::warn!(
"Notification system not initialized when trying to send ProfileUpdateCompleted event."
);
} }
} }
@@ -470,8 +453,9 @@ impl Handle {
info, info,
Type::Frontend, Type::Frontend,
true, true,
"发送{}条启动时累积的错误消息", "发送{}条启动时累积的错误消息: {:?}",
errors.len() errors.len(),
errors
); );
// 启动单独线程处理启动错误,避免阻塞主线程 // 启动单独线程处理启动错误,避免阻塞主线程
@@ -521,3 +505,54 @@ impl Handle {
*self.is_exiting.read() *self.is_exiting.read()
} }
} }
#[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())
}
}
pub fn set_activation_policy_regular(&self) {
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Regular) {
logging!(
warn,
Type::Setup,
true,
"Failed to set regular activation policy: {}",
e
);
}
}
pub fn set_activation_policy_accessory(&self) {
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Accessory) {
logging!(
warn,
Type::Setup,
true,
"Failed to set accessory activation policy: {}",
e
);
}
}
#[allow(dead_code)]
pub fn set_activation_policy_prohibited(&self) {
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Prohibited) {
logging!(
warn,
Type::Setup,
true,
"Failed to set prohibited activation policy: {}",
e
);
}
}
}
+115 -91
View File
@@ -1,12 +1,13 @@
use crate::utils::notification::{notify_event, NotificationEvent}; use crate::process::AsyncHandler;
use crate::utils::notification::{NotificationEvent, notify_event};
use crate::{ use crate::{
config::Config, core::handle, feat, logging, logging_error, config::Config, core::handle, feat, logging, logging_error,
module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type, module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type,
}; };
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri::Manager; use tauri::{AppHandle, Manager};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
/// Enum representing all available hotkey functions /// Enum representing all available hotkey functions
@@ -103,83 +104,86 @@ impl Hotkey {
} }
/// Execute the function associated with a hotkey function enum /// Execute the function associated with a hotkey function enum
fn execute_function(function: HotkeyFunction, app_handle: &tauri::AppHandle) { fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) {
let app_handle = app_handle.clone();
match function { match function {
HotkeyFunction::OpenOrCloseDashboard => { HotkeyFunction::OpenOrCloseDashboard => {
logging!( AsyncHandler::spawn(async move || {
debug, crate::feat::open_or_close_dashboard().await;
Type::Hotkey, notify_event(app_handle, NotificationEvent::DashboardToggled).await;
true, });
"=== Hotkey Dashboard Window Operation Start ==="
);
logging!(
info,
Type::Hotkey,
true,
"Using unified WindowManager for hotkey operation (bypass debounce)"
);
crate::feat::open_or_close_dashboard_hotkey();
logging!(
debug,
Type::Hotkey,
"=== Hotkey Dashboard Window Operation End ==="
);
notify_event(app_handle, NotificationEvent::DashboardToggled);
} }
HotkeyFunction::ClashModeRule => { HotkeyFunction::ClashModeRule => {
feat::change_clash_mode("rule".into()); AsyncHandler::spawn(async move || {
notify_event( feat::change_clash_mode("rule".into()).await;
app_handle, notify_event(
NotificationEvent::ClashModeChanged { mode: "Rule" }, app_handle,
); NotificationEvent::ClashModeChanged { mode: "Rule" },
)
.await;
});
} }
HotkeyFunction::ClashModeGlobal => { HotkeyFunction::ClashModeGlobal => {
feat::change_clash_mode("global".into()); AsyncHandler::spawn(async move || {
notify_event( feat::change_clash_mode("global".into()).await;
app_handle, notify_event(
NotificationEvent::ClashModeChanged { mode: "Global" }, app_handle,
); NotificationEvent::ClashModeChanged { mode: "Global" },
)
.await;
});
} }
HotkeyFunction::ClashModeDirect => { HotkeyFunction::ClashModeDirect => {
feat::change_clash_mode("direct".into()); AsyncHandler::spawn(async move || {
notify_event( feat::change_clash_mode("direct".into()).await;
app_handle, notify_event(
NotificationEvent::ClashModeChanged { mode: "Direct" }, app_handle,
); NotificationEvent::ClashModeChanged { mode: "Direct" },
)
.await;
});
} }
HotkeyFunction::ToggleSystemProxy => { HotkeyFunction::ToggleSystemProxy => {
feat::toggle_system_proxy(); AsyncHandler::spawn(async move || {
notify_event(app_handle, NotificationEvent::SystemProxyToggled); feat::toggle_system_proxy().await;
notify_event(app_handle, NotificationEvent::SystemProxyToggled).await;
});
} }
HotkeyFunction::ToggleTunMode => { HotkeyFunction::ToggleTunMode => {
feat::toggle_tun_mode(None); AsyncHandler::spawn(async move || {
notify_event(app_handle, NotificationEvent::TunModeToggled); feat::toggle_tun_mode(None).await;
notify_event(app_handle, NotificationEvent::TunModeToggled).await;
});
} }
HotkeyFunction::EntryLightweightMode => { HotkeyFunction::EntryLightweightMode => {
entry_lightweight_mode(); AsyncHandler::spawn(async move || {
notify_event(app_handle, NotificationEvent::LightweightModeEntered); entry_lightweight_mode().await;
notify_event(app_handle, NotificationEvent::LightweightModeEntered).await;
});
} }
HotkeyFunction::Quit => { HotkeyFunction::Quit => {
notify_event(app_handle, NotificationEvent::AppQuit); AsyncHandler::spawn(async move || {
feat::quit(); notify_event(app_handle, NotificationEvent::AppQuit).await;
feat::quit().await;
});
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
HotkeyFunction::Hide => { HotkeyFunction::Hide => {
feat::hide(); AsyncHandler::spawn(async move || {
notify_event(app_handle, NotificationEvent::AppHidden); feat::hide().await;
notify_event(app_handle, NotificationEvent::AppHidden).await;
});
} }
} }
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
/// Register a system hotkey using enum /// Register a system hotkey using enum
pub fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> { pub async fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
let hotkey_str = hotkey.to_string(); let hotkey_str = hotkey.to_string();
let function = hotkey.function(); let function = hotkey.function();
self.register_hotkey_with_function(&hotkey_str, function) self.register_hotkey_with_function(&hotkey_str, function)
.await
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -190,7 +194,8 @@ impl Hotkey {
} }
/// Register a hotkey with function enum /// Register a hotkey with function enum
pub fn register_hotkey_with_function( #[allow(clippy::unused_async)]
pub async fn register_hotkey_with_function(
&self, &self,
hotkey: &str, hotkey: &str,
function: HotkeyFunction, function: HotkeyFunction,
@@ -218,41 +223,55 @@ impl Hotkey {
manager.unregister(hotkey)?; manager.unregister(hotkey)?;
} }
let app_handle_clone = app_handle.clone();
let is_quit = matches!(function, HotkeyFunction::Quit); let is_quit = matches!(function, HotkeyFunction::Quit);
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| { let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
if event.state == ShortcutState::Pressed { let hotkey_event_owned = *hotkey_event;
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey_event); let event_owned = event;
let function_owned = function;
let is_quit_owned = is_quit;
if hotkey_event.key == Code::KeyQ && is_quit { let app_handle_cloned = app_handle.clone();
if let Some(window) = app_handle.get_webview_window("main") {
if window.is_focused().unwrap_or(false) { AsyncHandler::spawn(move || async move {
if event_owned.state == ShortcutState::Pressed {
logging!(
debug,
Type::Hotkey,
"Hotkey pressed: {:?}",
hotkey_event_owned
);
if hotkey_event_owned.key == Code::KeyQ && is_quit_owned {
if let Some(window) = app_handle_cloned.get_webview_window("main")
&& window.is_focused().unwrap_or(false)
{
logging!(debug, Type::Hotkey, "Executing quit function"); logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function, &app_handle_clone); Self::execute_function(function_owned, &app_handle_cloned);
} }
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly");
let is_enable_global_hotkey = Config::verge()
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if is_enable_global_hotkey {
Self::execute_function(function, &app_handle_clone);
} else { } else {
use crate::utils::window_manager::WindowManager; logging!(debug, Type::Hotkey, "Executing function directly");
let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible { let is_enable_global_hotkey = Config::verge()
Self::execute_function(function, &app_handle_clone); .await
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if is_enable_global_hotkey {
Self::execute_function(function_owned, &app_handle_cloned);
} else {
use crate::utils::window_manager::WindowManager;
let is_visible = WindowManager::is_main_window_visible();
let is_focused = WindowManager::is_main_window_focused();
if is_focused && is_visible {
Self::execute_function(function_owned, &app_handle_cloned);
}
} }
} }
} }
} });
}); });
logging!( logging!(
@@ -270,8 +289,8 @@ impl Hotkey {
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey"); singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
impl Hotkey { impl Hotkey {
pub fn init(&self) -> Result<()> { pub async fn init(&self) -> Result<()> {
let verge = Config::verge(); let verge = Config::verge().await;
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true); let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
logging!( logging!(
@@ -286,7 +305,10 @@ impl Hotkey {
return Ok(()); return Ok(());
} }
if let Some(hotkeys) = verge.latest_ref().hotkeys.as_ref() { // Extract hotkeys data before async operations
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
if let Some(hotkeys) = hotkeys {
logging!( logging!(
debug, debug,
Type::Hotkey, Type::Hotkey,
@@ -310,7 +332,7 @@ impl Hotkey {
key, key,
func func
); );
if let Err(e) = self.register(key, func) { if let Err(e) = self.register(key, func).await {
logging!( logging!(
error, error,
Type::Hotkey, Type::Hotkey,
@@ -344,7 +366,7 @@ impl Hotkey {
} }
} }
} }
self.current.lock().clone_from(hotkeys); self.current.lock().clone_from(&hotkeys);
} else { } else {
logging!(debug, Type::Hotkey, "No hotkeys configured"); logging!(debug, Type::Hotkey, "No hotkeys configured");
} }
@@ -362,9 +384,9 @@ impl Hotkey {
} }
/// Register a hotkey with string-based function (backward compatibility) /// Register a hotkey with string-based function (backward compatibility)
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> { pub async fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let function = HotkeyFunction::from_str(func)?; let function = HotkeyFunction::from_str(func)?;
self.register_hotkey_with_function(hotkey, function) self.register_hotkey_with_function(hotkey, function).await
} }
pub fn unregister(&self, hotkey: &str) -> Result<()> { pub fn unregister(&self, hotkey: &str) -> Result<()> {
@@ -377,9 +399,10 @@ impl Hotkey {
Ok(()) Ok(())
} }
pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> { pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
let mut current = self.current.lock(); // Extract current hotkeys before async operations
let old_map = Self::get_map_from_vec(&current); let current_hotkeys = self.current.lock().clone();
let old_map = Self::get_map_from_vec(&current_hotkeys);
let new_map = Self::get_map_from_vec(&new_hotkeys); let new_map = Self::get_map_from_vec(&new_hotkeys);
let (del, add) = Self::get_diff(old_map, new_map); let (del, add) = Self::get_diff(old_map, new_map);
@@ -388,11 +411,12 @@ impl Hotkey {
let _ = self.unregister(key); let _ = self.unregister(key);
}); });
add.iter().for_each(|(key, func)| { for (key, func) in add.iter() {
logging_error!(Type::Hotkey, self.register(key, func)); logging_error!(Type::Hotkey, self.register(key, func).await);
}); }
*current = new_hotkeys; // Update the current hotkeys after all async operations
*self.current.lock() = new_hotkeys;
Ok(()) Ok(())
} }
File diff suppressed because it is too large Load Diff
+186 -179
View File
@@ -1,11 +1,15 @@
#[cfg(target_os = "windows")]
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type}; use crate::{logging, utils::logging::Type};
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result, bail};
use backoff::{Error as BackoffError, ExponentialBackoff};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH}; 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) { const IPC_SOCKET_NAME: &str = if cfg!(windows) {
r"\\.\pipe\clash-verge-service" r"\\.\pipe\clash-verge-service"
@@ -112,170 +116,134 @@ pub fn verify_response_signature(response: &IpcResponse) -> Result<bool> {
Ok(expected_signature == response.signature) Ok(expected_signature == response.signature)
} }
// IPC连接管理-win fn create_backoff_strategy() -> ExponentialBackoff {
#[cfg(target_os = "windows")] 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( pub async fn send_ipc_request(
command: IpcCommand, command: IpcCommand,
payload: serde_json::Value, payload: serde_json::Value,
) -> Result<IpcResponse> { ) -> Result<IpcResponse> {
use std::{
ffi::CString,
fs::File,
io::{Read, Write},
os::windows::io::{FromRawHandle, RawHandle},
ptr,
};
use winapi::um::{
fileapi::{CreateFileA, OPEN_EXISTING},
handleapi::INVALID_HANDLE_VALUE,
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
};
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
let command_type = format!("{command:?}"); let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) { let operation = || async {
Ok(req) => req, match send_ipc_request_internal(command.clone(), payload.clone()).await {
Err(e) => { Ok(response) => Ok(response),
logging!(error, Type::Service, true, "创建签名请求失败: {}", e); Err(e) => {
return Err(e); logging!(
warn,
Type::Service,
true,
"IPC请求失败,准备重试: 命令={}, 错误={}",
command_type,
e
);
Err(BackoffError::transient(e))
}
} }
}; };
let request_json = serde_json::to_string(&request)?; match backoff::future::retry(create_backoff_strategy(), operation).await {
Ok(response) => {
let result = AsyncHandler::spawn_blocking(move || -> Result<IpcResponse> { // logging!(
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) { // info,
Ok(name) => name, // Type::Service,
Err(e) => { // true,
logging!(error, Type::Service, true, "创建CString失败: {}", e); // "IPC请求成功: 命令={}, 成功={}",
return Err(anyhow::anyhow!("创建CString失败: {}", e)); // command_type,
} // response.success
}; // );
Ok(response)
let handle = unsafe { }
CreateFileA( Err(e) => {
c_pipe_name.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
let error = std::io::Error::last_os_error();
logging!( logging!(
error, error,
Type::Service, Type::Service,
true, true,
"连接到服务命名管道失败: {}", "IPC请求最终失败,重试已耗尽: 命令={}, 错误={}",
error command_type,
e
); );
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error)); Err(anyhow::anyhow!("IPC请求重试失败: {}", e))
} }
}
}
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) }; // 内部IPC请求实现(不带重试)
logging!(info, Type::Service, true, "服务连接成功 (Windows)"); 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
}
}
let request_bytes = request_json.as_bytes(); // IPC连接管理-win
let len_bytes = (request_bytes.len() as u32).to_be_bytes(); #[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();
if let Err(e) = pipe.write_all(&len_bytes) { let mut pipe = match ClientOptions::new().open(IPC_SOCKET_NAME) {
logging!(error, Type::Service, true, "写入请求长度失败: {}", e); Ok(p) => p,
return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); Err(e) => {
logging!(error, Type::Service, true, "连接到服务命名管道失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", e));
} }
};
if let Err(e) = pipe.write_all(request_bytes) { logging!(info, Type::Service, true, "服务连接成功 (Windows)");
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
if let Err(e) = pipe.flush() { pipe.write_all(&len_bytes).await?;
logging!(error, Type::Service, true, "刷新管道失败: {}", e); pipe.write_all(request_bytes).await?;
return Err(anyhow::anyhow!("刷新管道失败: {}", e)); pipe.flush().await?;
}
let mut response_len_bytes = [0u8; 4]; let mut response_len_bytes = [0u8; 4];
if let Err(e) = pipe.read_exact(&mut response_len_bytes) { pipe.read_exact(&mut response_len_bytes).await?;
logging!(error, Type::Service, true, "读取响应长度失败: {}", e); let response_len = u32::from_be_bytes(response_len_bytes) as usize;
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
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 mut response_bytes = vec![0u8; response_len]; let response: IpcResponse = serde_json::from_slice(&response_bytes)
if let Err(e) = pipe.read_exact(&mut response_bytes) { .map_err(|e| anyhow::anyhow!("解析响应失败: {}", e))?;
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
}
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) { if !verify_response_signature(&response)? {
Ok(r) => r, logging!(error, Type::Service, true, "服务响应签名验证失败");
Err(e) => { bail!("服务响应签名验证失败");
logging!(error, Type::Service, true, "服务响应解析失败: {}", e); }
return Err(anyhow::anyhow!("解析响应失败: {}", e));
}
};
match verify_response_signature(&response) { Ok(response)
Ok(valid) => {
if !valid {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
}
Err(e) => {
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
return Err(e);
}
}
logging!(
info,
Type::Service,
true,
"IPC请求完成: 命令={}, 成功={}",
command_type,
response.success
);
Ok(response)
})
.await??;
Ok(result)
} }
// IPC连接管理-unix // IPC连接管理-unix
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
pub async fn send_ipc_request( async fn send_ipc_request_unix(
command: IpcCommand, command: IpcCommand,
payload: serde_json::Value, payload: serde_json::Value,
) -> Result<IpcResponse> { ) -> Result<IpcResponse> {
use std::os::unix::net::UnixStream; let request = create_signed_request(command, payload)?;
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
Err(e) => {
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
return Err(e);
}
};
let request_json = serde_json::to_string(&request)?; let request_json = serde_json::to_string(&request)?;
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) { let mut stream = match UnixStream::connect(IPC_SOCKET_NAME).await {
Ok(s) => { Ok(s) => s,
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
s
}
Err(e) => { Err(e) => {
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e); logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e)); return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
@@ -285,58 +253,97 @@ pub async fn send_ipc_request(
let request_bytes = request_json.as_bytes(); let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes(); let len_bytes = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) { stream.write_all(&len_bytes).await?;
logging!(error, Type::Service, true, "写入请求长度失败: {}", e); stream.write_all(request_bytes).await?;
return Err(anyhow::anyhow!("写入请求长度失败: {}", e)); stream.flush().await?;
}
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
// 读取响应长度
let mut response_len_bytes = [0u8; 4]; let mut response_len_bytes = [0u8; 4];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) { stream.read_exact(&mut response_len_bytes).await?;
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
let response_len = u32::from_be_bytes(response_len_bytes) as usize; let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len]; let mut response_bytes = vec![0u8; response_len];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) { stream.read_exact(&mut response_bytes).await?;
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e)); 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!("服务响应签名验证失败");
} }
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r,
Err(e) => {
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,);
return Err(anyhow::anyhow!("解析响应失败: {}", e));
}
};
match verify_response_signature(&response) {
Ok(valid) => {
if !valid {
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
}
Err(e) => {
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
return Err(e);
}
}
logging!(
info,
Type::Service,
true,
"IPC请求完成: 命令={}, 成功={}",
command_type,
response.success
);
Ok(response) 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);
}
}
}
}
+17 -14
View File
@@ -2,7 +2,7 @@
use crate::utils::autostart as startup_shortcut; use crate::utils::autostart as startup_shortcut;
use crate::{ use crate::{
config::{Config, IVerge}, config::{Config, IVerge},
core::{handle::Handle, EventDrivenProxyManager}, core::{EventDrivenProxyManager, handle::Handle},
logging, logging_error, singleton_lazy, logging, logging_error, singleton_lazy,
utils::logging::Type, utils::logging::Type,
}; };
@@ -24,16 +24,16 @@ static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;
static DEFAULT_BYPASS: &str = static DEFAULT_BYPASS: &str =
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1"; "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str = static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
fn get_bypass() -> String { async fn get_bypass() -> String {
let use_default = Config::verge() let use_default = Config::verge()
.await
.latest_ref() .latest_ref()
.use_default_bypass .use_default_bypass
.unwrap_or(true); .unwrap_or(true);
let res = { let res = {
let verge = Config::verge(); let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_ref();
verge.system_proxy_bypass.clone() verge.system_proxy_bypass.clone()
}; };
@@ -77,14 +77,17 @@ impl Sysopt {
pub async fn update_sysproxy(&self) -> Result<()> { pub async fn update_sysproxy(&self) -> Result<()> {
let _lock = self.update_sysproxy.lock().await; let _lock = self.update_sysproxy.lock().await;
let port = Config::verge() let port = {
.latest_ref() let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
.verge_mixed_port match verge_port {
.unwrap_or(Config::clash().latest_ref().get_mixed_port()); Some(port) => port,
None => Config::clash().await.latest_ref().get_mixed_port(),
}
};
let pac_port = IVerge::get_singleton_port(); let pac_port = IVerge::get_singleton_port();
let (sys_enable, pac_enable, proxy_host) = { let (sys_enable, pac_enable, proxy_host) = {
let verge = Config::verge(); let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_ref();
( (
verge.enable_system_proxy.unwrap_or(false), verge.enable_system_proxy.unwrap_or(false),
@@ -102,7 +105,7 @@ impl Sysopt {
enable: false, enable: false,
host: proxy_host.clone(), host: proxy_host.clone(),
port, port,
bypass: get_bypass(), bypass: get_bypass().await,
}; };
let mut auto = Autoproxy { let mut auto = Autoproxy {
enable: false, enable: false,
@@ -173,7 +176,7 @@ impl Sysopt {
.await? .await?
} else { } else {
let address = format!("{proxy_host}:{port}"); let address = format!("{proxy_host}:{port}");
let bypass = get_bypass(); let bypass = get_bypass().await;
let sysproxy_str = sysproxy_exe let sysproxy_str = sysproxy_exe
.as_path() .as_path()
.to_str() .to_str()
@@ -255,8 +258,8 @@ impl Sysopt {
} }
/// update the startup /// update the startup
pub fn update_launch(&self) -> Result<()> { pub async fn update_launch(&self) -> Result<()> {
let enable_auto_launch = { Config::verge().latest_ref().enable_auto_launch }; let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch };
let is_enable = enable_auto_launch.unwrap_or(false); let is_enable = enable_auto_launch.unwrap_or(false);
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable); logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
+145 -133
View File
@@ -4,9 +4,10 @@ use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use parking_lot::RwLock; use parking_lot::RwLock;
use std::{ use std::{
collections::HashMap, collections::HashMap,
pin::Pin,
sync::{ sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc, Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
}, },
}; };
@@ -48,7 +49,7 @@ impl Timer {
} }
/// Initialize timer with better error handling and atomic operations /// Initialize timer with better error handling and atomic operations
pub fn init(&self) -> Result<()> { pub async fn init(&self) -> Result<()> {
// Use compare_exchange for thread-safe initialization check // Use compare_exchange for thread-safe initialization check
if self if self
.initialized .initialized
@@ -59,57 +60,59 @@ impl Timer {
return Ok(()); return Ok(());
} }
logging!(info, Type::Timer, true, "Initializing timer...");
// Initialize timer tasks // Initialize timer tasks
if let Err(e) = self.refresh() { if let Err(e) = self.refresh().await {
// Reset initialization flag on error // Reset initialization flag on error
self.initialized.store(false, Ordering::SeqCst); self.initialized.store(false, Ordering::SeqCst);
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e); logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
return Err(e); return Err(e);
} }
let timer_map = self.timer_map.read(); // Log timer info first
logging!( {
info, let timer_map = self.timer_map.read();
Type::Timer,
"已注册的定时任务数量: {}",
timer_map.len()
);
for (uid, task) in timer_map.iter() {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"注册了定时任务 - uid={}, interval={}min, task_id={}", "已注册的定时任务数量: {}",
uid, timer_map.len()
task.interval_minutes,
task.task_id
); );
for (uid, task) in timer_map.iter() {
logging!(
info,
Type::Timer,
"注册了定时任务 - uid={}, interval={}min, task_id={}",
uid,
task.interval_minutes,
task.task_id
);
}
} }
let cur_timestamp = chrono::Local::now().timestamp(); let cur_timestamp = chrono::Local::now().timestamp();
// Collect profiles that need immediate update // Collect profiles that need immediate update
let profiles_to_update = if let Some(items) = Config::profiles().latest_ref().get_items() { let profiles_to_update =
items if let Some(items) = Config::profiles().await.latest_ref().get_items() {
.iter() items
.filter_map(|item| { .iter()
let interval = item.option.as_ref()?.update_interval? as i64; .filter_map(|item| {
let updated = item.updated? as i64; let interval = item.option.as_ref()?.update_interval? as i64;
let uid = item.uid.as_ref()?; let updated = item.updated? as i64;
let uid = item.uid.as_ref()?;
if interval > 0 && cur_timestamp - updated >= interval * 60 { if interval > 0 && cur_timestamp - updated >= interval * 60 {
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid); logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
Some(uid.clone()) Some(uid.clone())
} else { } else {
None None
} }
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
} else { } else {
Vec::new() Vec::new()
}; };
// Advance tasks outside of locks to minimize lock contention // Advance tasks outside of locks to minimize lock contention
if !profiles_to_update.is_empty() { if !profiles_to_update.is_empty() {
@@ -137,9 +140,9 @@ impl Timer {
} }
/// Refresh timer tasks with better error handling /// Refresh timer tasks with better error handling
pub fn refresh(&self) -> Result<()> { pub async fn refresh(&self) -> Result<()> {
// Generate diff outside of lock to minimize lock contention // Generate diff outside of lock to minimize lock contention
let diff_map = self.gen_diff(); let diff_map = self.gen_diff().await;
if diff_map.is_empty() { if diff_map.is_empty() {
logging!(debug, Type::Timer, "No timer changes needed"); logging!(debug, Type::Timer, "No timer changes needed");
@@ -153,72 +156,80 @@ impl Timer {
diff_map.len() diff_map.len()
); );
// Apply changes while holding locks // Apply changes - first collect operations to perform without holding locks
let mut timer_map = self.timer_map.write(); let mut operations_to_add: Vec<(String, TaskID, u64)> = Vec::new();
let mut delay_timer = self.delay_timer.write(); let _operations_to_remove: Vec<String> = Vec::new();
for (uid, diff) in diff_map { // Perform sync operations while holding locks
match diff { {
DiffFlag::Del(tid) => { let mut timer_map = self.timer_map.write();
timer_map.remove(&uid); let delay_timer = self.delay_timer.write();
if let Err(e) = delay_timer.remove_task(tid) {
logging!( for (uid, diff) in diff_map {
warn, match diff {
Type::Timer, DiffFlag::Del(tid) => {
"Failed to remove task {} for uid {}: {}", timer_map.remove(&uid);
tid, if let Err(e) = delay_timer.remove_task(tid) {
uid, logging!(
e warn,
); Type::Timer,
} else { "Failed to remove task {} for uid {}: {}",
logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid); tid,
uid,
e
);
} else {
logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid);
}
}
DiffFlag::Add(tid, interval) => {
let task = TimerTask {
task_id: tid,
interval_minutes: interval,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(uid.clone(), task);
operations_to_add.push((uid, tid, interval));
}
DiffFlag::Mod(tid, interval) => {
// Remove old task first
if let Err(e) = delay_timer.remove_task(tid) {
logging!(
warn,
Type::Timer,
"Failed to remove old task {} for uid {}: {}",
tid,
uid,
e
);
}
// Then add the new one
let task = TimerTask {
task_id: tid,
interval_minutes: interval,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(uid.clone(), task);
operations_to_add.push((uid, tid, interval));
} }
} }
DiffFlag::Add(tid, interval) => { }
let task = TimerTask { } // Locks are dropped here
task_id: tid,
interval_minutes: interval,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(uid.clone(), task); // Now perform async operations without holding locks
for (uid, tid, interval) in operations_to_add {
// Re-acquire locks for individual operations
let mut delay_timer = self.delay_timer.write();
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) { // Rollback on failure - remove from timer_map
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e); self.timer_map.write().remove(&uid);
timer_map.remove(&uid); // Rollback on failure } else {
} else { logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid);
logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid);
}
}
DiffFlag::Mod(tid, interval) => {
// Remove old task first
if let Err(e) = delay_timer.remove_task(tid) {
logging!(
warn,
Type::Timer,
"Failed to remove old task {} for uid {}: {}",
tid,
uid,
e
);
}
// Then add the new one
let task = TimerTask {
task_id: tid,
interval_minutes: interval,
last_run: chrono::Local::now().timestamp(),
};
timer_map.insert(uid.clone(), task);
if let Err(e) = self.add_task(&mut delay_timer, uid.clone(), tid, interval) {
logging_error!(Type::Timer, "Failed to update task for uid {}: {}", uid, e);
timer_map.remove(&uid); // Rollback on failure
} else {
logging!(debug, Type::Timer, "Updated task {} for uid {}", tid, uid);
}
}
} }
} }
@@ -226,24 +237,23 @@ impl Timer {
} }
/// Generate map of profile UIDs to update intervals /// Generate map of profile UIDs to update intervals
fn gen_map(&self) -> HashMap<String, u64> { async fn gen_map(&self) -> HashMap<String, u64> {
let mut new_map = HashMap::new(); let mut new_map = HashMap::new();
if let Some(items) = Config::profiles().latest_ref().get_items() { if let Some(items) = Config::profiles().await.latest_ref().get_items() {
for item in items.iter() { for item in items.iter() {
if let Some(option) = item.option.as_ref() { if let Some(option) = item.option.as_ref()
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) { && let (Some(interval), Some(uid)) = (option.update_interval, &item.uid)
if interval > 0 { && interval > 0
logging!( {
debug, logging!(
Type::Timer, debug,
"找到定时更新配置: uid={}, interval={}min", Type::Timer,
uid, "找到定时更新配置: uid={}, interval={}min",
interval uid,
); interval
new_map.insert(uid.clone(), interval); );
} new_map.insert(uid.clone(), interval);
}
} }
} }
} }
@@ -258,9 +268,9 @@ impl Timer {
} }
/// Generate differences between current and new timer configuration /// Generate differences between current and new timer configuration
fn gen_diff(&self) -> HashMap<String, DiffFlag> { async fn gen_diff(&self) -> HashMap<String, DiffFlag> {
let mut diff_map = HashMap::new(); let mut diff_map = HashMap::new();
let new_map = self.gen_map(); let new_map = self.gen_map().await;
// Read lock for comparing current state // Read lock for comparing current state
let timer_map = self.timer_map.read(); let timer_map = self.timer_map.read();
@@ -349,9 +359,9 @@ impl Timer {
.set_frequency_repeated_by_minutes(minutes) .set_frequency_repeated_by_minutes(minutes)
.spawn_async_routine(move || { .spawn_async_routine(move || {
let uid = uid.clone(); let uid = uid.clone();
async move { Box::pin(async move {
Self::async_task(uid).await; Self::async_task(uid).await;
} }) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
}) })
.context("failed to create timer task")?; .context("failed to create timer task")?;
@@ -363,21 +373,24 @@ impl Timer {
} }
/// Get next update time for a profile /// Get next update time for a profile
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> { pub async fn get_next_update_time(&self, uid: &str) -> Option<i64> {
logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid); logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid);
let timer_map = self.timer_map.read(); // First extract timer task data without holding the lock across await
let task = match timer_map.get(uid) { let task_interval = {
Some(t) => t, let timer_map = self.timer_map.read();
None => { match timer_map.get(uid) {
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid); Some(t) => t.interval_minutes,
return None; None => {
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
return None;
}
} }
}; };
// Get the profile updated timestamp // Get the profile updated timestamp - now safe to await
let profiles_config = Config::profiles(); let config_profiles = Config::profiles().await;
let profiles = profiles_config.latest_ref(); let profiles = config_profiles.data_ref().clone();
let items = match profiles.get_items() { let items = match profiles.get_items() {
Some(i) => i, Some(i) => i,
None => { None => {
@@ -397,8 +410,8 @@ impl Timer {
let updated = profile.updated.unwrap_or(0) as i64; let updated = profile.updated.unwrap_or(0) as i64;
// Calculate next update time // Calculate next update time
if updated > 0 && task.interval_minutes > 0 { if updated > 0 && task_interval > 0 {
let next_time = updated + (task.interval_minutes as i64 * 60); let next_time = updated + (task_interval as i64 * 60);
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
@@ -413,7 +426,7 @@ impl Timer {
Type::Timer, Type::Timer,
"更新时间或间隔无效,updated={}, interval={}", "更新时间或间隔无效,updated={}, interval={}",
updated, updated,
task.interval_minutes task_interval
); );
None None
} }
@@ -421,7 +434,6 @@ impl Timer {
/// Emit update events for frontend notification /// Emit update events for frontend notification
fn emit_update_event(_uid: &str, _is_start: bool) { fn emit_update_event(_uid: &str, _is_start: bool) {
#[cfg(any(feature = "verge-dev", feature = "default"))]
{ {
if _is_start { if _is_start {
super::handle::Handle::notify_profile_update_started(_uid.to_string()); super::handle::Handle::notify_profile_update_started(_uid.to_string());
@@ -439,7 +451,7 @@ impl Timer {
match tokio::time::timeout(std::time::Duration::from_secs(40), async { match tokio::time::timeout(std::time::Duration::from_secs(40), async {
Self::emit_update_event(&uid, true); Self::emit_update_event(&uid, true);
let is_current = Config::profiles().latest_ref().current.as_ref() == Some(&uid); let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(&uid);
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
#![cfg(target_os = "windows")] #![cfg(target_os = "windows")]
use crate::utils::dirs; use crate::utils::dirs;
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::process::Command as StdCommand; use std::process::Command as StdCommand;
@@ -1,3 +1,5 @@
// This function is exported for use by the Clash core
// eslint-disable-next-line no-unused-vars
function main(config, _name) { function main(config, _name) {
if (config.mode === "script") { if (config.mode === "script") {
config.mode = "rule"; config.mode = "rule";
@@ -1,3 +1,5 @@
// This function is exported for use by the Clash core
// eslint-disable-next-line no-unused-vars
function main(config, _name) { function main(config, _name) {
if (Array.isArray(config.proxies)) { if (Array.isArray(config.proxies)) {
config.proxies.forEach((p, i) => { config.proxies.forEach((p, i) => {
+67 -17
View File
@@ -3,7 +3,7 @@ use crate::{
config::PrfItem, config::PrfItem,
utils::{dirs, help}, utils::{dirs, help},
}; };
use serde_yaml::Mapping; use serde_yaml_ng::Mapping;
use std::fs; use std::fs;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -22,6 +22,7 @@ pub enum ChainType {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum ChainSupport { pub enum ChainSupport {
Clash, Clash,
ClashMeta, ClashMeta,
@@ -29,8 +30,49 @@ pub enum ChainSupport {
All, All,
} }
impl From<&PrfItem> for Option<ChainItem> { // impl From<&PrfItem> for Option<ChainItem> {
fn from(item: &PrfItem) -> Self { // fn from(item: &PrfItem) -> Self {
// let itype = item.itype.as_ref()?.as_str();
// let file = item.file.clone()?;
// let uid = item.uid.clone().unwrap_or("".into());
// let path = dirs::app_profiles_dir().ok()?.join(file);
// if !path.exists() {
// return None;
// }
// match itype {
// "script" => Some(ChainItem {
// uid,
// data: ChainType::Script(fs::read_to_string(path).ok()?),
// }),
// "merge" => Some(ChainItem {
// uid,
// data: ChainType::Merge(help::read_mapping(&path).ok()?),
// }),
// "rules" => Some(ChainItem {
// uid,
// data: ChainType::Rules(help::read_seq_map(&path).ok()?),
// }),
// "proxies" => Some(ChainItem {
// uid,
// data: ChainType::Proxies(help::read_seq_map(&path).ok()?),
// }),
// "groups" => Some(ChainItem {
// uid,
// data: ChainType::Groups(help::read_seq_map(&path).ok()?),
// }),
// _ => None,
// }
// }
// }
// Helper trait to allow async conversion
pub trait AsyncChainItemFrom {
async fn from_async(item: &PrfItem) -> Option<ChainItem>;
}
impl AsyncChainItemFrom for Option<ChainItem> {
async fn from_async(item: &PrfItem) -> Option<ChainItem> {
let itype = item.itype.as_ref()?.as_str(); let itype = item.itype.as_ref()?.as_str();
let file = item.file.clone()?; let file = item.file.clone()?;
let uid = item.uid.clone().unwrap_or("".into()); let uid = item.uid.clone().unwrap_or("".into());
@@ -47,25 +89,33 @@ impl From<&PrfItem> for Option<ChainItem> {
}), }),
"merge" => Some(ChainItem { "merge" => Some(ChainItem {
uid, uid,
data: ChainType::Merge(help::read_mapping(&path).ok()?), data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
}),
"rules" => Some(ChainItem {
uid,
data: ChainType::Rules(help::read_seq_map(&path).ok()?),
}),
"proxies" => Some(ChainItem {
uid,
data: ChainType::Proxies(help::read_seq_map(&path).ok()?),
}),
"groups" => Some(ChainItem {
uid,
data: ChainType::Groups(help::read_seq_map(&path).ok()?),
}), }),
"rules" => {
let seq_map = help::read_seq_map(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Rules(seq_map),
})
}
"proxies" => {
let seq_map = help::read_seq_map(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Proxies(seq_map),
})
}
"groups" => {
let seq_map = help::read_seq_map(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Groups(seq_map),
})
}
_ => None, _ => None,
} }
} }
} }
impl ChainItem { impl ChainItem {
/// 内建支持一些脚本 /// 内建支持一些脚本
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> { pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
+1 -1
View File
@@ -1,4 +1,4 @@
use serde_yaml::{Mapping, Value}; use serde_yaml_ng::{Mapping, Value};
use std::collections::HashSet; use std::collections::HashSet;
pub const HANDLE_FIELDS: [&str; 12] = [ pub const HANDLE_FIELDS: [&str; 12] = [
+6 -8
View File
@@ -1,5 +1,5 @@
use super::use_lowercase; use super::use_lowercase;
use serde_yaml::{self, Mapping, Value}; use serde_yaml_ng::{self, Mapping, Value};
fn deep_merge(a: &mut Value, b: &Value) { fn deep_merge(a: &mut Value, b: &Value) {
match (a, b) { match (a, b) {
@@ -18,12 +18,10 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
deep_merge(&mut config, &Value::from(merge)); deep_merge(&mut config, &Value::from(merge));
let config = config.as_mapping().cloned().unwrap_or_else(|| { config.as_mapping().cloned().unwrap_or_else(|| {
log::error!("Failed to convert merged config to mapping, using empty mapping"); log::error!("Failed to convert merged config to mapping, using empty mapping");
Mapping::new() Mapping::new()
}); })
config
} }
#[test] #[test]
@@ -54,10 +52,10 @@ fn test_merge() -> anyhow::Result<()> {
script1: test script1: test
"; ";
let merge = serde_yaml::from_str::<Mapping>(merge)?; let merge = serde_yaml_ng::from_str::<Mapping>(merge)?;
let config = serde_yaml::from_str::<Mapping>(config)?; let config = serde_yaml_ng::from_str::<Mapping>(config)?;
let _ = serde_yaml::to_string(&use_merge(merge, config))?; let _ = serde_yaml_ng::to_string(&use_merge(merge, config))?;
Ok(()) Ok(())
} }
+188 -90
View File
@@ -7,19 +7,19 @@ mod tun;
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*}; use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
use crate::{config::Config, utils::tmpl}; use crate::{config::Config, utils::tmpl};
use serde_yaml::Mapping; use serde_yaml_ng::Mapping;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
type ResultLog = Vec<(String, String)>; type ResultLog = Vec<(String, String)>;
/// Enhance mode /// Enhance mode
/// 返回最终订阅、该订阅包含的键、和script执行的结果 /// 返回最终订阅、该订阅包含的键、和script执行的结果
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) { pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的订阅 // config.yaml 的订阅
let clash_config = { Config::clash().latest_ref().0.clone() }; let clash_config = { Config::clash().await.latest_ref().0.clone() };
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = { let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
let verge = Config::verge(); let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_ref();
( (
Some(verge.get_valid_clash_core()), Some(verge.get_valid_clash_core()),
@@ -32,18 +32,18 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
}; };
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let redir_enabled = { let redir_enabled = {
let verge = Config::verge(); let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_ref();
verge.verge_redir_enabled.unwrap_or(false) verge.verge_redir_enabled.unwrap_or(false)
}; };
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let tproxy_enabled = { let tproxy_enabled = {
let verge = Config::verge(); let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_ref();
verge.verge_tproxy_enabled.unwrap_or(false) verge.verge_tproxy_enabled.unwrap_or(false)
}; };
// 从profiles里拿东西 // 从profiles里拿东西 - 先收集需要的数据,然后释放锁
let ( let (
mut config, mut config,
merge_item, merge_item,
@@ -55,74 +55,172 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
global_script, global_script,
profile_name, profile_name,
) = { ) = {
let profiles = Config::profiles(); // 收集所有需要的数据,然后释放profiles锁
let profiles = profiles.latest_ref(); let (
current,
merge_uid,
script_uid,
rules_uid,
proxies_uid,
groups_uid,
_current_profile_uid,
name,
) = {
// 分离async调用和数据获取,避免借用检查问题
let current = {
let profiles = Config::profiles().await;
let profiles_clone = profiles.latest_ref().clone();
profiles_clone.current_mapping().await.unwrap_or_default()
};
let current = profiles.current_mapping().unwrap_or_default(); // 重新获取锁进行其他操作
let merge = profiles let profiles = Config::profiles().await;
.get_item(&profiles.current_merge().unwrap_or_default()) let profiles_ref = profiles.latest_ref();
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Merge(Mapping::new()),
});
let script = profiles
.get_item(&profiles.current_script().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let rules = profiles
.get_item(&profiles.current_rules().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Rules(SeqMap::default()),
});
let proxies = profiles
.get_item(&profiles.current_proxies().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Proxies(SeqMap::default()),
});
let groups = profiles
.get_item(&profiles.current_groups().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Groups(SeqMap::default()),
});
let global_merge = profiles let merge_uid = profiles_ref.current_merge().unwrap_or_default();
.get_item(&"Merge".to_string()) let script_uid = profiles_ref.current_script().unwrap_or_default();
.ok() let rules_uid = profiles_ref.current_rules().unwrap_or_default();
.and_then(<Option<ChainItem>>::from) let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
.unwrap_or_else(|| ChainItem { let groups_uid = profiles_ref.current_groups().unwrap_or_default();
uid: "Merge".into(), let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
data: ChainType::Merge(Mapping::new()),
});
let global_script = profiles let name = profiles_ref
.get_item(&"Script".to_string()) .get_item(&current_profile_uid)
.ok() .ok()
.and_then(<Option<ChainItem>>::from) .and_then(|item| item.name.clone())
.unwrap_or_else(|| ChainItem { .unwrap_or_default();
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let name = profiles (
.get_item(&profiles.get_current().unwrap_or_default()) current,
.ok() merge_uid,
.and_then(|item| item.name.clone()) script_uid,
.unwrap_or_default(); rules_uid,
proxies_uid,
groups_uid,
current_profile_uid,
name,
)
};
// 现在获取具体的items,此时profiles锁已经释放
let merge = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&merge_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Merge(Mapping::new()),
});
let script = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&script_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let rules = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&rules_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Rules(SeqMap::default()),
});
let proxies = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&proxies_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Proxies(SeqMap::default()),
});
let groups = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&groups_uid).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Groups(SeqMap::default()),
});
let global_merge = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&"Merge".to_string()).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()),
});
let global_script = {
let item = {
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
profiles.get_item(&"Script".to_string()).ok().cloned()
};
if let Some(item) = item {
<Option<ChainItem>>::from_async(&item).await
} else {
None
}
}
.unwrap_or_else(|| ChainItem {
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
( (
current, current,
@@ -237,6 +335,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// 处理 external-controller 键的开关逻辑 // 处理 external-controller 键的开关逻辑
if key.as_str() == Some("external-controller") { if key.as_str() == Some("external-controller") {
let enable_external_controller = Config::verge() let enable_external_controller = Config::verge()
.await
.latest_ref() .latest_ref()
.enable_external_controller .enable_external_controller
.unwrap_or(false); .unwrap_or(false);
@@ -285,27 +384,26 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let Ok(app_dir) = dirs::app_home_dir() { if let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join("dns_config.yaml"); let dns_path = app_dir.join("dns_config.yaml");
if dns_path.exists() { if dns_path.exists()
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) { && let Ok(dns_yaml) = fs::read_to_string(&dns_path)
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) { && let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
// 处理hosts配置 {
if let Some(hosts_value) = dns_config.get("hosts") { // 处理hosts配置
if hosts_value.is_mapping() { if let Some(hosts_value) = dns_config.get("hosts")
config.insert("hosts".into(), hosts_value.clone()); && hosts_value.is_mapping()
log::info!(target: "app", "apply hosts configuration"); {
} config.insert("hosts".into(), hosts_value.clone());
} log::info!(target: "app", "apply hosts configuration");
}
if let Some(dns_value) = dns_config.get("dns") { if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() { if let Some(dns_mapping) = dns_value.as_mapping() {
config.insert("dns".into(), dns_mapping.clone().into()); config.insert("dns".into(), dns_mapping.clone().into());
log::info!(target: "app", "apply dns_config.yaml (dns section)"); log::info!(target: "app", "apply dns_config.yaml (dns section)");
}
} else {
config.insert("dns".into(), dns_config.into());
log::info!(target: "app", "apply dns_config.yaml");
}
} }
} else {
config.insert("dns".into(), dns_config.into());
log::info!(target: "app", "apply dns_config.yaml");
} }
} }
} }
+4 -4
View File
@@ -1,13 +1,13 @@
use super::use_lowercase; use super::use_lowercase;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use serde_yaml::Mapping; use serde_yaml_ng::Mapping;
pub fn use_script( pub fn use_script(
script: String, script: String,
config: Mapping, config: Mapping,
name: String, name: String,
) -> Result<(Mapping, Vec<(String, String)>)> { ) -> Result<(Mapping, Vec<(String, String)>)> {
use boa_engine::{native_function::NativeFunction, Context, JsString, JsValue, Source}; use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
let mut context = Context::default(); let mut context = Context::default();
@@ -149,11 +149,11 @@ fn test_script() {
enable: false enable: false
"; ";
let config = serde_yaml::from_str(config).expect("Failed to parse test config YAML"); let config = serde_yaml_ng::from_str(config).expect("Failed to parse test config YAML");
let (config, results) = use_script(script.into(), config, "".to_string()) let (config, results) = use_script(script.into(), config, "".to_string())
.expect("Script execution should succeed in test"); .expect("Script execution should succeed in test");
let _ = serde_yaml::to_string(&config).expect("Failed to serialize config to YAML"); let _ = serde_yaml_ng::to_string(&config).expect("Failed to serialize config to YAML");
let yaml_config_size = std::mem::size_of_val(&config); let yaml_config_size = std::mem::size_of_val(&config);
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config)); let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
assert!(box_yaml_config_size < yaml_config_size); assert!(box_yaml_config_size < yaml_config_size);
+33 -33
View File
@@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Sequence, Value}; use serde_yaml_ng::{Mapping, Sequence, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap { pub struct SeqMap {
@@ -44,39 +44,39 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
config.insert(Value::String(field.into()), Value::Sequence(new_seq)); config.insert(Value::String(field.into()), Value::Sequence(new_seq));
// If this is proxies field, we also need to filter proxy-groups // If this is proxies field, we also need to filter proxy-groups
if field == "proxies" { if field == "proxies"
if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") { && let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups")
let mut new_groups = Sequence::new(); {
for group in groups { let mut new_groups = Sequence::new();
if let Value::Mapping(group_map) = group { for group in groups {
let mut new_group = group_map.clone(); if let Value::Mapping(group_map) = group {
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") { let mut new_group = group_map.clone();
let filtered_proxies: Sequence = proxies if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
.iter() let filtered_proxies: Sequence = proxies
.filter(|p| { .iter()
if let Value::String(name) = p { .filter(|p| {
!delete.contains(name) if let Value::String(name) = p {
} else { !delete.contains(name)
true } else {
} true
}) }
.cloned() })
.collect(); .cloned()
new_group.insert( .collect();
Value::String("proxies".into()), new_group.insert(
Value::Sequence(filtered_proxies), Value::String("proxies".into()),
); Value::Sequence(filtered_proxies),
} );
new_groups.push(Value::Mapping(new_group));
} else {
new_groups.push(group.clone());
} }
new_groups.push(Value::Mapping(new_group));
} else {
new_groups.push(group.clone());
} }
config.insert(
Value::String("proxy-groups".into()),
Value::Sequence(new_groups),
);
} }
config.insert(
Value::String("proxy-groups".into()),
Value::Sequence(new_groups),
);
} }
config config
@@ -86,7 +86,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
mod tests { mod tests {
use super::*; use super::*;
#[allow(unused_imports)] #[allow(unused_imports)]
use serde_yaml::Value; use serde_yaml_ng::Value;
#[test] #[test]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
@@ -110,7 +110,7 @@ proxy-groups:
- "proxy1" - "proxy1"
"#; "#;
let mut config: Mapping = let mut config: Mapping =
serde_yaml::from_str(config_str).expect("Failed to parse test config YAML"); serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML");
let seq = SeqMap { let seq = SeqMap {
prepend: Sequence::new(), prepend: Sequence::new(),
+4 -4
View File
@@ -1,4 +1,4 @@
use serde_yaml::{Mapping, Value}; use serde_yaml_ng::{Mapping, Value};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use crate::process::AsyncHandler; use crate::process::AsyncHandler;
@@ -63,8 +63,8 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
crate::utils::resolve::restore_public_dns().await; crate::utils::resolve::dns::restore_public_dns().await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await; crate::utils::resolve::dns::set_public_dns("223.6.6.6".to_string()).await;
}); });
} }
} }
@@ -75,7 +75,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置 // TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
crate::utils::resolve::restore_public_dns().await; crate::utils::resolve::dns::restore_public_dns().await;
}); });
} }
+1 -1
View File
@@ -51,7 +51,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
/// Restore WebDAV backup /// Restore WebDAV backup
pub async fn restore_webdav_backup(filename: String) -> Result<()> { pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge(); let verge = Config::verge().await;
let verge_data = verge.latest_ref().clone(); let verge_data = verge.latest_ref().clone();
let webdav_url = verge_data.webdav_url.clone(); let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone(); let webdav_username = verge_data.webdav_username.clone();
+56 -55
View File
@@ -1,50 +1,45 @@
use crate::{ use crate::{
config::Config, config::Config,
core::{handle, tray, CoreManager}, core::{CoreManager, handle, tray},
ipc::IpcManager, ipc::IpcManager,
logging_error, logging_error,
process::AsyncHandler, process::AsyncHandler,
utils::{logging::Type, resolve}, utils::{logging::Type, resolve},
}; };
use serde_yaml::{Mapping, Value}; use serde_yaml_ng::{Mapping, Value};
use tauri::Manager;
/// Restart the Clash core /// Restart the Clash core
pub fn restart_clash_core() { pub async fn restart_clash_core() {
AsyncHandler::spawn(move || async move { match CoreManager::global().restart_core().await {
match CoreManager::global().restart_core().await { Ok(_) => {
Ok(_) => { handle::Handle::refresh_clash();
handle::Handle::refresh_clash(); handle::Handle::notice_message("set_config::ok", "ok");
handle::Handle::notice_message("set_config::ok", "ok");
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target:"app", "{err}");
}
} }
}); Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target:"app", "{err}");
}
}
} }
/// Restart the application /// Restart the application
pub fn restart_app() { pub async fn restart_app() {
AsyncHandler::spawn(move || async move { // logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
// logging_error!(Type::Core, true, CoreManager::global().stop_core().await); resolve::resolve_reset_async().await;
resolve::resolve_reset_async().await;
handle::Handle::global() handle::Handle::global()
.app_handle() .app_handle()
.map(|app_handle| { .map(|app_handle| {
tauri::process::restart(&app_handle.env()); app_handle.restart();
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
logging_error!( logging_error!(
Type::System, Type::System,
false, false,
"{}", "{}",
"Failed to get app handle for restart" "Failed to get app handle for restart"
); );
}); });
});
} }
fn after_change_clash_mode() { fn after_change_clash_mode() {
@@ -67,37 +62,42 @@ fn after_change_clash_mode() {
} }
/// Change Clash mode (rule/global/direct/script) /// Change Clash mode (rule/global/direct/script)
pub fn change_clash_mode(mode: String) { pub async fn change_clash_mode(mode: String) {
let mut mapping = Mapping::new(); let mut mapping = Mapping::new();
mapping.insert(Value::from("mode"), mode.clone().into()); mapping.insert(Value::from("mode"), mode.clone().into());
// Convert YAML mapping to JSON Value // Convert YAML mapping to JSON Value
let json_value = serde_json::json!({ let json_value = serde_json::json!({
"mode": mode "mode": mode
}); });
AsyncHandler::spawn(move || async move { log::debug!(target: "app", "change clash mode to {mode}");
log::debug!(target: "app", "change clash mode to {mode}"); match IpcManager::global().patch_configs(json_value).await {
match IpcManager::global().patch_configs(json_value).await { Ok(_) => {
Ok(_) => { // 更新订阅
// 更新订阅 Config::clash().await.data_mut().patch_config(mapping);
Config::clash().data_mut().patch_config(mapping);
if Config::clash().data_mut().save_config().is_ok() { // 分离数据获取和异步调用
handle::Handle::refresh_clash(); let clash_data = Config::clash().await.data_mut().clone();
logging_error!(Type::Tray, true, tray::Tray::global().update_menu()); if clash_data.save_config().await.is_ok() {
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None)); 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
);
}
let is_auto_close_connection = Config::verge() let is_auto_close_connection = Config::verge()
.data_mut() .await
.auto_close_connection .data_mut()
.unwrap_or(false); .auto_close_connection
if is_auto_close_connection { .unwrap_or(false);
after_change_clash_mode(); if is_auto_close_connection {
} after_change_clash_mode();
} }
Err(err) => log::error!(target: "app", "{err}"),
} }
}); Err(err) => log::error!(target: "app", "{err}"),
}
} }
/// Test connection delay to a URL /// Test connection delay to a URL
@@ -106,6 +106,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
use tokio::time::Instant; use tokio::time::Instant;
let tun_mode = Config::verge() let tun_mode = Config::verge()
.await
.latest_ref() .latest_ref()
.enable_tun_mode .enable_tun_mode
.unwrap_or(false); .unwrap_or(false);
@@ -121,7 +122,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
let start = Instant::now(); let start = Instant::now();
let response = NetworkManager::global() let response = NetworkManager::new()
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false) .get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
.await; .await;
+40 -26
View File
@@ -1,28 +1,35 @@
use crate::{ use crate::{
config::{Config, IVerge}, config::{Config, IVerge},
core::{handle, hotkey, sysopt, tray, CoreManager}, core::{CoreManager, handle, hotkey, sysopt, tray},
logging_error, logging_error,
module::lightweight, module::lightweight,
utils::logging::Type, utils::logging::Type,
}; };
use anyhow::Result; use anyhow::Result;
use serde_yaml::Mapping; use serde_yaml_ng::Mapping;
/// Patch Clash configuration /// Patch Clash configuration
pub async fn patch_clash(patch: Mapping) -> Result<()> { pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft_mut().patch_config(patch.clone()); Config::clash()
.await
.draft_mut()
.patch_config(patch.clone());
let res = { let res = {
// 激活订阅 // 激活订阅
if patch.get("secret").is_some() || patch.get("external-controller").is_some() { if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
Config::generate()?; Config::generate().await?;
CoreManager::global().restart_core().await?; CoreManager::global().restart_core().await?;
} else { } else {
if patch.get("mode").is_some() { if patch.get("mode").is_some() {
logging_error!(Type::Tray, true, tray::Tray::global().update_menu()); logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None)); logging_error!(
Type::Tray,
true,
tray::Tray::global().update_icon(None).await
);
} }
Config::runtime().draft_mut().patch_config(patch); Config::runtime().await.draft_mut().patch_config(patch);
CoreManager::global().update_config().await?; CoreManager::global().update_config().await?;
} }
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
@@ -30,12 +37,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
}; };
match res { match res {
Ok(()) => { Ok(()) => {
Config::clash().apply(); Config::clash().await.apply();
Config::clash().data_mut().save_config()?; // 分离数据获取和异步调用
let clash_data = Config::clash().await.data_mut().clone();
clash_data.save_config().await?;
Ok(()) Ok(())
} }
Err(err) => { Err(err) => {
Config::clash().discard(); Config::clash().await.discard();
Err(err) Err(err)
} }
} }
@@ -60,7 +69,10 @@ enum UpdateFlags {
/// Patch Verge configuration /// Patch Verge configuration
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> { pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge().draft_mut().patch_config(patch.clone()); Config::verge()
.await
.draft_mut()
.patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode; let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch; let auto_launch = patch.enable_auto_launch;
@@ -173,7 +185,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
// Process updates based on flags // Process updates based on flags
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 { if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
Config::generate()?; Config::generate().await?;
CoreManager::global().restart_core().await?; CoreManager::global().restart_core().await?;
} }
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 { if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
@@ -181,35 +193,35 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 { if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
Config::verge().draft_mut().enable_global_hotkey = enable_global_hotkey; Config::verge().await.draft_mut().enable_global_hotkey = enable_global_hotkey;
handle::Handle::refresh_verge(); handle::Handle::refresh_verge();
} }
if (update_flags & (UpdateFlags::Launch as i32)) != 0 { if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
sysopt::Sysopt::global().update_launch()?; sysopt::Sysopt::global().update_launch().await?;
} }
if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 { if (update_flags & (UpdateFlags::SysProxy as i32)) != 0 {
sysopt::Sysopt::global().update_sysproxy().await?; sysopt::Sysopt::global().update_sysproxy().await?;
} }
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 { if (update_flags & (UpdateFlags::Hotkey as i32)) != 0
if let Some(hotkeys) = patch.hotkeys { && let Some(hotkeys) = patch.hotkeys
hotkey::Hotkey::global().update(hotkeys)?; {
} hotkey::Hotkey::global().update(hotkeys).await?;
} }
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 { if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
tray::Tray::global().update_menu()?; tray::Tray::global().update_menu().await?;
} }
if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 { if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 {
tray::Tray::global().update_icon(None)?; tray::Tray::global().update_icon(None).await?;
} }
if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 { if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 {
tray::Tray::global().update_tooltip()?; tray::Tray::global().update_tooltip().await?;
} }
if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 { if (update_flags & (UpdateFlags::SystrayClickBehavior as i32)) != 0 {
tray::Tray::global().update_click_behavior()?; tray::Tray::global().update_click_behavior().await?;
} }
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 { if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
if enable_auto_light_weight.unwrap_or(false) { if enable_auto_light_weight.unwrap_or(false) {
lightweight::enable_auto_light_weight_mode(); lightweight::enable_auto_light_weight_mode().await;
} else { } else {
lightweight::disable_auto_light_weight_mode(); lightweight::disable_auto_light_weight_mode();
} }
@@ -219,15 +231,17 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
}; };
match res { match res {
Ok(()) => { Ok(()) => {
Config::verge().apply(); Config::verge().await.apply();
if !not_save_file { if !not_save_file {
Config::verge().data_mut().save_file()?; // 分离数据获取和异步调用
let verge_data = Config::verge().await.data_mut().clone();
verge_data.save_file().await?;
} }
Ok(()) Ok(())
} }
Err(err) => { Err(err) => {
Config::verge().discard(); Config::verge().await.discard();
Err(err) Err(err)
} }
} }
+25 -30
View File
@@ -1,33 +1,25 @@
use crate::{ use crate::{
cmd, cmd,
config::{Config, PrfItem, PrfOption}, config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
core::{handle, CoreManager, *}, core::{CoreManager, handle, tray},
logging, logging,
process::AsyncHandler,
utils::logging::Type, utils::logging::Type,
}; };
use anyhow::{bail, Result}; use anyhow::{Result, bail};
/// Toggle proxy profile /// Toggle proxy profile
pub fn toggle_proxy_profile(profile_index: String) { pub async fn toggle_proxy_profile(profile_index: String) {
AsyncHandler::spawn(|| async move { match cmd::patch_profiles_config_by_profile_index(profile_index).await {
let Some(app_handle) = handle::Handle::global().app_handle() else { Ok(_) => {
logging!( let result = tray::Tray::global().update_menu().await;
error, if let Err(err) = result {
Type::Config, logging!(error, Type::Tray, true, "更新菜单失败: {}", err);
"Failed to get app handle for profile toggle"
);
return;
};
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
Ok(_) => {
let _ = tray::Tray::global().update_menu();
}
Err(err) => {
log::error!(target: "app", "{err}");
} }
} }
}); Err(err) => {
log::error!(target: "app", "{err}");
}
}
} }
/// Update a profile /// Update a profile
@@ -42,7 +34,7 @@ pub async fn update_profile(
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性 let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
let url_opt = { let url_opt = {
let profiles = Config::profiles(); let profiles = Config::profiles().await;
let profiles = profiles.latest_ref(); let profiles = profiles.latest_ref();
let item = profiles.get_item(&uid)?; let item = profiles.get_item(&uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote"); let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
@@ -77,11 +69,13 @@ pub async fn update_profile(
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await { match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => { Ok(item) => {
log::info!(target: "app", "[订阅更新] 更新订阅配置成功"); log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles(); let profiles = Config::profiles().await;
let mut profiles = profiles.draft_mut();
profiles.update_item(uid.clone(), item)?;
let is_current = Some(uid.clone()) == profiles.get_current(); // 使用Send-safe helper函数
let result = profiles_draft_update_item_safe(uid.clone(), item).await;
result?;
let is_current = Some(uid.clone()) == profiles.latest_ref().get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}"); log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
is_current && auto_refresh is_current && auto_refresh
} }
@@ -113,9 +107,10 @@ pub async fn update_profile(
} }
// 更新到配置 // 更新到配置
let profiles = Config::profiles(); let profiles = Config::profiles().await;
let mut profiles = profiles.draft_mut();
profiles.update_item(uid.clone(), item.clone())?; // 使用 Send-safe 方法进行数据操作
profiles_draft_update_item_safe(uid.clone(), item.clone()).await?;
// 获取配置名称用于通知 // 获取配置名称用于通知
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone()); let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
@@ -123,7 +118,7 @@ pub async fn update_profile(
// 发送通知告知用户自动更新使用了回退机制 // 发送通知告知用户自动更新使用了回退机制
handle::Handle::notice_message("update_with_clash_proxy", profile_name); handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.get_current(); let is_current = Some(uid.clone()) == profiles.data_ref().get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}"); log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
is_current && auto_refresh is_current && auto_refresh
} }
+56 -50
View File
@@ -3,75 +3,80 @@ use crate::{
core::handle, core::handle,
ipc::IpcManager, ipc::IpcManager,
logging, logging,
process::AsyncHandler,
utils::logging::Type, utils::logging::Type,
}; };
use std::env; use std::env;
use tauri_plugin_clipboard_manager::ClipboardExt; use tauri_plugin_clipboard_manager::ClipboardExt;
/// Toggle system proxy on/off /// Toggle system proxy on/off
pub fn toggle_system_proxy() { pub async fn toggle_system_proxy() {
let enable = Config::verge().draft_mut().enable_system_proxy; // 获取当前系统代理状态
let enable = enable.unwrap_or(false); let enable = {
let auto_close_connection = Config::verge() let verge = Config::verge().await;
.data_mut()
.auto_close_connection
.unwrap_or(false);
AsyncHandler::spawn(move || async move { verge.latest_ref().enable_system_proxy.unwrap_or(false)
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接 };
if enable && auto_close_connection { // 获取自动关闭连接设置
if let Err(err) = IpcManager::global().close_all_connections().await { let auto_close_connection = {
log::error!(target: "app", "Failed to close all connections: {err}"); let verge = Config::verge().await;
}
}
match super::patch_verge( verge.latest_ref().auto_close_connection.unwrap_or(false)
IVerge { };
enable_system_proxy: Some(!enable),
..IVerge::default() // 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
}, if enable
false, && auto_close_connection
) && let Err(err) = IpcManager::global().close_all_connections().await
.await {
{ log::error!(target: "app", "Failed to close all connections: {err}");
Ok(_) => handle::Handle::refresh_verge(), }
Err(err) => log::error!(target: "app", "{err}"),
} let patch_result = super::patch_verge(
}); IVerge {
enable_system_proxy: Some(!enable),
..IVerge::default()
},
false,
)
.await;
match patch_result {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
} }
/// Toggle TUN mode on/off /// Toggle TUN mode on/off
pub fn toggle_tun_mode(not_save_file: Option<bool>) { pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().data_mut().enable_tun_mode; let enable = Config::verge().await.data_mut().enable_tun_mode;
let enable = enable.unwrap_or(false); let enable = enable.unwrap_or(false);
AsyncHandler::spawn(async move || { match super::patch_verge(
match super::patch_verge( IVerge {
IVerge { enable_tun_mode: Some(!enable),
enable_tun_mode: Some(!enable), ..IVerge::default()
..IVerge::default() },
}, not_save_file.unwrap_or(false),
not_save_file.unwrap_or(false), )
) .await
.await {
{ Ok(_) => handle::Handle::refresh_verge(),
Ok(_) => handle::Handle::refresh_verge(), Err(err) => log::error!(target: "app", "{err}"),
Err(err) => log::error!(target: "app", "{err}"), }
}
});
} }
/// Copy proxy environment variables to clipboard /// Copy proxy environment variables to clipboard
pub fn copy_clash_env() { pub async fn copy_clash_env() {
// 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1 // 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| { let clash_verge_rev_ip = match env::var("CLASH_VERGE_REV_IP") {
Config::verge() Ok(ip) => ip,
Err(_) => Config::verge()
.await
.latest_ref() .latest_ref()
.proxy_host .proxy_host
.clone() .clone()
.unwrap_or_else(|| "127.0.0.1".to_string()) .unwrap_or_else(|| "127.0.0.1".to_string()),
}); };
let Some(app_handle) = handle::Handle::global().app_handle() else { let Some(app_handle) = handle::Handle::global().app_handle() else {
logging!( logging!(
@@ -83,6 +88,7 @@ pub fn copy_clash_env() {
}; };
let port = { let port = {
Config::verge() Config::verge()
.await
.latest_ref() .latest_ref()
.verge_mixed_port .verge_mixed_port
.unwrap_or(7897) .unwrap_or(7897)
@@ -91,7 +97,7 @@ pub fn copy_clash_env() {
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}"); let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
let cliboard = app_handle.clipboard(); let cliboard = app_handle.clipboard();
let env_type = { Config::verge().latest_ref().env_type.clone() }; let env_type = { Config::verge().await.latest_ref().env_type.clone() };
let env_type = match env_type { let env_type = match env_type {
Some(env_type) => env_type, Some(env_type) => env_type,
None => { None => {
+35 -73
View File
@@ -1,69 +1,27 @@
#[cfg(target_os = "macos")] use crate::utils::window_manager::WindowManager;
use crate::AppHandleManager;
use crate::{ use crate::{
config::Config, config::Config,
core::{handle, sysopt, CoreManager}, core::{CoreManager, handle, sysopt},
ipc::IpcManager, ipc::IpcManager,
logging, logging,
module::lightweight,
utils::logging::Type, utils::logging::Type,
}; };
/// Open or close the dashboard window /// Open or close the dashboard window
#[allow(dead_code)] pub async fn open_or_close_dashboard() {
pub fn open_or_close_dashboard() { open_or_close_dashboard_internal().await
open_or_close_dashboard_internal(false)
}
/// Open or close the dashboard window (hotkey call, dispatched to main thread)
#[allow(dead_code)]
pub fn open_or_close_dashboard_hotkey() {
open_or_close_dashboard_internal(true)
} }
/// Internal implementation for opening/closing dashboard /// Internal implementation for opening/closing dashboard
fn open_or_close_dashboard_internal(bypass_debounce: bool) { async fn open_or_close_dashboard_internal() {
use crate::process::AsyncHandler; let _ = lightweight::exit_lightweight_mode().await;
use crate::utils::window_manager::WindowManager; let result = WindowManager::toggle_main_window().await;
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
// 热键调用调度到主线程执行,避免 WebView 创建死锁
if bypass_debounce {
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
AsyncHandler::spawn(move || async move {
log::info!(target: "app", "主线程中执行热键窗口操作");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode();
log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window();
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
let result = WindowManager::toggle_main_window();
log::info!(target: "app", "Window toggle result: {result:?}");
});
return;
}
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
crate::module::lightweight::exit_lightweight_mode();
log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window();
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
let result = WindowManager::toggle_main_window();
log::info!(target: "app", "Window toggle result: {result:?}"); log::info!(target: "app", "Window toggle result: {result:?}");
} }
/// 异步优化的应用退出函数 /// 异步优化的应用退出函数
pub fn quit() { pub async fn quit() {
use crate::process::AsyncHandler;
logging!(debug, Type::System, true, "启动退出流程"); logging!(debug, Type::System, true, "启动退出流程");
// 获取应用句柄并设置退出标志 // 获取应用句柄并设置退出标志
@@ -84,28 +42,31 @@ pub fn quit() {
} }
// 使用异步任务处理资源清理,避免阻塞 // 使用异步任务处理资源清理,避免阻塞
AsyncHandler::spawn(move || async move { logging!(info, Type::System, true, "开始异步清理资源");
logging!(info, Type::System, true, "开始异步清理资源"); let cleanup_result = clean_async().await;
let cleanup_result = clean_async().await;
logging!( logging!(
info, info,
Type::System, Type::System,
true, true,
"资源清理完成,退出代码: {}", "资源清理完成,退出代码: {}",
if cleanup_result { 0 } else { 1 } if cleanup_result { 0 } else { 1 }
); );
app_handle.exit(if cleanup_result { 0 } else { 1 }); app_handle.exit(if cleanup_result { 0 } else { 1 });
});
} }
async fn clean_async() -> bool { async fn clean_async() -> bool {
use tokio::time::{timeout, Duration}; use tokio::time::{Duration, timeout};
logging!(info, Type::System, true, "开始执行异步清理操作..."); logging!(info, Type::System, true, "开始执行异步清理操作...");
// 1. 处理TUN模式 // 1. 处理TUN模式
let tun_success = if Config::verge().data_mut().enable_tun_mode.unwrap_or(false) { let tun_success = if Config::verge()
.await
.data_mut()
.enable_tun_mode
.unwrap_or(false)
{
let disable_tun = serde_json::json!({"tun": {"enable": false}}); let disable_tun = serde_json::json!({"tun": {"enable": false}});
match timeout( match timeout(
Duration::from_secs(3), Duration::from_secs(3),
@@ -169,7 +130,7 @@ async fn clean_async() -> bool {
let dns_task = async { let dns_task = async {
match timeout( match timeout(
Duration::from_millis(1000), Duration::from_millis(1000),
crate::utils::resolve::restore_public_dns(), crate::utils::resolve::dns::restore_public_dns(),
) )
.await .await
{ {
@@ -242,22 +203,23 @@ pub fn clean() -> bool {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn hide() { pub async fn hide() {
use crate::module::lightweight::add_light_weight_timer; use crate::module::lightweight::add_light_weight_timer;
let enable_auto_light_weight_mode = Config::verge() let enable_auto_light_weight_mode = Config::verge()
.await
.data_mut() .data_mut()
.enable_auto_light_weight_mode .enable_auto_light_weight_mode
.unwrap_or(false); .unwrap_or(false);
if enable_auto_light_weight_mode { if enable_auto_light_weight_mode {
add_light_weight_timer(); add_light_weight_timer().await;
} }
if let Some(window) = handle::Handle::global().get_window() { if let Some(window) = handle::Handle::global().get_window()
if window.is_visible().unwrap_or(false) { && window.is_visible().unwrap_or(false)
let _ = window.hide(); {
} let _ = window.hide();
} }
AppHandleManager::global().set_activation_policy_accessory(); handle::Handle::global().set_activation_policy_accessory();
} }
+29 -45
View File
@@ -1,11 +1,15 @@
use std::time::Duration; use std::time::Duration;
use kode_bridge::{ use kode_bridge::{
errors::{AnyError, AnyResult},
pool::PoolConfig,
ClientConfig, IpcHttpClient, LegacyResponse, 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},
}; };
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
// 定义用于URL路径的编码集合,只编码真正必要的字符 // 定义用于URL路径的编码集合,只编码真正必要的字符
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
@@ -16,57 +20,38 @@ const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b'&') // 和号 .add(b'&') // 和号
.add(b'%'); // 百分号 .add(b'%'); // 百分号
use crate::{logging, singleton_with_logging, utils::dirs::ipc_path};
// Helper function to create AnyError from string // Helper function to create AnyError from string
fn create_error(msg: impl Into<String>) -> AnyError { fn create_error(msg: impl Into<String>) -> AnyError {
Box::new(std::io::Error::other(msg.into())) Box::new(std::io::Error::other(msg.into()))
} }
pub struct IpcManager { pub struct IpcManager {
ipc_path: String, client: IpcHttpClient,
config: ClientConfig,
} }
impl IpcManager { impl IpcManager {
fn new() -> Self { pub fn new() -> Self {
logging!(info, Type::Ipc, true, "Creating new IpcManager instance");
let ipc_path_buf = ipc_path().unwrap_or_else(|e| { let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
logging!( logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
error,
crate::utils::logging::Type::Ipc,
true,
"Failed to get IPC path: {}",
e
);
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
}); });
let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
Self { let config = ClientConfig {
ipc_path: ipc_path.to_string(), default_timeout: Duration::from_secs(5),
config: ClientConfig { enable_pooling: false,
default_timeout: Duration::from_secs(5), max_retries: 4,
enable_pooling: true, retry_delay: Duration::from_millis(125),
max_retries: 3, max_concurrent_requests: 16,
max_concurrent_requests: 32, max_requests_per_second: Some(64.0),
max_requests_per_second: Some(5.0), ..Default::default()
pool_config: PoolConfig { };
max_size: 32, #[allow(clippy::unwrap_used)]
min_idle: 2, let client = IpcHttpClient::with_config(ipc_path, config).unwrap();
max_idle_time_ms: 10_000, Self { client }
max_retries: 3,
max_concurrent_requests: 32,
max_requests_per_second: Some(5.0),
..Default::default()
},
..Default::default()
},
}
} }
} }
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
impl IpcManager { impl IpcManager {
pub async fn request( pub async fn request(
&self, &self,
@@ -74,8 +59,7 @@ impl IpcManager {
path: &str, path: &str,
body: Option<&serde_json::Value>, body: Option<&serde_json::Value>,
) -> AnyResult<LegacyResponse> { ) -> AnyResult<LegacyResponse> {
let client = IpcHttpClient::with_config(&self.ipc_path, self.config.clone())?; self.client.request(method, path, body).await
client.request(method, path, body).await
} }
} }
@@ -213,8 +197,7 @@ impl IpcManager {
// 测速URL不再编码,直接传递 // 测速URL不再编码,直接传递
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}"); let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await; self.send_request("GET", &url, None).await
response
} }
// 版本和配置相关 // 版本和配置相关
@@ -293,7 +276,6 @@ impl IpcManager {
let payload = serde_json::json!({ let payload = serde_json::json!({
"name": proxy "name": proxy
}); });
match self.send_request("PUT", &url, Some(&payload)).await { match self.send_request("PUT", &url, Some(&payload)).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(e) => { Err(e) => {
@@ -354,8 +336,7 @@ impl IpcManager {
// 测速URL不再编码,直接传递 // 测速URL不再编码,直接传递
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}"); let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await; self.send_request("GET", &url, None).await
response
} }
// 调试相关 // 调试相关
@@ -384,3 +365,6 @@ impl IpcManager {
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理 // 日志相关功能已迁移到 logs.rs 模块,使用流式处理
} }
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
+139 -287
View File
@@ -1,3 +1,7 @@
#![allow(non_snake_case)]
#![recursion_limit = "512"]
mod cache;
mod cmd; mod cmd;
pub mod config; pub mod config;
mod core; mod core;
@@ -6,151 +10,38 @@ mod feat;
mod ipc; mod ipc;
mod module; mod module;
mod process; mod process;
mod state;
mod utils; mod utils;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
use crate::{ use crate::{
core::handle,
core::hotkey, core::hotkey,
process::AsyncHandler, process::AsyncHandler,
utils::{resolve, resolve::resolve_scheme, server}, utils::{resolve, server},
}; };
use config::Config; use config::Config;
use parking_lot::Mutex;
use tauri::AppHandle; use tauri::AppHandle;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::Manager; use tauri::Manager;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{timeout, Duration}; use tokio::time::{Duration, timeout};
use utils::logging::Type; use utils::logging::Type;
/// A global singleton handle to the application.
pub struct AppHandleManager {
handle: Mutex<Option<AppHandle>>,
}
impl AppHandleManager {
/// Create a new AppHandleManager instance
fn new() -> Self {
Self {
handle: Mutex::new(None),
}
}
/// Initialize the app handle manager with an app handle.
pub fn init(&self, handle: AppHandle) {
let mut app_handle = self.handle.lock();
if app_handle.is_none() {
*app_handle = Some(handle);
logging!(
info,
Type::Setup,
true,
"AppHandleManager initialized with handle"
);
}
}
/// Get the app handle if it has been initialized.
pub fn get(&self) -> Option<AppHandle> {
self.handle.lock().clone()
}
/// Get the app handle, panics if it hasn't been initialized.
pub fn get_handle(&self) -> AppHandle {
if let Some(handle) = self.get() {
handle
} else {
logging!(
error,
Type::Setup,
"AppHandle not initialized - ensure init() was called first"
);
std::process::exit(1)
}
}
/// Check if the app handle has been initialized.
pub fn is_initialized(&self) -> bool {
self.handle.lock().is_some()
}
#[cfg(target_os = "macos")]
pub fn set_activation_policy(&self, policy: tauri::ActivationPolicy) -> Result<(), String> {
let app_handle = self.handle.lock();
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())
}
}
pub fn set_activation_policy_regular(&self) {
#[cfg(target_os = "macos")]
{
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Regular) {
logging!(
warn,
Type::Setup,
true,
"Failed to set regular activation policy: {}",
e
);
}
}
}
pub fn set_activation_policy_accessory(&self) {
#[cfg(target_os = "macos")]
{
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Accessory) {
logging!(
warn,
Type::Setup,
true,
"Failed to set accessory activation policy: {}",
e
);
}
}
}
pub fn set_activation_policy_prohibited(&self) {
#[cfg(target_os = "macos")]
{
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Prohibited) {
logging!(
warn,
Type::Setup,
true,
"Failed to set prohibited activation policy: {}",
e
);
}
}
}
}
// Use unified singleton macro
singleton_with_logging!(AppHandleManager, INSTANCE, "AppHandleManager");
/// Application initialization helper functions /// Application initialization helper functions
mod app_init { mod app_init {
use super::*; use super::*;
/// Initialize singleton monitoring for other instances /// Initialize singleton monitoring for other instances
pub fn init_singleton_check() { pub fn init_singleton_check() {
AsyncHandler::spawn(move || async move { AsyncHandler::spawn_blocking(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例..."); logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await { match timeout(Duration::from_millis(500), server::check_singleton()).await {
Ok(result) => { Ok(result) => {
if result.is_err() { if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行"); logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if let Some(app_handle) = AppHandleManager::global().get() { if let Some(app_handle) = handle::Handle::global().app_handle() {
app_handle.exit(0); app_handle.exit(0);
} else { } else {
std::process::exit(0); std::process::exit(0);
@@ -183,14 +74,16 @@ mod app_init {
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init()); .plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_http::init());
#[cfg(all(debug_assertions, not(feature = "tokio-trace")))] // Devtools plugin only in debug mode with feature tauri-dev
// to avoid duplicated registering of logger since the devtools plugin also registers a logger
#[cfg(all(debug_assertions, not(feature = "tokio-trace"), feature = "tauri-dev"))]
{ {
builder = builder.plugin(tauri_plugin_devtools::init()); builder = builder.plugin(tauri_plugin_devtools::init());
} }
builder
builder.manage(Mutex::new(state::lightweight::LightWeightState::default()))
} }
/// Setup deep link handling /// Setup deep link handling
@@ -202,16 +95,14 @@ mod app_init {
} }
app.deep_link().on_open_url(|event| { app.deep_link().on_open_url(|event| {
AsyncHandler::spawn(move || { let url = event.urls().first().map(|u| u.to_string());
let url = event.urls().first().map(|u| u.to_string()); if let Some(url) = url {
async move { AsyncHandler::spawn(|| async {
if let Some(url) = url { if let Err(e) = resolve::resolve_scheme(url).await {
if let Err(e) = resolve_scheme(url).await { logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
}
} }
} });
}); }
}); });
Ok(()) Ok(())
@@ -245,52 +136,9 @@ mod app_init {
Ok(()) Ok(())
} }
/// Initialize core components asynchronously
pub fn init_core_async(app_handle: tauri::AppHandle) {
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "异步执行应用设置...");
match timeout(
Duration::from_secs(30),
resolve::resolve_setup_async(&app_handle),
)
.await
{
Ok(_) => {
logging!(info, Type::Setup, true, "应用设置成功完成");
}
Err(_) => {
logging!(
error,
Type::Setup,
true,
"应用设置超时(30秒),继续执行后续流程"
);
}
}
});
}
/// Initialize core components synchronously
pub fn init_core_sync(app_handle: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
AppHandleManager::global().init(app_handle.clone());
logging!(info, Type::Setup, true, "初始化核心句柄...");
core::handle::Handle::global().init(app_handle);
logging!(info, Type::Setup, true, "初始化配置...");
utils::init::init_config()?;
logging!(info, Type::Setup, true, "初始化资源...");
utils::init::init_resources()?;
logging!(info, Type::Setup, true, "核心组件初始化完成");
Ok(())
}
/// Generate all command handlers for the application /// Generate all command handlers for the application
pub fn generate_handlers( pub fn generate_handlers()
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static { -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
tauri::generate_handler![ tauri::generate_handler![
// Common commands // Common commands
cmd::get_sys_proxy, cmd::get_sys_proxy,
@@ -310,7 +158,6 @@ mod app_init {
// Application lifecycle // Application lifecycle
cmd::notify_ui_ready, cmd::notify_ui_ready,
cmd::update_ui_stage, cmd::update_ui_stage,
cmd::reset_ui_ready_state,
cmd::get_running_mode, cmd::get_running_mode,
cmd::get_app_uptime, cmd::get_app_uptime,
cmd::get_auto_launch_status, cmd::get_auto_launch_status,
@@ -333,11 +180,15 @@ mod app_init {
cmd::get_runtime_yaml, cmd::get_runtime_yaml,
cmd::get_runtime_exists, cmd::get_runtime_exists,
cmd::get_runtime_logs, cmd::get_runtime_logs,
cmd::get_runtime_proxy_chain_config,
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool, cmd::invoke_uwp_tool,
cmd::copy_clash_env, cmd::copy_clash_env,
cmd::get_proxies, cmd::get_proxies,
cmd::force_refresh_proxies, cmd::force_refresh_proxies,
cmd::get_providers_proxies, cmd::get_providers_proxies,
cmd::sync_tray_proxy_selection,
cmd::update_proxy_and_sync,
cmd::save_dns_config, cmd::save_dns_config,
cmd::apply_dns_config, cmd::apply_dns_config,
cmd::check_dns_config_exists, cmd::check_dns_config_exists,
@@ -422,16 +273,14 @@ pub fn run() {
// Setup singleton check // Setup singleton check
app_init::init_singleton_check(); app_init::init_singleton_check();
// Initialize network manager
utils::network::NetworkManager::global().init();
// Initialize portable flag
let _ = utils::dirs::init_portable_flag(); let _ = utils::dirs::init_portable_flag();
// Set Linux environment variable // Set Linux environment variable
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); unsafe {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP") let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default() .unwrap_or_default()
@@ -440,7 +289,9 @@ pub fn run() {
let is_plasma_desktop = desktop_env.contains("PLASMA"); let is_plasma_desktop = desktop_env.contains("PLASMA");
if is_kde_desktop || is_plasma_desktop { if is_kde_desktop || is_plasma_desktop {
std::env::set_var("GTK_CSD", "0"); unsafe {
std::env::set_var("GTK_CSD", "0");
}
logging!( logging!(
info, info,
Type::Setup, Type::Setup,
@@ -482,22 +333,13 @@ pub fn run() {
); );
} }
// Initialize core components asynchronously let app_handle = app.handle().clone();
app_init::init_core_async(app.handle().clone());
logging!(info, Type::Setup, true, "执行主要设置操作..."); logging!(info, Type::Setup, true, "执行主要设置操作...");
// Initialize core components synchronously resolve::resolve_setup_handle(app_handle);
if let Err(e) = app_init::init_core_sync(app.handle()) { resolve::resolve_setup_async();
logging!( resolve::resolve_setup_sync();
error,
Type::Setup,
true,
"Failed to initialize core components: {}",
e
);
return Err(e);
}
logging!(info, Type::Setup, true, "初始化完成,继续执行"); logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(()) Ok(())
@@ -506,12 +348,14 @@ pub fn run() {
/// Event handling helper functions /// Event handling helper functions
mod event_handlers { mod event_handlers {
use crate::core::handle;
use super::*; use super::*;
/// Handle application ready/resumed events /// Handle application ready/resumed events
pub fn handle_ready_resumed(app_handle: &tauri::AppHandle) { pub fn handle_ready_resumed(app_handle: &AppHandle) {
logging!(info, Type::System, true, "应用就绪或恢复"); logging!(info, Type::System, true, "应用就绪或恢复");
AppHandleManager::global().init(app_handle.clone()); handle::Handle::global().init(app_handle.clone());
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
@@ -524,7 +368,7 @@ pub fn run() {
/// Handle application reopen events (macOS) /// Handle application reopen events (macOS)
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn handle_reopen(app_handle: &tauri::AppHandle, has_visible_windows: bool) { pub async fn handle_reopen(app_handle: &AppHandle, has_visible_windows: bool) {
logging!( logging!(
info, info,
Type::System, Type::System,
@@ -533,15 +377,15 @@ pub fn run() {
has_visible_windows has_visible_windows
); );
AppHandleManager::global().init(app_handle.clone()); handle::Handle::global().init(app_handle.clone());
if !has_visible_windows { if !has_visible_windows {
// 当没有可见窗口时,设置为 regular 模式并显示主窗口 // 当没有可见窗口时,设置为 regular 模式并显示主窗口
AppHandleManager::global().set_activation_policy_regular(); handle::Handle::global().set_activation_policy_regular();
logging!(info, Type::System, true, "没有可见窗口,尝试显示主窗口"); logging!(info, Type::System, true, "没有可见窗口,尝试显示主窗口");
let result = WindowManager::show_main_window(); let result = WindowManager::show_main_window().await;
logging!( logging!(
info, info,
Type::System, Type::System,
@@ -557,7 +401,7 @@ pub fn run() {
/// Handle window close requests /// Handle window close requests
pub fn handle_window_close(api: &tauri::WindowEvent) { pub fn handle_window_close(api: &tauri::WindowEvent) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory(); handle::Handle::global().set_activation_policy_accessory();
if core::handle::Handle::global().is_exiting() { if core::handle::Handle::global().is_exiting() {
return; return;
@@ -576,68 +420,71 @@ pub fn run() {
/// Handle window focus events /// Handle window focus events
pub fn handle_window_focus(focused: bool) { pub fn handle_window_focus(focused: bool) {
let is_enable_global_hotkey = Config::verge() AsyncHandler::spawn(move || async move {
.latest_ref() let is_enable_global_hotkey = Config::verge()
.enable_global_hotkey .await
.unwrap_or(true); .latest_ref()
.enable_global_hotkey
.unwrap_or(true);
if focused { if focused {
#[cfg(target_os = "macos")]
{
use crate::core::hotkey::SystemHotkey;
if let Err(e) = hotkey::Hotkey::global()
.register_system_hotkey(SystemHotkey::CmdQ)
.await
{
logging!(error, Type::Hotkey, true, "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);
}
}
if !is_enable_global_hotkey
&& let Err(e) = hotkey::Hotkey::global().init().await
{
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
}
return;
}
// Handle unfocused state
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
use crate::core::hotkey::SystemHotkey; use crate::core::hotkey::SystemHotkey;
if let Err(e) = if let Err(e) =
hotkey::Hotkey::global().register_system_hotkey(SystemHotkey::CmdQ) hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
{ {
logging!(error, Type::Hotkey, true, "Failed to register CMD+Q: {}", e); logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+Q: {}",
e
);
} }
if let Err(e) = if let Err(e) =
hotkey::Hotkey::global().register_system_hotkey(SystemHotkey::CmdW) hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
{ {
logging!(error, Type::Hotkey, true, "Failed to register CMD+W: {}", e); logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+W: {}",
e
);
} }
} }
if !is_enable_global_hotkey { if !is_enable_global_hotkey && let Err(e) = hotkey::Hotkey::global().reset() {
if let Err(e) = hotkey::Hotkey::global().init() {
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
}
}
return;
}
// Handle unfocused state
#[cfg(target_os = "macos")]
{
use crate::core::hotkey::SystemHotkey;
if let Err(e) =
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
{
logging!(
error,
Type::Hotkey,
true,
"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
);
}
}
if !is_enable_global_hotkey {
if let Err(e) = hotkey::Hotkey::global().reset() {
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e); logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
} }
} });
} }
/// Handle window destroyed events /// Handle window destroyed events
@@ -685,45 +532,50 @@ pub fn run() {
std::process::exit(1); std::process::exit(1);
}); });
app.run(|app_handle, e| match e { app.run(|app_handle, e| {
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => { match e {
event_handlers::handle_ready_resumed(app_handle); tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
} event_handlers::handle_ready_resumed(app_handle);
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen {
has_visible_windows,
..
} => {
event_handlers::handle_reopen(app_handle, has_visible_windows);
}
tauri::RunEvent::ExitRequested { api, code, .. } => {
if code.is_none() {
api.prevent_exit();
} }
} #[cfg(target_os = "macos")]
tauri::RunEvent::Exit => { tauri::RunEvent::Reopen {
// Avoid duplicate cleanup has_visible_windows,
if core::handle::Handle::global().is_exiting() { ..
return; } => {
let app_handle = app_handle.clone();
AsyncHandler::spawn(move || async move {
event_handlers::handle_reopen(&app_handle, has_visible_windows).await;
});
} }
feat::clean(); tauri::RunEvent::ExitRequested { api, code, .. } => {
} if code.is_none() {
tauri::RunEvent::WindowEvent { label, event, .. } => { api.prevent_exit();
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { .. } => {
event_handlers::handle_window_close(&event);
}
tauri::WindowEvent::Focused(focused) => {
event_handlers::handle_window_focus(focused);
}
tauri::WindowEvent::Destroyed => {
event_handlers::handle_window_destroyed();
}
_ => {}
} }
} }
tauri::RunEvent::Exit => {
// Avoid duplicate cleanup
if core::handle::Handle::global().is_exiting() {
return;
}
feat::clean();
}
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { .. } => {
event_handlers::handle_window_close(&event);
}
tauri::WindowEvent::Focused(focused) => {
event_handlers::handle_window_focus(focused);
}
tauri::WindowEvent::Destroyed => {
event_handlers::handle_window_destroyed();
}
_ => {}
}
}
}
_ => {}
} }
_ => {}
}); });
} }
+180 -154
View File
@@ -1,130 +1,143 @@
use crate::{ use crate::{
cache::CacheProxy,
config::Config, config::Config,
core::{handle, timer::Timer, tray::Tray}, core::{handle, timer::Timer, tray::Tray},
log_err, logging, log_err, logging,
state::lightweight::LightWeightState, process::AsyncHandler,
utils::logging::Type, utils::logging::Type,
}; };
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use crate::logging_error; use crate::logging_error;
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use crate::utils::window_manager::WindowManager;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder; use delay_timer::prelude::TaskBuilder;
use parking_lot::Mutex; use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
use std::sync::atomic::{AtomicBool, Ordering}; use tauri::Listener;
use tauri::{Listener, Manager};
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
// 添加退出轻量模式的锁,防止并发调用 #[repr(u8)]
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false); #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LightweightState {
Normal = 0,
In = 1,
Exiting = 2,
}
fn with_lightweight_status<F, R>(f: F) -> Option<R> impl From<u8> for LightweightState {
where fn from(v: u8) -> Self {
F: FnOnce(&mut LightWeightState) -> R, match v {
{ 1 => LightweightState::In,
if let Some(app_handle) = handle::Handle::global().app_handle() { 2 => LightweightState::Exiting,
// Try to get state, but don't panic if it's not managed yet _ => LightweightState::Normal,
if let Some(state) = app_handle.try_state::<Mutex<LightWeightState>>() {
let mut guard = state.lock();
Some(f(&mut guard))
} else {
// State not managed yet, return None
None
} }
} else {
// App handle not available yet
None
} }
} }
pub fn run_once_auto_lightweight() { impl LightweightState {
LightWeightState::default().run_once_time(|| { fn as_u8(self) -> u8 {
let is_silent_start = Config::verge() self as u8
.latest_ref() }
.enable_silent_start
.unwrap_or(false);
let enable_auto = Config::verge()
.data_mut()
.enable_auto_light_weight_mode
.unwrap_or(false);
if enable_auto && is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
);
set_lightweight_mode(false);
enable_auto_light_weight_mode();
// 触发托盘更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {e}");
}
}
});
} }
pub fn auto_lightweight_mode_init() { static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
if let Some(app_handle) = handle::Handle::global().app_handle() {
// Check if state is available before accessing it static WINDOW_CLOSE_HANDLER: AtomicU32 = AtomicU32::new(0);
if app_handle.try_state::<Mutex<LightWeightState>>().is_none() { static WEBVIEW_FOCUS_HANDLER: AtomicU32 = AtomicU32::new(0);
logging!(
warn, fn set_state(new: LightweightState) {
Type::Lightweight, LIGHTWEIGHT_STATE.store(new.as_u8(), Ordering::Release);
true, match new {
"LightWeightState 尚未初始化,跳过自动轻量模式初始化" LightweightState::Normal => {
); logging!(info, Type::Lightweight, true, "轻量模式已关闭");
return;
} }
LightweightState::In => {
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false); logging!(info, Type::Lightweight, true, "轻量模式已开启");
let enable_auto = }
{ Config::verge().latest_ref().enable_auto_light_weight_mode }.unwrap_or(false); LightweightState::Exiting => {
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
if enable_auto && !is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_lightweight_mode(true);
enable_auto_light_weight_mode();
// 确保托盘状态更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {e}");
}
} }
} }
} }
fn get_state() -> LightweightState {
LIGHTWEIGHT_STATE.load(Ordering::Acquire).into()
}
// 检查是否处于轻量模式 // 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool { pub fn is_in_lightweight_mode() -> bool {
with_lightweight_status(|state| state.is_lightweight).unwrap_or(false) get_state() == LightweightState::In
} }
// 设置轻量模式状态 // 设置轻量模式状态(仅 Normal <-> In)
pub fn set_lightweight_mode(value: bool) { async fn set_lightweight_mode(value: bool) {
if with_lightweight_status(|state| { let current = get_state();
state.set_lightweight_mode(value); if value && current != LightweightState::In {
}) set_state(LightweightState::In);
.is_some() } else if !value && current != LightweightState::Normal {
{ set_state(LightweightState::Normal);
// 只有在状态可用时才触发托盘更新 }
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {e}"); // 只有在状态可用时才触发托盘更新
} if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
} }
} }
pub fn enable_auto_light_weight_mode() { pub async fn run_once_auto_lightweight() {
if let Err(e) = Timer::global().init() { let verge_config = Config::verge().await;
let enable_auto = verge_config
.data_mut()
.enable_auto_light_weight_mode
.unwrap_or(false);
let is_silent_start = verge_config
.latest_ref()
.enable_silent_start
.unwrap_or(false);
if !(enable_auto && is_silent_start) {
logging!(
info,
Type::Lightweight,
true,
"不满足静默启动且自动进入轻量模式的条件,跳过自动进入轻量模式"
);
return;
}
set_lightweight_mode(true).await;
enable_auto_light_weight_mode().await;
}
pub async fn auto_lightweight_mode_init() -> Result<()> {
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
let enable_auto = {
Config::verge()
.await
.latest_ref()
.enable_auto_light_weight_mode
}
.unwrap_or(false);
if enable_auto && !is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_state(LightweightState::Normal);
enable_auto_light_weight_mode().await;
}
Ok(())
}
pub async fn enable_auto_light_weight_mode() {
if let Err(e) = Timer::global().init().await {
logging!(error, Type::Lightweight, "Failed to initialize timer: {e}"); logging!(error, Type::Lightweight, "Failed to initialize timer: {e}");
return; return;
} }
@@ -137,83 +150,82 @@ pub fn disable_auto_light_weight_mode() {
logging!(info, Type::Lightweight, true, "关闭自动轻量模式"); logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
let _ = cancel_light_weight_timer(); let _ = cancel_light_weight_timer();
cancel_window_close_listener(); cancel_window_close_listener();
cancel_webview_focus_listener();
} }
pub fn entry_lightweight_mode() { pub async fn entry_lightweight_mode() -> bool {
use crate::utils::window_manager::WindowManager; // 尝试从 Normal -> In
if LIGHTWEIGHT_STATE
let result = WindowManager::hide_main_window(); .compare_exchange(
logging!( LightweightState::Normal as u8,
info, LightweightState::In as u8,
Type::Lightweight, Ordering::Acquire,
true, Ordering::Relaxed,
"轻量模式隐藏窗口结果: {:?}", )
result .is_err()
); {
logging!(info, Type::Lightweight, true, "无需进入轻量模式,跳过调用");
if let Some(window) = handle::Handle::global().get_window() { return false;
if let Some(webview) = window.get_webview_window("main") {
let _ = webview.destroy();
}
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory();
} }
set_lightweight_mode(true);
WindowManager::destroy_main_window();
set_lightweight_mode(true).await;
let _ = cancel_light_weight_timer(); let _ = cancel_light_weight_timer();
// 更新托盘显示 // 回到 In
let _tray = crate::core::tray::Tray::global(); set_state(LightweightState::In);
CacheProxy::global().clean_default_keys();
true
} }
// 添加从轻量模式恢复的函数 // 添加从轻量模式恢复的函数
pub fn exit_lightweight_mode() { pub async fn exit_lightweight_mode() -> bool {
// 使用原子操作检查是否已经在退出过程中,防止并发调用 // 尝试从 In -> Exiting
if EXITING_LIGHTWEIGHT if LIGHTWEIGHT_STATE
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .compare_exchange(
LightweightState::In as u8,
LightweightState::Exiting as u8,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err() .is_err()
{ {
logging!( logging!(
info, info,
Type::Lightweight, Type::Lightweight,
true, true,
"轻量模式退出操作已在进行中,跳过重复调用" "轻量模式不在退出条件(可能已退出或正在退出),跳过调用"
); );
return; return false;
} }
// 使用defer确保无论如何都会重置标志 WindowManager::show_main_window().await;
let _guard = scopeguard::guard((), |_| {
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
});
// 确保当前确实处于轻量模式才执行退出操作 set_lightweight_mode(false).await;
if !is_in_lightweight_mode() { let _ = cancel_light_weight_timer();
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
return;
}
set_lightweight_mode(false); // 回到 Normal
set_state(LightweightState::Normal);
// macOS激活策略 logging!(info, Type::Lightweight, true, "轻量模式退出完成");
#[cfg(target_os = "macos")] true
AppHandleManager::global().set_activation_policy_regular();
// 重置UI就绪状态
crate::utils::resolve::reset_ui_ready();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn add_light_weight_timer() { pub async fn add_light_weight_timer() {
logging_error!(Type::Lightweight, setup_light_weight_timer()); logging_error!(Type::Lightweight, setup_light_weight_timer().await);
} }
fn setup_window_close_listener() -> u32 { fn setup_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() { if let Some(window) = handle::Handle::global().get_window() {
let handler = window.listen("tauri://close-requested", move |_event| { let handler = window.listen("tauri://close-requested", move |_event| {
let _ = setup_light_weight_timer(); 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!( logging!(
info, info,
Type::Lightweight, Type::Lightweight,
@@ -221,12 +233,22 @@ fn setup_window_close_listener() -> u32 {
"监听到关闭请求,开始轻量模式计时" "监听到关闭请求,开始轻量模式计时"
); );
}); });
return handler;
WINDOW_CLOSE_HANDLER.store(handler, Ordering::Release);
} }
0
} }
fn setup_webview_focus_listener() -> u32 { fn cancel_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() {
let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel);
if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
}
}
}
fn setup_webview_focus_listener() {
if let Some(window) = handle::Handle::global().get_window() { if let Some(window) = handle::Handle::global().get_window() {
let handler = window.listen("tauri://focus", move |_event| { let handler = window.listen("tauri://focus", move |_event| {
log_err!(cancel_light_weight_timer()); log_err!(cancel_light_weight_timer());
@@ -236,21 +258,25 @@ fn setup_webview_focus_listener() -> u32 {
"监听到窗口获得焦点,取消轻量模式计时" "监听到窗口获得焦点,取消轻量模式计时"
); );
}); });
return handler;
WEBVIEW_FOCUS_HANDLER.store(handler, Ordering::Release);
} }
0
} }
fn cancel_window_close_listener() { fn cancel_webview_focus_listener() {
if let Some(window) = handle::Handle::global().get_window() { if let Some(window) = handle::Handle::global().get_window() {
window.unlisten(setup_window_close_listener()); let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel);
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听"); if handler != 0 {
window.unlisten(handler);
logging!(info, Type::Lightweight, true, "取消了窗口焦点监听");
}
} }
} }
fn setup_light_weight_timer() -> Result<()> { async fn setup_light_weight_timer() -> Result<()> {
Timer::global().init()?; Timer::global().init().await?;
let once_by_minutes = Config::verge() let once_by_minutes = Config::verge()
.await
.latest_ref() .latest_ref()
.auto_light_weight_minutes .auto_light_weight_minutes
.unwrap_or(10); .unwrap_or(10);
@@ -269,7 +295,7 @@ fn setup_light_weight_timer() -> Result<()> {
.set_frequency_once_by_minutes(once_by_minutes) .set_frequency_once_by_minutes(once_by_minutes)
.spawn_async_routine(move || async move { .spawn_async_routine(move || async move {
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式"); logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
entry_lightweight_mode(); entry_lightweight_mode().await;
}) })
.context("failed to create timer task")?; .context("failed to create timer task")?;
+8 -2
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
cmd::system, cmd::system,
core::{handle, CoreManager}, core::{CoreManager, handle},
}; };
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use sysinfo::System; use sysinfo::System;
@@ -20,7 +20,13 @@ impl Debug for PlatformSpecification {
write!( write!(
f, f,
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}", "System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}",
self.system_name, self.system_version, self.system_kernel_version, self.system_arch, self.verge_version, self.running_mode, self.is_admin self.system_name,
self.system_version,
self.system_kernel_version,
self.system_arch,
self.verge_version,
self.running_mode,
self.is_admin
) )
} }
} }
+62
View File
@@ -1,22 +1,84 @@
#[cfg(feature = "tokio-trace")]
use std::any::type_name;
use std::future::Future; use std::future::Future;
#[cfg(feature = "tokio-trace")]
use std::panic::Location;
use tauri::{async_runtime, async_runtime::JoinHandle}; use tauri::{async_runtime, async_runtime::JoinHandle};
pub struct AsyncHandler; pub struct AsyncHandler;
impl AsyncHandler { impl AsyncHandler {
pub fn handle() -> async_runtime::RuntimeHandle {
async_runtime::handle()
}
#[track_caller]
pub fn spawn<F, Fut>(f: F) -> JoinHandle<()> pub fn spawn<F, Fut>(f: F) -> JoinHandle<()>
where where
F: FnOnce() -> Fut + Send + 'static, F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static, Fut: Future<Output = ()> + Send + 'static,
{ {
#[cfg(feature = "tokio-trace")]
Self::log_task_info(&f);
async_runtime::spawn(f()) async_runtime::spawn(f())
} }
#[track_caller]
pub fn spawn_blocking<T, F>(f: F) -> JoinHandle<T> pub fn spawn_blocking<T, F>(f: F) -> JoinHandle<T>
where where
F: FnOnce() -> T + Send + 'static, F: FnOnce() -> T + Send + 'static,
T: Send + 'static, T: Send + 'static,
{ {
#[cfg(feature = "tokio-trace")]
Self::log_task_info(&f);
async_runtime::spawn_blocking(f) async_runtime::spawn_blocking(f)
} }
#[allow(dead_code)]
#[track_caller]
pub fn block_on<Fut>(fut: Fut) -> Fut::Output
where
Fut: Future + Send + 'static,
{
#[cfg(feature = "tokio-trace")]
Self::log_task_info(&fut);
async_runtime::block_on(fut)
}
#[cfg(feature = "tokio-trace")]
#[track_caller]
fn log_task_info<F>(f: &F)
where
F: ?Sized,
{
const TRACE_SPECIAL_SIZE: [usize; 3] = [0, 4, 24];
let size = std::mem::size_of_val(f);
if TRACE_SPECIAL_SIZE.contains(&size) {
return;
}
let location = Location::caller();
let type_str = type_name::<F>();
let size_str = format!("{} bytes", size);
let loc_str = format!(
"{}:{}:{}",
location.file(),
location.line(),
location.column()
);
println!(
"┌────────────────────┬─────────────────────────────────────────────────────────────────────────────┐"
);
println!("│ {:<18} │ {:<80} │", "Field", "Value");
println!(
"├────────────────────┼─────────────────────────────────────────────────────────────────────────────┤"
);
println!("│ {:<18} │ {:<80} │", "Type of task", type_str);
println!("│ {:<18} │ {:<80} │", "Size of task", size_str);
println!("│ {:<18} │ {:<80} │", "Called from", loc_str);
println!(
"└────────────────────┴─────────────────────────────────────────────────────────────────────────────┘"
);
}
} }
-44
View File
@@ -1,44 +0,0 @@
use std::sync::{Arc, Once, OnceLock};
use crate::{logging, utils::logging::Type};
#[derive(Clone)]
pub struct LightWeightState {
#[allow(unused)]
once: Arc<Once>,
pub is_lightweight: bool,
}
impl LightWeightState {
pub fn new() -> Self {
Self {
once: Arc::new(Once::new()),
is_lightweight: false,
}
}
#[allow(unused)]
pub fn run_once_time<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
self.once.call_once(f);
}
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
self.is_lightweight = value;
if value {
logging!(info, Type::Lightweight, true, "轻量模式已开启");
} else {
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
}
self
}
}
impl Default for LightWeightState {
fn default() -> Self {
static INSTANCE: OnceLock<LightWeightState> = OnceLock::new();
INSTANCE.get_or_init(LightWeightState::new).clone()
}
}
-5
View File
@@ -1,5 +0,0 @@
// Tauri Manager 会进行 Arc 管理,无需额外 Arc
// https://tauri.app/develop/state-management/#do-you-need-arc
pub mod lightweight;
pub mod proxy;
-74
View File
@@ -1,74 +0,0 @@
use crate::singleton;
use dashmap::DashMap;
use serde_json::Value;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::OnceCell;
pub struct CacheEntry {
pub value: Arc<Value>,
pub expires_at: Instant,
}
pub struct ProxyRequestCache {
pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>,
}
impl ProxyRequestCache {
fn new() -> Self {
ProxyRequestCache {
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<Value>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Value>,
{
let now = Instant::now();
let key_cloned = key.clone();
let cell = self
.map
.entry(key)
.or_insert_with(|| Arc::new(OnceCell::new()))
.clone();
if let Some(entry) = cell.get() {
if entry.expires_at > now {
return Arc::clone(&entry.value);
}
}
if let Some(entry) = cell.get() {
if entry.expires_at <= now {
self.map
.remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell));
let new_cell = Arc::new(OnceCell::new());
self.map.insert(key_cloned.clone(), Arc::clone(&new_cell));
return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await;
}
}
let value = fetch_fn().await;
let entry = CacheEntry {
value: Arc::new(value),
expires_at: Instant::now() + ttl,
};
let _ = cell.set(entry);
// Safe to unwrap here as we just set the value
Arc::clone(
&cell
.get()
.unwrap_or_else(|| unreachable!("Cell value should exist after set"))
.value,
)
}
}
// Use singleton macro
singleton!(ProxyRequestCache, INSTANCE);
+1 -1
View File
@@ -1,5 +1,5 @@
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use anyhow::{anyhow, Result}; use anyhow::{Result, anyhow};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use log::info; use log::info;
+5 -6
View File
@@ -151,12 +151,11 @@ pub fn find_target_icons(target: &str) -> Result<Option<String>> {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
if file_name.starts_with(target) && file_name.starts_with(target)
&& (file_name.ends_with(".ico") || file_name.ends_with(".png")) && (file_name.ends_with(".ico") || file_name.ends_with(".png"))
{ {
matching_files.push(path); matching_files.push(path);
}
} }
} }
+25 -31
View File
@@ -1,38 +1,33 @@
use crate::{enhance::seq::SeqMap, logging, utils::logging::Type}; use crate::{enhance::seq::SeqMap, logging, utils::logging::Type};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{Context, Result, anyhow, bail};
use nanoid::nanoid; use nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize}; use serde::{Serialize, de::DeserializeOwned};
use serde_yaml::Mapping; use serde_yaml_ng::Mapping;
use std::{fs, path::PathBuf, str::FromStr}; use std::{path::PathBuf, str::FromStr};
/// read data from yaml as struct T /// read data from yaml as struct T
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> { pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
if !path.exists() { if !tokio::fs::try_exists(path).await.unwrap_or(false) {
bail!("file not found \"{}\"", path.display()); bail!("file not found \"{}\"", path.display());
} }
let yaml_str = fs::read_to_string(path) let yaml_str = tokio::fs::read_to_string(path).await?;
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
serde_yaml::from_str::<T>(&yaml_str).with_context(|| { Ok(serde_yaml_ng::from_str::<T>(&yaml_str)?)
format!(
"failed to read the file with yaml format \"{}\"",
path.display()
)
})
} }
/// read mapping from yaml /// read mapping from yaml
pub fn read_mapping(path: &PathBuf) -> Result<Mapping> { pub async fn read_mapping(path: &PathBuf) -> Result<Mapping> {
if !path.exists() { if !tokio::fs::try_exists(path).await.unwrap_or(false) {
bail!("file not found \"{}\"", path.display()); bail!("file not found \"{}\"", path.display());
} }
let yaml_str = fs::read_to_string(path) let yaml_str = tokio::fs::read_to_string(path)
.await
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?; .with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
// YAML语法检查 // YAML语法检查
match serde_yaml::from_str::<serde_yaml::Value>(&yaml_str) { match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&yaml_str) {
Ok(mut val) => { Ok(mut val) => {
val.apply_merge() val.apply_merge()
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?; .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
@@ -60,16 +55,18 @@ pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
} }
/// read mapping from yaml fix #165 /// read mapping from yaml fix #165
pub fn read_seq_map(path: &PathBuf) -> Result<SeqMap> { pub async fn read_seq_map(path: &PathBuf) -> Result<SeqMap> {
let val: SeqMap = read_yaml(path)?; read_yaml(path).await
Ok(val)
} }
/// save the data to the file /// save the data to the file
/// can set `prefix` string to add some comments /// can set `prefix` string to add some comments
pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> { pub async fn save_yaml<T: Serialize + Sync>(
let data_str = serde_yaml::to_string(data)?; path: &PathBuf,
data: &T,
prefix: Option<&str>,
) -> Result<()> {
let data_str = serde_yaml_ng::to_string(data)?;
let yaml_str = match prefix { let yaml_str = match prefix {
Some(prefix) => format!("{prefix}\n\n{data_str}"), Some(prefix) => format!("{prefix}\n\n{data_str}"),
@@ -77,7 +74,8 @@ pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -
}; };
let path_str = path.as_os_str().to_string_lossy().to_string(); let path_str = path.as_os_str().to_string_lossy().to_string();
fs::write(path, yaml_str.as_bytes()) tokio::fs::write(path, yaml_str.as_bytes())
.await
.with_context(|| format!("failed to save file \"{path_str}\"")) .with_context(|| format!("failed to save file \"{path_str}\""))
} }
@@ -120,7 +118,7 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
} }
/// open file /// open file
pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> { pub fn open_file(path: PathBuf) -> Result<()> {
open::that_detached(path.as_os_str())?; open::that_detached(path.as_os_str())?;
Ok(()) Ok(())
} }
@@ -156,10 +154,6 @@ macro_rules! ret_err {
#[macro_export] #[macro_export]
macro_rules! t { macro_rules! t {
($en:expr, $zh:expr, $use_zh:expr) => { ($en:expr, $zh:expr, $use_zh:expr) => {
if $use_zh { if $use_zh { $zh } else { $en }
$zh
} else {
$en
}
}; };
} }
+23 -21
View File
@@ -15,14 +15,14 @@ fn get_locales_dir() -> Option<PathBuf> {
pub fn get_supported_languages() -> Vec<String> { pub fn get_supported_languages() -> Vec<String> {
let mut languages = Vec::new(); let mut languages = Vec::new();
if let Some(locales_dir) = get_locales_dir() { if let Some(locales_dir) = get_locales_dir()
if let Ok(entries) = fs::read_dir(locales_dir) { && let Ok(entries) = fs::read_dir(locales_dir)
for entry in entries.flatten() { {
if let Some(file_name) = entry.file_name().to_str() { for entry in entries.flatten() {
if let Some(lang) = file_name.strip_suffix(".json") { if let Some(file_name) = entry.file_name().to_str()
languages.push(lang.to_string()); && let Some(lang) = file_name.strip_suffix(".json")
} {
} languages.push(lang.to_string());
} }
} }
} }
@@ -39,10 +39,10 @@ static TRANSLATIONS: Lazy<HashMap<String, Value>> = Lazy::new(|| {
if let Some(locales_dir) = get_locales_dir() { if let Some(locales_dir) = get_locales_dir() {
for lang in get_supported_languages() { for lang in get_supported_languages() {
let file_path = locales_dir.join(format!("{lang}.json")); let file_path = locales_dir.join(format!("{lang}.json"));
if let Ok(content) = fs::read_to_string(file_path) { if let Ok(content) = fs::read_to_string(file_path)
if let Ok(json) = serde_json::from_str(&content) { && let Ok(json) = serde_json::from_str(&content)
translations.insert(lang.to_string(), json); {
} translations.insert(lang.to_string(), json);
} }
} }
} }
@@ -57,8 +57,11 @@ fn get_system_language() -> String {
.unwrap_or_else(|| DEFAULT_LANGUAGE.to_string()) .unwrap_or_else(|| DEFAULT_LANGUAGE.to_string())
} }
pub fn t(key: &str) -> String { pub async fn t(key: &str) -> String {
let key = key.to_string(); // own the string
let current_lang = Config::verge() let current_lang = Config::verge()
.await
.latest_ref() .latest_ref()
.language .language
.as_deref() .as_deref()
@@ -67,21 +70,20 @@ pub fn t(key: &str) -> String {
if let Some(text) = TRANSLATIONS if let Some(text) = TRANSLATIONS
.get(&current_lang) .get(&current_lang)
.and_then(|trans| trans.get(key)) .and_then(|trans| trans.get(&key))
.and_then(|val| val.as_str()) .and_then(|val| val.as_str())
{ {
return text.to_string(); return text.to_string();
} }
if current_lang != DEFAULT_LANGUAGE { if current_lang != DEFAULT_LANGUAGE
if let Some(text) = TRANSLATIONS && let Some(text) = TRANSLATIONS
.get(DEFAULT_LANGUAGE) .get(DEFAULT_LANGUAGE)
.and_then(|trans| trans.get(key)) .and_then(|trans| trans.get(&key))
.and_then(|val| val.as_str()) .and_then(|val| val.as_str())
{ {
return text.to_string(); return text.to_string();
}
} }
key.to_string() key
} }
+213 -126
View File
@@ -1,79 +1,70 @@
cfg_if::cfg_if! {
if #[cfg(not(feature = "tauri-dev"))] {
use crate::utils::logging::{console_colored_format, file_format};
use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, LogSpecification, Logger};
use log::LevelFilter;
}
}
use crate::{ use crate::{
config::*, config::*,
core::handle, core::handle,
utils::{dirs, help}, logging,
process::AsyncHandler,
utils::{dirs, help, logging::Type},
}; };
use anyhow::Result; use anyhow::Result;
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use log::LevelFilter; use std::{path::PathBuf, str::FromStr};
use log4rs::{
append::{console::ConsoleAppender, file::FileAppender},
config::{Appender, Logger, Root},
encode::pattern::PatternEncoder,
};
use std::{
fs::{self, DirEntry},
path::PathBuf,
str::FromStr,
};
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
use tokio::fs;
use tokio::fs::DirEntry;
/// initialize this instance's log file /// initialize this instance's log file
fn init_log() -> Result<()> { #[cfg(not(feature = "tauri-dev"))]
let log_dir = dirs::app_logs_dir()?; pub async fn init_logger() -> Result<()> {
if !log_dir.exists() { let log_level = Config::verge().await.latest_ref().get_log_level();
let _ = fs::create_dir_all(&log_dir);
}
let log_level = Config::verge().latest_ref().get_log_level();
if log_level == LevelFilter::Off { if log_level == LevelFilter::Off {
return Ok(()); return Err(anyhow::anyhow!("logging is disabled"));
} }
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string(); let log_dir = dirs::app_logs_dir()?;
let log_file = format!("{local_time}.log"); let logger = Logger::with(LogSpecification::from(log_level))
let log_file = log_dir.join(log_file); .log_to_file(FileSpec::default().directory(log_dir).basename(""))
.duplicate_to_stdout(Duplicate::Debug)
.format(console_colored_format)
.format_for_files(file_format)
.rotate(
// ? 总是保持单个日志最大 10 MB
Criterion::Size(10 * 1024 * 1024),
flexi_logger::Naming::TimestampsCustomFormat {
current_infix: Some("latest"),
format: "%Y-%m-%d_%H-%M-%S",
},
// TODO 提供前端设置最大保留文件数量
Cleanup::Never,
);
let log_pattern = match log_level { let _handle = logger.start()?;
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
};
let encode = Box::new(PatternEncoder::new(log_pattern)); // TODO 全局 logger handle 控制
// GlobalLoggerProxy::global().set_inner(handle);
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build(); // TODO 提供前端设置等级,热更新等级
let tofile = FileAppender::builder().encoder(encode).build(log_file)?; // logger.parse_new_spec(spec)
let mut logger_builder = Logger::builder();
let mut root_builder = Root::builder();
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
logger_builder = logger_builder.appenders(["file"]);
if log_more {
root_builder = root_builder.appenders(["file"]);
}
let (config, _) = log4rs::config::Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("file", Box::new(tofile)))
.logger(logger_builder.additive(false).build("app", log_level))
.build_lossy(root_builder.build(log_level));
log4rs::init_config(config)?;
Ok(()) Ok(())
} }
// TODO flexi_logger 提供了最大保留天数,或许我们应该用内置删除log文件
/// 删除log文件 /// 删除log文件
pub fn delete_log() -> Result<()> { pub async fn delete_log() -> Result<()> {
let log_dir = dirs::app_logs_dir()?; let log_dir = dirs::app_logs_dir()?;
if !log_dir.exists() { if !log_dir.exists() {
return Ok(()); return Ok(());
} }
let auto_log_clean = { let auto_log_clean = {
let verge = Config::verge(); let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_ref();
verge.auto_log_clean.unwrap_or(0) verge.auto_log_clean.unwrap_or(0)
}; };
@@ -87,7 +78,13 @@ pub fn delete_log() -> Result<()> {
_ => return Ok(()), _ => return Ok(()),
}; };
log::info!(target: "app", "try to delete log files, day: {day}"); logging!(
info,
Type::Setup,
true,
"try to delete log files, day: {}",
day
);
// %Y-%m-%d to NaiveDateTime // %Y-%m-%d to NaiveDateTime
let parse_time_str = |s: &str| { let parse_time_str = |s: &str| {
@@ -106,7 +103,7 @@ pub fn delete_log() -> Result<()> {
Ok(time) Ok(time)
}; };
let process_file = |file: DirEntry| -> Result<()> { let process_file = async move |file: DirEntry| -> Result<()> {
let file_name = file.file_name(); let file_name = file.file_name();
let file_name = file_name.to_str().unwrap_or_default(); let file_name = file_name.to_str().unwrap_or_default();
@@ -121,31 +118,33 @@ pub fn delete_log() -> Result<()> {
let duration = now.signed_duration_since(file_time); let duration = now.signed_duration_since(file_time);
if duration.num_days() > day { if duration.num_days() > day {
let file_path = file.path(); let file_path = file.path();
let _ = fs::remove_file(file_path); let _ = fs::remove_file(file_path).await;
log::info!(target: "app", "delete log file: {file_name}"); logging!(info, Type::Setup, true, "delete log file: {}", file_name);
} }
} }
Ok(()) Ok(())
}; };
for file in fs::read_dir(&log_dir)?.flatten() { let mut log_read_dir = fs::read_dir(&log_dir).await?;
let _ = process_file(file); while let Some(entry) = log_read_dir.next_entry().await? {
std::mem::drop(process_file(entry).await);
} }
let service_log_dir = log_dir.join("service"); let service_log_dir = log_dir.join("service");
for file in fs::read_dir(service_log_dir)?.flatten() { let mut service_log_read_dir = fs::read_dir(service_log_dir).await?;
let _ = process_file(file); while let Some(entry) = service_log_read_dir.next_entry().await? {
std::mem::drop(process_file(entry).await);
} }
Ok(()) Ok(())
} }
/// 初始化DNS配置文件 /// 初始化DNS配置文件
fn init_dns_config() -> Result<()> { async fn init_dns_config() -> Result<()> {
use serde_yaml::Value; use serde_yaml_ng::Value;
// 创建DNS子配置 // 创建DNS子配置
let dns_config = serde_yaml::Mapping::from_iter([ let dns_config = serde_yaml_ng::Mapping::from_iter([
("enable".into(), Value::Bool(true)), ("enable".into(), Value::Bool(true)),
("listen".into(), Value::String(":53".into())), ("listen".into(), Value::String(":53".into())),
("enhanced-mode".into(), Value::String("fake-ip".into())), ("enhanced-mode".into(), Value::String("fake-ip".into())),
@@ -197,7 +196,7 @@ fn init_dns_config() -> Result<()> {
("fallback".into(), Value::Sequence(vec![])), ("fallback".into(), Value::Sequence(vec![])),
( (
"nameserver-policy".into(), "nameserver-policy".into(),
Value::Mapping(serde_yaml::Mapping::new()), Value::Mapping(serde_yaml_ng::Mapping::new()),
), ),
( (
"proxy-server-nameserver".into(), "proxy-server-nameserver".into(),
@@ -211,7 +210,7 @@ fn init_dns_config() -> Result<()> {
("direct-nameserver-follow-policy".into(), Value::Bool(false)), ("direct-nameserver-follow-policy".into(), Value::Bool(false)),
( (
"fallback-filter".into(), "fallback-filter".into(),
Value::Mapping(serde_yaml::Mapping::from_iter([ Value::Mapping(serde_yaml_ng::Mapping::from_iter([
("geoip".into(), Value::Bool(true)), ("geoip".into(), Value::Bool(true)),
("geoip-code".into(), Value::String("CN".into())), ("geoip-code".into(), Value::String("CN".into())),
( (
@@ -234,9 +233,12 @@ fn init_dns_config() -> Result<()> {
]); ]);
// 获取默认DNS和host配置 // 获取默认DNS和host配置
let default_dns_config = serde_yaml::Mapping::from_iter([ let default_dns_config = serde_yaml_ng::Mapping::from_iter([
("dns".into(), Value::Mapping(dns_config)), ("dns".into(), Value::Mapping(dns_config)),
("hosts".into(), Value::Mapping(serde_yaml::Mapping::new())), (
"hosts".into(),
Value::Mapping(serde_yaml_ng::Mapping::new()),
),
]); ]);
// 检查DNS配置文件是否存在 // 检查DNS配置文件是否存在
@@ -244,77 +246,149 @@ fn init_dns_config() -> Result<()> {
let dns_path = app_dir.join("dns_config.yaml"); let dns_path = app_dir.join("dns_config.yaml");
if !dns_path.exists() { if !dns_path.exists() {
log::info!(target: "app", "Creating default DNS config file"); logging!(info, Type::Setup, true, "Creating default DNS config file");
help::save_yaml( help::save_yaml(
&dns_path, &dns_path,
&default_dns_config, &default_dns_config,
Some("# Clash Verge DNS Config"), Some("# Clash Verge DNS Config"),
)?; )
.await?;
} }
Ok(()) Ok(())
} }
/// 确保目录结构存在
async fn ensure_directories() -> Result<()> {
let directories = [
("app_home", dirs::app_home_dir()?),
("app_profiles", dirs::app_profiles_dir()?),
("app_logs", dirs::app_logs_dir()?),
];
for (name, dir) in directories {
if !dir.exists() {
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
);
}
}
Ok(())
}
/// 初始化配置文件
async fn initialize_config_files() -> Result<()> {
if let Ok(path) = dirs::clash_path()
&& !path.exists()
{
let template = IClashTemp::template().0;
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
);
}
if let Ok(path) = dirs::verge_path()
&& !path.exists()
{
let template = IVerge::template();
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
);
}
if let Ok(path) = dirs::profiles_path()
&& !path.exists()
{
let template = IProfiles::template();
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
);
}
// 验证并修正verge配置
IVerge::validate_and_fix_config()
.await
.map_err(|e| anyhow::anyhow!("Failed to validate verge config: {}", e))?;
Ok(())
}
/// Initialize all the config files /// Initialize all the config files
/// before tauri setup /// before tauri setup
pub fn init_config() -> Result<()> { pub async fn init_config() -> Result<()> {
let _ = dirs::init_portable_flag(); // We do not need init_portable_flag here anymore due to lib.rs will to the things
let _ = init_log(); // let _ = dirs::init_portable_flag();
let _ = delete_log();
crate::log_err!(dirs::app_home_dir().map(|app_dir| { // We do not need init_log here anymore due to resolve will to the things
if !app_dir.exists() { // if let Err(e) = init_log().await {
let _ = fs::create_dir_all(&app_dir); // eprintln!("Failed to initialize logging: {}", e);
// }
ensure_directories().await?;
initialize_config_files().await?;
AsyncHandler::spawn(|| async {
if let Err(e) = delete_log().await {
logging!(warn, Type::Setup, true, "Failed to clean old logs: {}", e);
} }
})); logging!(info, Type::Setup, true, "后台日志清理任务完成");
});
crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| { if let Err(e) = init_dns_config().await {
if !profiles_dir.exists() { logging!(
let _ = fs::create_dir_all(&profiles_dir); warn,
} Type::Setup,
})); true,
"DNS config initialization failed: {}",
crate::log_err!(dirs::clash_path().map(|path| { e
if !path.exists() { );
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Vergeasu"))?; }
}
<Result<()>>::Ok(())
}));
crate::log_err!(dirs::verge_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
}
<Result<()>>::Ok(())
}));
// 验证并修正verge.yaml中的clash_core配置
crate::log_err!(IVerge::validate_and_fix_config());
crate::log_err!(dirs::profiles_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
}
<Result<()>>::Ok(())
}));
// 初始化DNS配置文件
let _ = init_dns_config();
Ok(()) Ok(())
} }
/// initialize app resources /// initialize app resources
/// after tauri setup /// after tauri setup
pub fn init_resources() -> Result<()> { pub async fn init_resources() -> Result<()> {
let app_dir = dirs::app_home_dir()?; let app_dir = dirs::app_home_dir()?;
let res_dir = dirs::app_resources_dir()?; let res_dir = dirs::app_resources_dir()?;
if !app_dir.exists() { if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir); std::mem::drop(fs::create_dir_all(&app_dir).await);
} }
if !res_dir.exists() { if !res_dir.exists() {
let _ = fs::create_dir_all(&res_dir); std::mem::drop(fs::create_dir_all(&res_dir).await);
} }
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"]; let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
@@ -324,36 +398,49 @@ pub fn init_resources() -> Result<()> {
for file in file_list.iter() { for file in file_list.iter() {
let src_path = res_dir.join(file); let src_path = res_dir.join(file);
let dest_path = app_dir.join(file); let dest_path = app_dir.join(file);
log::debug!(target: "app", "src_path: {src_path:?}, dest_path: {dest_path:?}");
let handle_copy = |dest: &PathBuf| { let handle_copy = |src: PathBuf, dest: PathBuf, file: String| async move {
match fs::copy(&src_path, dest) { match fs::copy(&src, &dest).await {
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"), Ok(_) => {
logging!(debug, Type::Setup, true, "resources copied '{}'", file);
}
Err(err) => { Err(err) => {
log::error!(target: "app", "failed to copy resources '{file}' to '{dest:?}', {err}") logging!(
error,
Type::Setup,
true,
"failed to copy resources '{}' to '{:?}', {}",
file,
dest,
err
);
} }
}; };
}; };
if src_path.exists() && !dest_path.exists() { if src_path.exists() && !dest_path.exists() {
handle_copy(&dest_path); handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
continue; continue;
} }
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified()); let src_modified = fs::metadata(&src_path).await.and_then(|m| m.modified());
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified()); let dest_modified = fs::metadata(&dest_path).await.and_then(|m| m.modified());
match (src_modified, dest_modified) { match (src_modified, dest_modified) {
(Ok(src_modified), Ok(dest_modified)) => { (Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified { if src_modified > dest_modified {
handle_copy(&dest_path); handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
} else {
log::debug!(target: "app", "skipping resource copy '{file}'");
} }
} }
_ => { _ => {
log::debug!(target: "app", "failed to get modified '{file}'"); logging!(
handle_copy(&dest_path); debug,
Type::Setup,
true,
"failed to get modified '{}'",
file
);
handle_copy(src_path.clone(), dest_path.clone(), file.to_string()).await;
} }
}; };
} }
@@ -365,7 +452,7 @@ pub fn init_resources() -> Result<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn init_scheme() -> Result<()> { pub fn init_scheme() -> Result<()> {
use tauri::utils::platform::current_exe; use tauri::utils::platform::current_exe;
use winreg::{enums::*, RegKey}; use winreg::{RegKey, enums::*};
let app_exe = current_exe()?; let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?; let app_exe = dunce::canonicalize(app_exe)?;
@@ -413,7 +500,7 @@ pub async fn startup_script() -> Result<()> {
}; };
let script_path = { let script_path = {
let verge = Config::verge(); let verge = Config::verge().await;
let verge = verge.latest_ref(); let verge = verge.latest_ref();
verge.startup_script.clone().unwrap_or("".to_string()) verge.startup_script.clone().unwrap_or("".to_string())
}; };
+84 -6
View File
@@ -1,4 +1,14 @@
use std::fmt; 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};
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type { pub enum Type {
@@ -18,6 +28,7 @@ pub enum Type {
Network, Network,
ProxyMode, ProxyMode,
Ipc, Ipc,
// Cache,
} }
impl fmt::Display for Type { impl fmt::Display for Type {
@@ -39,6 +50,7 @@ impl fmt::Display for Type {
Type::Network => write!(f, "[Network]"), Type::Network => write!(f, "[Network]"),
Type::ProxyMode => write!(f, "[ProxMode]"), Type::ProxyMode => write!(f, "[ProxMode]"),
Type::Ipc => write!(f, "[IPC]"), Type::Ipc => write!(f, "[IPC]"),
// Type::Cache => write!(f, "[Cache]"),
} }
} }
} }
@@ -78,22 +90,35 @@ macro_rules! trace_err {
/// transform the error to String /// transform the error to String
#[macro_export] #[macro_export]
macro_rules! wrap_err { macro_rules! wrap_err {
($stat: expr) => { // Case 1: Future<Result<T, E>>
($stat:expr, async) => {{
match $stat.await {
Ok(a) => Ok(a),
Err(err) => {
log::error!(target: "app", "{}", err);
Err(err.to_string())
}
}
}};
// Case 2: Result<T, E>
($stat:expr) => {{
match $stat { match $stat {
Ok(a) => Ok(a), Ok(a) => Ok(a),
Err(err) => { Err(err) => {
log::error!(target: "app", "{}", err.to_string()); log::error!(target: "app", "{}", err);
Err(format!("{}", err.to_string())) Err(err.to_string())
} }
} }
}; }};
} }
#[macro_export] #[macro_export]
macro_rules! logging { macro_rules! logging {
// 带 println 的版本(支持格式化参数) // 带 println 的版本(支持格式化参数)
($level:ident, $type:expr, true, $($arg:tt)*) => { ($level:ident, $type:expr, true, $($arg:tt)*) => {
println!("{} {}", $type, format_args!($($arg)*)); // We dont need println here anymore
// println!("{} {}", $type, format_args!($($arg)*));
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*)); log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
}; };
@@ -143,3 +168,56 @@ macro_rules! logging_error {
logging_error!($type, false, $fmt $(, $arg)*); logging_error!($type, false, $fmt $(, $arg)*);
}; };
} }
#[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(),
)
}
+188 -319
View File
@@ -1,334 +1,189 @@
use anyhow::Result; use anyhow::Result;
use parking_lot::Mutex; use base64::{Engine as _, engine::general_purpose};
use reqwest::{Client, ClientBuilder, Proxy, RequestBuilder, Response}; use isahc::prelude::*;
use std::{ use isahc::{HttpClient, config::SslOption};
sync::{ use isahc::{
atomic::{AtomicUsize, Ordering}, config::RedirectPolicy,
Arc, Once, http::{
StatusCode, Uri,
header::{HeaderMap, HeaderValue, USER_AGENT},
}, },
time::{Duration, Instant},
}; };
use tokio::runtime::{Builder, Runtime}; use std::time::{Duration, Instant};
use sysproxy::Sysproxy;
use tauri::Url;
use tokio::sync::Mutex;
use tokio::time::timeout;
use crate::{config::Config, logging, process::AsyncHandler, singleton_lazy, utils::logging::Type}; use crate::config::Config;
// HTTP2 相关 #[derive(Debug)]
const H2_CONNECTION_WINDOW_SIZE: u32 = 1024 * 1024; pub struct HttpResponse {
const H2_STREAM_WINDOW_SIZE: u32 = 1024 * 1024; status: StatusCode,
const H2_MAX_FRAME_SIZE: u32 = 16 * 1024; headers: HeaderMap,
const H2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(5); body: String,
const H2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(5);
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
const POOL_MAX_IDLE_PER_HOST: usize = 5;
const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(15);
/// 网络管理器
pub struct NetworkManager {
runtime: Arc<Runtime>,
self_proxy_client: Mutex<Option<Client>>,
system_proxy_client: Mutex<Option<Client>>,
no_proxy_client: Mutex<Option<Client>>,
init: Once,
last_connection_error: Mutex<Option<(Instant, String)>>,
connection_error_count: AtomicUsize,
} }
// Use singleton_lazy macro to replace lazy_static! impl HttpResponse {
singleton_lazy!(NetworkManager, NETWORK_MANAGER, NetworkManager::new); pub fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
Self {
status,
headers,
body,
}
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn text_with_charset(&self) -> Result<&str> {
Ok(&self.body)
}
}
#[derive(Debug, Clone, Copy)]
pub enum ProxyType {
None,
Localhost,
System,
}
pub struct NetworkManager {
self_proxy_client: Mutex<Option<HttpClient>>,
system_proxy_client: Mutex<Option<HttpClient>>,
no_proxy_client: Mutex<Option<HttpClient>>,
last_connection_error: Mutex<Option<(Instant, String)>>,
connection_error_count: Mutex<usize>,
}
impl NetworkManager { impl NetworkManager {
fn new() -> Self { pub fn new() -> Self {
// 创建专用的异步运行时,线程数限制为4个 Self {
let runtime = match Builder::new_multi_thread()
.worker_threads(4)
.thread_name("clash-verge-network")
.enable_io()
.enable_time()
.build()
{
Ok(runtime) => runtime,
Err(e) => {
log::error!(
"Failed to create network runtime: {}. Using fallback single-threaded runtime.",
e
);
// Fallback to current thread runtime
match Builder::new_current_thread()
.enable_io()
.enable_time()
.thread_name("clash-verge-network-fallback")
.build()
{
Ok(fallback_runtime) => fallback_runtime,
Err(fallback_err) => {
log::error!(
"Failed to create fallback runtime: {}. This is critical.",
fallback_err
);
std::process::exit(1);
}
}
}
};
NetworkManager {
runtime: Arc::new(runtime),
self_proxy_client: Mutex::new(None), self_proxy_client: Mutex::new(None),
system_proxy_client: Mutex::new(None), system_proxy_client: Mutex::new(None),
no_proxy_client: Mutex::new(None), no_proxy_client: Mutex::new(None),
init: Once::new(),
last_connection_error: Mutex::new(None), last_connection_error: Mutex::new(None),
connection_error_count: AtomicUsize::new(0), connection_error_count: Mutex::new(0),
} }
} }
/// 初始化网络客户端 async fn record_connection_error(&self, error: &str) {
pub fn init(&self) { let mut last_error = self.last_connection_error.lock().await;
self.init.call_once(|| {
self.runtime.spawn(async {
logging!(info, Type::Network, true, "初始化网络管理器");
// 创建无代理客户端
let no_proxy_client = match ClientBuilder::new()
.use_rustls_tls()
.no_proxy()
.pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
.pool_idle_timeout(POOL_IDLE_TIMEOUT)
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30))
.build()
{
Ok(client) => client,
Err(e) => {
logging!(
error,
Type::Network,
true,
"Failed to build no_proxy client: {}",
e
);
return;
}
};
let mut no_proxy_guard = NetworkManager::global().no_proxy_client.lock();
*no_proxy_guard = Some(no_proxy_client);
logging!(info, Type::Network, true, "网络管理器初始化完成");
});
});
}
fn record_connection_error(&self, error: &str) {
let mut last_error = self.last_connection_error.lock();
*last_error = Some((Instant::now(), error.to_string())); *last_error = Some((Instant::now(), error.to_string()));
self.connection_error_count.fetch_add(1, Ordering::Relaxed); let mut count = self.connection_error_count.lock().await;
*count += 1;
} }
fn should_reset_clients(&self) -> bool { async fn should_reset_clients(&self) -> bool {
let error_count = self.connection_error_count.load(Ordering::Relaxed); let count = *self.connection_error_count.lock().await;
let last_error = self.last_connection_error.lock(); let last_error_guard = self.last_connection_error.lock().await;
if error_count > 5 { if count > 5 {
return true; return true;
} }
if let Some((time, _)) = *last_error { if let Some((time, _)) = &*last_error_guard
if time.elapsed() < Duration::from_secs(30) && error_count > 2 { && time.elapsed() < Duration::from_secs(30)
return true; && count > 2
} {
return true;
} }
false false
} }
pub fn reset_clients(&self) { pub async fn reset_clients(&self) {
logging!(info, Type::Network, true, "正在重置所有HTTP客户端"); *self.self_proxy_client.lock().await = None;
{ *self.system_proxy_client.lock().await = None;
let mut client = self.self_proxy_client.lock(); *self.no_proxy_client.lock().await = None;
*client = None; *self.connection_error_count.lock().await = 0;
}
{
let mut client = self.system_proxy_client.lock();
*client = None;
}
{
let mut client = self.no_proxy_client.lock();
*client = None;
}
self.connection_error_count.store(0, Ordering::Relaxed);
} }
/// 创建带有自定义选项的HTTP请求 fn build_client(
pub fn create_request( &self,
proxy_uri: Option<Uri>,
default_headers: HeaderMap,
accept_invalid_certs: bool,
timeout_secs: Option<u64>,
) -> Result<HttpClient> {
let proxy_uri_clone = proxy_uri.clone();
let headers_clone = default_headers.clone();
{
let mut builder = HttpClient::builder();
builder = match proxy_uri_clone {
Some(uri) => builder.proxy(Some(uri)),
None => builder.proxy(None),
};
for (name, value) in headers_clone.iter() {
builder = builder.default_header(name, value);
}
if accept_invalid_certs {
builder = builder.ssl_options(SslOption::DANGER_ACCEPT_INVALID_CERTS);
}
if let Some(secs) = timeout_secs {
builder = builder.timeout(Duration::from_secs(secs));
}
builder = builder.redirect_policy(RedirectPolicy::Follow);
Ok(builder.build()?)
}
}
pub async fn create_request(
&self, &self,
url: &str,
proxy_type: ProxyType, proxy_type: ProxyType,
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
user_agent: Option<String>, user_agent: Option<String>,
accept_invalid_certs: bool, accept_invalid_certs: bool,
) -> RequestBuilder { ) -> Result<HttpClient> {
if self.should_reset_clients() { let proxy_uri = match proxy_type {
self.reset_clients(); ProxyType::None => None,
}
let mut builder = ClientBuilder::new()
.use_rustls_tls()
.pool_max_idle_per_host(POOL_MAX_IDLE_PER_HOST)
.pool_idle_timeout(POOL_IDLE_TIMEOUT)
.connect_timeout(DEFAULT_CONNECT_TIMEOUT)
.http2_initial_stream_window_size(H2_STREAM_WINDOW_SIZE)
.http2_initial_connection_window_size(H2_CONNECTION_WINDOW_SIZE)
.http2_adaptive_window(true)
.http2_keep_alive_interval(Some(H2_KEEP_ALIVE_INTERVAL))
.http2_keep_alive_timeout(H2_KEEP_ALIVE_TIMEOUT)
.http2_max_frame_size(H2_MAX_FRAME_SIZE)
.tcp_keepalive(Some(Duration::from_secs(10)))
.http2_max_header_list_size(16 * 1024);
if let Some(timeout) = timeout_secs {
builder = builder.timeout(Duration::from_secs(timeout));
} else {
builder = builder.timeout(DEFAULT_REQUEST_TIMEOUT);
}
match proxy_type {
ProxyType::None => {
builder = builder.no_proxy();
}
ProxyType::Localhost => { ProxyType::Localhost => {
let port = Config::verge() let port = {
.latest_ref() let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
.verge_mixed_port match verge_port {
.unwrap_or(Config::clash().latest_ref().get_mixed_port()); Some(port) => port,
None => Config::clash().await.latest_ref().get_mixed_port(),
}
};
let proxy_scheme = format!("http://127.0.0.1:{port}"); let proxy_scheme = format!("http://127.0.0.1:{port}");
proxy_scheme.parse::<Uri>().ok()
if let Ok(proxy) = Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
} }
ProxyType::System => { ProxyType::System => {
use sysproxy::Sysproxy;
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() { if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
let proxy_scheme = format!("http://{}:{}", p.host, p.port); let proxy_scheme = format!("http://{}:{}", p.host, p.port);
proxy_scheme.parse::<Uri>().ok()
if let Ok(proxy) = Proxy::http(&proxy_scheme) { } else {
builder = builder.proxy(proxy); None
}
if let Ok(proxy) = Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
}
}
builder = builder.danger_accept_invalid_certs(accept_invalid_certs);
if let Some(ua) = user_agent {
builder = builder.user_agent(ua);
} else {
use crate::utils::resolve::VERSION;
let version = match VERSION.get() {
Some(v) => format!("clash-verge/v{v}"),
None => "clash-verge/unknown".to_string(),
};
builder = builder.user_agent(version);
}
let client = match builder.build() {
Ok(client) => client,
Err(e) => {
logging!(
error,
Type::Network,
true,
"Failed to build custom HTTP client: {}",
e
);
// Return a simple no-proxy client as fallback
match ClientBuilder::new()
.use_rustls_tls()
.no_proxy()
.timeout(DEFAULT_REQUEST_TIMEOUT)
.build()
{
Ok(fallback_client) => fallback_client,
Err(fallback_err) => {
logging!(
error,
Type::Network,
true,
"Failed to create fallback client: {}",
fallback_err
);
self.record_connection_error(&format!(
"Critical client build failure: {}",
fallback_err
));
// Return a minimal client that will likely fail but won't panic
ClientBuilder::new().build().unwrap_or_else(|_| {
// If even the most basic client fails, this is truly critical
std::process::exit(1);
})
}
} }
} }
}; };
client.get(url) let mut headers = HeaderMap::new();
} headers.insert(
USER_AGENT,
/* /// 执行GET请求,添加错误跟踪 HeaderValue::from_str(
pub async fn get( &user_agent
&self, .unwrap_or_else(|| format!("clash-verge/v{}", env!("CARGO_PKG_VERSION"))),
url: &str, )?,
proxy_type: ProxyType,
timeout_secs: Option<u64>,
user_agent: Option<String>,
accept_invalid_certs: bool,
) -> Result<Response> {
let request = self.create_request(
url,
proxy_type,
timeout_secs,
user_agent,
accept_invalid_certs,
); );
let timeout_duration = timeout_secs.unwrap_or(30); let client = self.build_client(proxy_uri, headers, accept_invalid_certs, timeout_secs)?;
match tokio::time::timeout(Duration::from_secs(timeout_duration), request.send()).await { Ok(client)
Ok(result) => match result { }
Ok(response) => Ok(response),
Err(e) => {
self.record_connection_error(&e.to_string());
Err(anyhow::anyhow!("Failed to send HTTP request: {}", e))
}
},
Err(_) => {
self.record_connection_error("Request timeout");
Err(anyhow::anyhow!(
"HTTP request timed out after {} seconds",
timeout_duration
))
}
}
} */
pub async fn get_with_interrupt( pub async fn get_with_interrupt(
&self, &self,
@@ -337,49 +192,63 @@ impl NetworkManager {
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
user_agent: Option<String>, user_agent: Option<String>,
accept_invalid_certs: bool, accept_invalid_certs: bool,
) -> Result<Response> { ) -> Result<HttpResponse> {
let request = self.create_request( if self.should_reset_clients().await {
url, self.reset_clients().await;
proxy_type, }
timeout_secs,
user_agent,
accept_invalid_certs,
);
let timeout_duration = timeout_secs.unwrap_or(20); let parsed = Url::parse(url)?;
let mut extra_headers = HeaderMap::new();
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>(); if !parsed.username().is_empty()
&& let Some(pass) = parsed.password()
{
let auth_str = format!("{}:{}", parsed.username(), pass);
let encoded = general_purpose::STANDARD.encode(auth_str);
extra_headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded))?,
);
}
let url_clone = url.to_string(); let clean_url = {
let watchdog = AsyncHandler::spawn(move || async move { let mut no_auth = parsed.clone();
tokio::time::sleep(Duration::from_secs(timeout_duration)).await; no_auth.set_username("").ok();
let _ = cancel_tx.send(()); no_auth.set_password(None).ok();
logging!(warn, Type::Network, true, "请求超时取消: {}", url_clone); no_auth.to_string()
}); };
let result = tokio::select! { let client = self
result = request.send() => result, .create_request(proxy_type, timeout_secs, user_agent, accept_invalid_certs)
_ = cancel_rx => { .await?;
self.record_connection_error(&format!("Request interrupted for: {url}"));
return Err(anyhow::anyhow!("Request interrupted after {} seconds", timeout_duration)); let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(20));
let response = match timeout(timeout_duration, async {
let mut req = isahc::Request::get(&clean_url);
for (k, v) in extra_headers.iter() {
req = req.header(k, v);
}
let mut response = client.send_async(req.body(())?).await?;
let status = response.status();
let headers = response.headers().clone();
let body = response.text().await?;
Ok::<_, anyhow::Error>(HttpResponse::new(status, headers, body))
})
.await
{
Ok(res) => res?,
Err(_) => {
self.record_connection_error(&format!("Request interrupted: {}", url))
.await;
return Err(anyhow::anyhow!(
"Request interrupted after {}s",
timeout_duration.as_secs()
));
} }
}; };
watchdog.abort();
match result { Ok(response)
Ok(response) => Ok(response),
Err(e) => {
self.record_connection_error(&e.to_string());
Err(anyhow::anyhow!("Failed to send HTTP request: {}", e))
}
}
} }
} }
/// 代理类型
#[derive(Debug, Clone, Copy)]
pub enum ProxyType {
None,
Localhost,
System,
}
+26 -18
View File
@@ -1,3 +1,5 @@
use crate::utils::i18n::t;
use tauri::AppHandle; use tauri::AppHandle;
use tauri_plugin_notification::NotificationExt; use tauri_plugin_notification::NotificationExt;
@@ -23,48 +25,54 @@ fn notify(app: &AppHandle, title: &str, body: &str) {
.ok(); .ok();
} }
pub fn notify_event(app: &AppHandle, event: NotificationEvent) { pub async fn notify_event<'a>(app: AppHandle, event: NotificationEvent<'a>) {
use crate::utils::i18n::t;
match event { match event {
NotificationEvent::DashboardToggled => { NotificationEvent::DashboardToggled => {
notify(app, &t("DashboardToggledTitle"), &t("DashboardToggledBody")); notify(
&app,
&t("DashboardToggledTitle").await,
&t("DashboardToggledBody").await,
);
} }
NotificationEvent::ClashModeChanged { mode } => { NotificationEvent::ClashModeChanged { mode } => {
notify( notify(
app, &app,
&t("ClashModeChangedTitle"), &t("ClashModeChangedTitle").await,
&t_with_args("ClashModeChangedBody", mode), &t_with_args("ClashModeChangedBody", mode).await,
); );
} }
NotificationEvent::SystemProxyToggled => { NotificationEvent::SystemProxyToggled => {
notify( notify(
app, &app,
&t("SystemProxyToggledTitle"), &t("SystemProxyToggledTitle").await,
&t("SystemProxyToggledBody"), &t("SystemProxyToggledBody").await,
); );
} }
NotificationEvent::TunModeToggled => { NotificationEvent::TunModeToggled => {
notify(app, &t("TunModeToggledTitle"), &t("TunModeToggledBody")); notify(
&app,
&t("TunModeToggledTitle").await,
&t("TunModeToggledBody").await,
);
} }
NotificationEvent::LightweightModeEntered => { NotificationEvent::LightweightModeEntered => {
notify( notify(
app, &app,
&t("LightweightModeEnteredTitle"), &t("LightweightModeEnteredTitle").await,
&t("LightweightModeEnteredBody"), &t("LightweightModeEnteredBody").await,
); );
} }
NotificationEvent::AppQuit => { NotificationEvent::AppQuit => {
notify(app, &t("AppQuitTitle"), &t("AppQuitBody")); notify(&app, &t("AppQuitTitle").await, &t("AppQuitBody").await);
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
NotificationEvent::AppHidden => { NotificationEvent::AppHidden => {
notify(app, &t("AppHiddenTitle"), &t("AppHiddenBody")); notify(&app, &t("AppHiddenTitle").await, &t("AppHiddenBody").await);
} }
} }
} }
// 辅助函数,带参数的i18n // 辅助函数,带参数的i18n
fn t_with_args(key: &str, mode: &str) -> String { async fn t_with_args(key: &str, mode: &str) -> String {
use crate::utils::i18n::t; t(key).await.replace("{mode}", mode)
t(key).replace("{mode}", mode)
} }
-753
View File
@@ -1,753 +0,0 @@
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use crate::{
config::{Config, PrfItem},
core::*,
logging, logging_error,
module::lightweight::{self, auto_lightweight_mode_init},
process::AsyncHandler,
utils::{init, logging::Type, server},
wrap_err,
};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock};
use percent_encoding::percent_decode_str;
use scopeguard;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Manager};
use tauri::Url;
//#[cfg(not(target_os = "linux"))]
// use window_shadows::set_shadow;
pub static VERSION: OnceCell<String> = OnceCell::new();
// 定义默认窗口尺寸常量
const DEFAULT_WIDTH: u32 = 940;
const DEFAULT_HEIGHT: u32 = 700;
// 添加全局UI准备就绪标志
static UI_READY: OnceCell<RwLock<bool>> = OnceCell::new();
// 窗口创建锁,防止并发创建窗口
static WINDOW_CREATING: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
// UI就绪阶段状态枚举
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UiReadyStage {
NotStarted,
Loading,
DomReady,
ResourcesLoaded,
Ready,
}
// UI就绪详细状态
#[derive(Debug)]
struct UiReadyState {
stage: RwLock<UiReadyStage>,
}
impl Default for UiReadyState {
fn default() -> Self {
Self {
stage: RwLock::new(UiReadyStage::NotStarted),
}
}
}
// 获取UI就绪状态细节
static UI_READY_STATE: OnceCell<UiReadyState> = OnceCell::new();
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
}
fn get_ui_ready() -> &'static RwLock<bool> {
UI_READY.get_or_init(|| RwLock::new(false))
}
fn get_ui_ready_state() -> &'static UiReadyState {
UI_READY_STATE.get_or_init(UiReadyState::default)
}
// 更新UI准备阶段
pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state();
let mut stage_lock = state.stage.write();
*stage_lock = stage;
// 如果是最终阶段,标记UI完全就绪
if stage == UiReadyStage::Ready {
mark_ui_ready();
}
}
// 标记UI已准备就绪
pub fn mark_ui_ready() {
let mut ready = get_ui_ready().write();
*ready = true;
logging!(info, Type::Window, true, "UI已标记为完全就绪");
}
// 重置UI就绪状态
pub fn reset_ui_ready() {
{
let mut ready = get_ui_ready().write();
*ready = false;
}
{
let state = get_ui_ready_state();
let mut stage = state.stage.write();
*stage = UiReadyStage::NotStarted;
}
logging!(info, Type::Window, true, "UI就绪状态已重置");
}
/// 异步方式处理启动后的额外任务
pub async fn resolve_setup_async(app_handle: &AppHandle) {
let start_time = std::time::Instant::now();
logging!(
info,
Type::Setup,
true,
"开始执行异步设置任务... 线程ID: {:?}",
std::thread::current().id()
);
if VERSION.get().is_none() {
let version = app_handle.package_info().version.to_string();
VERSION.get_or_init(|| {
logging!(info, Type::Setup, true, "初始化版本信息: {}", version);
version.clone()
});
}
logging_error!(Type::Setup, true, init::init_scheme());
logging_error!(Type::Setup, true, init::startup_script().await);
logging!(
info,
Type::Config,
true,
"开始初始化配置... 线程ID: {:?}",
std::thread::current().id()
);
logging_error!(Type::Config, true, Config::init_config().await);
logging!(info, Type::Config, true, "配置初始化完成");
// 启动时清理冗余的 Profile 文件
logging!(info, Type::Setup, true, "开始清理冗余的Profile文件...");
match Config::profiles().latest_ref().auto_cleanup() {
Ok(_) => {
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
}
Err(e) => {
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
}
}
logging!(trace, Type::Core, true, "启动核心管理器...");
logging_error!(Type::Core, true, CoreManager::global().init().await);
log::trace!(target: "app", "启动内嵌服务器...");
server::embed_server();
logging!(trace, Type::Core, true, "启动 IPC 监控服务...");
logging_error!(Type::Tray, true, tray::Tray::global().init());
if let Some(app_handle) = handle::Handle::global().app_handle() {
logging!(info, Type::Tray, true, "创建系统托盘...");
let result = tray::Tray::global().create_tray_from_handle(&app_handle);
if result.is_ok() {
logging!(info, Type::Tray, true, "系统托盘创建成功");
} else if let Err(e) = result {
logging!(error, Type::Tray, true, "系统托盘创建失败: {}", e);
}
} else {
logging!(
error,
Type::Tray,
true,
"无法创建系统托盘: app_handle不存在"
);
}
// 更新系统代理
logging_error!(
Type::System,
true,
sysopt::Sysopt::global().update_sysproxy().await
);
logging_error!(
Type::System,
true,
sysopt::Sysopt::global().init_guard_sysproxy()
);
// 创建窗口
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")]
{
if is_silent_start {
use crate::AppHandleManager;
AppHandleManager::global().set_activation_policy_accessory();
}
}
create_window(!is_silent_start);
// 初始化定时器
logging_error!(Type::System, true, timer::Timer::global().init());
// 自动进入轻量模式
auto_lightweight_mode_init();
logging_error!(Type::Tray, true, tray::Tray::global().update_part());
logging!(trace, Type::System, true, "初始化热键...");
logging_error!(Type::System, true, hotkey::Hotkey::global().init());
let elapsed = start_time.elapsed();
logging!(
info,
Type::Setup,
true,
"异步设置任务完成,耗时: {:?}",
elapsed
);
// 如果初始化时间过长,记录警告
if elapsed.as_secs() > 10 {
logging!(
warn,
Type::Setup,
true,
"异步设置任务耗时较长({:?})",
elapsed
);
}
}
/// reset system proxy (异步)
pub async fn resolve_reset_async() {
#[cfg(target_os = "macos")]
logging!(info, Type::Tray, true, "Unsubscribing from traffic updates");
#[cfg(target_os = "macos")]
tray::Tray::global().unsubscribe_traffic();
logging_error!(
Type::System,
true,
sysopt::Sysopt::global().reset_sysproxy().await
);
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
#[cfg(target_os = "macos")]
{
logging!(info, Type::System, true, "Restoring system DNS settings");
restore_public_dns().await;
}
}
/// Create the main window
pub fn create_window(is_show: bool) -> bool {
logging!(
info,
Type::Window,
true,
"开始创建/显示主窗口, is_show={}",
is_show
);
if !is_show {
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
lightweight::set_lightweight_mode(true);
handle::Handle::notify_startup_completed();
return false;
}
if let Some(app_handle) = handle::Handle::global().app_handle() {
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
if is_show {
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
}
return true;
}
}
let creating_lock = get_window_creating_lock();
let mut creating = creating_lock.lock();
let (is_creating, last_time) = *creating;
let elapsed = last_time.elapsed();
if is_creating && elapsed < Duration::from_secs(2) {
logging!(
info,
Type::Window,
true,
"窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
elapsed.as_millis()
);
return false;
}
*creating = (true, Instant::now());
// ScopeGuard 确保创建状态重置,防止 webview 卡死
let _guard = scopeguard::guard(creating, |mut creating_guard| {
*creating_guard = (false, Instant::now());
logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置");
});
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
logging!(
error,
Type::Window,
true,
"无法获取app_handle,窗口创建失败"
);
return false;
}
};
match tauri::WebviewWindowBuilder::new(
&app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.center()
.decorations(true)
.fullscreen(false)
.inner_size(DEFAULT_WIDTH as f64, DEFAULT_HEIGHT as f64)
.min_inner_size(520.0, 520.0)
.visible(true) // 立即显示窗口,避免用户等待
.initialization_script(
r#"
console.log('[Tauri] 窗口初始化脚本开始执行');
function createLoadingOverlay() {
if (document.getElementById('initial-loading-overlay')) {
console.log('[Tauri] 加载指示器已存在');
return;
}
console.log('[Tauri] 创建加载指示器');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'initial-loading-overlay';
loadingDiv.innerHTML = `
<div style="
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transition: opacity 0.3s ease;
">
<div style="margin-bottom: 20px;">
<div style="
width: 40px; height: 40px; border: 3px solid #e3e3e3;
border-top: 3px solid #3498db; border-radius: 50%;
animation: spin 1s linear infinite;
"></div>
</div>
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (prefers-color-scheme: dark) {
:root { --bg-color: #1a1a1a; --text-color: #ffffff; }
}
</style>
`;
if (document.body) {
document.body.appendChild(loadingDiv);
} else {
document.addEventListener('DOMContentLoaded', () => {
if (document.body && !document.getElementById('initial-loading-overlay')) {
document.body.appendChild(loadingDiv);
}
});
}
}
createLoadingOverlay();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createLoadingOverlay);
} else {
createLoadingOverlay();
}
console.log('[Tauri] 窗口初始化脚本执行完成');
"#,
)
.build()
{
Ok(newly_created_window) => {
logging!(debug, Type::Window, true, "主窗口实例创建成功");
update_ui_ready_stage(UiReadyStage::NotStarted);
AsyncHandler::spawn(move || async move {
handle::Handle::global().mark_startup_completed();
logging!(
debug,
Type::Window,
true,
"异步窗口任务开始 (启动已标记完成)"
);
// 先运行轻量模式检测
lightweight::run_once_auto_lightweight();
// 发送启动完成事件,触发前端开始加载
logging!(
debug,
Type::Window,
true,
"发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
if is_show {
let window_clone = newly_created_window.clone();
// 立即显示窗口
let _ = window_clone.show();
let _ = window_clone.set_focus();
logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
let timeout_seconds = if crate::module::lightweight::is_in_lightweight_mode() {
3
} else {
8
};
logging!(
info,
Type::Window,
true,
"开始监控UI加载状态 (最多{}秒)...",
timeout_seconds
);
// 异步监控UI状态,使用try_read避免死锁
AsyncHandler::spawn(move || async move {
logging!(
debug,
Type::Window,
true,
"启动UI状态监控线程,超时{}秒",
timeout_seconds
);
let ui_ready_checker = || async {
let (mut check_count, mut consecutive_failures) = (0, 0);
loop {
let is_ready = get_ui_ready()
.try_read()
.map(|guard| *guard)
.unwrap_or_else(|| {
consecutive_failures += 1;
if consecutive_failures > 50 {
logging!(
warn,
Type::Window,
true,
"UI状态监控连续{}次无法获取读锁,可能存在死锁",
consecutive_failures
);
consecutive_failures = 0;
}
false
});
if is_ready {
logging!(
debug,
Type::Window,
true,
"UI状态监控检测到就绪信号,退出监控"
);
return;
}
consecutive_failures = 0;
tokio::time::sleep(Duration::from_millis(100)).await;
check_count += 1;
if check_count % 20 == 0 {
logging!(
debug,
Type::Window,
true,
"UI加载状态检查... ({}秒)",
check_count / 10
);
}
}
};
let wait_result = tokio::time::timeout(
Duration::from_secs(timeout_seconds),
ui_ready_checker(),
)
.await;
match wait_result {
Ok(_) => {
logging!(info, Type::Window, true, "UI已完全加载就绪");
handle::Handle::global()
.get_window()
.map(|window| window.eval(r"
const overlay = document.getElementById('initial-loading-overlay');
if (overlay) {
overlay.style.opacity = '0';
setTimeout(() => overlay.remove(), 300);
}
"));
}
Err(_) => {
logging!(
warn,
Type::Window,
true,
"UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds
);
get_ui_ready()
.try_write()
.map(|mut guard| {
*guard = true;
logging!(
info,
Type::Window,
true,
"超时后成功设置UI就绪状态"
);
})
.unwrap_or_else(|| {
logging!(
error,
Type::Window,
true,
"超时后无法获取UI状态写锁,可能存在严重死锁"
);
});
}
}
});
logging!(info, Type::Window, true, "窗口显示流程完成");
} else {
logging!(
debug,
Type::Window,
true,
"is_show为false,窗口保持隐藏状态"
);
}
});
true
}
Err(e) => {
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
false
}
}
}
pub async fn resolve_scheme(param: String) -> Result<()> {
log::info!(target:"app", "received deep link: {param}");
let param_str = if param.starts_with("[") && param.len() > 4 {
param
.get(2..param.len() - 2)
.ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))?
} else {
param.as_str()
};
// 解析 URL
let link_parsed = match Url::parse(param_str) {
Ok(url) => url,
Err(e) => {
bail!("failed to parse deep link: {:?}, param: {:?}", e, param);
}
};
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
let name = link_parsed
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.into_owned());
let url_param = if let Some(query) = link_parsed.query() {
let prefix = "url=";
if let Some(pos) = query.find(prefix) {
let raw_url = &query[pos + prefix.len()..];
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
} else {
None
}
} else {
None
};
match url_param {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}");
create_window(false);
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = match item.uid.clone() {
Some(uid) => uid,
None => {
logging!(error, Type::Config, true, "Profile item missing UID");
handle::Handle::notice_message(
"import_sub_url::error",
"Profile item missing UID".to_string(),
);
return Ok(());
}
};
let _ = wrap_err!(Config::profiles().data_mut().append_item(item));
handle::Handle::notice_message("import_sub_url::ok", uid);
}
Err(e) => {
handle::Handle::notice_message("import_sub_url::error", e.to_string());
}
}
}
None => bail!("failed to get profile url"),
}
}
Ok(())
}
#[cfg(target_os = "macos")]
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;
}
};
log::info!(target: "app", "try to set system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("set_dns.sh");
if !script.exists() {
log::error!(target: "app", "set_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script, dns_server])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "set system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "set system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "set system dns failed: {err}");
}
}
}
#[cfg(target_os = "macos")]
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;
}
};
log::info!(target: "app", "try to unset system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("unset_dns.sh");
if !script.exists() {
log::error!(target: "app", "unset_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "unset system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "unset system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "unset system dns failed: {err}");
}
}
}
+94
View File
@@ -0,0 +1,94 @@
#[cfg(target_os = "macos")]
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;
}
};
log::info!(target: "app", "try to set system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("set_dns.sh");
if !script.exists() {
log::error!(target: "app", "set_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script, dns_server])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "set system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "set system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "set system dns failed: {err}");
}
}
}
#[cfg(target_os = "macos")]
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;
}
};
log::info!(target: "app", "try to unset system dns");
let resource_dir = match dirs::app_resources_dir() {
Ok(dir) => dir,
Err(e) => {
log::error!(target: "app", "Failed to get resource directory: {}", e);
return;
}
};
let script = resource_dir.join("unset_dns.sh");
if !script.exists() {
log::error!(target: "app", "unset_dns.sh not found");
return;
}
let script = script.to_string_lossy().into_owned();
match app_handle
.shell()
.command("bash")
.args([script])
.current_dir(resource_dir)
.status()
.await
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "unset system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "unset system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "unset system dns failed: {err}");
}
}
}
+266
View File
@@ -0,0 +1,266 @@
use anyhow::Result;
use tauri::AppHandle;
use crate::{
config::Config,
core::{
CoreManager, Timer, handle, hotkey::Hotkey, service::SERVICE_MANAGER, sysopt, tray::Tray,
},
logging, logging_error,
module::lightweight::{auto_lightweight_mode_init, run_once_auto_lightweight},
process::AsyncHandler,
utils::{init, logging::Type, server, window_manager::WindowManager},
};
pub mod dns;
pub mod scheme;
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_sync() {
AsyncHandler::spawn(|| async {
AsyncHandler::spawn_blocking(init_scheme);
AsyncHandler::spawn_blocking(init_embed_server);
});
}
pub fn resolve_setup_async() {
let start_time = std::time::Instant::now();
logging!(
info,
Type::Setup,
true,
"开始执行异步设置任务... 线程ID: {:?}",
std::thread::current().id()
);
AsyncHandler::spawn(|| async {
#[cfg(not(feature = "tauri-dev"))]
resolve_setup_logger().await;
init_service_manager().await;
futures::join!(
init_work_config(),
init_resources(),
init_startup_script(),
init_hotkey(),
);
init_timer().await;
init_once_auto_lightweight().await;
init_auto_lightweight_mode().await;
init_verge_config().await;
init_core_manager().await;
init_system_proxy().await;
AsyncHandler::spawn_blocking(|| {
init_system_proxy_guard();
});
let tray_and_refresh = async {
init_tray().await;
refresh_tray_menu().await;
};
futures::join!(init_window(), tray_and_refresh,);
});
let elapsed = start_time.elapsed();
logging!(
info,
Type::Setup,
true,
"异步设置任务完成,耗时: {:?}",
elapsed
);
if elapsed.as_secs() > 10 {
logging!(
warn,
Type::Setup,
true,
"异步设置任务耗时较长({:?})",
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
);
logging!(info, Type::Core, true, "Stopping core service");
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
#[cfg(target_os = "macos")]
{
use dns::restore_public_dns;
logging!(info, Type::System, true, "Restoring system DNS settings");
restore_public_dns().await;
}
}
pub fn init_handle(app_handle: AppHandle) {
logging!(info, Type::Setup, true, "Initializing app handle...");
handle::Handle::global().init(app_handle);
}
pub(super) fn init_scheme() {
logging!(info, Type::Setup, true, "Initializing custom URL scheme");
logging_error!(Type::Setup, true, 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);
}
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);
Ok(())
}
pub(super) fn init_embed_server() {
logging!(info, Type::Setup, true, "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);
}
pub(super) async fn init_startup_script() {
logging!(info, Type::Setup, true, "Initializing startup script");
logging_error!(Type::Setup, true, 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);
}
pub(super) async fn init_hotkey() {
logging!(info, Type::Setup, true, "Initializing hotkey...");
logging_error!(Type::Setup, true, 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);
}
pub async fn init_work_config() {
logging!(
info,
Type::Setup,
true,
"Initializing work configuration..."
);
logging_error!(Type::Setup, true, 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);
}
pub(super) async fn init_verge_config() {
logging!(
info,
Type::Setup,
true,
"Initializing verge configuration..."
);
logging_error!(Type::Setup, true, 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
);
}
pub(super) async fn init_core_manager() {
logging!(info, Type::Setup, true, "Initializing core manager...");
logging_error!(Type::Setup, true, CoreManager::global().init().await);
}
pub(super) async fn init_system_proxy() {
logging!(info, Type::Setup, true, "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()
);
}
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);
}
pub(super) async fn init_window() {
logging!(info, Type::Setup, true, "Initializing main window...");
let is_silent_start =
{ Config::verge().await.latest_ref().enable_silent_start }.unwrap_or(false);
#[cfg(target_os = "macos")]
{
if is_silent_start {
use crate::core::handle::Handle;
Handle::global().set_activation_policy_accessory();
}
}
WindowManager::create_window(!is_silent_start).await;
}
+74
View File
@@ -0,0 +1,74 @@
use anyhow::{Result, bail};
use percent_encoding::percent_decode_str;
use tauri::Url;
use crate::{config::PrfItem, core::handle, logging, utils::logging::Type, wrap_err};
pub(super) async fn resolve_scheme(param: String) -> Result<()> {
log::info!(target:"app", "received deep link: {param}");
let param_str = if param.starts_with("[") && param.len() > 4 {
param
.get(2..param.len() - 2)
.ok_or_else(|| anyhow::anyhow!("Invalid string slice boundaries"))?
} else {
param.as_str()
};
// 解析 URL
let link_parsed = match Url::parse(param_str) {
Ok(url) => url,
Err(e) => {
bail!("failed to parse deep link: {:?}, param: {:?}", e, param);
}
};
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
let name = link_parsed
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value.into_owned());
let url_param = if let Some(query) = link_parsed.query() {
let prefix = "url=";
if let Some(pos) = query.find(prefix) {
let raw_url = &query[pos + prefix.len()..];
Some(percent_decode_str(raw_url).decode_utf8_lossy().to_string())
} else {
None
}
} else {
None
};
match url_param {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}");
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = match item.uid.clone() {
Some(uid) => uid,
None => {
logging!(error, Type::Config, true, "Profile item missing UID");
handle::Handle::notice_message(
"import_sub_url::error",
"Profile item missing UID".to_string(),
);
return Ok(());
}
};
let result = crate::config::profiles::profiles_append_item_safe(item).await;
let _ = wrap_err!(result);
handle::Handle::notice_message("import_sub_url::ok", uid);
}
Err(e) => {
handle::Handle::notice_message("import_sub_url::error", e.to_string());
}
}
}
None => bail!("failed to get profile url"),
}
}
Ok(())
}
+73
View File
@@ -0,0 +1,73 @@
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use tokio::sync::Notify;
use crate::{logging, utils::logging::Type};
// 使用 AtomicBool 替代 RwLock<bool>,性能更好且无锁
static UI_READY: OnceCell<AtomicBool> = OnceCell::new();
// 获取UI就绪状态细节
static UI_READY_STATE: OnceCell<UiReadyState> = OnceCell::new();
// 添加通知机制,用于事件驱动的 UI 就绪检测
static UI_READY_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
// UI就绪阶段状态枚举
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UiReadyStage {
NotStarted,
Loading,
DomReady,
ResourcesLoaded,
Ready,
}
// UI就绪详细状态
#[derive(Debug)]
struct UiReadyState {
stage: RwLock<UiReadyStage>,
}
impl Default for UiReadyState {
fn default() -> Self {
Self {
stage: RwLock::new(UiReadyStage::NotStarted),
}
}
}
pub(super) fn get_ui_ready() -> &'static AtomicBool {
UI_READY.get_or_init(|| AtomicBool::new(false))
}
fn get_ui_ready_state() -> &'static UiReadyState {
UI_READY_STATE.get_or_init(UiReadyState::default)
}
fn get_ui_ready_notify() -> &'static Arc<Notify> {
UI_READY_NOTIFY.get_or_init(|| Arc::new(Notify::new()))
}
// 更新UI准备阶段
pub fn update_ui_ready_stage(stage: UiReadyStage) {
let state = get_ui_ready_state();
let mut stage_lock = state.stage.write();
*stage_lock = stage;
// 如果是最终阶段,标记UI完全就绪
if stage == UiReadyStage::Ready {
mark_ui_ready();
}
}
// 标记UI已准备就绪
pub fn mark_ui_ready() {
get_ui_ready().store(true, Ordering::Release);
logging!(info, Type::Window, true, "UI已标记为完全就绪");
// 通知所有等待的任务
get_ui_ready_notify().notify_waiters();
}
+52
View File
@@ -0,0 +1,52 @@
use tauri::WebviewWindow;
use crate::{
core::handle,
logging, logging_error,
utils::{
logging::Type,
resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT},
},
};
// 定义默认窗口尺寸常量
const DEFAULT_WIDTH: f64 = 940.0;
const DEFAULT_HEIGHT: f64 = 700.0;
const MINIMAL_WIDTH: f64 = 520.0;
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()
})?;
match tauri::WebviewWindowBuilder::new(
&app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.center()
.decorations(true)
.fullscreen(false)
.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
.min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT)
.visible(true) // 立即显示窗口,避免用户等待
.initialization_script(WINDOW_INITIAL_SCRIPT)
.build()
{
Ok(window) => {
logging_error!(Type::Window, true, window.eval(INITIAL_LOADING_OVERLAY));
Ok(window)
}
Err(e) => Err(e.to_string()),
}
}
@@ -0,0 +1,71 @@
pub const WINDOW_INITIAL_SCRIPT: &str = r#"
console.log('[Tauri] 窗口初始化脚本开始执行');
function createLoadingOverlay() {
if (document.getElementById('initial-loading-overlay')) {
console.log('[Tauri] 加载指示器已存在');
return;
}
console.log('[Tauri] 创建加载指示器');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'initial-loading-overlay';
loadingDiv.innerHTML = `
<div style="
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color, #f5f5f5); color: var(--text-color, #333);
display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transition: opacity 0.3s ease;
">
<div style="margin-bottom: 20px;">
<div style="
width: 40px; height: 40px; border: 3px solid #e3e3e3;
border-top: 3px solid #3498db; border-radius: 50%;
animation: spin 1s linear infinite;
"></div>
</div>
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (prefers-color-scheme: dark) {
:root { --bg-color: #1a1a1a; --text-color: #ffffff; }
}
</style>
`;
if (document.body) {
document.body.appendChild(loadingDiv);
} else {
document.addEventListener('DOMContentLoaded', () => {
if (document.body && !document.getElementById('initial-loading-overlay')) {
document.body.appendChild(loadingDiv);
}
});
}
}
createLoadingOverlay();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createLoadingOverlay);
} else {
createLoadingOverlay();
}
console.log('[Tauri] 窗口初始化脚本执行完成');
"#;
pub const INITIAL_LOADING_OVERLAY: &str = r"
const overlay = document.getElementById('initial-loading-overlay');
if (overlay) {
overlay.style.opacity = '0';
setTimeout(() => overlay.remove(), 300);
}
";
+37 -29
View File
@@ -1,15 +1,12 @@
extern crate warp;
use super::resolve; use super::resolve;
use crate::{ use crate::{
config::{Config, IVerge, DEFAULT_PAC}, config::{Config, DEFAULT_PAC, IVerge},
logging_error, logging_error,
process::AsyncHandler, process::AsyncHandler,
utils::logging::Type, utils::logging::Type,
}; };
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use port_scanner::local_port_available; use port_scanner::local_port_available;
use std::convert::Infallible;
use warp::Filter; use warp::Filter;
#[derive(serde::Deserialize, Debug)] #[derive(serde::Deserialize, Debug)]
@@ -48,39 +45,50 @@ pub fn embed_server() {
let port = IVerge::get_singleton_port(); let port = IVerge::get_singleton_port();
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
let visible = warp::path!("commands" / "visible").map(|| { let visible = warp::path!("commands" / "visible").and_then(|| async {
resolve::create_window(false); Ok::<_, warp::Rejection>(warp::reply::with_status(
warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK) "ok".to_string(),
warp::http::StatusCode::OK,
))
}); });
let pac = warp::path!("commands" / "pac").map(|| { let verge_config = Config::verge().await;
let content = Config::verge() let clash_config = Config::clash().await;
.latest_ref()
.pac_file_content let content = verge_config
.clone() .latest_ref()
.unwrap_or(DEFAULT_PAC.to_string()); .pac_file_content
let port = Config::verge() .clone()
.latest_ref() .unwrap_or(DEFAULT_PAC.to_string());
.verge_mixed_port
.unwrap_or(Config::clash().latest_ref().get_mixed_port()); let mixed_port = verge_config
let content = content.replace("%mixed-port%", &format!("{port}")); .latest_ref()
.verge_mixed_port
.unwrap_or(clash_config.latest_ref().get_mixed_port());
// Clone the content and port for the closure to avoid borrowing issues
let pac_content = content.clone();
let pac_port = mixed_port;
let pac = warp::path!("commands" / "pac").map(move || {
let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}"));
warp::http::Response::builder() warp::http::Response::builder()
.header("Content-Type", "application/x-ns-proxy-autoconfig") .header("Content-Type", "application/x-ns-proxy-autoconfig")
.body(content) .body(processed_content)
.unwrap_or_default() .unwrap_or_default()
}); });
async fn scheme_handler(query: QueryParam) -> Result<String, Infallible> {
logging_error!(
Type::Setup,
true,
resolve::resolve_scheme(query.param).await
);
Ok("ok".to_string())
}
// Use map instead of and_then to avoid Send issues
let scheme = warp::path!("commands" / "scheme") let scheme = warp::path!("commands" / "scheme")
.and(warp::query::<QueryParam>()) .and(warp::query::<QueryParam>())
.and_then(scheme_handler); .map(|query: QueryParam| {
// 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);
});
warp::reply::with_status("ok".to_string(), warp::http::StatusCode::OK)
});
let commands = visible.or(scheme).or(pac); let commands = visible.or(scheme).or(pac);
warp::serve(commands).run(([127, 0, 0, 1], port)).await; warp::serve(commands).run(([127, 0, 0, 1], port)).await;
}); });

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