Compare commits

...
265 Commits
Author SHA1 Message Date
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
wonfen a9464ff776 chore: update release log & fix workflow2 2025-08-22 16:30:07 +08:00
Tunglies 335ca817d2 refactor: restrict AsyncHandler usage to Windows platform only 2025-08-22 04:18:21 +08:00
Tunglies 6d112c387d refactor: replace tokio::task::spawn_blocking with AsyncHandler::spawn_blocking for improved task management 2025-08-22 04:05:35 +08:00
Tunglies e4c243de2d refactor: Replace tokio::spawn with AsyncHandler::spawn for better task management
- Replace direct tokio::spawn calls with AsyncHandler::spawn across multiple modules
- Improves task lifecycle management and error handling consistency
- Affected files:
  - src-tauri/src/cmd/network.rs
  - src-tauri/src/core/core.rs
  - src-tauri/src/core/event_driven_proxy.rs
  - src-tauri/src/enhance/tun.rs
  - src-tauri/src/ipc/logs.rs
  - src-tauri/src/ipc/memory.rs
  - src-tauri/src/ipc/monitor.rs
  - src-tauri/src/ipc/traffic.rs
  - src-tauri/src/utils/network.rs
  - src-tauri/src/utils/resolve.rs

This change provides better control over async task spawning and helps prevent
potential issues with unmanaged background tasks.
2025-08-22 03:41:14 +08:00
Tunglies 02f67961a9 feat: add tokio-stream dependency and refactor event loop handling in EventDrivenProxyManager 2025-08-22 03:16:59 +08:00
Tunglies 7d5fd295ed feat: add dev:trace script for enhanced debugging in development 2025-08-22 02:28:44 +08:00
Tunglies daa0b1592d chore: bump version to 2.4.1 2025-08-22 00:29:42 +08:00
Tunglies b411783bbe fix: enhance tag version check for consistency in release workflow 2025-08-22 00:15:56 +08:00
Tunglies 40a59bbc1a fix: disable automatic generation of release notes in workflow 2025-08-21 23:56:02 +08:00
wonfen 93fc4932ee Release 2.4.0 2025-08-21 22:45:55 +08:00
Tunglies 2277d7232e refactor: improve code formatting and readability in autobuild and telegram scripts 2025-08-21 21:23:47 +08:00
Tunglies 435318cf1d fix: simplify return statements in updateProxy method for clarity 2025-08-21 21:23:46 +08:00
wonfen a9a9d8a78f fix(workflow): file version & format 2025-08-21 21:21:52 +08:00
❤是纱雾酱哟~ a2544d237e ci: improve commit checking and update release version script (#4471)
- Increase `fetch-depth` to 50 for more accurate commit history in CI
- Update `release-version.mjs` to use `bash` explicitly for improved compatibility
  - Also avoid errors when invoking the script

Signed-off-by: Dragon1573 <49941141+Dragon1573@users.noreply.github.com>
2025-08-21 21:16:15 +08:00
wonfen 9397ac0174 fix(workflow): download files name & release note 2025-08-21 19:49:17 +08:00
wonfen 4c719da096 chore(workflow): add release info & telegram notification 2025-08-21 18:33:54 +08:00
Tunglies 7613417c33 feat: enhance Tauri build steps with clearer naming and include updater JSON 2025-08-20 23:06:55 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a2a65cade7 chore(deps): update npm dependencies (#4461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:54:47 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 3a07402aa2 chore(deps): update npm dependencies (#4454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 22:49:45 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tunglies
59b67f5d3f chore(deps): update cargo dependencies (#4451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2025-08-20 22:49:37 +08:00
Tunglies e30cfc3a2f chore: update dependencies and improve IPC request handling 2025-08-20 22:48:08 +08:00
Tunglies 52655d9702 feat: add configuration options to IpcManager for improved client setup 2025-08-20 18:05:31 +08:00
Tunglies e93846ddc1 refactor: improve log management by introducing a constant for max logs and simplifying log level handling 2025-08-19 22:43:36 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4cf2f6b1e6 chore(deps): update cargo dependencies (#4443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 16:36:36 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 43a3cb74ac chore(deps): update npm dependencies (#4430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 16:36:24 +08:00
TianHua Liu 3360339c08 docs: move Windows ARM devices note to CONTRIBUTING.md (#4446)
* docs: how to develop locally

* docs: move Windows ARM devices note to CONTRIBUTING.md
2025-08-19 16:28:42 +08:00
Tunglies f0dbe9fa60 feat: add console-subscriber for improved logging and tracing support 2025-08-18 23:40:25 +08:00
Tunglies 756751b765 refactor: simplify app restart logic and improve error handling 2025-08-18 22:33:29 +08:00
Tunglies 85a9f6c8d4 fix: correct log cleanup day mapping and update logging level #4434 2025-08-18 19:14:08 +08:00
AhaoandTunglies 7fe0381850 renew: remove whether to enable_random_port (#4401)
* refactor: streamline clean old assets job by using reusable workflow

* refactor: update clean old assets job to include steps section

* refactor: add checkout step in clean_old_assets job for improved repository access

* fix: correct path to clean old assets workflow in autobuild.yml

* fix: update path to clean old assets workflow in autobuild.yml

* refactor: simplify clean_old_assets job by removing unnecessary steps

* refactor: enhance clean_old_assets job dependencies for improved execution flow

* Revert "refactor: enhance clean_old_assets job dependencies for improved execution flow"

This reverts commit 1a5108b5ad.

* feat: implement get_latest_tauri_commit script and update release versioning logic

* renew: remove whether to enable_random_port

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-08-18 02:03:06 +08:00
Tunglies 537d27d10b fix: clippy errors with new config (#4428)
* refactor: improve code quality with clippy fixes and standardized logging

- Replace dangerous unwrap()/expect() calls with proper error handling
- Standardize logging from log:: to logging\! macro with Type:: classifications
- Fix app handle panics with graceful fallback patterns
- Improve error resilience across 35+ modules without breaking functionality
- Reduce clippy warnings from 300+ to 0 in main library code

* chore: update Cargo.toml configuration

* refactor: resolve all clippy warnings
- Fix Arc clone warnings using explicit Arc::clone syntax across 9 files
- Add #[allow(clippy::expect_used)] to test functions for appropriate expect usage
- Remove no-effect statements from debug code cleanup
- Apply clippy auto-fixes for dbg\! macro removals and path statements
- Achieve zero clippy warnings on all targets with -D warnings flag

* chore: update Cargo.toml clippy configuration

* refactor: simplify macOS job configuration and improve caching

* refactor: remove unnecessary async/await from service and proxy functions

* refactor: streamline pnpm installation in CI configuration

* refactor: simplify error handling and remove unnecessary else statements

* refactor: replace async/await with synchronous locks for core management

* refactor: add workflow_dispatch trigger to clippy job

* refactor: convert async functions to synchronous for service management

* refactor: convert async functions to synchronous for UWP tool invocation

* fix: change wrong logging

* refactor: convert proxy restoration functions to async

* Revert "refactor: convert proxy restoration functions to async"

This reverts commit b82f5d250b.

* refactor: update proxy restoration functions to return Result types

* fix: handle errors during proxy restoration and update async function signatures

* fix: handle errors during proxy restoration and update async function signatures

* refactor: update restore_pac_proxy and restore_sys_proxy functions to async

* fix: convert restore_pac_proxy and restore_sys_proxy functions to async

* fix: await restore_sys_proxy calls in proxy restoration logic

* fix: suppress clippy warnings for unused async functions in proxy restoration

* fix: suppress clippy warnings for unused async functions in proxy restoration
2025-08-18 02:02:25 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a5fdd3f1a2 chore(deps): update npm dependencies (#4400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-16 15:43:15 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> be8a632a09 chore(deps): update rust crate async-trait to 0.1.89 (#4404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-16 15:42:31 +08:00
ONEUI8andONEUI-7 95c34f1df5 Update profile.rs (#4408)
Co-authored-by: ONEUI-7 <150009830+ONEUI-7@users.noreply.github.com>
2025-08-16 15:42:21 +08:00
TianHua Liu 8b53a7bd99 docs: how to develop locally (#4409) 2025-08-16 15:38:32 +08:00
Tunglies b169ee8149 Revert "refactor: replace tokio Mutex with parking_lot Mutex for improved performance"
This reverts commit 9cc6dde999.
2025-08-16 04:24:03 +00:00
Tunglies 9cc6dde999 refactor: replace tokio Mutex with parking_lot Mutex for improved performance 2025-08-16 02:44:00 +08:00
Tunglies cf1fbb63c4 refactor: replace log macros with logging utility for IPC monitoring 2025-08-15 20:41:45 +08:00
Tunglies 2aa629ff5d feat: refactor check_commit job to use new workflow inputs and streamline build checks
fix: update tag_name in check_commit job to use a static value

refactor: streamline latest.json handling and improve commit hash comparison logic

fix: update curl command to follow redirects when fetching latest.json
2025-08-14 20:42:26 +08:00
Tunglies 1e2b453c24 refactor: streamline clean old assets job by using reusable workflow
refactor: update clean old assets job to include steps section

refactor: add checkout step in clean_old_assets job for improved repository access

fix: correct path to clean old assets workflow in autobuild.yml

fix: update path to clean old assets workflow in autobuild.yml

refactor: simplify clean_old_assets job by removing unnecessary steps

refactor: enhance clean_old_assets job dependencies for improved execution flow

Revert "refactor: enhance clean_old_assets job dependencies for improved execution flow"

This reverts commit 1a5108b5ad.

feat: implement get_latest_tauri_commit script and update release versioning logic
2025-08-14 20:42:24 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 331e4a4970 chore(deps): update cargo dependencies (#4360)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 16:27:14 +08:00
Tunglies ee3ffaef1d refactor: simplify log warning message formatting and remove unused LogLevel enum 2025-08-13 16:25:36 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7cc3bc83a0 chore(deps): update npm dependencies (#4367)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 16:09:56 +08:00
Tunglies 558e28ddaf refactor: simplify log retrieval by removing level parameter and relying on server-side filtering #4293 2025-08-13 01:15:33 +08:00
Sergey KharenkoandTunglies 45e69543b3 fix: auto-detect KDE/Plasma and disable GTK CSD to fix titlebar button freeze (#4380)
* fix: auto-detect KDE/Plasma and disable GTK CSD to fix titlebar button freeze

* chore: update UPDATELOG

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-08-12 19:27:46 +08:00
wonfen 67ee41c5ea fix: ensure TUN is disabled before core exit 2025-08-11 23:19:49 +08:00
wonfen 160ed05178 fix: unify TUN status detection logic 2025-08-11 20:45:23 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 5ecfe121b3 chore(deps): update dependency swr to ^2.3.5 (#4359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-09 17:42:09 +08:00
Tunglies a654137af9 fix: adjust cleanup function and reduce refresh interval to 1 second 2025-08-09 10:04:38 +08:00
Tunglies cb591f19fb feat: add workflow to clean old release assets with versioning and dry run support 2025-08-08 23:50:52 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4823a348be chore(deps): update rust crate warp to 0.4.1 (#4342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 23:49:09 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 32da6ae808 chore(deps): update npm dependencies (#4347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 23:48:55 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7eb70b0f0d chore(deps): update dependency @vitejs/plugin-react to v5 (#4348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-08 23:47:18 +08:00
Tunglies 52e8e45daf fix: correct log level hierarchy and enhance log filtering logic 2025-08-08 23:30:24 +08:00
Tunglies 319c5b84fa Revert "feat: enhance log filtering by adding support for debug level and updating log hierarchy #4293"
This reverts commit a9cfb2cfaa.
2025-08-08 15:17:46 +08:00
Tunglies 6a93ff1fc1 fix: streamline autobuild versioning by utilizing outputs from check_commit job 2025-08-08 15:15:27 +08:00
Tunglies 6069b654d1 fix: ensure app quit notification is sent before quitting 2025-08-08 14:57:47 +08:00
Tunglies 2af8c32497 Revert "add: home card drag (#4215)"
This reverts commit 84989e0ea3.
2025-08-07 20:05:36 +08:00
Tunglies a3957289c8 fix: enhance asset cleanup and versioning logic in autobuild workflow 2025-08-07 19:47:31 +08:00
Tunglies 3f5cd6c26a fix: resolve system proxy status detection and display inconsistency
- Fixed getSystemProxyActualState logic to properly check actual system status
- Unified system proxy state display across all components
- Replaced systemProxyIndicator with actualState for consistent UI display
- Updated components: setting-system, ProxyControlSwitches, proxy-tun-card
- Added entry to v2.4.0 changelog
2025-08-07 02:18:50 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8046dad56d chore(deps): update npm dependencies (#4324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 22:47:01 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>TungliesTunglies
cdc1fd2d87 chore(deps): update cargo dependencies (#4329)
* chore(deps): update cargo dependencies

* fix: update warp dependency to include server feature

* fix: update return type of scheme_handler to String for consistency

* fix: add hyper-util dependency and update warp path handlers for response status

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-08-06 22:46:46 +08:00
Tunglies 499626b946 fix: resolve intermittent startup deadlock issues
- Optimize configuration access locks to prevent race conditions
- Enhance UI monitoring thread with non-blocking lock operations
- Improve window creation timing and synchronization
- Add comprehensive deadlock detection and debugging logs
- Simplify code structure with better error handling patterns
- Update changelog with user-friendly descriptions
2025-08-06 22:12:00 +08:00
Tunglies a9cfb2cfaa feat: enhance log filtering by adding support for debug level and updating log hierarchy #4293 2025-08-06 20:49:54 +08:00
Tunglies 7b976c16eb feat: enhance autobuild logic to check for Tauri-related changes and manage versioning 2025-08-06 20:34:57 +08:00
Tunglies 44e8a035aa fix: improve profile import validation and handle async lock correctly
fix: refactor import_profile function for improved readability and maintainability
2025-08-05 23:23:11 +08:00
Tunglies 6b57607926 fix: update autobuild versioning logic and improve asset checking regex 2025-08-05 23:12:15 +08:00
Tunglies c3675e48fd fix: update tauri.conf.json version to use full version information including build metadata 2025-08-05 22:56:34 +08:00
Tunglies a66393c609 feat: enhance profile import functionality with timeout and robust refresh strategy 2025-08-05 20:29:36 +08:00
Tunglies 776abaf56d fix: update service messages to use PRODUCTNAME variable for consistency 2025-08-05 20:09:35 +08:00
Tunglies 18808004f4 fix: update Cargo.toml version handling to use the provided version directly 2025-08-05 20:02:47 +08:00
Tunglies db8761946d feat: add autobuild check logic workflow for version and source changes 2025-08-05 06:31:49 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2194a96145 chore(deps): update npm dependencies (#4294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 14:57:24 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> ecd396d70f chore(deps): update cargo dependencies (#4297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 14:46:54 +08:00
Tunglies 5ab0438397 fix: duplicated build version checks 2025-08-04 14:44:51 +08:00
Tunglies bcde047695 refactor: enhance logging in autobuild workflow with emojis for better visibility 2025-08-03 18:52:26 +08:00
Tunglies 109f5f9648 fix: resolve macOS window management issues and improve logging during app reopen events 2025-08-03 18:40:19 +08:00
Tunglies d16c691c0f fix: can not filiter log level as expected 2025-08-03 09:52:27 +08:00
Tunglies 3eb2a5b3ef refactor: optimize timer and network management with atomic operations 2025-08-01 23:02:11 +08:00
Tunglies 569e2d5192 refactor: enhance traffic monitoring system with unified data management
✨ New Features:
- Implement unified traffic monitoring hook with reference counting
- Add intelligent data sampling and compression for better performance
- Introduce enhanced canvas traffic graph with mouse hover tooltips
- Add Y-axis labels and improved time axis display strategies
- Support multiple time ranges (1, 5, 10 minutes) with adaptive formatting

🚀 Performance Improvements:
- Smart data compression reduces memory usage by 80%
- Reference counting prevents unnecessary data collection when no components need it
- Debounced data updates reduce UI thrashing
- Optimized canvas rendering with controlled frame rates

🔧 Technical Improvements:
- Consolidate traffic monitoring logic into single hook (use-traffic-monitor.ts)
- Remove duplicate hook implementations
- Improve error handling with fallback to last valid data
- Add comprehensive traffic statistics and monitoring diagnostics
- Enhance tooltip system with precise data point highlighting

🐞 Bug Fixes:
- Fix connection speed display issues after IPC migration
- Improve data freshness indicators
- Better handling of network errors and stale data
- Consistent traffic parsing across all components

📝 Code Quality:
- Add TypeScript interfaces for better type safety
- Implement proper cleanup for animation frames and references
- Add error boundaries for traffic components
- Improve component naming and organization

This refactoring provides a more robust, performant, and feature-rich traffic monitoring system while maintaining backward compatibility.
2025-07-31 20:38:11 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 0077157d28 chore(deps): update cargo dependencies (#4275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 14:43:09 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 9e19bab5a7 chore(deps): update dependency zustand to ^5.0.7 (#4286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 14:42:45 +08:00
Tunglies b1e2940db6 refactor: optimize singleton macro usage with Default trait implementations (#4279)
* refactor: implement DRY principle improvements across backend

Major DRY violations identified and addressed:

1. **IPC Stream Monitor Pattern**:
   - Created `utils/ipc_monitor.rs` with generic `IpcStreamMonitor` trait
   - Added `IpcMonitorManager` for common async task management patterns
   - Eliminates duplication across traffic.rs, memory.rs, and logs.rs

2. **Singleton Pattern Duplication**:
   - Created `utils/singleton.rs` with `singleton\!` and `singleton_with_logging\!` macros
   - Replaces 16+ duplicate singleton implementations across codebase
   - Provides consistent, tested patterns for global instances

3. **macOS Activation Policy Refactoring**:
   - Consolidated 3 duplicate methods into single parameterized `set_activation_policy()`
   - Eliminated code duplication while maintaining backward compatibility
   - Reduced maintenance burden for macOS-specific functionality

These improvements enhance maintainability, reduce bug potential, and ensure consistent patterns across the backend codebase.

* fix: resolve test failures and clippy warnings

- Fix doctest in singleton.rs by using rust,ignore syntax and proper code examples
- Remove unused time::Instant import from ipc_monitor.rs
- Add #[allow(dead_code)] attributes to future-use utility modules
- All 11 unit tests now pass successfully
- All clippy checks pass with -D warnings strict mode
- Documentation tests properly ignore example code that requires full context

* refactor: migrate code to use new utility tools (partial)

Progress on systematic migration to use created utility tools:

1. **Reorganized IPC Monitor**:
   - Moved ipc_monitor.rs to src-tauri/src/ipc/monitor.rs for better organization
   - Updated module structure to emphasize IPC relationship

2. **IpcManager Singleton Migration**:
   - Replaced manual OnceLock singleton pattern with singleton_with_logging\! macro
   - Simplified initialization code and added consistent logging
   - Removed unused imports (OnceLock, logging::Type)

3. **ProxyRequestCache Singleton Migration**:
   - Migrated from once_cell::sync::OnceCell to singleton\! macro
   - Cleaner, more maintainable singleton pattern
   - Consistent with project-wide singleton approach

These migrations demonstrate the utility and effectiveness of the created tools:
- Less boilerplate code
- Consistent patterns across codebase
- Easier maintenance and debugging

* feat: complete migration to new utility tools - phase 1

Successfully migrated core components to use the created utility tools:

- Moved `ipc_monitor.rs` to `src-tauri/src/ipc/monitor.rs`
- Better organization emphasizing IPC relationship
- Updated module exports and imports

- **IpcManager**: Migrated to `singleton_with_logging\!` macro
- **ProxyRequestCache**: Migrated to `singleton\!` macro
- Eliminated ~30 lines of boilerplate singleton code
- Consistent logging and initialization patterns

- Removed unused imports (OnceLock, once_cell, logging::Type)
- Cleaner, more maintainable code structure
- All 11 unit tests pass successfully
- Zero compilation warnings

- **Lines of code reduced**: ~50+ lines of boilerplate
- **Consistency improved**: Unified singleton patterns
- **Maintainability enhanced**: Centralized utility functions
- **Test coverage maintained**: 100% test pass rate

Remaining complex monitors (traffic, memory, logs) will be migrated to use the shared IPC monitoring patterns in the next phase, which requires careful refactoring of their streaming logic.

* refactor: complete singleton pattern migration to utility macros

Migrate remaining singleton patterns across the backend to use standardized
utility macros, achieving significant code reduction and consistency improvements.

- **LogsMonitor** (ipc/logs.rs): `OnceLock` → `singleton_with_logging\!`
- **Sysopt** (core/sysopt.rs): `OnceCell` → `singleton_lazy\!`
- **Tray** (core/tray/mod.rs): Complex `OnceCell` → `singleton_lazy\!`
- **Handle** (core/handle.rs): `OnceCell` → `singleton\!`
- **CoreManager** (core/core.rs): `OnceCell` → `singleton_lazy\!`
- **TrafficMonitor** (ipc/traffic.rs): `OnceLock` → `singleton_lazy_with_logging\!`
- **MemoryMonitor** (ipc/memory.rs): `OnceLock` → `singleton_lazy_with_logging\!`

- `singleton_lazy\!` - For complex initialization patterns
- `singleton_lazy_with_logging\!` - For complex initialization with logging

- **Code Reduction**: -33 lines of boilerplate singleton code
- **DRY Compliance**: Eliminated duplicate initialization patterns
- **Consistency**: Unified singleton approach across codebase
- **Maintainability**: Centralized singleton logic in utility macros
- **Zero Breaking Changes**: All existing APIs remain compatible

All tests pass and clippy warnings resolved.

* refactor: optimize singleton macros using Default trait implementation

Simplify singleton macro usage by implementing Default trait for complex
initialization patterns, significantly improving code readability and maintainability.

- **MemoryMonitor**: Move IPC client initialization to Default impl
- **TrafficMonitor**: Move IPC client initialization to Default impl
- **Sysopt**: Move Arc<Mutex> initialization to Default impl
- **Tray**: Move struct field initialization to Default impl
- **CoreManager**: Move Arc<Mutex> initialization to Default impl

```rust
singleton_lazy_with_logging\!(MemoryMonitor, INSTANCE, "MemoryMonitor", || {
    let ipc_path_buf = ipc_path().unwrap();
    let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
    let client = IpcStreamClient::new(ipc_path).unwrap();
    MemoryMonitor::new(client)
});
```

```rust
impl Default for MemoryMonitor { /* initialization logic */ }
singleton_lazy_with_logging\!(MemoryMonitor, INSTANCE, "MemoryMonitor", MemoryMonitor::default);
```

- **Code Reduction**: -17 lines of macro closure code (80%+ simplification)
- **Separation of Concerns**: Initialization logic moved to proper Default impl
- **Readability**: Single-line macro calls vs multi-line closures
- **Testability**: Default implementations can be tested independently
- **Rust Idioms**: Using standard Default trait pattern
- **Performance**: Function calls more efficient than closures

All tests pass and clippy warnings resolved.

* refactor: implement MonitorData and StreamingParser traits for IPC monitors

* refactor: add timeout and retry_interval fields to IpcStreamMonitor; update TrafficMonitorState to derive Default

* refactor: migrate AppHandleManager to unified singleton control

- Replace manual singleton implementation with singleton_with_logging\! macro
- Remove std::sync::Once dependency in favor of OnceLock-based pattern
- Improve error handling for macOS activation policy methods
- Maintain thread safety with parking_lot::Mutex for AppHandle storage
- Add proper initialization check to prevent duplicate handle assignment
- Enhance logging consistency across AppHandleManager operations

* refactor: improve hotkey management with enum-based operations

- Add HotkeyFunction enum for type-safe function selection
- Add SystemHotkey enum for predefined system shortcuts
- Implement Display and FromStr traits for type conversions
- Replace string-based hotkey registration with enum methods
- Add register_system_hotkey() and unregister_system_hotkey() methods
- Maintain backward compatibility with string-based register() method
- Migrate singleton pattern to use singleton_with_logging\! macro
- Extract hotkey function execution logic into centralized execute_function()
- Update lib.rs to use new enum-based SystemHotkey operations
- Improve type safety and reduce string manipulation errors

Benefits:
- Type safety prevents invalid hotkey function names
- Centralized function execution reduces code duplication
- Enum-based API provides better IDE autocomplete support
- Maintains full backward compatibility with existing configurations

* fix: resolve LightWeightState initialization order panic

- Modify with_lightweight_status() to safely handle unmanaged state using try_state()
- Return Option<R> instead of R to gracefully handle state unavailability
- Update is_in_lightweight_mode() to use unwrap_or(false) for safe defaults
- Add state availability check in auto_lightweight_mode_init() before access
- Maintain singleton check priority while preventing early state access panics
- Fix clippy warnings for redundant pattern matching

Resolves runtime panic: "state() called before manage() for LightWeightState"

* refactor: add unreachable patterns for non-macOS in hotkey handling

* refactor: simplify SystemHotkey enum by removing redundant cfg attributes

* refactor: add macOS conditional compilation for system hotkey registration methods

* refactor: streamline hotkey unregistration and error logging for macOS
2025-07-31 14:35:13 +08:00
Tunglies 4113cd619c feat: add devcontainer configuration for Docker and Codespaces support 2025-07-31 11:39:14 +08:00
Tunglies 1f78d576a3 feat: migrate logs API from REST to IPC streaming (#4277)
* feat: migrate logs API from REST to IPC streaming

- Replace REST API `/logs` calls with IPC streaming implementation
- Add new `src-tauri/src/ipc/logs.rs` with `LogsMonitor` for real-time log streaming
- Implement duplicate stream prevention with level tracking
- Add frontend-backend communication via Tauri commands for log management
- Remove WebSocket compatibility, maintain IPC-only mode
- Fix duplicate monitoring task startup when toggling log service
- Add proper task lifecycle management with JoinHandle cleanup

* refactor: remove dead code from logs.rs to fix clippy warnings

- Remove unused `timestamp` field from LogItem struct
- Remove unused `client` field from LogsMonitor struct
- Remove unused methods: `is_fresh`, `get_current_monitoring_level`, `get_current_logs`
- Simplify LogsMonitor initialization by removing client dependency
- All clippy warnings with -D warnings now resolved

* refactor: extract duplicate fmt_bytes function to utils module

- Create new utils/format.rs module with fmt_bytes function
- Remove duplicate fmt_bytes implementations from traffic.rs and memory.rs
- Update imports to use shared utils::format::fmt_bytes
- Add comprehensive unit tests for fmt_bytes function
- Ensure DRY principle compliance and code maintainability
2025-07-30 23:11:21 +08:00
Tunglies e2a548f6a5 fix: update bzip2 and sysinfo versions, remove unused libbz2-rs-sys package 2025-07-30 23:00:46 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tunglies
2956725e66 chore(deps): update cargo dependencies (#4231)
* chore(deps): update cargo dependencies

* chore(deps): update cargo dependencies

* fix: sysinfo crate use limit features

* chore(deps): update npm dependencies (#4254)

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

* chore: update sysinfo and zip dependency versions in Cargo.toml; add ignored dependencies in renovate.json

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-07-30 21:22:33 +08:00
Dyna 5131d37d58 fix: the issue of home page port being out of sync (#4271)
* synchronize port 7897

* Update UPDATELOG.md
2025-07-30 19:59:11 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 1dfba159e0 chore(deps): update npm dependencies (#4254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-29 20:12:54 +08:00
Tunglies 631718a138 feat: enhance autobuild workflow to check for existing assets before running 2025-07-29 20:09:55 +08:00
Tunglies 3172ab2d1c chore: update Mihomo version references in prebuild script 2025-07-29 19:55:25 +08:00
Dyna 6a6bde3764 refactor: external-controller-cors to always show (#4243)
* refactor: external-controller-cors to always show

* i18n: update translation

* Update UPDATELOG.md
2025-07-28 11:24:16 +08:00
wonfen f756b37f97 i18n: add missing trans 2025-07-28 09:12:40 +08:00
Tunglies a26c28517a chore: update dependencies and versions in Cargo files 2025-07-27 22:51:51 +08:00
Tunglies 894428642b fix: add sysinfo to ignored dependencies in renovate configuration 2025-07-27 08:42:44 +08:00
Tunglies 36d58d05b3 chore: downgrade sysinfo version 2025-07-27 08:30:23 +08:00
Tunglies 80de055fc2 fix: resolve connection speed display issues after IPC migration #4208 (#4229)
🐞 Bug Fixes:
- Fix missing upload/download rate display in connections page after IPC migration
- Implement real-time connection speed calculation based on data differences
- Add connection speed data types and calculation logic

🔧 Technical Improvements:
- Add connection speed calculation function in AppDataProvider
- Use useRef to store previous connection data for speed difference calculation
- Add curUpload and curDownload fields to connection data
- Optimize connection data processing flow for accurate speed calculations

📝 Changelog:
- Update relevant issue descriptions in UPDATELOG.md
2025-07-27 03:00:08 +08:00
Tungliesandrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4905b44c8a fix: resolve speed test functionality issue after IPC migration #4221, #4218 (#4228)
* chore(deps): update cargo dependencies

* fix: sysinfo crate use limit features

* fix: update headers-core dependency and kode-bridge version; enhance system monitor status validation

* fix: extend overall_status type in ISystemMonitorOverview to include 'healthy'

* refactor: update URL encoding strategy in IpcManager and cmdGetProxyDelay function

* fix: resolve speed test functionality issue after IPC migration

* fix: resolve speed test functionality issue after IPC migration #4221, #4218

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-27 02:07:00 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 02e19bb132 chore(deps): update dependency cross-env to v10 (#4214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-26 22:55:16 +08:00
Dyna 84989e0ea3 add: home card drag (#4215)
* add home page control buttons

* Update UPDATELOG.md

* update zh en

* Revert "add home page control buttons"

This reverts commit e184843855.

* Update UPDATELOG.md

* Revert "update zh en"

This reverts commit cbaddf59fb.

* remove unnecessary code

* fix: home.tsx

* add  react-beautiful-dnd script

* add home page drag

* fix: react-beautiful-dndA setup problem was encountered

* Revert "fix: react-beautiful-dndA setup problem was encountered"

This reverts commit 81c34dd472.

* fix: react-beautiful-dndA setup problem was encountered

* Update types.d.ts

* Revert "Update types.d.ts"

This reverts commit 854046cf2f.

* update @types/react-beautiful-dnd

* Update home.tsx

* Update UPDATELOG.md

* remove unnecessary components

* Revert "add  react-beautiful-dnd script"

This reverts commit e84d569225.

* Reapply "add  react-beautiful-dnd script"

This reverts commit 2379fd27c4.

* fix: home page error
2025-07-26 22:13:44 +08:00
Dyna 9661c5fd82 fix: Windows installer parameter problem (#4213)
* repair and delete the startup item

* fix: parameter usage issues

* Update UPDATELOG.md

* Update UPDATELOG.md

* fix: install
2025-07-26 07:52:21 +08:00
Tunglies c8dfdb7a5a feat: add shared cache key for autobuild workflows 2025-07-25 18:36:27 +08:00
Tunglies df5897c908 Squashed commit of the following:
commit 8928e6438277995f7167e400d4d77657a0ab0113
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Fri Jul 25 18:25:13 2025 +0800

    feat: add release step to development workflow for versioning

commit 14085c4f7c8943669fdacae3bd2b6a07c0c0389a
Author: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date:   Fri Jul 25 18:19:36 2025 +0800

    feat: add release commands for autobuild and deploytest to package.json and update version script
2025-07-25 18:25:40 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8d0af75145 chore(deps): update npm dependencies (#4098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 17:58:20 +08:00
Mimi 36c6f0ca8d feat: unify switch component styles and BaseDialog behavior in the UI (#4183)
* feat: unify switch component styles in the UI

* fix: prettier

* feat: unify Item styles
2025-07-25 17:57:33 +08:00
Tunglies 3048a2ae08 feat: Implement configuration caching mechanism and force-refresh feature**
✨ **New Features**:

* Added API and frontend support for forcibly refreshing Clash configuration cache
* Implemented a configuration cache TTL mechanism (60 seconds) to reduce redundant requests
* Introduced `ProxyRequestCache` system to manage backend data caching
* Automatically refresh frontend state after core operations to enhance user experience

🚀 **Performance Optimizations**:

* Increased Clash configuration refresh interval from 5 seconds to 60 seconds
* Force refresh cache after configuration updates to resolve data inconsistency
* Automatically trigger state refresh after core switch, start, stop, and restart actions

🔧 **Technical Improvements**:

* Removed unused dependencies: `ab_glyph`, `owned_ttf_parser`, `ttf-parser`
* Simplified WebSocket dependency management, unified `tungstenite` version
* Refactored configuration save validation process, improved merge file handling
* Improved error handling and overall user experience
2025-07-24 01:59:25 +08:00
Tunglies 27535c7bb7 refactor: remove unused dependencies from Cargo.toml and Cargo.lock (#4179) 2025-07-24 00:51:36 +08:00
Tunglies 15a1770ee9 feat: migrate mihomo to use kode-bridge IPC on Windows and Unix (#4051)
* Refactor Mihomo API integration and remove crate_mihomo_api

- Removed the `mihomo_api` crate and its dependencies from the project.
- Introduced `IpcManager` for handling IPC communication with Mihomo.
- Implemented IPC methods for managing proxies, connections, and configurations.
- Updated `MihomoManager` to utilize `IpcManager` instead of the removed crate.
- Added platform-specific IPC socket path handling for macOS, Linux, and Windows.
- Cleaned up related tests and configuration files.

* fix: remove duplicate permission entry in desktop capabilities

* refactor: replace MihomoManager with IpcManager and remove Mihomo module

* fix: restore tempfile dependency in dev-dependencies

* fix: update kode-bridge dependency to use git source from the dev branch

* feat: migrate mihomo to use kode-bridge IPC on Windows

This commit implements a comprehensive migration from legacy service IPC to the kode-bridge library for Windows IPC communication. Key changes include:

Replace service_ipc with kode-bridge IpcManager for all mihomo communications
Simplify proxy commands using new caching mechanism with ProxyRequestCache
Add Windows named pipe (\.\pipe\mihomo) and Unix socket IPC endpoint configuration
Update Tauri permissions and dependencies (dashmap, tauri-plugin-notification)
Add IPC logging support and improve error handling
Fix Windows IPC path handling in directory utilities
This migration enables better cross-platform IPC support and improved performance for mihomo proxy core communication.

* doc: add IPC communication with Mihomo kernel, removing Restful API dependency

* fix: standardize logging type naming from IPC to Ipc for consistency

* refactor: clean up and optimize code structure across multiple components and services

- Removed unnecessary comments and whitespace in various files.
- Improved code readability and maintainability by restructuring functions and components.
- Updated localization files for consistency and accuracy.
- Enhanced performance by optimizing hooks and utility functions.
- General code cleanup in settings, pages, and services to adhere to best practices.

* fix: simplify URL formatting in test_proxy_delay method

* fix: update kode-bridge dependency to version 0.1.3 and change source to crates.io

* fix: update macOS target versions in development workflow

* Revert "fix: update macOS target versions in development workflow"

This reverts commit b9831357e4.

* feat: enhance IPC path handling for Unix systems and improve directory safety checks

* feat: add conditional compilation for Unix-specific IPC path handling

* chore: update cagro.lock

* feat: add external controller configuration and UI support

* Refactor proxy and connection management to use IPC-based commands

- Updated `get_proxies` function in `proxy.rs` to call the new IPC command.
- Renamed `get_refresh_proxies` to `get_proxies` in `ipc/general.rs` for consistency.
- Added new IPC commands for managing proxies, connections, and configurations in `cmds.ts`.
- Refactored API calls in various components to use the new IPC commands instead of HTTP requests.
- Improved error handling and response management in the new IPC functions.
- Cleaned up unused API functions in `api.ts` and redirected relevant calls to `cmds.ts`.
- Enhanced connection management features including health checks and updates for proxy providers.

* chore: update dependencies and improve error handling in IPC manager

* fix: downgrade zip dependency from 4.3.0 to 4.2.0

* feat: Implement traffic and memory data monitoring service

- Added `TrafficService` and `TrafficManager` to manage traffic and memory data collection.
- Introduced commands to get traffic and memory data, start and stop the traffic service.
- Integrated IPC calls for traffic and memory data retrieval in the frontend.
- Updated `AppDataProvider` and `EnhancedTrafficStats` components to utilize new data fetching methods.
- Removed WebSocket connections for traffic and memory data, replaced with IPC polling.
- Added logging for better traceability of data fetching and service status.

* refactor: unify external controller handling and improve IPC path resolution

* fix: replace direct IPC path retrieval with guard function for external controller

* fix: convert external controller IPC path to string for proper insertion in config map

* fix: update dependencies and improve IPC response handling

* fix: remove unnecessary unix conditional for ipc path import

* Refactor traffic and memory monitoring to use IPC stream; remove TrafficService and TrafficManager. Introduce new IPC-based data retrieval methods for traffic and memory, including formatted data and system overview. Update frontend components to utilize new APIs for enhanced data display and management.

* chore: bump crate rand version to 0.9.2

* feat: Implement enhanced traffic monitoring system with data compression and sampling

- Introduced `useTrafficMonitorEnhanced` hook for advanced traffic data management.
- Added `TrafficDataSampler` class for handling raw and compressed traffic data.
- Implemented reference counting to manage data collection based on component usage.
- Enhanced data validation with `SystemMonitorValidator` for API responses.
- Created diagnostic tools for monitoring performance and error tracking.
- Updated existing hooks to utilize the new enhanced monitoring features.
- Added utility functions for generating and formatting diagnostic reports.

* feat(ipc): improve URL encoding and error handling for IPC requests

- Add percent-encoding for URL paths to handle special characters properly
- Enhance error handling in update_proxy with proper logging
- Remove excessive debug logging to reduce noise
- Update kode-bridge dependency to v0.1.5
- Fix JSON parsing error handling in PUT requests

Changes include:
- Proper URL encoding for connection IDs, proxy names, and test URLs
- Enhanced error handling with fallback responses in updateProxy
- Comment out verbose debug logs in traffic monitoring and data validation
- Update dependency version for improved IPC functionality

* feat: major improvements in architecture, traffic monitoring, and data validation

* Refactor traffic graph components: Replace EnhancedTrafficGraph with EnhancedCanvasTrafficGraph, improve rendering performance, and enhance visual elements. Remove deprecated code and ensure compatibility with global data management.

* chore: update UPDATELOG.md for v2.4.0 release, refine traffic monitoring system details, and enhance IPC functionality

* chore: update UPDATELOG.md to reflect removal of deprecated MihomoManager and unify IPC control

* refactor: remove global traffic service testing method from cmds.ts

* Update src/components/home/enhanced-canvas-traffic-graph.tsx

* Update src/hooks/use-traffic-monitor-enhanced.ts

* Update src/components/layout/layout-traffic.tsx

* refactor: remove debug state management from LayoutTraffic component

---------
2025-07-24 00:49:55 +08:00
Dyna f580409ade add: Verge Version copy button (#4164) 2025-07-23 11:11:34 +08:00
Tunglies f209d17e3c chore: bump clash-verge-rev version to 2.4.0 2025-07-22 18:42:25 +08:00
Dyna 5047b0f614 fix: the problem of inconsistent color of system theme window (#4133)
* fix-title

* Update UPDATELOG.md
2025-07-22 11:01:23 +08:00
wonfen ad623da86d chore: update i18n (#4128) 2025-07-22 08:41:02 +08:00
Tunglies 3ec7772e5d fix: update sysinfo and zip dependencies to specific versions in Cargo.toml 2025-07-22 08:05:19 +08:00
Tunglies e6b57561e3 chore: update cargo.lock 2025-07-22 08:02:07 +08:00
Tunglies 3b1c2c95ec Revert "chore(deps): update rust crate zip to 4.3.0 (#4073)"
This reverts commit fed64bcd08.
2025-07-22 07:59:26 +08:00
Tunglies 364abafffe fix: update prerequisites link in CONTRIBUTING.md and update dependencies in Cargo.lock
- Updated the link for installing Rust and Node.js in CONTRIBUTING.md.
- Added new dependencies: wayland-backend, wayland-client, wayland-protocols.
- Updated versions for several dependencies including async-channel, async-io, async-process, and others.
- Removed deprecated dispatch2 version and added new dlib dependency.
- Updated various dependencies to their latest versions for improved stability and features.
2025-07-20 04:19:25 +08:00
Copilot 389281b96c Revert sysinfo dependency from 0.36.0 to 0.35.2 (#4086) 2025-07-16 09:34:03 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> fed64bcd08 chore(deps): update rust crate zip to 4.3.0 (#4073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 22:25:25 +08:00
Tunglies 17a5c4d094 chore: update sysinfo dependency to version 0.36.0 (#4082)
* chore: update sysinfo dependency to version 0.36.0

* fix: update sysinfo dependency to enable specific features
2025-07-15 22:23:24 +08:00
Dyna f2317c7816 fix: log levels will not display their respective logs correctly (#4074)
* fix-log levels will not display their respective logs correctly

* fix-logs

* Update UPDATELOG.md

* Update UPDATELOG.md
2025-07-15 18:55:53 +08:00
Tunglies 7fce7ca62f Revert "chore(deps): update cargo dependencies (#4023)" (#4072)
This reverts commit 2c9aa4bca7.
2025-07-14 23:55:39 +08:00
Tunglies 167bcb222d fix: add platform-specific input options for workflow dispatch in dev.yml 2025-07-14 20:14:35 +08:00
Tunglies 108a599666 fix: add platform input options for workflow dispatch in dev.yml 2025-07-14 20:09:46 +08:00
Tunglies 57476741cd fix: improve logging format for old shortcut removal failure 2025-07-14 19:31:19 +08:00
Tunglies 077f3e79f8 fix: unify startup shortcut name to "Clash Verge" on Windows 2025-07-14 04:22:06 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2c9aa4bca7 chore(deps): update cargo dependencies (#4023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 23:34:47 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 9cf0f1e0a7 chore(deps): update npm dependencies (#4014)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 23:34:38 +08:00
Dyna 660c03a564 optimization: (fix: the crash when inputting special characters in the search box (#4026)) (#4029)
* add a test rule

* add translation support
2025-07-10 21:09:02 +08:00
Dyna 1d49d79af2 fix: the crash when inputting special characters in the search box (#4026)
* add special character escapes

* update logs
2025-07-10 13:34:37 +08:00
Dyna 4ccb17dde6 fix: the problem that the DNS override nameserver-policy field cannot correctly recognize multiple writing methods (#4011)
* fix: the problem that the DNS override nameserver-policy field cannot correctly recognize multiple writing methods

* update logs

* fix-dns-viewer.tsx
2025-07-08 17:05:17 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8bc433711d chore(deps): update rust crate tokio to 1.46.1 (#3989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 18:04:40 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2e82bd8624 chore(deps): update npm dependencies (#3986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 18:04:32 +08:00
❤是纱雾酱哟~ 4b57d513a2 Chore (Renovate): Disable Dependency Dashboard (#3999)
* Chore (Renovate): Disable Dependency Dashboard

- Close #3658

* Fix (Renovate): Prettify JSON format according to "prettier"
2025-07-07 18:02:29 +08:00
Dyna d36c3a83a9 fix:DNS override server support is left blank by default (#3997)
* fix:DNS server support being left blank by default

* update logs
2025-07-06 11:01:05 +08:00
Tunglies ac3afe4dee fix: manage setup Mutex crash (#3995)
* Revert "Revert "refactor: Replace std::sync::Mutex with parking_lot::Mutex for improved performance and consistency in multiple files" (#3990)"

This reverts commit 667844aa12.

* refactor: Manage lightweight state in the app setup and clean up unused proxy client code

* refactor: Move macOS-specific Manager import under conditional compilation
2025-07-06 02:14:48 +08:00
Dyna 667844aa12 Revert "refactor: Replace std::sync::Mutex with parking_lot::Mutex for improved performance and consistency in multiple files" (#3990) 2025-07-05 12:17:02 +08:00
Tunglies 6d192233d1 refactor: Replace std::sync::Mutex with parking_lot::Mutex for improved performance and consistency in multiple files 2025-07-05 00:33:53 +08:00
Tunglies 764ef48fd1 refactor(Draft): Replace latest() with latest_ref() and data() with data_mut() in multiple files for improved mutability handling and consistency across the codebase (#3987)
* feat: add benchmarking for draft operations and new draft management structure

* Refactor Config Access: Replace `latest()` with `latest_ref()` and `data()` with `data_mut()` in multiple files for improved mutability handling and consistency across the codebase.

* refactor: remove DraftNew implementation and related benchmarks for cleaner codebase
2025-07-04 22:43:23 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 3f95c81243 chore(deps): update rust crate tokio to 1.46.0 (#3943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 18:51:25 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7c385d329d chore(deps): update npm dependencies (#3944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 18:51:18 +08:00
Dyna 553438d219 fix windows installer (#3979)
* add loop detection Clash Verge Lnk

* add delete the window-state.json file .window-state.json file

* add cycle through the registry and delete useless registry entries

* add delete the specified registry path

* update logs
2025-07-04 12:28:15 +08:00
rebecca554owen 941921904c fix: remove unused redir-port and tproxy-port on Windows (#3969)
Directly remove redir-port and tproxy-port configurations on Windows platform
2025-07-04 12:27:59 +08:00
wonfen 7d808ed798 fix: prevent freeze caused by enabling Proxy Guard 2025-07-04 12:22:22 +08:00
❤是纱雾酱哟~ 7704072a65 Chore (ISSUE_TEMPLATE): 进一步优化问题反馈模板 (#3975)
- 当前模板强依赖于用户把日志贴到正确的位置上
- 参考 WinGet 的问题模板,放弃日志折叠功能,强制用户以代码块形式粘贴日志
2025-07-03 18:41:46 +08:00
❤是纱雾酱哟~ 9076baf1c8 Chore (ISSUE_TEMPLATE): 修复错误的 YAML 语法 (#3951)
- 修改「OS」分类的类型:"dropdown" -> "checkbox"
  - 一个问题可能不只影响一个特定的操作系统
- 修改「软件版本」分类,增加 "render" 属性
  - 这能让 GFM 直接把这部分内容渲染为 render 指定语法的代码片段
2025-07-03 14:30:46 +08:00
954ff53d9b refactor: use React in its intended way (#3963)
* refactor: replace `useEffect` w/ `useLocalStorage`

* refactor: replace `useEffect` w/ `useSWR`

* refactor: replace `useEffect` and `useSWR`. clean up `useRef`

* refactor: use `requestIdleCallback`

* refactor: replace `useEffect` w/ `useMemo`

* fix: clean up `useEffect`

* refactor: replace `useEffect` w/ `useSWR`

* refactor: remove unused `useCallback`

* refactor: enhance performance and memory management in frontend processes

* refactor: improve pre-push script structure and readability

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2025-07-02 23:34:13 +08:00
Tunglies 37d268bb16 chore(deps): remove deprecated dependencies (#3960)
* chore(deps):  remove deprecated dependencies

* refactor: simplify string formatting in make_key function
2025-07-02 03:07:50 +08:00
Tunglies 5befa90f81 chore(deps): update dashmap to version 7.0.0-rc2 2025-07-01 02:02:14 +08:00
Tunglies 6e3313f00d chore: remove redundant bug fix section from UPDATELOG.md 2025-06-30 21:11:01 +08:00
7b5afb7afe Add cors (#3909)
* add external `cors` control panel

* optimize format

* fix-script.rs

* fix-service.rs

* fix-rs

async_proxy_query.rs

event_driven_proxy.rs

service_ipc.rs

service.rs

sysopt.rs

* lower the prettier version number to 3.5.3

* Revert "lower the prettier version number to 3.5.3"

This reverts commit 0f1c3dfa8a.

* fix: prttier erros

* add developer environment detection and controlled the display of development environment URL

* submit required

* fix-external-controller-cors

* use the custom component ToggleButton to ensure a uniform button style

* fix-tsx

hotkey-viewer.tsx

external-controller-cors.tsx

* fix-bug_report.yml

* remove the annoying title

* fix-write overload problem

* Individual button settings

* fix-setting-clash.tsx

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2025-06-30 20:48:20 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> fe13dad06f chore(deps): update rust crate ab_glyph to 0.2.30 (#3932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 20:22:43 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> b41bad0ae2 chore(deps): update npm dependencies (#3918)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 20:22:37 +08:00
Tunglies 18ef7f0272 feat: enhance proxy management with caching and refresh logic 2025-06-30 20:14:04 +08:00
❤是纱雾酱哟~ 4435a5aee4 Chore (ISSUE_TEMPLATE): 修复错误的 YAML 缩进 (bug_report.yml) (#3933) 2025-06-29 19:27:13 +08:00
Tunglies d9e3a47894 fix: adjust formatting of log content section in bug report template 2025-06-29 10:41:18 +08:00
❤是纱雾酱哟~andTunglies c96be18187 chore (ISSUE_TEMPLATE): 修改问题反馈模板 (#3927)
* chore (ISSUE_TEMPLATE): 修改问题反馈模板

- 修改 bug_report.yml
- 增加一段预置的格式模板,指引用户将日志粘贴在规范的 Code Block 中以保持日志原有的格式排版
- 使用 <details /> 块以压缩日志片段,对于非 Contributors / Collaborators 用户友好,规避日志霸屏效果,简化爬楼。

* 接受来自审查的建议

- 增加英语支持,以方便非中文母语用户

Co-authored-by: Tunglies <tunglies.dev@outlook.com>

---------

Co-authored-by: Tunglies <tunglies.dev@outlook.com>
2025-06-28 21:23:46 +08:00
Tunglies 47416dd3f8 chore: update UPDATELOG 2025-06-28 20:03:30 +08:00
Tunglies 4486f734bb fix: correct traffic percentage calculation to handle zero total gracefully #3920 2025-06-28 20:00:49 +08:00
Tunglies bea6a2c8f7 chore: update Prettier configuration and dependencies; refactor code formatting for consistency (#3926) 2025-06-28 18:37:53 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 82af9ed78e chore(deps): update cargo dependencies (#3908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 23:48:42 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 2b9e38d259 chore(deps): update npm dependencies (#3910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 23:48:18 +08:00
Tunglies 7c4222aed2 chore: update Node.js setup steps in formatting workflow to use corepack for package management (#3914) 2025-06-27 23:46:51 +08:00
Tunglies a574ced428 Refactor logging statements to use the new formatting syntax for improved readability and consistency across the codebase. This includes updating error, warning, and info logs in various modules such as system commands, configuration, core functionalities, and utilities. Additionally, minor adjustments were made to string formatting in backup and proxy features to enhance clarity. 2025-06-27 23:30:59 +08:00
wonfen cf437e6d94 chore: reduce default auto clean log to 7 days 2025-06-27 09:29:55 +08:00
Tunglies e1bb8aa125 chore(pre-push): update comments for clarity and refine format check condition 2025-06-27 00:36:00 +08:00
Tunglies c11bdd81e9 chore(i18n): add "Saved Successfully" message to English and Chinese locales 2025-06-27 00:32:37 +08:00
Tunglies f1192c95a8 feat: add notification system with hotkey events and permission handling (#3867)
* feat: add notification system with hotkey events and permission handling

* Add macOS-specific handling for AppHidden notification

Introduces conditional support for the AppHidden notification event,
enabling macOS-specific behavior. Updates the enum and notification
logic to include this platform-specific feature.

Improves macOS user experience by accommodating system-level
application hiding events.

* Implement feature X to enhance user experience and fix bug Y in module Z

* refactor(notification): update notification keys for consistency and clarity

* chore(deps): update dependencies to latest versions
2025-06-26 23:09:07 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tunglies
ae187cc21a chore(deps): update npm dependencies (#3871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-06-26 22:33:58 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a7875718f7 chore(deps): update rust crate tauri to 2.6.1 (#3906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 22:31:28 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 5db1f7cda7 chore(deps): update cargo dependencies (#3896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 22:29:13 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> cb98b17052 chore(deps): update npm dependencies to v7 (#3886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 01:09:50 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7aee1c6d6e chore(deps): update cargo dependencies (#3872)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 01:09:36 +08:00
TRH f22199b7d9 chore(logging): 去除无用日志 (#3882)
* chore(logging): 去除无用日志

* chore: 删除相关计时代码
2025-06-24 16:49:06 +08:00
Tunglies 0a8d6e5147 fix: add validation for profile URL format during import 2025-06-23 23:30:33 +08:00
希亚的西红柿andTunglies 6d519dac1e fix: auto light-weight mode doesn't take effect in silent-start mode (#3875)
* fix: auto light-weight mode does not take effect when silent-start mode is enabled

* refactor: streamline window state retrieval and hiding logic

* fix: add checks for remote name and existence before format check in pre-push hook

* fix: simplify remote checks in pre-push hook to enhance clarity and maintainability

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2025-06-23 23:13:31 +08:00
wonfen d5a174c71b perf: improve proxy status indicator and toggle responsiveness 2025-06-23 12:59:24 +08:00
wonfen 628de70e89 chore: remove unused imports 2025-06-23 00:09:17 +08:00
wonfen fee08f3826 fix: correct address display error caused by async system proxy retrieval 2025-06-22 23:19:11 +08:00
wonfen bdfc383a18 refactor: enhance Windows proxy retrieval by using WinAPI for registry access 2025-06-22 21:05:50 +08:00
wonfen f6b5524e0e refactor: replace shell command check with WinAPI call 2025-06-22 18:45:38 +08:00
Tunglies e7461fccab refactor: remove unused macOS tray speed display and improve tray icon handling (#3862)
* refactor: remove unused macOS tray speed display and improve tray icon handling

* refactor: disable macOS specific logging during core initialization

* feat: add linux elevator function to determine privilege escalation command
2025-06-22 16:28:06 +08:00
wonfen a92872c831 fix: resolve race condition freeze on rapid tray icon clicks in lightweight mode 2025-06-22 15:44:09 +08:00
Tunglies 094feb74ec feat: add new monochrome tray icons for system and tunnel 2025-06-22 15:32:33 +08:00
Tunglies 9b1c660306 feat: add new monochrome tray icons for system and tunnel 2025-06-22 15:31:59 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 4b860ba897 chore(deps): update npm dependencies (#3831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-22 13:24:25 +08:00
Tunglies 3d8b2cf35f fix: improve error handling for application handle retrieval (#3860)
* fix: improve error handling for application handle retrieval

* fix: correct argument passing for command execution and improve URL stripping logic
2025-06-22 01:16:57 +08:00
wonfen 41fc13cfe2 fix: format & update 2025-06-21 22:39:12 +08:00
wonfen 5fde5dcc7c feat: implement async proxy lookup and optimize system/auto proxy retrieval logic 2025-06-21 21:56:15 +08:00
wonfen 034885d810 feat: introduce event-driven proxy manager and optimize proxy config updates 2025-06-21 21:56:15 +08:00
Tunglies 3f7a7b8cd2 chore: add cargo deny config 2025-06-21 21:22:20 +08:00
Tunglies 98dc50a9ed refactor: remove redundant steps for cleaning old release assets 2025-06-21 20:42:27 +08:00
Tunglies 5dd820d12e feat: add job to clean old release assets before uploading new ones 2025-06-21 20:36:09 +08:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> bc30db2875 chore(deps): update rust crate libc to 0.2.174 (#3794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-21 18:41:06 +08:00
Dyna abe914d446 fix the issue that CSS cannot customize background (#3854)
* fixed the issue that css background cannot be used

* update logs

* fix logs
2025-06-21 16:53:10 +08:00
wonfen cc65ce6812 chore: update to 2.3.2 2025-06-21 10:09:22 +08:00
wonfen 1a6454ee79 perf: optimize profile switching logic with interrupt support to prevent freeze 2025-06-21 10:04:12 +08:00
Just want to protect you b72f397369 fix the issue of system proxy port being out of sync (#3841)
* repair system proxy port real-time response

* update logs

* fix-logs
2025-06-21 10:03:06 +08:00
wonfen e698fe8d18 feat: add cleanup for redundant mihomo processes 2025-06-21 08:13:12 +08:00
wonfen c8cad1c295 chore: add funding button 2025-06-20 20:52:40 +08:00
213 changed files with 21978 additions and 11396 deletions
+101
View File
@@ -0,0 +1,101 @@
{
"name": "Clash Verge Rev Development Environment",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "latest",
"profile": "default"
},
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"customizations": {
"vscode": {
"extensions": [
"rust-lang.rust-analyzer",
"tauri-apps.tauri-vscode",
"ms-vscode.vscode-typescript-next",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-json",
"redhat.vscode-yaml",
"formulahendry.auto-rename-tag",
"ms-vscode.hexeditor",
"christian-kohler.path-intellisense",
"yzhang.markdown-all-in-one",
"streetsidesoftware.code-spell-checker",
"ms-vscode.vscode-eslint"
],
"settings": {
"rust-analyzer.cargo.features": ["verge-dev"],
"rust-analyzer.check.command": "clippy",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "redhat.vscode-yaml"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}
},
"forwardPorts": [1420, 3000, 8080, 9090, 7890, 7891],
"portsAttributes": {
"1420": {
"label": "Tauri Dev Server",
"onAutoForward": "notify"
},
"3000": {
"label": "Vite Dev Server",
"onAutoForward": "notify"
},
"7890": {
"label": "Clash HTTP Proxy",
"onAutoForward": "silent"
},
"7891": {
"label": "Clash SOCKS Proxy",
"onAutoForward": "silent"
},
"9090": {
"label": "Clash API",
"onAutoForward": "silent"
}
},
"postCreateCommand": "bash .devcontainer/post-create.sh",
"mounts": [
"source=clash-verge-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
"source=clash-verge-cargo-registry,target=/usr/local/cargo/registry,type=volume",
"source=clash-verge-cargo-git,target=/usr/local/cargo/git,type=volume"
],
"containerEnv": {
"RUST_BACKTRACE": "1",
"NODE_OPTIONS": "--max-old-space-size=4096",
"TAURI_DEV_WATCHER_IGNORE_FILE": ".taurignore"
},
"remoteUser": "vscode",
"workspaceFolder": "/workspaces/clash-verge-rev",
"shutdownAction": "stopContainer"
}
+1
View File
@@ -0,0 +1 @@
github: clash-verge-rev
+12 -4
View File
@@ -9,13 +9,16 @@ body:
attributes:
value: |
## 在提交问题之前,请确认以下事项:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
5. 请 **务必** 尝试 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本,确定问题是否仍然存在
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
@@ -34,6 +37,7 @@ body:
attributes:
label: 软件版本 / Verge Version
description: 请提供Verge的具体版本,如果是alpha版本,请注明下载时间(精确到小时分钟) / Please provide the specific version of Verge. If it is an alpha version, please indicate the download time (accurate to hours and minutes)
render: text
validations:
required: true
- type: textarea
@@ -42,13 +46,13 @@ body:
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
validations:
required: true
- type: dropdown
- type: checkboxes
attributes:
label: 操作系统 / OS
options:
- Windows
- Linux
- MacOS
- label: Windows
- label: Linux
- label: MacOS
validations:
required: true
- type: input
@@ -61,5 +65,9 @@ body:
attributes:
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to debug, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
placeholder: |
日志目录一般位于 Clash Verge Rev 安装目录的 "logs/" 子目录中,请将日志内容粘贴到此处。
Log directory is usually located in the "logs/" subdirectory of the Clash Verge Rev installation directory, please paste the log content here.
render: log
validations:
required: true
+9
View File
@@ -294,6 +294,15 @@ jobs:
sudo apt-get update
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
uses: actions/setup-node@v4
with:
+133
View File
@@ -0,0 +1,133 @@
name: Autobuild Check Logic Test
on:
workflow_dispatch:
jobs:
check_autobuild_logic:
name: Check Autobuild Should Run Logic
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check if version or source changed, or assets already exist
id: check
run: |
# # 仅用于测试逻辑,手动触发自动跳过
# if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# echo "should_run=skip" >> $GITHUB_OUTPUT
# echo "🟡 手动触发,跳过 should_run 检查"
# exit 0
# fi
# 确保有 HEAD~1
if ! git rev-parse HEAD~1 > /dev/null 2>&1; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 没有前一个提交,默认需要构建"
exit 0
fi
# 版本号变更判断
CURRENT_VERSION=$(jq -r '.version' package.json)
PREVIOUS_VERSION=$(git show HEAD~1:package.json | jq -r '.version' 2>/dev/null || echo "")
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 版本号变更: $PREVIOUS_VERSION → $CURRENT_VERSION"
exit 0
fi
# 检查 src 变更(排除常见产物与缓存)
SRC_DIFF=$(git diff --name-only HEAD~1 HEAD -- src/ | grep -Ev '^src/(dist|build|node_modules|\.next|\.cache)' || true)
TAURI_DIFF=$(git diff --name-only HEAD~1 HEAD -- src-tauri/ | grep -Ev '^src-tauri/(target|node_modules|dist|\.cache)' || true)
if [ -n "$SRC_DIFF" ] || [ -n "$TAURI_DIFF" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 源码变更 detected"
exit 0
fi
# 找到最后一个修改 Tauri 相关文件的 commit
echo "🔍 查找最后一个 Tauri 相关变更的 commit..."
LAST_TAURI_COMMIT=""
for commit in $(git rev-list HEAD --max-count=50); do
# 检查此 commit 是否修改了 Tauri 相关文件
CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ')
HAS_TAURI_CHANGES=false
# 检查各个模式
if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then
HAS_TAURI_CHANGES=true
elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then
HAS_TAURI_CHANGES=true
fi
if [ "$HAS_TAURI_CHANGES" = true ]; then
LAST_TAURI_COMMIT=$(git rev-parse --short $commit)
break
fi
done
if [ -z "$LAST_TAURI_COMMIT" ]; then
echo "⚠️ 最近的 commits 中未找到 Tauri 相关变更,使用当前 commit"
LAST_TAURI_COMMIT=$(git rev-parse --short HEAD)
fi
CURRENT_COMMIT=$(git rev-parse --short HEAD)
echo "📝 最后 Tauri 相关 commit: $LAST_TAURI_COMMIT"
echo "📝 当前 commit: $CURRENT_COMMIT"
# 检查 autobuild release 是否存在
AUTOBUILD_RELEASE_EXISTS=$(gh release view "autobuild" --json id -q '.id' 2>/dev/null || echo "")
if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 没有 autobuild release,需构建"
else
# 检查 latest.json 是否存在
LATEST_JSON_EXISTS=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "")
if [ -z "$LATEST_JSON_EXISTS" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 没有 latest.json,需构建"
else
# 下载并解析 latest.json 检查版本和 commit hash
echo "📥 下载 latest.json 检查版本..."
LATEST_JSON_URL=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .browser_download_url' 2>/dev/null)
if [ -n "$LATEST_JSON_URL" ]; then
LATEST_JSON_CONTENT=$(curl -s "$LATEST_JSON_URL" 2>/dev/null || echo "")
if [ -n "$LATEST_JSON_CONTENT" ]; then
LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "")
echo "📦 最新 autobuild 版本: $LATEST_VERSION"
# 从版本字符串中提取 commit hash (格式: X.Y.Z+autobuild.MMDD.commit)
LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "")
echo "📝 最新 autobuild commit: $LATEST_COMMIT"
if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "🟢 Tauri commit hash 不匹配 ($LAST_TAURI_COMMIT != $LATEST_COMMIT),需构建"
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "🔴 相同 Tauri commit hash ($LAST_TAURI_COMMIT),不需构建"
fi
else
echo "should_run=true" >> $GITHUB_OUTPUT
echo "⚠️ 无法下载或解析 latest.json,需构建"
fi
else
echo "should_run=true" >> $GITHUB_OUTPUT
echo "⚠️ 无法获取 latest.json 下载 URL,需构建"
fi
fi
fi
- name: Output should_run result
run: |
echo "Result: ${{ steps.check.outputs.should_run }}"
+177 -73
View File
@@ -18,55 +18,10 @@ concurrency:
jobs:
check_commit:
name: Check Commit Needs Build
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check if version changed or src changed
id: check
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "Current version: $CURRENT_VERSION"
git checkout HEAD~1 package.json
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
echo "Previous version: $PREVIOUS_VERSION"
git checkout HEAD package.json
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
CURRENT_SRC_HASH=$(git rev-parse HEAD:src)
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
echo "Current src hash: $CURRENT_SRC_HASH"
echo "Previous src hash: $PREVIOUS_SRC_HASH"
echo "Current tauri hash: $CURRENT_TAURI_HASH"
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
echo "Source directories changed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "Version and source directories unchanged"
echo "should_run=false" >> $GITHUB_OUTPUT
fi
uses: clash-verge-rev/clash-verge-rev/.github/workflows/check-commit-needs-build.yml@dev
with:
tag_name: autobuild
force_build: ${{ github.event_name == 'workflow_dispatch' }}
update_tag:
name: Update tag
@@ -95,9 +50,28 @@ jobs:
fi
shell: bash
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Release AutoBuild Version
run: pnpm release-version autobuild-latest
- name: Set Env
run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
VERSION=$(jq -r .version package.json)
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
shell: bash
- run: |
@@ -111,25 +85,24 @@ jobs:
cat > release.txt << EOF
$UPDATE_LOGS
## 我应该下载哪个版本?
### MacOS
- MacOS intel芯片: x64.dmg
- MacOS apple M芯片: aarch64.dmg
### Linux
- Linux 64位: amd64.deb/amd64.rpm
- Linux arm64 architecture: arm64.deb/aarch64.rpm
- Linux armv7架构: armhf.deb/armhfp.rpm
## 下载地址
### Windows (不再支持Win7)
#### 正常版本(推荐)
- 64位: x64-setup.exe
- arm64架构: arm64-setup.exe
#### 便携版问题很多不再提供
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
- 64位: x64_fixed_webview2-setup.exe
- arm64架构: arm64_fixed_webview2-setup.exe
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
### Linux
#### 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)
#### 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)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
@@ -150,9 +123,20 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
clean_old_assets:
name: Clean Old Release Assets
needs: [check_commit, update_tag]
if: ${{ needs.check_commit.outputs.should_run == 'true' && needs.update_tag.result == 'success' }}
uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev
with:
tag_name: autobuild
dry_run: false
autobuild-x86-windows-macos-linux:
name: Autobuild x86 Windows, MacOS and Linux
needs: update_tag
needs: [check_commit, update_tag]
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
strategy:
fail-fast: false
matrix:
@@ -184,6 +168,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -191,6 +176,15 @@ jobs:
sudo apt-get update
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
name: Install pnpm
with:
@@ -208,9 +202,9 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
run: pnpm release-version autobuild-latest
- name: Tauri build
- name: Tauri build for Windows-macOS-Linux
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
@@ -231,10 +225,12 @@ jobs:
prerelease: true
tauriScript: pnpm
args: --target ${{ matrix.target }}
# includeUpdaterJson: true
autobuild-arm-linux:
name: Autobuild ARM Linux
needs: update_tag
needs: [check_commit, update_tag]
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
strategy:
fail-fast: false
matrix:
@@ -262,6 +258,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -280,7 +277,7 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
run: pnpm release-version autobuild-latest
- name: Setup for linux
run: |
@@ -331,7 +328,7 @@ jobs:
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
- name: Tauri Build for Linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
@@ -367,7 +364,8 @@ jobs:
autobuild-x86-arm-windows_webview2:
name: Autobuild x86 and ARM Windows with WebView2
needs: update_tag
needs: [check_commit, update_tag]
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
strategy:
fail-fast: false
matrix:
@@ -392,6 +390,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
save-if: ${{ github.ref == 'refs/heads/dev' }}
shared-key: autobuild-shared
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -410,7 +409,7 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version ${{ env.TAG_NAME }}
run: pnpm release-version autobuild-latest
- name: Download WebView2 Runtime
run: |
@@ -419,7 +418,7 @@ jobs:
Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
- name: Tauri build
- name: Tauri build for Windows
id: build
uses: tauri-apps/tauri-action@v0
env:
@@ -430,6 +429,7 @@ jobs:
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
# includeUpdaterJson: true
- name: Rename
run: |
@@ -464,3 +464,107 @@ jobs:
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
notify-telegram:
name: Notify Telegram
runs-on: ubuntu-latest
needs:
[
update_tag,
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Release AutoBuild Version
run: pnpm release-version autobuild-latest
- name: Get Version and Release Info
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Generate release.txt
run: |
if [ -z "$UPDATE_LOGS" ]; then
echo "No update logs found, using default message"
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found update logs"
fi
cat > release.txt << EOF
$UPDATE_LOGS
## 下载地址
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
### Linux
#### 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)
#### 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)
### FAQ
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Send Telegram Notification
run: node scripts/telegram.mjs
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
BUILD_TYPE: autobuild
VERSION: ${{ env.VERSION }}
DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }}
@@ -0,0 +1,159 @@
name: Check Commit Needs Build
on:
workflow_dispatch:
inputs:
tag_name:
description: "Release tag name to check against (default: autobuild)"
required: false
default: "autobuild"
type: string
force_build:
description: "Force build regardless of checks"
required: false
default: false
type: boolean
workflow_call:
inputs:
tag_name:
description: "Release tag name to check against (default: autobuild)"
required: false
default: "autobuild"
type: string
force_build:
description: "Force build regardless of checks"
required: false
default: false
type: boolean
outputs:
should_run:
description: "Whether the build should run"
value: ${{ jobs.check_commit.outputs.should_run }}
last_tauri_commit:
description: "The last commit hash with Tauri-related changes"
value: ${{ jobs.check_commit.outputs.last_tauri_commit }}
autobuild_version:
description: "The generated autobuild version string"
value: ${{ jobs.check_commit.outputs.autobuild_version }}
permissions:
contents: read
actions: read
env:
TAG_NAME: ${{ inputs.tag_name || 'autobuild' }}
jobs:
check_commit:
name: Check Commit Needs Build
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }}
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Check if version changed or src changed
id: check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Force build if requested
if [ "${{ inputs.force_build }}" == "true" ]; then
echo "🚀 Force build requested"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "📦 Current version: $CURRENT_VERSION"
git checkout HEAD~1 package.json
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
echo "📦 Previous version: $PREVIOUS_VERSION"
git checkout HEAD package.json
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
echo "✅ Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
echo "should_run=true" >> $GITHUB_OUTPUT
exit 0
fi
# Use get_latest_tauri_commit.bash to find the latest Tauri-related commit
echo "🔍 Finding last commit with Tauri-related changes using script..."
# Make script executable
chmod +x scripts-workflow/get_latest_tauri_commit.bash
# Get the latest Tauri-related commit hash (full hash)
LAST_TAURI_COMMIT_FULL=$(./scripts-workflow/get_latest_tauri_commit.bash)
if [[ $? -ne 0 ]] || [[ -z "$LAST_TAURI_COMMIT_FULL" ]]; then
echo "❌ Failed to get Tauri-related commit, using current commit"
LAST_TAURI_COMMIT_FULL=$(git rev-parse HEAD)
fi
# Get short hash for display and version tagging
LAST_TAURI_COMMIT=$(git rev-parse --short "$LAST_TAURI_COMMIT_FULL")
echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT"
# 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')
MONTH=$(TZ=Asia/Shanghai date +%m)
DAY=$(TZ=Asia/Shanghai date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Autobuild version: $AUTOBUILD_VERSION"
echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT"
# Set outputs for other jobs to use
echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT
echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT
# Check if autobuild release exists
echo "🔍 Checking autobuild release and latest.json..."
AUTOBUILD_RELEASE_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json id -q '.id' 2>/dev/null || echo "")
if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then
echo "✅ No autobuild release exists, build needed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
# Check if latest.json exists in the release
LATEST_JSON_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "")
if [ -z "$LATEST_JSON_EXISTS" ]; then
echo "✅ No latest.json found in autobuild release, build needed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
# Download and parse latest.json to check version and commit hash
echo "📥 Downloading latest.json to check version..."
LATEST_JSON_URL="https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild/latest.json"
LATEST_JSON_CONTENT=$(curl -sL "$LATEST_JSON_URL" 2>/dev/null || echo "")
if [ -n "$LATEST_JSON_CONTENT" ]; then
LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "")
echo "📦 Latest autobuild version: $LATEST_VERSION"
# Extract commit hash from version string (format: X.Y.Z+autobuild.MMDD.commit)
LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "")
echo "📝 Latest autobuild commit: $LATEST_COMMIT"
if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then
echo "✅ Tauri commit hash mismatch ($LAST_TAURI_COMMIT != $LATEST_COMMIT), build needed"
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "❌ Same Tauri commit hash ($LAST_TAURI_COMMIT), no build needed"
echo "should_run=false" >> $GITHUB_OUTPUT
fi
else
echo "⚠️ Failed to download or parse latest.json, build needed"
echo "should_run=true" >> $GITHUB_OUTPUT
fi
fi
fi
+220
View File
@@ -0,0 +1,220 @@
name: Clean Old Assets
on:
workflow_dispatch:
inputs:
tag_name:
description: "Release tag name to clean (default: autobuild)"
required: false
default: "autobuild"
type: string
dry_run:
description: "Dry run mode (only show what would be deleted)"
required: false
default: false
type: boolean
workflow_call:
inputs:
tag_name:
description: "Release tag name to clean (default: autobuild)"
required: false
default: "autobuild"
type: string
dry_run:
description: "Dry run mode (only show what would be deleted)"
required: false
default: false
type: boolean
permissions: write-all
env:
TAG_NAME: ${{ inputs.tag_name || 'autobuild' }}
TAG_CHANNEL: AutoBuild
jobs:
check_current_version:
name: Check Current Version and Commit
runs-on: ubuntu-latest
outputs:
current_version: ${{ steps.check.outputs.current_version }}
last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }}
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Get current version and find last Tauri commit
id: check
run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "📦 Current version: $CURRENT_VERSION"
# Find the last commit that changed Tauri-related files
echo "🔍 Finding last commit with Tauri-related changes..."
# Define patterns for Tauri-related files
TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities"
# Get the last commit that changed any of these patterns (excluding build artifacts)
LAST_TAURI_COMMIT=""
for commit in $(git rev-list HEAD --max-count=50); do
# Check if this commit changed any Tauri-related files
CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ')
HAS_TAURI_CHANGES=false
# Check each pattern
if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then
HAS_TAURI_CHANGES=true
elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then
HAS_TAURI_CHANGES=true
fi
if [ "$HAS_TAURI_CHANGES" = true ]; then
LAST_TAURI_COMMIT=$(git rev-parse --short $commit)
break
fi
done
if [ -z "$LAST_TAURI_COMMIT" ]; then
echo "⚠️ No Tauri-related changes found in recent commits, using current commit"
LAST_TAURI_COMMIT=$(git rev-parse --short HEAD)
fi
echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT"
echo "📝 Current commit: $(git rev-parse --short HEAD)"
# 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')
MONTH=$(TZ=Asia/Shanghai date +%m)
DAY=$(TZ=Asia/Shanghai date +%d)
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION"
# Set outputs for other jobs to use
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT
echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT
clean_old_assets:
name: Clean Old Release Assets
runs-on: ubuntu-latest
needs: check_current_version
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Clean old assets from release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ env.TAG_NAME }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
# Use values from check_current_version job
CURRENT_AUTOBUILD_VERSION="${{ needs.check_current_version.outputs.autobuild_version }}"
LAST_TAURI_COMMIT="${{ needs.check_current_version.outputs.last_tauri_commit }}"
CURRENT_VERSION="${{ needs.check_current_version.outputs.current_version }}"
echo "📦 Current version: $CURRENT_VERSION"
echo "📦 Current autobuild version: $CURRENT_AUTOBUILD_VERSION"
echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT"
echo "🏷️ Target tag: $TAG_NAME"
echo "🔍 Dry run mode: $DRY_RUN"
# Check if release exists
RELEASE_EXISTS=$(gh release view "$TAG_NAME" --json id -q '.id' 2>/dev/null || echo "")
if [ -z "$RELEASE_EXISTS" ]; then
echo "❌ Release '$TAG_NAME' not found"
exit 1
fi
echo "✅ Found release '$TAG_NAME'"
# Get all assets
echo "📋 Getting list of all assets..."
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
if [ -z "$assets" ]; then
echo "ℹ️ No assets found in release '$TAG_NAME'"
exit 0
fi
echo "📋 Found assets:"
echo "$assets" | sed 's/^/ - /'
# Count assets to keep and delete
ASSETS_TO_KEEP=""
ASSETS_TO_DELETE=""
for asset in $assets; do
# Keep assets that match current autobuild version or are non-versioned files (like latest.json)
if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then
ASSETS_TO_KEEP="$ASSETS_TO_KEEP$asset\n"
else
ASSETS_TO_DELETE="$ASSETS_TO_DELETE$asset\n"
fi
done
echo ""
echo "🔒 Assets to keep (current version: $CURRENT_AUTOBUILD_VERSION):"
if [ -n "$ASSETS_TO_KEEP" ]; then
echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | sed 's/^/ - /'
else
echo " - None"
fi
echo ""
echo "🗑️ Assets to delete:"
if [ -n "$ASSETS_TO_DELETE" ]; then
echo -e "$ASSETS_TO_DELETE" | grep -v '^$' | sed 's/^/ - /'
else
echo " - None"
echo "ℹ️ No old assets to clean"
exit 0
fi
if [ "$DRY_RUN" = "true" ]; then
echo ""
echo "🔍 DRY RUN MODE: No assets will actually be deleted"
echo " To actually delete these assets, run this workflow again with dry_run=false"
else
echo ""
echo "🗑️ Deleting old assets..."
DELETED_COUNT=0
FAILED_COUNT=0
for asset in $assets; do
# Skip assets that should be kept
if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then
continue
fi
echo " Deleting: $asset"
if gh release delete-asset "$TAG_NAME" "$asset" -y 2>/dev/null; then
DELETED_COUNT=$((DELETED_COUNT + 1))
else
echo " ⚠️ Failed to delete $asset"
FAILED_COUNT=$((FAILED_COUNT + 1))
fi
done
echo ""
echo "📊 Cleanup summary:"
echo " - Deleted: $DELETED_COUNT assets"
if [ $FAILED_COUNT -gt 0 ]; then
echo " - Failed: $FAILED_COUNT assets"
fi
echo " - Kept: $(echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | wc -l) assets"
if [ $FAILED_COUNT -gt 0 ]; then
echo "⚠️ Some assets failed to delete. Please check the logs above."
exit 1
else
echo "✅ Cleanup completed successfully!"
fi
fi
+44 -11
View File
@@ -2,12 +2,25 @@ name: Development Test
on:
workflow_dispatch:
inputs:
run_windows:
description: "运行 Windows"
required: false
type: boolean
default: true
run_macos_aarch64:
description: "运行 macOS aarch64"
required: false
type: boolean
default: true
permissions: write-all
env:
TAG_NAME: deploytest
TAG_CHANNEL: DeployTest
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
concurrency:
# only allow per workflow per commit (and not pr) to run at a time
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -20,46 +33,66 @@ jobs:
- os: windows-latest
target: x86_64-pc-windows-msvc
bundle: nsis
id: windows
input: run_windows
- os: macos-latest
target: aarch64-apple-darwin
bundle: dmg
- os: macos-latest
target: x86_64-apple-darwin
bundle: dmg
id: macos-aarch64
input: run_macos_aarch64
runs-on: ${{ matrix.os }}
steps:
- name: Skip job if not selected
if: github.event.inputs[matrix.input] != 'true'
run: echo "Job ${{ matrix.id }} skipped as requested"
- name: Checkout Repository
if: github.event.inputs[matrix.input] == 'true'
uses: actions/checkout@v4
- name: Install Rust Stable
if: github.event.inputs[matrix.input] == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
if: github.event.inputs[matrix.input] == 'true'
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
if: github.event.inputs[matrix.input] == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
save-if: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache-all-crates: true
shared-key: autobuild-shared
- uses: pnpm/action-setup@v4
name: Install pnpm
if: github.event.inputs[matrix.input] == 'true'
with:
run_install: false
- name: Install Node
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- name: Pnpm install and check
if: github.event.inputs[matrix.input] == 'true'
run: |
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
if: github.event.inputs[matrix.input] == 'true'
run: pnpm release-version ${{ env.TAG_NAME }}
- name: Tauri build
if: github.event.inputs[matrix.input] == 'true'
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
@@ -77,7 +110,7 @@ jobs:
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
- name: Upload Artifacts
if: matrix.os == 'macos-latest'
if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
@@ -85,7 +118,7 @@ jobs:
if-no-files-found: error
- name: Upload Artifacts
if: matrix.os == 'windows-latest'
if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
+2 -2
View File
@@ -26,11 +26,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm i -g --force corepack
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
- run: pnpm i --frozen-lockfile
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm format:check
# taplo:
@@ -2,6 +2,7 @@ name: Clippy Lint
on:
pull_request:
workflow_dispatch:
jobs:
clippy:
@@ -32,6 +33,8 @@ jobs:
with:
workspaces: src-tauri
save-if: false
cache-all-crates: false
shared-key: autobuild-shared
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
@@ -39,21 +42,25 @@ jobs:
sudo apt-get update
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
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Pnpm install and check
run: |
pnpm i
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
run: pnpm run web:build
env:
+237 -17
View File
@@ -5,10 +5,6 @@ on:
# ! 不再使用 workflow_dispatch 触发。
# workflow_dispatch:
push:
# 应当限制在 main 分支上触发发布。
branches:
- main
# 应当限制 v*.*.* 的 tag 触发发布。
tags:
- "v*.*.*"
permissions: write-all
@@ -27,22 +23,132 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if tag is from main branch
run: |
TAG_REF="${GITHUB_REF##*/}"
echo "Checking if tag $TAG_REF is from main branch..."
TAG_COMMIT=$(git rev-list -n 1 $TAG_REF)
MAIN_COMMITS=$(git rev-list origin/main)
if echo "$MAIN_COMMITS" | grep -q "$TAG_COMMIT"; then
echo "✅ Tag $TAG_REF is from main branch"
else
echo "❌ Tag $TAG_REF is not from main branch"
echo "This release workflow only accepts tags from main branch."
exit 1
fi
- name: Check tag and package.json version
run: |
TAG_REF="${GITHUB_REF##*/}"
TAG_REF="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
echo "Current tag: $TAG_REF"
PKG_VERSION=$(jq -r .version package.json)
echo "package.json version: $PKG_VERSION"
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
EXPECTED_TAG="v$PKG_VERSION"
if [[ "$TAG_REF" != "$EXPECTED_TAG" ]]; then
echo "❌ Version mismatch:"
echo " Git tag : $TAG_REF"
echo " package.json : $EXPECTED_TAG"
exit 1
fi
echo "Tag and package.json version are consistent."
echo "✅ Tag and package.json version are consistent."
update_tag:
name: Update tag
runs-on: ubuntu-latest
needs: [release, release-for-linux-arm, release-for-fixed-webview2]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Set Env
run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
TAG_REF="${GITHUB_REF##*/}"
echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV
VERSION=$(echo "$TAG_REF" | sed 's/^v//')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/$TAG_REF" >> $GITHUB_ENV
shell: bash
- run: |
if [ -z "$UPDATE_LOGS" ]; then
echo "No update logs found, using default message"
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found update logs"
fi
cat > release.txt << EOF
$UPDATE_LOGS
## 下载地址
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [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 ./路径 安装
- [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
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG_NAME }}
name: "Clash Verge Rev ${{ env.TAG_NAME }}"
body_path: release.txt
draft: false
prerelease: false
token: ${{ secrets.GITHUB_TOKEN }}
# generate_release_notes: true
release:
name: Release Build
needs: check_tag_version
needs: [check_tag_version]
strategy:
fail-fast: false
matrix:
@@ -81,6 +187,15 @@ jobs:
sudo apt-get update
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
uses: actions/setup-node@v4
with:
@@ -110,14 +225,18 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: v__VERSION__
releaseName: "Clash Verge Rev v__VERSION__"
releaseBody: "More new features are now supported."
tagName: ${{ github.ref_name }}
releaseName: "Clash Verge Rev ${{ github.ref_name }}"
releaseBody: "Draft release, will be updated later."
releaseDraft: true
prerelease: false
tauriScript: pnpm
args: --target ${{ matrix.target }}
includeUpdaterJson: true
release-for-linux-arm:
name: Release Build for Linux ARM
needs: [check_tag_version]
strategy:
fail-fast: false
matrix:
@@ -232,7 +351,7 @@ jobs:
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}"
body: "More new features are now supported."
body: "See release notes for detailed changelog."
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
@@ -240,6 +359,7 @@ jobs:
release-for-fixed-webview2:
name: Release Build for Fixed WebView2
needs: [check_tag_version]
strategy:
fail-fast: false
matrix:
@@ -323,7 +443,7 @@ jobs:
with:
tag_name: v${{steps.build.outputs.appVersion}}
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
body: "More new features are now supported."
body: "See release notes for detailed changelog."
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
@@ -335,7 +455,7 @@ jobs:
release-update:
name: Release Update
runs-on: ubuntu-latest
needs: [release, release-for-linux-arm]
needs: [update_tag]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -360,7 +480,7 @@ jobs:
release-update-for-fixed-webview2:
runs-on: ubuntu-latest
needs: [release-for-fixed-webview2]
needs: [update_tag]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -386,7 +506,7 @@ jobs:
submit-to-winget:
name: Submit to Winget
runs-on: ubuntu-latest
needs: [release-update]
needs: [update_tag, release-update]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -405,3 +525,103 @@ jobs:
release-tag: v${{env.VERSION}}
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
token: ${{ secrets.WINGET_TOKEN }}
notify-telegram:
name: Notify Telegram
runs-on: ubuntu-latest
needs:
[
update_tag,
release-update,
release-update-for-fixed-webview2,
submit-to-winget,
]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch UPDATE logs
id: fetch_update_logs
run: |
if [ -f "UPDATELOG.md" ]; then
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
if [ -n "$UPDATE_LOGS" ]; then
echo "Found update logs"
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
echo "$UPDATE_LOGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "No update sections found in UPDATELOG.md"
fi
else
echo "UPDATELOG.md file not found"
fi
shell: bash
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Get Version and Release Info
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Generate release.txt
run: |
if [ -z "$UPDATE_LOGS" ]; then
echo "No update logs found, using default message"
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found update logs"
fi
cat > release.txt << EOF
$UPDATE_LOGS
## 下载地址
### Windows (不再支持Win7)
#### 正常版本(推荐)
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
### macOS
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
### Linux
#### DEB包(Debian系) 使用 apt ./路径 安装
- [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 ./路径 安装
- [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
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
### 稳定机场VPN推荐
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Send Telegram Notification
run: node scripts/telegram.mjs
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
BUILD_TYPE: release
VERSION: ${{ env.VERSION }}
DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }}
+18 -11
View File
@@ -1,5 +1,8 @@
#!/bin/bash
# $1: remote name (e.g., origin)
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
if git diff --cached --name-only | grep -q '^src-tauri/'; then
cargo clippy --manifest-path ./src-tauri/Cargo.toml
if [ $? -ne 0 ]; then
@@ -8,20 +11,24 @@ if git diff --cached --name-only | grep -q '^src-tauri/'; then
fi
fi
# Only run format check if the remote exists and is the main repo
remote_name="$1"
remote_url=$(git remote get-url "$remote_name")
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
echo "[pre-push] Running pnpm format:check..."
pnpm format:check
if [ $? -ne 0 ]; then
echo "❌ Code format check failed. Please fix formatting before pushing."
exit 1
if git remote get-url "$remote_name" >/dev/null 2>&1; then
remote_url=$(git remote get-url "$remote_name")
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
echo "[pre-push] Running pnpm format:check..."
pnpm format:check
if [ $? -ne 0 ]; then
echo "❌ Code format check failed. Please fix formatting before pushing."
exit 1
fi
else
echo "[pre-push] Not pushing to target repo. Skipping format check."
fi
else
echo "[pre-push] Not pushing to target repo. Skipping format check."
echo "[pre-push] Remote $remote_name does not exist. Skipping format check."
fi
exit 0
+14 -4
View File
@@ -1,6 +1,16 @@
{
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"experimentalOperatorPosition": "start"
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto"
}
+8 -1
View File
@@ -8,10 +8,15 @@ Before you start contributing to the project, you need to set up your developmen
### Prerequisites
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/start/prerequisites/) to install them on your system.
### Setup for Windows Users
> [!NOTE]
> **If you are using a Windows ARM device, you additionally need to install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the environment variable.**
>
> Because the `ring` crate is compiled based on `clang` under Windows ARM.
If you're a Windows user, you may need to perform some additional steps:
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
@@ -51,11 +56,13 @@ apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev
You have two options for downloading the clash binary:
- Automatically download it via the provided script:
```shell
pnpm run prebuild
# Use '--force' to force update to the latest version
# 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).
### Run the Development Server
+1 -1
View File
@@ -88,7 +88,7 @@ To run the development server, execute the following commands after all prerequi
```shell
pnpm i
pnpm run check
pnpm run prebuild
pnpm dev
```
+175 -23
View File
@@ -1,3 +1,169 @@
## v2.4.2
### ✨ 新增功能
- 增加托盘节点选择
### 🚀 性能优化
- 优化前端首页加载速度
- 优化前端未使用 i18n 文件缓存呢
- 优化后端内存占用
- 优化后端启动速度
### 🐞 修复问题
- 修复首页节点切换失效的问题
- 修复和优化服务检查流程
- 修复2.4.1引入的订阅地址重定向报错问题
- 修复 rpm/deb 包名称问题
- 修复托盘轻量模式状态检测异常
- 修复通过 scheme 导入订阅崩溃
- 修复单例检测实效
- 修复启动阶段可能导致的无法连接内核
- 修复导入订阅无法 Auth Basic
### 👙 界面样式
- 简化和改进代理设置样式
## v2.4.1
### 🏆 重大改进
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.13**
### 🚀 性能优化
- 优化热键响应速度,提升快捷键操作体验
- 改进服务管理响应性,减少系统服务操作等待时间
- 提升文件和配置处理性能
- 优化任务管理和日志记录效率
- 优化异步内存管理,减少内存占用并提升多任务处理效率
- 优化启动阶段初始化性能
### 🐞 修复问题
- 修复应用在某些操作中可能出现的响应延迟问题
- 修复任务管理中的潜在并发问题
- 修复通过托盘重启应用无法恢复
- 修复订阅在某些情况下无法导入
- 修复无法新建订阅时使用远程链接
- 修复卸载服务后的 tun 开关状态问题
- 修复页面快速切换订阅时导致崩溃
- 修复丢失工作目录时无法恢复环境
- 修复从轻量模式恢复导致崩溃
### 👙 界面样式
- 统一代理设置样式
### 🗑️ 移除内容
- 移除启动阶段自动清理过期订阅
## v2.4.0
**发行代号:融**
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
### 🏆 重大改进
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
### ✨ 新增功能
- **Mihomo(Meta) 内核升级至 v1.19.12**
- 新增版本信息复制按钮
- 增强型流量监控,支持更详细的数据分析
- 新增流量图表多种显示模式
- 新增强制刷新配置和节点缓存功能
- 首页流量统计支持查看刻度线详情
### 🚀 性能优化
- 全面提升数据传输和处理效率
- 优化内存使用,减少系统资源消耗
- 改进流量图表渲染性能
- 优化配置和节点刷新策略,从5秒延长到60秒
- 改进数据缓存机制,减少重复请求
- 优化异步程序性能
### 🐞 修复问题
- 修复系统代理状态检测和显示不一致问题
- 修复系统主题窗口颜色不一致问题
- 修复特殊字符 URL 处理问题
- 修复配置修改后缓存不同步问题
- 修复 Windows 安装器自启设置问题
- 修复 macOS 下 Dock 图标恢复窗口问题
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
- 修复架构升级后节点测速功能异常
- 修复架构升级后流量统计功能异常
- 修复架构升级后日志功能异常
- 修复外部控制器跨域配置保存问题
- 修复首页端口显示不一致问题
- 修复首页流量统计刻度线显示问题
- 修复日志页面按钮功能混淆问题
- 修复日志等级设置保存问题
- 修复日志等级异常过滤
- 修复清理日志天数功能异常
- 修复偶发性启动卡死问题
- 修复首页虚拟网卡开关在管理模式下的状态问题
### 🔧 技术改进
- 统一使用新的内核通信方式
- 新增外部控制器配置界面
- 改进跨平台兼容性支持
## v2.3.2
### 🐞 修复问题
- 修复系统代理端口不同步问题
- 修复自定义 `css` 背景图无法生效问题
- 修复在轻量模式下快速点击托盘图标带来的竞争态卡死问题
- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题
- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步
- 修复导入订阅时非 http 协议链接被错误尝试导入
- 修复切换节点后页面长时间 loading 及缓存过期导致的数据不同步问题
- 修复将快捷键名称更名为 `Clash Verge`之后无法删除图标和无法删除注册表
- 修复`DNS`覆写 `fallback` `proxy server` `nameserver` `direct Nameserver` 字段支持留空
- 修复`DNS`覆写 `nameserver-policy` 字段无法正确识别 `geo` 库
- 修复搜索框输入特殊字符崩溃
- 修复 Windows 下 Start UP 名称与 exe 名称不统一
- 修复显示 Mihomo 内核日志等级应该大于设置等级
### ✨ 新增功能
- `sidecar` 模式下清理多余的内核进程,防止运行出现异常
- 新 macOS 下 TUN 和系统代理模式托盘图标(暂测)
- 快捷键事件通过系统通知
- 添加外部 `cors` 控制面板
### 🚀 优化改进
- 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死
- 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死
- 改进主页订阅卡流量已使用比例计算精度
- 优化后端缓存刷新机制,支持毫秒级 TTL(默认 3000ms),减少重复请求并提升性能,切换节点时强制刷新后端数据,前端 UI 实时更新,操作更流畅
- 解耦前端数据拉取与后端缓存刷新,提升节点切换速度和一致性
### 🗑️ 移除内容
- 移除了 macOS tray 图标显示网络速率
### 🌐 国际化更新
- 修复部分翻译缺失和不一致问题
## v2.3.1
### 🐞 修复问题
@@ -18,7 +184,8 @@
- 优化 托盘 统一响应
- 优化 静默启动+自启动轻量模式 运行方式
- 升级依赖
- 降低前端潜在内存泄漏风险,提升运行时性能
- 优化 React 状态、副作用、数据获取、清理等流程。
## v2.3.0
@@ -31,7 +198,6 @@
- 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
- macOS:
- MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
- 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
- 彩色 Tray 图标颜色偏淡;
@@ -69,13 +235,11 @@
- 托盘新增当前轻量模式状态显示
- 关闭系统代理时同时断开已建立的连接
- 新增 WebDAV 功能:
- 加入 UA 请求头
- 支持目录重定向
- 备份目录检查与上传重试机制
- 自动订阅更新机制:
- 加入请求超时机制防止卡死
- 支持在代理状态下自动重试订阅更新
- 支持订阅卡片点击切换下次自动更新时间,并显示更新结果提示
@@ -92,13 +256,11 @@
### 🚀 优化改进
- **系统相关:**
- 系统代理 Bypass 设置优化
- 优化代理设置更新逻辑与守卫机制
- Windows 启动方式调整为 Startup 文件夹,解决管理员模式下自启问题
- **性能与稳定性:**
- 全面异步化处理配置加载、UI 启动、事件通知等关键流程,解决卡顿问题
- 优化 MihomoManager 实现与窗口创建流程
- 改进内核日志等级为 `warn`,减少噪音输出
@@ -109,7 +271,6 @@
- 优化缓存 Mihomo proxy 和 providers 信息内存使用
- **前端与界面体验:**
- 切换规则页自动刷新数据
- 非激活订阅编辑时不再触发配置重载
- 优化托盘速率显示,macOS 下默认关闭
@@ -120,7 +281,6 @@
- 优化保存机制,使用平滑函数防止卡顿
- **配置增强与安全性:**
- 配置缺失 `secret` 字段时自动补全为 `set-your-secret`
- 强制为 Mihomo 配置补全 `external-controller-cors` 字段(默认不允许跨域,限制本地访问)计划后续支持自定义 cors
- 优化窗口权限设置与状态初始化逻辑
@@ -255,7 +415,6 @@
#### 新增功能
1. **首页**
- 新增首页功能,默认启动页面改为首页。
- 首页流量图卡片显示上传/下载名称。
- 首页支持轻量模式切换。
@@ -263,21 +422,17 @@
- 限制首页配置文件卡片URL长度。
2. **DNS 设置与覆写**
- 新增 DNS 覆写功能。
- 默认启用 DNS 覆写。
3. **解锁测试**
- 新增解锁测试页面。
4. **轻量模式**
- 新增轻量模式及设置。
- 添加自动轻量模式定时器。
5. **系统支持**
- Mihomo(meta)内核升级 1.19.3
- macOS 支持 CMD+W 关闭窗口。
- 新增 macOS 应用菜单。
@@ -292,7 +447,6 @@
#### 修复
1. **系统**
- 修复 Windows 热键崩溃。
- 修复 macOS 无框标题。
- 修复 macOS 静默启动崩溃。
@@ -307,7 +461,6 @@
#### 优化
1. **性能**
- 重构后端,巨幅性能优化。
- 优化首页组件性能。
- 优化流量图表资源使用。
@@ -320,7 +473,6 @@
- 优化修改verge配置性能。
2. **重构**
- 重构后端,巨幅性能优化。
- 优化定时器管理。
- 重构 MihomoManager 处理流量。
@@ -363,7 +515,7 @@
- 新增窗口状态实时监控与自动保存功能
- 增强核心配置变更时的验证与错误处理机制
- 支持通过环境变量`CLASH_VERGE_REV_IP`自定义复制IP地址
- 支持通过环境变量 `CLASH_VERGE_REV_IP`自定义复制IP地址
- 添加连接表列宽持久化设置与进程过滤功能
- 新增代理组首字母导航与动态滚动定位功能
- 实现连接追踪暂停/恢复功能
@@ -684,7 +836,7 @@
- 禁用部分 Webview2 快捷键
- 热键配置新增连接符 + 号
- 新增部分悬浮提示按钮,用于解释说明
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 设置页面右上角新增 TG 频道链接
- 各种细节优化和界面性能优化
@@ -724,7 +876,7 @@
- 禁用部分 Webview2 快捷键
- 热键配置新增连接符 + 号
- 新增部分悬浮提示按钮,用于解释说明
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 设置页面右上角新增 TG 频道链接
- 各种细节优化和界面性能优化
@@ -760,7 +912,7 @@
- 禁用部分 Webview2 快捷键
- 热键配置新增连接符 + 号
- 新增部分悬浮提示按钮,用于解释说明
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 设置页面右上角新增 TG 频道链接
### Bugs Fixes
@@ -934,7 +1086,7 @@
### Features
- 缓存代理组图标
- 使用`boa_engine` 代替 `rquickjs`
- 使用 `boa_engine` 代替 `rquickjs`
- 支持 Linux armv7
### Bugs Fixes
@@ -999,7 +1151,7 @@
- 支持自定义托盘图标
- 支持禁用代理组图标
- 代理组显示当前代理
- 修改 `打开面板` 快捷键为`打开/关闭面板`
- 修改 `打开面板` 快捷键为 `打开/关闭面板`
---
@@ -1163,7 +1315,7 @@
### Bugs Fixes
- Windows 下更新时无法覆盖`clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
- Windows 下更新时无法覆盖 `clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
- 窗口最大化按钮变化问题
- 窗口尺寸保存错误问题
- 复制环境变量类型无法切换问题
+16
View File
@@ -0,0 +1,16 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.browser },
},
tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
]);
+53 -57
View File
@@ -1,10 +1,11 @@
{
"name": "clash-verge",
"version": "2.3.1",
"version": "2.4.2",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
"dev:trace": "cross-env RUST_BACKTRACE=1 RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
"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",
"tauri": "tauri",
@@ -18,9 +19,12 @@
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
"release-version": "node scripts/release-version.mjs",
"release:autobuild": "pnpm release-version autobuild",
"release:deploytest": "pnpm release-version deploytest",
"publish-version": "node scripts/publish-version.mjs",
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
"lint": "eslint src",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
@@ -29,87 +33,79 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^7.1.1",
"@mui/lab": "7.0.0-beta.13",
"@mui/material": "^7.1.1",
"@mui/x-data-grid": "^8.5.2",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.3",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
"@tauri-apps/plugin-notification": "^2.2.3",
"@tauri-apps/plugin-process": "^2.2.2",
"@tauri-apps/plugin-shell": "2.2.2",
"@tauri-apps/plugin-updater": "2.8.1",
"@tauri-apps/plugin-window-state": "^2.2.3",
"@types/d3-shape": "^3.1.7",
"@mui/icons-material": "^7.3.2",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.1",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-notification": "^2.3.1",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "2.3.1",
"@tauri-apps/plugin-updater": "2.9.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.8.5",
"axios": "^1.10.0",
"chart.js": "^4.5.0",
"ahooks": "^3.9.5",
"axios": "^1.11.0",
"cli-color": "^2.0.4",
"d3-shape": "^3.2.0",
"dayjs": "1.11.13",
"dayjs": "1.11.18",
"foxact": "^0.2.49",
"glob": "^11.0.3",
"i18next": "^25.2.1",
"js-base64": "^3.7.7",
"i18next": "^25.5.2",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.5",
"peggy": "^5.0.3",
"react": "19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "19.1.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.58.1",
"react-i18next": "15.5.3",
"react-hook-form": "^7.62.0",
"react-i18next": "15.7.3",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.58.0",
"react-router-dom": "7.6.2",
"react-virtuoso": "^4.13.0",
"react-monaco-editor": "0.59.0",
"react-router-dom": "7.8.2",
"react-virtuoso": "^4.14.0",
"sockette": "^2.0.6",
"swr": "^2.3.3",
"swr": "^2.3.6",
"tar": "^7.4.3",
"types-pac": "^1.0.3",
"zustand": "^5.0.5"
"zustand": "^5.0.8"
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@tauri-apps/cli": "2.5.0",
"@types/js-cookie": "^3.0.6",
"@eslint/js": "^9.35.0",
"@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@vitejs/plugin-legacy": "^6.1.1",
"@vitejs/plugin-react": "4.5.2",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.0.2",
"adm-zip": "^0.5.16",
"commander": "^14.0.0",
"cross-env": "^7.0.3",
"cross-env": "^10.0.0",
"eslint": "^9.35.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"meta-json-schema": "^1.19.10",
"jiti": "^2.5.1",
"meta-json-schema": "^1.19.13",
"node-fetch": "^3.3.2",
"prettier": "^3.5.3",
"pretty-quick": "^4.2.2",
"sass": "^1.89.2",
"terser": "^5.43.0",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"path": "^0.12.7",
"prettier": "^3.6.2",
"process": "^0.11.10",
"sass": "^1.92.1",
"terser": "^5.44.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.42.0",
"vite": "^7.1.4",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.0"
},
"prettier": {
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"endOfLine": "lf"
"vite-plugin-svgr": "^4.5.0"
},
"type": "module",
"packageManager": "pnpm@9.13.2"
+2900 -1192
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,5 +1,5 @@
{
"extends": ["config:recommended"],
"extends": ["config:recommended", ":disableDependencyDashboard"],
"baseBranches": ["dev"],
"enabledManagers": ["cargo", "npm"],
"labels": ["dependencies"],
@@ -38,5 +38,5 @@
}
],
"postUpdateOptions": ["pnpmDedupe"],
"ignoreDeps": ["serde_yaml"]
"ignoreDeps": ["criterion"]
}
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
# 获取最近一个和 Tauri 相关的改动的 commit hash
# This script finds the latest commit that modified Tauri-related files
# Tauri 相关文件的模式
TAURI_PATTERNS=(
"src-tauri/"
"Cargo.toml"
"Cargo.lock"
"tauri.*.conf.json"
"package.json"
"pnpm-lock.yaml"
"src/"
)
# 排除的文件模式(build artifacts 等)
EXCLUDE_PATTERNS=(
"src-tauri/target/"
"src-tauri/gen/"
"*.log"
"*.tmp"
"node_modules/"
".git/"
)
# 构建 git log 的路径过滤参数
PATHS=""
for pattern in "${TAURI_PATTERNS[@]}"; do
if [[ -e "$pattern" ]]; then
PATHS="$PATHS $pattern"
fi
done
# 如果没有找到相关路径,返回错误
if [[ -z "$PATHS" ]]; then
echo "Error: No Tauri-related paths found in current directory" >&2
exit 1
fi
# 获取最新的 commit hash
# 使用 git log 查找最近修改了 Tauri 相关文件的提交
LATEST_COMMIT=$(git log --format="%H" -n 1 -- $PATHS)
# 验证是否找到了 commit
if [[ -z "$LATEST_COMMIT" ]]; then
echo "Error: No commits found for Tauri-related files" >&2
exit 1
fi
# 输出结果
echo "$LATEST_COMMIT"
# 如果需要更多信息,可以取消注释以下行
# echo "Latest Tauri-related commit: $LATEST_COMMIT"
# git show --stat --oneline "$LATEST_COMMIT"
+6 -6
View File
@@ -61,12 +61,12 @@ const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/down
let META_ALPHA_VERSION;
const META_ALPHA_MAP = {
"win32-x64": "mihomo-windows-amd64-compatible",
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-compatible",
"darwin-x64": "mihomo-darwin-amd64-v2",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
@@ -108,12 +108,12 @@ const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
let META_VERSION;
const META_MAP = {
"win32-x64": "mihomo-windows-amd64-compatible",
"win32-x64": "mihomo-windows-amd64-v2",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64-compatible",
"darwin-x64": "mihomo-darwin-amd64-v2",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-x64": "mihomo-linux-amd64-v2",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
+1 -1
View File
@@ -54,7 +54,7 @@ async function run() {
execSync(`git tag ${tag}`, { stdio: "inherit" });
execSync(`git push origin ${tag}`, { stdio: "inherit" });
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
} catch (e) {
} catch {
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
process.exit(1);
}
+63 -12
View File
@@ -6,15 +6,19 @@
*
* <version> can be:
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
* - A tag: "alpha", "beta", "rc", or "autobuild"
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d)
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530)
*
* Examples:
* pnpm release-version 1.2.3
* pnpm release-version v1.2.3-beta
* pnpm release-version beta
* pnpm release-version autobuild
* pnpm release-version autobuild-latest
* pnpm release-version deploytest
*
* The script will:
* - Validate and normalize the version argument
@@ -37,21 +41,51 @@ import { execSync } from "child_process";
function getGitShortCommit() {
try {
return execSync("git rev-parse --short HEAD").toString().trim();
} catch (e) {
} catch {
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
return "nogit";
}
}
/**
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
* 获取最新 Tauri 相关提交的短 hash
* @returns {string}
*/
function getLatestTauriCommit() {
try {
const fullHash = execSync(
"bash ./scripts-workflow/get_latest_tauri_commit.bash",
)
.toString()
.trim();
return execSync(`git rev-parse --short ${fullHash}`).toString().trim();
} catch {
console.warn(
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
);
return getGitShortCommit();
}
}
/**
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
* 使用 Asia/Shanghai 时区
* @param {boolean} withCommit 是否带 commit
* @returns {string}
*/
function generateShortTimestamp(withCommit = false) {
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) {
const gitShort = getGitShortCommit();
return `${month}${day}.${gitShort}`;
@@ -135,20 +169,19 @@ async function updateCargoVersion(newVersion) {
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV);
const updatedLines = lines.map((line) => {
if (line.trim().startsWith("version =")) {
return line.replace(
/version\s*=\s*"[^"]+"/,
`version = "${baseVersion}"`,
`version = "${versionWithoutV}"`,
);
}
return line;
});
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
console.log(`[INFO]: Cargo.toml version updated to: ${baseVersion}`);
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
} catch (error) {
console.error("Error updating Cargo.toml version:", error);
throw error;
@@ -168,19 +201,23 @@ async function updateTauriConfigVersion(newVersion) {
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const baseVersion = getBaseVersion(versionWithoutV);
console.log(
"[INFO]: Current tauri.conf.json version is: ",
tauriConfig.version,
);
tauriConfig.version = baseVersion;
// 使用完整版本信息,包含build metadata
tauriConfig.version = versionWithoutV;
await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
"utf8",
);
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
console.log(
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
);
} catch (error) {
console.error("Error updating tauri.conf.json version:", error);
throw error;
@@ -214,7 +251,14 @@ async function main(versionArg) {
try {
let newVersion;
const validTags = ["alpha", "beta", "rc", "autobuild"];
const validTags = [
"alpha",
"beta",
"rc",
"autobuild",
"autobuild-latest",
"deploytest",
];
if (validTags.includes(versionArg.toLowerCase())) {
const currentVersion = await getCurrentVersion();
@@ -223,6 +267,13 @@ async function main(versionArg) {
if (versionArg.toLowerCase() === "autobuild") {
// 格式: 2.3.0+autobuild.250613.cc39b27
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
} else if (versionArg.toLowerCase() === "autobuild-latest") {
// 格式: 2.3.0+autobuild.0614.a1b2c3d (使用最新 Tauri 提交)
const latestTauriCommit = getLatestTauriCommit();
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
} else if (versionArg.toLowerCase() === "deploytest") {
// 格式: 2.3.0+deploytest.250613.cc39b27
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true)}`;
} else {
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
}
+110
View File
@@ -0,0 +1,110 @@
import axios from "axios";
import { readFileSync } from "fs";
import { log_success, log_error, log_info } from "./utils.mjs";
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
const CHAT_ID_TEST = "@vergetest"; // 测试频道
async function sendTelegramNotification() {
if (!process.env.TELEGRAM_BOT_TOKEN) {
throw new Error("TELEGRAM_BOT_TOKEN is required");
}
const version =
process.env.VERSION ||
(() => {
const pkg = readFileSync("package.json", "utf-8");
return JSON.parse(pkg).version;
})();
const downloadUrl =
process.env.DOWNLOAD_URL ||
`https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}`;
const isAutobuild =
process.env.BUILD_TYPE === "autobuild" || version.includes("autobuild");
const chatId = isAutobuild ? CHAT_ID_TEST : CHAT_ID_RELEASE;
const buildType = isAutobuild ? "滚动更新版" : "正式版";
log_info(`Preparing Telegram notification for ${buildType} ${version}`);
log_info(`Target channel: ${chatId}`);
log_info(`Download URL: ${downloadUrl}`);
// 读取发布说明和下载地址
let releaseContent = "";
try {
releaseContent = readFileSync("release.txt", "utf-8");
log_info("成功读取 release.txt 文件");
} catch (error) {
log_error("无法读取 release.txt,使用默认发布说明", error);
releaseContent = "更多新功能现已支持,详细更新日志请查看发布页面。";
}
// Markdown 转换为 HTML
function convertMarkdownToTelegramHTML(content) {
return content
.split("\n")
.map((line) => {
if (line.trim().length === 0) {
return "";
} else if (line.startsWith("## ")) {
return `<b>${line.replace("## ", "")}</b>`;
} else if (line.startsWith("### ")) {
return `<b>${line.replace("### ", "")}</b>`;
} else if (line.startsWith("#### ")) {
return `<b>${line.replace("#### ", "")}</b>`;
} else {
let processedLine = line.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
(match, text, url) => {
const encodedUrl = encodeURI(url);
return `<a href="${encodedUrl}">${text}</a>`;
},
);
processedLine = processedLine.replace(
/\*\*([^*]+)\*\*/g,
"<b>$1</b>",
);
return processedLine;
}
})
.join("\n");
}
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
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
try {
await axios.post(
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
{
chat_id: chatId,
text: content,
link_preview_options: {
is_disabled: false,
url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${encodedVersion}`,
prefer_large_media: true,
},
parse_mode: "HTML",
},
);
log_success(`✅ Telegram 通知发送成功到 ${chatId}`);
} catch (error) {
log_error(
`❌ Telegram 通知发送失败到 ${chatId}:`,
error.response?.data || error.message,
error,
);
process.exit(1);
}
}
// 执行函数
sendTelegramNotification().catch((error) => {
log_error("脚本执行失败:", error);
process.exit(1);
});
+2 -2
View File
@@ -8,7 +8,7 @@ const UPDATE_LOG = "UPDATELOG.md";
export async function resolveUpdateLog(tag) {
const cwd = process.cwd();
const reTitle = /^## v[\d\.]+/;
const reTitle = /^## v[\d.]+/;
const reEnd = /^---/;
const file = path.join(cwd, UPDATE_LOG);
@@ -54,7 +54,7 @@ export async function resolveUpdateLogDefault() {
const data = await fsp.readFile(file, "utf-8");
const reTitle = /^## v[\d\.]+/;
const reTitle = /^## v[\d.]+/;
const reEnd = /^---/;
let isCapturing = false;
+2032 -1773
View File
File diff suppressed because it is too large Load Diff
+96 -45
View File
@@ -1,8 +1,8 @@
[package]
name = "clash-verge"
version = "2.3.1"
version = "2.4.2"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
@@ -13,73 +13,76 @@ build = "build.rs"
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.2.0", features = [] }
tauri-build = { version = "2.4.1", features = [] }
[dependencies]
warp = "0.3.7"
anyhow = "1.0.98"
warp = { version = "0.4.2", features = ["server"] }
anyhow = "1.0.99"
dirs = "6.0"
open = "5.3.2"
log = "0.4.27"
log = "0.4.28"
dunce = "1.0.5"
log4rs = "1.3.0"
log4rs = "1.4.0"
nanoid = "0.4"
chrono = "0.4.41"
sysinfo = "0.35.2"
sysinfo = { version = "0.37.0", features = ["network", "system"] }
boa_engine = "0.20.0"
serde_json = "1.0.140"
serde_yaml = "0.9.34-deprecated"
serde_json = "1.0.143"
serde_yaml_ng = "0.10.0"
once_cell = "1.21.3"
lazy_static = "1.5.0"
port_scanner = "0.1.5"
delay_timer = "0.11.6"
parking_lot = "0.12.4"
percent-encoding = "2.3.1"
tokio = { version = "1.45.1", features = [
percent-encoding = "2.3.2"
tokio = { version = "1.47.1", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
serde = { version = "1.0.219", features = ["derive"] }
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] }
regex = "1.11.1"
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.2"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
image = "0.25.6"
imageproc = "0.25.0"
tauri = { version = "2.5.1", features = [
tauri = { version = "2.8.5", features = [
"protocol-asset",
"devtools",
"tray-icon",
"image-ico",
"image-png",
] }
network-interface = { version = "2.0.1", features = ["serde"] }
tauri-plugin-shell = "2.2.2"
tauri-plugin-dialog = "2.2.2"
tauri-plugin-fs = "2.3.0"
tauri-plugin-process = "2.2.2"
tauri-plugin-clipboard-manager = "2.2.3"
tauri-plugin-deep-link = "2.3.0"
tauri-plugin-devtools = "2.0.0"
tauri-plugin-window-state = "2.2.3"
zip = "4.1.0"
reqwest_dav = "0.2.1"
network-interface = { version = "2.0.3", features = ["serde"] }
tauri-plugin-shell = "2.3.1"
tauri-plugin-dialog = "2.4.0"
tauri-plugin-fs = "2.4.2"
tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.3"
tauri-plugin-devtools = "2.0.1"
tauri-plugin-window-state = "2.4.0"
zip = "5.0.0"
reqwest_dav = "0.2.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.3.3"
tokio-tungstenite = "0.27.0"
futures = "0.3.31"
sys-locale = "0.3.2"
async-trait = "0.1.88"
mihomo_api = { path = "src_crates/crate_mihomo_api" }
ab_glyph = "0.2.29"
tungstenite = "0.27.0"
libc = "0.2.173"
libc = "0.2.175"
gethostname = "1.0.2"
hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
kode-bridge = "0.2.1-rc2"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.1"
console-subscriber = { version = "0.4.1", optional = true }
tokio-stream = "0.1.17"
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
"parking_lot",
] }
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
@@ -93,20 +96,25 @@ winapi = { version = "0.3.9", features = [
"errhandlingapi",
"minwindef",
"winerror",
"tlhelp32",
"processthreadsapi",
"winhttp",
"winreg",
] }
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.4.0"
tauri-plugin-global-shortcut = "2.2.1"
tauri-plugin-updater = "2.8.1"
tauri-plugin-autostart = "2.5.0"
tauri-plugin-global-shortcut = "2.3.0"
tauri-plugin-updater = "2.9.0"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = []
tokio-trace = ["console-subscriber"]
[profile.release]
panic = "abort"
@@ -136,12 +144,55 @@ name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[dev-dependencies]
tempfile = "3.20.0"
criterion = "0.7.0"
[workspace]
members = ["src_crates/crate_mihomo_api"]
[lints.clippy]
# Core categories - most important for code safety and correctness
correctness = { level = "deny", priority = -1 }
suspicious = { level = "deny", priority = -1 }
# [patch.crates-io]
# bitflags = { git = "https://github.com/bitflags/bitflags", rev = "2.9.0" }
# zerocopy = { git = "https://github.com/google/zerocopy", rev = "v0.8.24" }
# tungstenite = { git = "https://github.com/snapview/tungstenite-rs", rev = "v0.26.2" }
# Critical safety lints - warn for now due to extensive existing usage
unwrap_used = "warn"
expect_used = "warn"
panic = "deny"
unimplemented = "deny"
# Development quality lints
todo = "warn"
dbg_macro = "warn"
#print_stdout = "warn"
#print_stderr = "warn"
# Performance lints for proxy application
clone_on_ref_ptr = "warn"
rc_clone_in_vec_init = "warn"
large_stack_arrays = "warn"
large_const_arrays = "warn"
# Security lints
#integer_division = "warn"
#lossy_float_literal = "warn"
#default_numeric_fallback = "warn"
# Mutex and async lints - strict control
async_yields_async = "deny" # Prevents missing await in async blocks
mutex_atomic = "deny" # Use atomics instead of Mutex<bool/int>
mutex_integer = "deny" # Use AtomicInt instead of Mutex<int>
rc_mutex = "deny" # Single-threaded Rc with Mutex is wrong
unused_async = "deny" # Too many false positives in Tauri/framework code
await_holding_lock = "deny"
large_futures = "deny"
future_not_send = "deny"
# Common style improvements
redundant_else = "deny" # Too many in existing code
needless_continue = "deny" # Too many in existing code
needless_raw_string_hashes = "deny" # Too many in existing code
# Disable noisy categories for existing codebase but keep them available
#style = { level = "allow", priority = -1 }
#complexity = { level = "allow", priority = -1 }
#perf = { level = "allow", priority = -1 }
#pedantic = { level = "allow", priority = -1 }
#nursery = { level = "allow", priority = -1 }
#restriction = { level = "allow", priority = -1 }
+91
View File
@@ -0,0 +1,91 @@
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
// 业务模型 & Draft
use app_lib::config::Draft as DraftNew;
use app_lib::config::IVerge;
// fn bench_apply_old(c: &mut Criterion) {
// c.bench_function("apply_draft_old", |b| {
// b.iter(|| {
// let verge = Box::new(IVerge {
// enable_auto_launch: Some(true),
// enable_tun_mode: Some(false),
// ..Default::default()
// });
// let draft = DraftOld::from(black_box(verge));
// {
// let mut d = draft.draft_mut();
// d.enable_auto_launch = Some(false);
// }
// let _ = draft.apply();
// });
// });
// }
// fn bench_discard_old(c: &mut Criterion) {
// c.bench_function("discard_draft_old", |b| {
// b.iter(|| {
// let verge = Box::new(IVerge::default());
// let draft = DraftOld::from(black_box(verge));
// {
// let mut d = draft.draft_mut();
// d.enable_auto_launch = Some(false);
// }
// let _ = draft.discard();
// });
// });
// }
/// 基准:修改草稿并 apply()
fn bench_apply_new(c: &mut Criterion) {
c.bench_function("apply_draft_new", |b| {
b.iter(|| {
let verge = Box::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..Default::default()
});
let draft = DraftNew::from(black_box(verge));
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.apply();
});
});
}
/// 基准:修改草稿并 discard()
fn bench_discard_new(c: &mut Criterion) {
c.bench_function("discard_draft_new", |b| {
b.iter(|| {
let verge = Box::new(IVerge::default());
let draft = DraftNew::from(black_box(verge));
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
}
let _ = draft.discard();
});
});
}
criterion_group!(
benches,
// bench_apply_old,
// bench_discard_old,
bench_apply_new,
bench_discard_new
);
criterion_main!(benches);
+2 -1
View File
@@ -17,6 +17,7 @@
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",
"core:window:allow-set-theme"
"core:window:allow-set-theme",
"notification:default"
]
}
+236
View File
@@ -0,0 +1,236 @@
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#"x86_64-unknown-linux-musl",
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory databases are cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs"
# The url(s) of the advisory databases to use
#db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
"RUSTSEC-2024-0415",
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
#"MIT",
#"Apache-2.0",
#"Apache-2.0 WITH LLVM-exception",
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.85
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], crate = "adler32" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The package spec the clarification applies to
#crate = "ring"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
]
# List of crates to deny
deny = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = []
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
bitbucket = []
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

+179 -47
View File
@@ -523,7 +523,7 @@ FunctionEnd
${If} $0 == 0
Push $0
${If} $1 == 0
DetailPrint "Restart Clash Verge Service..."
DetailPrint "Restart ${PRODUCTNAME} Service..."
SimpleSC::StartService "clash_verge_service" "" 30
${EndIf}
${ElseIf} $0 != 0
@@ -549,20 +549,20 @@ FunctionEnd
${If} $0 == 0
Push $0
${If} $1 == 1
DetailPrint "Stop Clash Verge Service..."
DetailPrint "Stop ${PRODUCTNAME} Service..."
SimpleSC::StopService "clash_verge_service" 1 30
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
${If} $0 == 0
DetailPrint "Removing Clash Verge Service..."
DetailPrint "Removing ${PRODUCTNAME} Service..."
SimpleSC::RemoveService "clash_verge_service"
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
MessageBox MB_OK|MB_ICONSTOP "${PRODUCTNAME} Service Stop Error ($0)"
${EndIf}
${ElseIf} $1 == 0
DetailPrint "Removing Clash Verge Service..."
DetailPrint "Removing ${PRODUCTNAME} Service..."
SimpleSC::RemoveService "clash_verge_service"
${EndIf}
${ElseIf} $0 != 0
@@ -661,7 +661,7 @@ SectionEnd
Pop $R0
${If} $R0 = 0
IfSilent kill 0
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|}
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "${PRODUCTNAME} 正在运行 $\n点击确定以终止运行" IDOK kill IDCANCEL cancel ${|}
kill:
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
@@ -691,8 +691,6 @@ SectionEnd
app_check_done:
!macroend
Var VC_REDIST_URL
Var VC_REDIST_EXE
@@ -701,25 +699,25 @@ Section CheckAndInstallVSRuntime
${If} ${IsNativeARM64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.arm64.exe"
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${ElseIf} ${RunningX64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x64.exe"
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${Else}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x86.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x86.exe"
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${EndIf}
@@ -754,30 +752,44 @@ Section CheckAndInstallVSRuntime
${Else}
DetailPrint "Visual C++ Redistributable 下载失败"
${EndIf}
Done:
SectionEnd
Section Install
SetOutPath $INSTDIR
nsExec::Exec 'netsh int tcp res'
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
; 修复系统级启动文件夹
CreateDirectory "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"
DetailPrint "系统级启动文件夹已确保存在"
; 修复用户级启动文件夹
SetShellVarContext current
StrCpy $0 "$SMPROGRAMS\Startup"
CreateDirectory "$0"
DetailPrint "已确保用户级启动文件夹存在: $0"
; 删除 window-state.json 文件 .window-state.json 文件
DetailPrint "开始删除 window-state.json or .window-state.json"
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
SetRegView 64
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge"
@@ -788,7 +800,7 @@ Section Install
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
@@ -798,7 +810,7 @@ Section Install
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Copy main executable
File "${MAINBINARYSRCPATH}"
@@ -916,26 +928,29 @@ FunctionEnd
!macroend
Section Uninstall
;删除 window-state.json 文件
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
!insertmacro RemoveVergeService
; 删除 window-state.json 文件 .window-state.json 文件
DetailPrint "开始删除 window-state.json or .window-state.json"
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge"
@@ -946,7 +961,7 @@ Section Uninstall
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
@@ -978,24 +993,146 @@ Section Uninstall
{{/each}}
RMDir "$INSTDIR"
; 删除固定栏
!insertmacro DeleteAppUserModelId
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
!insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk"
; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
; 删除所有用户的桌面快捷方式
DetailPrint "开始删除所有用户桌面的 ${PRODUCTNAME} 快捷方式..."
; 删除公共桌面快捷方式
Delete "C:\Users\Public\Desktop\Clash Verge.lnk"
Delete "C:\Users\Public\Desktop\clash-verge.lnk"
; 枚举所有用户配置文件目录
SetRegView 64
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" ""
; 初始化循环
StrCpy $R1 0
Loop:
EnumRegKey $R2 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $R1
${If} $R2 == ""
Goto Done
${EndIf}
; 读取用户配置文件路径
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$R2" "ProfileImagePath"
${If} $R3 != ""
; 构建用户桌面路径
StrCpy $R4 "$R3\Desktop"
; 删除该用户桌面的快捷方式
Delete "$R4\Clash Verge.lnk"
Delete "$R4\clash-verge.lnk"
DetailPrint "尝试删除用户 '$R3' 桌面的 ${PRODUCTNAME} 快捷方式"
${EndIf}
; 递增循环计数器
IntOp $R1 $R1 + 1
Goto Loop
Done:
DetailPrint "所有用户桌面快捷方式删除完成"
; 删除用户级开始菜单中的应用程序文件夹和快捷方式
DetailPrint "删除用户级开始菜单中的应用程序文件夹和快捷方式..."
RMDir /r /REBOOTOK "$SMPROGRAMS\Clash Verge"
RMDir /r /REBOOTOK "$SMPROGRAMS\clash-verge"
DetailPrint "删除用户级开始菜单中的应用程序文件夹和快捷方式完成"
; 删除系统级开始菜单中的应用程序文件夹和快捷方式
DetailPrint "删除系统级开始菜单中的应用程序文件夹和快捷方式..."
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge"
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\clash-verge"
DetailPrint "删除系统级开始菜单中的应用程序文件夹和快捷方式完成"
; 删除所有带 Clash Verge 或 clash-verge 的注册表项
DetailPrint "开始清理所有 ${PRODUCTNAME} 相关的注册表项..."
; 设置注册表查看模式 (64位)
SetRegView 64
; 清理 CurrentVersion\Run 中的自启动项
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
DeleteRegValue HKCU "$R1" "Clash Verge"
DeleteRegValue HKCU "$R1" "clash-verge"
DeleteRegValue HKLM "$R1" "Clash Verge"
DeleteRegValue HKLM "$R1" "clash-verge"
; 清理 App Paths
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Clash Verge.exe"
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\clash-verge.exe"
; 删除指定的注册表路径
DeleteRegKey HKLM "Software\Clash Verge Rev"
DeleteRegKey HKCU "Software\Clash Verge Rev"
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\ClashVerge"
DeleteRegKey HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Clash Verge"
; 清理 Uninstall 信息
StrCpy $R1 0
EnumUninstallLoop:
EnumRegKey $R2 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $R1
${If} $R2 == ""
Goto EnumUninstallDone
${EndIf}
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$R2" "DisplayName"
${If} $R3 != ""
StrCmp $R3 "Clash Verge" 0 +3
StrCmp $R3 "clash-verge" 0 +2
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$R2"
${EndIf}
IntOp $R1 $R1 + 1
Goto EnumUninstallLoop
EnumUninstallDone:
; 清理用户特定的注册表项
StrCpy $R1 0
EnumHKCULoop:
EnumRegKey $R2 HKCU "SOFTWARE" $R1
${If} $R2 == ""
Goto EnumHKCUDone
${EndIf}
ReadRegStr $R3 HKCU "SOFTWARE\$R2" ""
${If} $R3 != ""
StrCmp $R3 "Clash Verge" 0 +3
StrCmp $R3 "clash-verge" 0 +2
DeleteRegKey HKCU "SOFTWARE\$R2"
${EndIf}
IntOp $R1 $R1 + 1
Goto EnumHKCULoop
EnumHKCUDone:
; 清理系统范围的注册表项
StrCpy $R1 0
EnumHKLMLoop:
EnumRegKey $R2 HKLM "SOFTWARE" $R1
${If} $R2 == ""
Goto EnumHKLMDone
${EndIf}
ReadRegStr $R3 HKLM "SOFTWARE\$R2" ""
${If} $R3 != ""
StrCmp $R3 "Clash Verge" 0 +3
StrCmp $R3 "clash-verge" 0 +2
DeleteRegKey HKLM "SOFTWARE\$R2"
${EndIf}
IntOp $R1 $R1 + 1
Goto EnumHKLMLoop
EnumHKLMDone:
DetailPrint "注册表清理完成"
; Remove desktop shortcuts
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式
Delete "$DESKTOP\clash-verge.lnk"
; Remove registry information for add/remove programs
!if "${INSTALLMODE}" == "both"
@@ -1015,10 +1152,6 @@ Section Uninstall
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
${EndIf}
;删除 window-state.json 文件
SetShellVarContext current
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
${GetOptions} $CMDLINE "/P" $R0
IfErrors +2 0
SetAutoClose true
@@ -1051,7 +1184,6 @@ FunctionEnd
System::Free $4
System::Free $5
${IPropertyStore::Commit} $2 ""
${IUnknown::Release} $2 ""
${IPersistFile::Save} $1 '("${shortcut}",1)'
${EndIf}
${IUnknown::Release} $1 ""
+17 -18
View File
@@ -4,18 +4,18 @@ use crate::{
utils::{dirs, logging::Type},
wrap_err,
};
use tauri::Manager;
use tauri::{AppHandle, Manager};
/// 打开应用程序所在目录
#[tauri::command]
pub fn open_app_dir() -> CmdResult<()> {
pub async fn open_app_dir() -> CmdResult<()> {
let app_dir = wrap_err!(dirs::app_home_dir())?;
wrap_err!(open::that(app_dir))
}
/// 打开核心所在目录
#[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 = core_dir.parent().ok_or("failed to get core dir")?;
wrap_err!(open::that(core_dir))
@@ -23,7 +23,7 @@ pub fn open_core_dir() -> CmdResult<()> {
/// 打开日志目录
#[tauri::command]
pub fn open_logs_dir() -> CmdResult<()> {
pub async fn open_logs_dir() -> CmdResult<()> {
let log_dir = wrap_err!(dirs::app_logs_dir())?;
wrap_err!(open::that(log_dir))
}
@@ -36,7 +36,7 @@ pub fn open_web_url(url: String) -> CmdResult<()> {
/// 打开/关闭开发者工具
#[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 !window.is_devtools_open() {
window.open_devtools();
@@ -48,14 +48,14 @@ pub fn open_devtools(app_handle: tauri::AppHandle) {
/// 退出应用
#[tauri::command]
pub fn exit_app() {
feat::quit();
pub async fn exit_app() {
feat::quit().await;
}
/// 重启应用
#[tauri::command]
pub async fn restart_app() -> CmdResult<()> {
feat::restart_app();
feat::restart_app().await;
Ok(())
}
@@ -121,9 +121,8 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
Err(_) => {
if icon_path.exists() {
return Ok(icon_path.to_string_lossy().to_string());
} else {
return Err("Failed to create temporary file".into());
}
return Err("Failed to create temporary file".into());
}
};
@@ -147,7 +146,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
Ok(icon_path.to_string_lossy().to_string())
} else {
let _ = std::fs::remove_file(&temp_path);
Err(format!("下载的内容不是有效图片: {}", url))
Err(format!("下载的内容不是有效图片: {url}"))
}
}
@@ -210,16 +209,16 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
#[tauri::command]
pub fn notify_ui_ready() -> CmdResult<()> {
log::info!(target: "app", "前端UI已准备就绪");
crate::utils::resolve::mark_ui_ready();
crate::utils::resolve::ui::mark_ui_ready();
Ok(())
}
/// UI加载阶段
#[tauri::command]
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() {
"NotStarted" => UiReadyStage::NotStarted,
@@ -228,12 +227,12 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
"Ready" => UiReadyStage::Ready,
_ => {
log::warn!(target: "app", "未知的UI加载阶段: {}", stage);
return Err(format!("未知的UI加载阶段: {}", stage));
log::warn!(target: "app", "未知的UI加载阶段: {stage}");
return Err(format!("未知的UI加载阶段: {stage}"));
}
};
crate::utils::resolve::update_ui_ready_stage(stage_enum);
crate::utils::resolve::ui::update_ui_ready_stage(stage_enum);
Ok(())
}
@@ -241,6 +240,6 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
#[tauri::command]
pub fn reset_ui_ready_state() -> CmdResult<()> {
log::info!(target: "app", "重置UI就绪状态");
crate::utils::resolve::reset_ui_ready();
crate::utils::resolve::ui::reset_ui_ready();
Ok(())
}
+462 -108
View File
@@ -1,20 +1,33 @@
use super::CmdResult;
use crate::{
config::*, core::*, feat, module::mihomo::MihomoManager, process::AsyncHandler, wrap_err,
config::Config,
core::{handle, CoreManager},
};
use serde_yaml::Mapping;
use crate::{
config::*,
feat,
ipc::{self, IpcManager},
logging,
state::proxy::ProxyRequestCache,
utils::logging::Type,
wrap_err,
};
use serde_yaml_ng::Mapping;
use std::time::Duration;
const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
/// 复制Clash环境变量
#[tauri::command]
pub fn copy_clash_env() -> CmdResult {
feat::copy_clash_env();
pub async fn copy_clash_env() -> CmdResult {
feat::copy_clash_env().await;
Ok(())
}
/// 获取Clash信息
#[tauri::command]
pub fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().latest().get_client_info())
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().await.latest_ref().get_client_info())
}
/// 修改Clash配置
@@ -26,14 +39,14 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
/// 修改Clash模式
#[tauri::command]
pub async fn patch_clash_mode(payload: String) -> CmdResult {
feat::change_clash_mode(payload);
feat::change_clash_mode(payload).await;
Ok(())
}
/// 切换Clash核心
#[tauri::command]
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
log::info!(target: "app", "changing core to {clash_core}");
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global()
.change_core(Some(clash_core.clone()))
@@ -43,14 +56,18 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
// 切换内核后重启内核
match CoreManager::global().restart_core().await {
Ok(_) => {
log::info!(target: "app", "core changed and restarted to {clash_core}");
logging!(
info,
Type::Core,
"core changed and restarted to {clash_core}"
);
handle::Handle::notice_message("config_core::change_success", &clash_core);
handle::Handle::refresh_clash();
Ok(None)
}
Err(err) => {
let error_msg = format!("Core changed but failed to restart: {}", err);
log::error!(target: "app", "{}", error_msg);
let error_msg = format!("Core changed but failed to restart: {err}");
logging!(error, Type::Core, "{error_msg}");
handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg))
}
@@ -58,7 +75,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
}
Err(err) => {
let error_msg = err.to_string();
log::error!(target: "app", "failed to change core: {error_msg}");
logging!(error, Type::Core, "failed to change core: {error_msg}");
handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg))
}
@@ -68,19 +85,31 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
/// 启动核心
#[tauri::command]
pub async fn start_core() -> CmdResult {
wrap_err!(CoreManager::global().start_core().await)
let result = wrap_err!(CoreManager::global().start_core().await);
if result.is_ok() {
handle::Handle::refresh_clash();
}
result
}
/// 关闭核心
#[tauri::command]
pub async fn stop_core() -> CmdResult {
wrap_err!(CoreManager::global().stop_core().await)
let result = wrap_err!(CoreManager::global().stop_core().await);
if result.is_ok() {
handle::Handle::refresh_clash();
}
result
}
/// 重启核心
#[tauri::command]
pub async fn restart_core() -> CmdResult {
wrap_err!(CoreManager::global().restart_core().await)
let result = wrap_err!(CoreManager::global().restart_core().await);
if result.is_ok() {
handle::Handle::refresh_clash();
}
result
}
/// 获取代理延迟
@@ -90,23 +119,32 @@ pub async fn clash_api_get_proxy_delay(
url: Option<String>,
timeout: i32,
) -> CmdResult<serde_json::Value> {
MihomoManager::global()
.test_proxy_delay(&name, url, timeout)
.await
wrap_err!(
IpcManager::global()
.test_proxy_delay(&name, url, timeout)
.await
)
}
/// 测试URL延迟
#[tauri::command]
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配置到单独文件
#[tauri::command]
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
use crate::utils::dirs;
use serde_yaml;
use std::fs;
use serde_yaml_ng;
use tokio::fs;
// 获取DNS配置文件路径
let dns_path = dirs::app_home_dir()
@@ -114,104 +152,103 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
.join("dns_config.yaml");
// 保存DNS配置到文件
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
log::info!(target: "app", "DNS config saved to {:?}", dns_path);
let yaml_str = serde_yaml_ng::to_string(&dns_config).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:?}");
Ok(())
}
/// 应用或撤销DNS配置
#[tauri::command]
pub fn apply_dns_config(apply: bool) -> CmdResult {
pub async fn apply_dns_config(apply: bool) -> CmdResult {
use crate::{
config::Config,
core::{handle, CoreManager},
utils::dirs,
};
// 使用spawn来处理异步操作
AsyncHandler::spawn(move || async move {
if apply {
// 读取DNS配置文件
let dns_path = match dirs::app_home_dir() {
Ok(path) => path.join("dns_config.yaml"),
Err(e) => {
log::error!(target: "app", "Failed to get home dir: {}", e);
return;
}
};
if apply {
// 读取DNS配置文件
let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())?
.join("dns_config.yaml");
if !dns_path.exists() {
log::warn!(target: "app", "DNS config file not found");
return;
}
let dns_yaml = match std::fs::read_to_string(&dns_path) {
Ok(content) => content,
Err(e) => {
log::error!(target: "app", "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) => {
log::error!(target: "app", "Failed to parse DNS config: {}", e);
return;
}
};
log::info!(target: "app", "Applying DNS config from file");
// 重新生成配置,确保DNS配置被正确应用
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
Config::runtime()
.latest()
.patch_config(patch_config.clone());
// 首先重新生成配置
if let Err(err) = Config::generate().await {
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
return;
}
// 然后应用新配置
if let Err(err) = CoreManager::global().update_config().await {
log::error!(target: "app", "Failed to apply config with DNS: {}", err);
} else {
log::info!(target: "app", "DNS config successfully applied");
handle::Handle::refresh_clash();
}
} else {
// 当关闭DNS设置时,不需要对配置进行任何修改
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
log::info!(target: "app", "DNS settings disabled, regenerating config");
// 重新生成配置
if let Err(err) = Config::generate().await {
log::error!(target: "app", "Failed to regenerate config: {}", err);
return;
}
// 应用新配置
match CoreManager::global().update_config().await {
Ok(_) => {
log::info!(target: "app", "Config regenerated successfully");
handle::Handle::refresh_clash();
}
Err(err) => {
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
}
}
if !dns_path.exists() {
logging!(warn, Type::Config, "DNS config file not found");
return Err("DNS config file not found".into());
}
});
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(())
}
@@ -232,17 +269,19 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
#[tauri::command]
pub async fn get_dns_config_content() -> CmdResult<String> {
use crate::utils::dirs;
use std::fs;
use tokio::fs;
let dns_path = dirs::app_home_dir()
.map_err(|e| e.to_string())?
.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());
}
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)
}
@@ -267,3 +306,318 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
Err(e) => Err(e.to_string()),
}
}
/// 获取Clash版本信息
#[tauri::command]
pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_version().await)
}
/// 获取Clash配置
#[tauri::command]
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
let manager = IpcManager::global();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("clash_config", "default");
let value = cache
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
manager.get_config().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch clash config: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
}
/// 强制刷新Clash配置缓存
#[tauri::command]
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("clash_config", "default");
cache.map.remove(&key);
get_clash_config().await
}
/// 更新地理数据
#[tauri::command]
pub async fn update_geo_data() -> CmdResult {
wrap_err!(IpcManager::global().update_geo_data().await)
}
/// 升级Clash核心
#[tauri::command]
pub async fn upgrade_clash_core() -> CmdResult {
wrap_err!(IpcManager::global().upgrade_core().await)
}
/// 获取规则
#[tauri::command]
pub async fn get_clash_rules() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_rules().await)
}
/// 更新代理选择
#[tauri::command]
pub async fn update_proxy_choice(group: String, proxy: String) -> CmdResult {
wrap_err!(IpcManager::global().update_proxy(&group, &proxy).await)
}
/// 获取代理提供者
#[tauri::command]
pub async fn get_proxy_providers() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_providers_proxies().await)
}
/// 获取规则提供者
#[tauri::command]
pub async fn get_rule_providers() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_rule_providers().await)
}
/// 代理提供者健康检查
#[tauri::command]
pub async fn proxy_provider_health_check(name: String) -> CmdResult {
wrap_err!(
IpcManager::global()
.proxy_provider_health_check(&name)
.await
)
}
/// 更新代理提供者
#[tauri::command]
pub async fn update_proxy_provider(name: String) -> CmdResult {
wrap_err!(IpcManager::global().update_proxy_provider(&name).await)
}
/// 更新规则提供者
#[tauri::command]
pub async fn update_rule_provider(name: String) -> CmdResult {
wrap_err!(IpcManager::global().update_rule_provider(&name).await)
}
/// 获取连接
#[tauri::command]
pub async fn get_clash_connections() -> CmdResult<serde_json::Value> {
wrap_err!(IpcManager::global().get_connections().await)
}
/// 删除连接
#[tauri::command]
pub async fn delete_clash_connection(id: String) -> CmdResult {
wrap_err!(IpcManager::global().delete_connection(&id).await)
}
/// 关闭所有连接
#[tauri::command]
pub async fn close_all_clash_connections() -> CmdResult {
wrap_err!(IpcManager::global().close_all_connections().await)
}
/// 获取流量数据 (使用新的IPC流式监控)
#[tauri::command]
pub async fn get_traffic_data() -> CmdResult<serde_json::Value> {
let traffic = crate::ipc::get_current_traffic().await;
let result = serde_json::json!({
"up": traffic.total_up,
"down": traffic.total_down,
"up_rate": traffic.up_rate,
"down_rate": traffic.down_rate,
"last_updated": traffic.last_updated.elapsed().as_secs()
});
Ok(result)
}
/// 获取内存数据 (使用新的IPC流式监控)
#[tauri::command]
pub async fn get_memory_data() -> CmdResult<serde_json::Value> {
let memory = crate::ipc::get_current_memory().await;
let usage_percent = if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
};
let result = serde_json::json!({
"inuse": memory.inuse,
"oslimit": memory.oslimit,
"usage_percent": usage_percent,
"last_updated": memory.last_updated.elapsed().as_secs()
});
Ok(result)
}
/// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留)
#[tauri::command]
pub async fn start_traffic_service() -> CmdResult {
logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)");
// 新的IPC监控在首次访问时自动启动
// 触发一次访问以确保监控器已初始化
let _ = crate::ipc::get_current_traffic().await;
let _ = crate::ipc::get_current_memory().await;
logging!(info, Type::Ipc, "IPC流式监控已激活");
Ok(())
}
/// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留)
#[tauri::command]
pub async fn stop_traffic_service() -> CmdResult {
logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)");
// 新的IPC监控是持久的,无需显式停止
logging!(info, Type::Ipc, "IPC流式监控继续运行");
Ok(())
}
/// 获取格式化的流量数据 (包含单位,便于前端显示)
#[tauri::command]
pub async fn get_formatted_traffic_data() -> CmdResult<serde_json::Value> {
logging!(trace, Type::Ipc, "获取格式化流量数据");
let (up_rate, down_rate, total_up, total_down, is_fresh) =
crate::ipc::get_formatted_traffic().await;
let result = serde_json::json!({
"up_rate_formatted": up_rate,
"down_rate_formatted": down_rate,
"total_up_formatted": total_up,
"total_down_formatted": total_down,
"is_fresh": is_fresh
});
logging!(
debug,
Type::Ipc,
"格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"
);
Ok(result)
}
/// 获取格式化的内存数据 (包含单位,便于前端显示)
#[tauri::command]
pub async fn get_formatted_memory_data() -> CmdResult<serde_json::Value> {
logging!(info, Type::Ipc, "获取格式化内存数据");
let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await;
let result = serde_json::json!({
"inuse_formatted": inuse,
"oslimit_formatted": oslimit,
"usage_percent": usage_percent,
"is_fresh": is_fresh
});
logging!(
debug,
Type::Ipc,
"格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"
);
Ok(result)
}
/// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态)
#[tauri::command]
pub async fn get_system_monitor_overview() -> CmdResult<serde_json::Value> {
logging!(debug, Type::Ipc, "获取系统监控概览");
// 并发获取流量和内存数据
let (traffic, memory) = tokio::join!(
crate::ipc::get_current_traffic(),
crate::ipc::get_current_memory()
);
let (traffic_formatted, memory_formatted) = tokio::join!(
crate::ipc::get_formatted_traffic(),
crate::ipc::get_formatted_memory()
);
let traffic_is_fresh = traffic.last_updated.elapsed().as_secs() < 5;
let memory_is_fresh = memory.last_updated.elapsed().as_secs() < 10;
let result = serde_json::json!({
"traffic": {
"raw": {
"up": traffic.total_up,
"down": traffic.total_down,
"up_rate": traffic.up_rate,
"down_rate": traffic.down_rate
},
"formatted": {
"up_rate": traffic_formatted.0,
"down_rate": traffic_formatted.1,
"total_up": traffic_formatted.2,
"total_down": traffic_formatted.3
},
"is_fresh": traffic_is_fresh
},
"memory": {
"raw": {
"inuse": memory.inuse,
"oslimit": memory.oslimit,
"usage_percent": if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
}
},
"formatted": {
"inuse": memory_formatted.0,
"oslimit": memory_formatted.1,
"usage_percent": memory_formatted.2
},
"is_fresh": memory_is_fresh
},
"overall_status": if traffic_is_fresh && memory_is_fresh { "healthy" } else { "stale" }
});
Ok(result)
}
/// 获取代理组延迟
#[tauri::command]
pub async fn get_group_proxy_delays(
group_name: String,
url: Option<String>,
timeout: Option<i32>,
) -> CmdResult<serde_json::Value> {
wrap_err!(
IpcManager::global()
.get_group_proxy_delays(&group_name, url, timeout.unwrap_or(10000))
.await
)
}
/// 检查调试是否启用
#[tauri::command]
pub async fn is_clash_debug_enabled() -> CmdResult<bool> {
match IpcManager::global().is_debug_enabled().await {
Ok(enabled) => Ok(enabled),
Err(_) => Ok(false),
}
}
/// 垃圾回收
#[tauri::command]
pub async fn clash_gc() -> CmdResult {
wrap_err!(IpcManager::global().gc().await)
}
/// 获取日志 (使用新的流式实现)
#[tauri::command]
pub async fn get_clash_logs() -> CmdResult<serde_json::Value> {
Ok(ipc::get_logs_json().await)
}
/// 启动日志监控
#[tauri::command]
pub async fn start_logs_monitoring(level: Option<String>) -> CmdResult {
ipc::start_logs_monitoring(level).await;
Ok(())
}
/// 停止日志监控
#[tauri::command]
pub async fn stop_logs_monitoring() -> CmdResult {
ipc::stop_logs_monitoring().await;
Ok(())
}
/// 清除日志
#[tauri::command]
pub async fn clear_logs() -> CmdResult {
ipc::clear_logs().await;
Ok(())
}
+2 -2
View File
@@ -4,12 +4,12 @@ use super::CmdResult;
#[tauri::command]
pub async fn entry_lightweight_mode() -> CmdResult {
lightweight::entry_lightweight_mode();
lightweight::entry_lightweight_mode().await;
Ok(())
}
#[tauri::command]
pub async fn exit_lightweight_mode() -> CmdResult {
lightweight::exit_lightweight_mode();
lightweight::exit_lightweight_mode().await;
Ok(())
}
+381 -98
View File
@@ -1,3 +1,4 @@
use crate::{logging, utils::logging::Type};
use chrono::Local;
use regex::Regex;
use reqwest::Client;
@@ -37,7 +38,7 @@ fn country_code_to_emoji(country_code: &str) -> String {
let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
char::from_u32(c1)
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{}{}", c1, c2)))
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}")))
.unwrap_or_default()
}
@@ -163,7 +164,7 @@ async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
map.get("loc").map(|loc| {
let emoji = country_code_to_emoji(loc);
format!("{}{}", emoji, loc)
format!("{emoji}{loc}")
})
} else {
None
@@ -250,12 +251,28 @@ async fn check_gemini(client: &Client) -> UnlockItem {
let status = if is_ok { "Yes" } else { "No" };
// 尝试提取国家代码
let re = Regex::new(r#",2,1,200,"([A-Z]{3})""#).unwrap();
let re = match Regex::new(r#",2,1,200,"([A-Z]{3})""#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Gemini regex: {}",
e
);
return UnlockItem {
name: "Gemini".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let region = re.captures(&body).and_then(|caps| {
caps.get(1).map(|m| {
let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code);
format!("{}{}", emoji, country_code)
format!("{emoji}{country_code}")
})
});
@@ -303,12 +320,28 @@ async fn check_youtube_premium(client: &Client) -> UnlockItem {
}
} else if body_lower.contains("ad-free") {
// 尝试解析国家代码
let re = Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#).unwrap();
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile YouTube Premium regex: {}",
e
);
return UnlockItem {
name: "Youtube Premium".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let region = re.captures(&body).and_then(|caps| {
caps.get(1).map(|m| {
let country_code = m.as_str().trim();
let emoji = country_code_to_emoji(country_code);
format!("{}{}", emoji, country_code)
format!("{emoji}{country_code}")
})
});
@@ -350,11 +383,16 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
let cookie_store = Arc::new(reqwest::cookie::Jar::default());
// 使用带Cookie的客户端
let client_with_cookies = reqwest::Client::builder()
let client_with_cookies = match reqwest::Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
.cookie_provider(Arc::clone(&cookie_store))
.build()
.unwrap_or_else(|_| client.clone());
.build() {
Ok(client) => client,
Err(e) => {
logging!(error, Type::Network, "Failed to create client with cookies for Bahamut Anime: {}", e);
client.clone()
}
};
// 第一步:获取设备ID (会自动保存Cookie)
let device_url = "https://ani.gamer.com.tw/ajax/getdeviceid.php";
@@ -363,10 +401,21 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
match response.text().await {
Ok(text) => {
// 使用正则提取deviceid
let re = Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#).unwrap();
re.captures(&text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
.unwrap_or_default()
match Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#) {
Ok(re) => re
.captures(&text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
.unwrap_or_default(),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile deviceid regex for Bahamut Anime: {}",
e
);
String::new()
}
}
}
Err(_) => String::new(),
}
@@ -384,10 +433,8 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
}
// 第二步:使用设备ID检查访问权限 (使用相同的Cookie)
let url = format!(
"https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={}",
device_id
);
let url =
format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
let token_result = match client_with_cookies.get(&url).send().await {
Ok(response) => {
@@ -423,17 +470,25 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
.await
{
Ok(response) => match response.text().await {
Ok(body) => {
let region_re = Regex::new(r#"data-geo="([^"]+)"#).unwrap();
region_re
Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) {
Ok(region_re) => region_re
.captures(&body)
.and_then(|caps| caps.get(1))
.map(|m| {
let country_code = m.as_str();
let emoji = country_code_to_emoji(country_code);
format!("{}{}", emoji, country_code)
})
}
format!("{emoji}{country_code}")
}),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile region regex for Bahamut Anime: {}",
e
);
None
}
},
Err(_) => None,
},
Err(_) => None,
@@ -470,7 +525,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
// 检查连接失败情况
if let Err(e) = &result1 {
eprintln!("Netflix请求错误: {}", e);
eprintln!("Netflix请求错误: {e}");
return UnlockItem {
name: "Netflix".to_string(),
status: "Failed".to_string(),
@@ -487,7 +542,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
.await;
if let Err(e) = &result2 {
eprintln!("Netflix请求错误: {}", e);
eprintln!("Netflix请求错误: {e}");
return UnlockItem {
name: "Netflix".to_string(),
status: "Failed".to_string(),
@@ -497,8 +552,40 @@ async fn check_netflix(client: &Client) -> UnlockItem {
}
// 获取状态码
let status1 = result1.unwrap().status().as_u16();
let status2 = result2.unwrap().status().as_u16();
let status1 = match result1 {
Ok(response) => response.status().as_u16(),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Netflix response 1: {}",
e
);
return UnlockItem {
name: "Netflix".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let status2 = match result2 {
Ok(response) => response.status().as_u16(),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Netflix response 2: {}",
e
);
return UnlockItem {
name: "Netflix".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
// 根据状态码判断解锁状况
if status1 == 404 && status2 == 404 {
@@ -541,7 +628,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region_code)),
region: Some(format!("{emoji}{region_code}")),
check_time: Some(get_local_date_string()),
};
}
@@ -557,7 +644,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
}
}
Err(e) => {
eprintln!("获取Netflix区域信息失败: {}", e);
eprintln!("获取Netflix区域信息失败: {e}");
UnlockItem {
name: "Netflix".to_string(),
status: "Yes (但无法获取区域)".to_string(),
@@ -570,7 +657,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
// 其他未知错误状态
UnlockItem {
name: "Netflix".to_string(),
status: format!("Failed (状态码: {}_{}", status1, status2),
status: format!("Failed (状态码: {status1}_{status2}"),
region: None,
check_time: Some(get_local_date_string()),
}
@@ -614,7 +701,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Netflix".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, country)),
region: Some(format!("{emoji}{country}")),
check_time: Some(get_local_date_string()),
};
}
@@ -631,7 +718,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
}
}
Err(e) => {
eprintln!("解析Fast.com API响应失败: {}", e);
eprintln!("解析Fast.com API响应失败: {e}");
UnlockItem {
name: "Netflix".to_string(),
status: "Failed (解析错误)".to_string(),
@@ -642,7 +729,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
}
}
Err(e) => {
eprintln!("Fast.com API请求失败: {}", e);
eprintln!("Fast.com API请求失败: {e}");
UnlockItem {
name: "Netflix".to_string(),
status: "Failed (CDN API)".to_string(),
@@ -687,7 +774,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
}
let device_response = device_result.unwrap();
let device_response = match device_result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Disney+ device response: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Network Connection)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
// 检查是否 403 错误
if device_response.status().as_u16() == 403 {
@@ -712,7 +815,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
// 提取 assertion
let re = Regex::new(r#""assertion"\s*:\s*"([^"]+)"#).unwrap();
let re = match Regex::new(r#""assertion"\s*:\s*"([^"]+)"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile assertion regex for Disney+: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Regex Error)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let assertion = match re.captures(&device_body) {
Some(caps) => caps.get(1).map(|m| m.as_str().to_string()),
None => None,
@@ -731,7 +850,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
let token_url = "https://disney.api.edge.bamgrid.com/token";
// 构建请求体 - 使用表单数据格式而非 JSON
let assertion_str = assertion.unwrap();
let assertion_str = match assertion {
Some(assertion) => assertion,
None => {
logging!(error, Type::Network, "No assertion found for Disney+");
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (No Assertion)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let token_body = [
(
"grant_type",
@@ -764,7 +894,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
}
let token_response = token_result.unwrap();
let token_response = match token_result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Disney+ token response: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Network Connection)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let token_status = token_response.status();
// 保存原始响应用于调试
@@ -800,10 +946,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
.map(|s| s.to_string()),
Err(_) => {
// 如果 JSON 解析失败,尝试使用正则表达式
let refresh_token_re = Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#).unwrap();
refresh_token_re
.captures(&token_body_text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
match Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#) {
Ok(refresh_token_re) => refresh_token_re
.captures(&token_body_text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile refresh_token regex for Disney+: {}",
e
);
None
}
}
}
};
@@ -827,7 +983,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
// GraphQL API 通常接受 JSON 格式
let graphql_payload = format!(
r#"{{"query":"mutation refreshToken($input: RefreshTokenInput!) {{ refreshToken(refreshToken: $input) {{ activeSession {{ sessionId }} }} }}","variables":{{"input":{{"refreshToken":"{}"}}}}}}"#,
refresh_token.unwrap()
refresh_token.unwrap_or_default()
);
let graphql_result = client
@@ -859,21 +1015,56 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
// 解析 GraphQL 响应获取区域信息
let graphql_response = graphql_result.unwrap();
let graphql_response = match graphql_result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Disney+ GraphQL response: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Network Connection)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let graphql_status = graphql_response.status();
let graphql_body_text = (graphql_response.text().await).unwrap_or_default();
let graphql_body_text = match graphql_response.text().await {
Ok(text) => text,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to read Disney+ GraphQL response text: {}",
e
);
String::new()
}
};
// 如果 GraphQL 响应为空或明显错误,尝试直接获取区域信息
if graphql_body_text.is_empty() || graphql_status.as_u16() >= 400 {
// 尝试直接从主页获取区域信息
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
Ok(response) => match response.text().await {
Ok(body) => {
let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap();
region_re
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
Ok(region_re) => region_re
.captures(&body)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Disney+ main page region regex: {}",
e
);
None
}
},
Err(_) => None,
},
Err(_) => None,
@@ -884,7 +1075,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Disney+".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{} (from main page)", emoji, region)),
region: Some(format!("{emoji}{region} (from main page)")),
check_time: Some(get_local_date_string()),
};
}
@@ -900,28 +1091,59 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
region: None,
check_time: Some(get_local_date_string()),
};
} else {
}
return UnlockItem {
name: "Disney+".to_string(),
status: format!(
"Failed (GraphQL error: {}, status: {})",
graphql_body_text.chars().take(50).collect::<String>() + "...",
graphql_status.as_u16()
),
region: None,
check_time: Some(get_local_date_string()),
};
}
// 提取国家代码
let region_re = match Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Disney+ countryCode regex: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: format!(
"Failed (GraphQL error: {}, status: {})",
graphql_body_text.chars().take(50).collect::<String>() + "...",
graphql_status.as_u16()
),
status: "Failed (Regex Error)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
}
// 提取国家代码
let region_re = Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#).unwrap();
};
let region_code = region_re
.captures(&graphql_body_text)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
// 提取支持状态
let supported_re = Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#).unwrap();
let supported_re = match Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Disney+ supported location regex: {}",
e
);
return UnlockItem {
name: "Disney+".to_string(),
status: "Failed (Regex Error)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let in_supported_location = supported_re
.captures(&graphql_body_text)
.and_then(|caps| caps.get(1).map(|m| m.as_str() == "true"));
@@ -931,12 +1153,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
// 尝试直接从主页获取区域信息
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
Ok(response) => match response.text().await {
Ok(body) => {
let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap();
region_re
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
Ok(region_re) => region_re
.captures(&body)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Disney+ main page region regex: {}",
e
);
None
}
},
Err(_) => None,
},
Err(_) => None,
@@ -947,7 +1177,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Disney+".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{} (from main page)", emoji, region)),
region: Some(format!("{emoji}{region} (from main page)")),
check_time: Some(get_local_date_string()),
};
}
@@ -960,7 +1190,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
};
}
let region = region_code.unwrap();
let region = match region_code {
Some(code) => code,
None => {
logging!(error, Type::Network, "No region code found for Disney+");
return UnlockItem {
name: "Disney+".to_string(),
status: "No".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
// 判断日本地区
if region == "JP" {
@@ -968,7 +1209,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Disney+".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region)),
region: Some(format!("{emoji}{region}")),
check_time: Some(get_local_date_string()),
};
}
@@ -990,7 +1231,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
UnlockItem {
name: "Disney+".to_string(),
status: "Soon".to_string(),
region: Some(format!("{}{}(即将上线)", emoji, region)),
region: Some(format!("{emoji}{region}(即将上线)")),
check_time: Some(get_local_date_string()),
}
}
@@ -999,13 +1240,13 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
UnlockItem {
name: "Disney+".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region)),
region: Some(format!("{emoji}{region}")),
check_time: Some(get_local_date_string()),
}
}
None => UnlockItem {
name: "Disney+".to_string(),
status: format!("Failed (Error: Unknown region status for {})", region),
status: format!("Failed (Error: Unknown region status for {region})"),
region: None,
check_time: Some(get_local_date_string()),
},
@@ -1030,13 +1271,47 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
}
// 解析响应内容
match result.unwrap().text().await {
let response = match result {
Ok(response) => response,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to get Prime Video response: {}",
e
);
return UnlockItem {
name: "Prime Video".to_string(),
status: "Failed (Network Connection)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
match response.text().await {
Ok(body) => {
// 检查是否被地区限制
let is_blocked = body.contains("isServiceRestricted");
// 提取地区信息
let region_re = Regex::new(r#""currentTerritory":"([^"]+)"#).unwrap();
let region_re = match Regex::new(r#""currentTerritory":"([^"]+)"#) {
Ok(re) => re,
Err(e) => {
logging!(
error,
Type::Network,
"Failed to compile Prime Video region regex: {}",
e
);
return UnlockItem {
name: "Prime Video".to_string(),
status: "Failed (Regex Error)".to_string(),
region: None,
check_time: Some(get_local_date_string()),
};
}
};
let region_code = region_re
.captures(&body)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
@@ -1056,7 +1331,7 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
return UnlockItem {
name: "Prime Video".to_string(),
status: "Yes".to_string(),
region: Some(format!("{}{}", emoji, region)),
region: Some(format!("{emoji}{region}")),
check_time: Some(get_local_date_string()),
};
}
@@ -1170,7 +1445,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
.connection_verbose(true) // 详细连接信息
.build() {
Ok(client) => client,
Err(e) => return Err(format!("创建HTTP客户端失败: {}", e)),
Err(e) => return Err(format!("创建HTTP客户端失败: {e}")),
};
// 创建共享的结果集
@@ -1184,8 +1459,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加哔哩哔哩大陆检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_bilibili_china_mainland(&client).await;
let mut results = results.lock().await;
@@ -1195,8 +1470,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加哔哩哔哩港澳台检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_bilibili_hk_mc_tw(&client).await;
let mut results = results.lock().await;
@@ -1206,8 +1481,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加合并的ChatGPT检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let chatgpt_results = check_chatgpt_combined(&client).await;
let mut results = results.lock().await;
@@ -1217,8 +1492,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加Gemini检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_gemini(&client).await;
let mut results = results.lock().await;
@@ -1228,8 +1503,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加YouTube Premium检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_youtube_premium(&client).await;
let mut results = results.lock().await;
@@ -1239,8 +1514,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加动画疯检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_bahamut_anime(&client).await;
let mut results = results.lock().await;
@@ -1250,8 +1525,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加 Netflix 检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_netflix(&client).await;
let mut results = results.lock().await;
@@ -1261,8 +1536,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加 Disney+ 检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_disney_plus(&client).await;
let mut results = results.lock().await;
@@ -1272,8 +1547,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 添加 Prime Video 检测任务
{
let client = client_arc.clone();
let results = results.clone();
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_prime_video(&client).await;
let mut results = results.lock().await;
@@ -1284,14 +1559,22 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
// 等待所有任务完成
while let Some(res) = tasks.join_next().await {
if let Err(e) = res {
eprintln!("任务执行失败: {}", e);
eprintln!("任务执行失败: {e}");
}
}
// 获取所有结果
let results = Arc::try_unwrap(results)
.expect("无法获取结果,可能仍有引用存在")
.into_inner();
let results = match Arc::try_unwrap(results) {
Ok(mutex) => mutex.into_inner(),
Err(_) => {
logging!(
error,
Type::Network,
"Failed to unwrap results Arc, references still exist"
);
return Err("Failed to collect results".to_string());
}
};
Ok(results)
}
-1
View File
@@ -1,6 +1,5 @@
use anyhow::Result;
// Common result type used by command functions
pub type CmdResult<T = ()> = Result<T, String>;
// Command modules
+20 -14
View File
@@ -1,17 +1,16 @@
use super::CmdResult;
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
use crate::process::AsyncHandler;
use crate::wrap_err;
use network_interface::NetworkInterface;
use serde_yaml::Mapping;
use sysproxy::{Autoproxy, Sysproxy};
use tokio::task::spawn_blocking;
use serde_yaml_ng::Mapping;
/// get the system proxy
#[tauri::command]
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
let current = spawn_blocking(Sysproxy::get_system_proxy)
.await
.map_err(|e| format!("Failed to spawn blocking task for sysproxy: {}", e))?
.map_err(|e| format!("Failed to get system proxy: {}", e))?;
log::debug!(target: "app", "异步获取系统代理配置");
let current = AsyncProxyQuery::get_system_proxy().await;
let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into());
@@ -21,21 +20,28 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
);
map.insert("bypass".into(), current.bypass.into());
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
Ok(map)
}
/// get the system proxy
/// 获取自动代理配置
#[tauri::command]
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
let current = spawn_blocking(Autoproxy::get_auto_proxy)
.await
.map_err(|e| format!("Failed to spawn blocking task for autoproxy: {}", e))?
.map_err(|e| format!("Failed to get auto proxy: {}", e))?;
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
let proxy_manager = EventDrivenProxyManager::global();
let current = proxy_manager.get_auto_proxy_cached().await;
// 异步请求更新,立即返回缓存数据
AsyncHandler::spawn(move || async move {
let _ = proxy_manager.get_auto_proxy_async().await;
});
let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into());
map.insert("url".into(), current.url.into());
map.insert("url".into(), current.url.clone().into());
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
Ok(map)
}
@@ -49,7 +55,7 @@ pub fn get_system_hostname() -> CmdResult<String> {
Ok(name) => name,
Err(os_string) => {
// 对于包含非UTF-8的主机名,使用调试格式化
let fallback = format!("{:?}", os_string);
let fallback = format!("{os_string:?}");
// 去掉可能存在的引号
fallback.trim_matches('"').to_string()
}
+358 -133
View File
@@ -1,78 +1,75 @@
use super::CmdResult;
use crate::{
config::{Config, IProfiles, PrfItem, PrfOption},
config::{
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, Config, IProfiles, PrfItem, PrfOption,
},
core::{handle, timer::Timer, tray::Tray, CoreManager},
feat, logging, ret_err,
feat, logging,
process::AsyncHandler,
ret_err,
utils::{dirs, help, logging::Type},
wrap_err,
};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
use tokio::sync::Mutex;
// 添加全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
// 全局请求序列号跟踪,用于避免队列化执行
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
/// 获取配置文件避免锁竞争
#[tauri::command]
pub async fn get_profiles() -> CmdResult<IProfiles> {
// 策略1: 尝试快速获取latest数据
let latest_result = tokio::time::timeout(
Duration::from_millis(500),
tokio::task::spawn_blocking(move || {
let profiles = Config::profiles();
let latest = profiles.latest();
IProfiles {
current: latest.current.clone(),
items: latest.items.clone(),
}
}),
)
let latest_result = tokio::time::timeout(Duration::from_millis(500), async {
let profiles = Config::profiles().await;
let latest = profiles.latest_ref();
IProfiles {
current: latest.current.clone(),
items: latest.items.clone(),
}
})
.await;
match latest_result {
Ok(Ok(profiles)) => {
Ok(profiles) => {
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => {
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
}
Err(_) => {
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
}
}
// 策略2: 如果快速获取失败,尝试获取data()
let data_result = tokio::time::timeout(
Duration::from_secs(2),
tokio::task::spawn_blocking(move || {
let profiles = Config::profiles();
let data = profiles.data();
IProfiles {
current: data.current.clone(),
items: data.items.clone(),
}
}),
)
let data_result = tokio::time::timeout(Duration::from_secs(2), async {
let profiles = Config::profiles().await;
let data = profiles.latest_ref();
IProfiles {
current: data.current.clone(),
items: data.items.clone(),
}
})
.await;
match data_result {
Ok(Ok(profiles)) => {
Ok(profiles) => {
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => {
Err(join_err) => {
logging!(
error,
Type::Cmd,
true,
"获取draft配置任务失败: {}",
"获取draft配置任务失败或超时: {}",
join_err
);
}
Err(_) => {
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
}
}
// 策略3: fallback,尝试重新创建配置
@@ -83,26 +80,19 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
"所有获取配置策略都失败,尝试fallback"
);
match tokio::task::spawn_blocking(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![]),
})
}
}
Ok(IProfiles::new().await)
}
/// 增强配置文件
#[tauri::command]
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();
Ok(())
}
@@ -110,40 +100,152 @@ pub async fn enhance_profiles() -> CmdResult {
/// 导入配置文件
#[tauri::command]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
wrap_err!(Config::profiles().data().append_item(item))
logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url);
let import_result = tokio::time::timeout(Duration::from_secs(60), async {
let item = PrfItem::from_url(&url, None, None, option).await?;
logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置");
let profiles = Config::profiles().await;
let pre_count = profiles
.latest_ref()
.items
.as_ref()
.map_or(0, |items| items.len());
let result = profiles_append_item_safe(item.clone()).await;
result?;
let post_count = profiles
.latest_ref()
.items
.as_ref()
.map_or(0, |items| items.len());
if post_count <= pre_count {
logging!(
error,
Type::Cmd,
true,
"[导入订阅] 配置未增加,导入可能失败"
);
return Err(anyhow::anyhow!("配置导入后数量未增加"));
}
logging!(
info,
Type::Cmd,
true,
"[导入订阅] 配置保存成功,数量: {} -> {}",
pre_count,
post_count
);
// 立即发送配置变更通知
if let Some(uid) = &item.uid {
logging!(
info,
Type::Cmd,
true,
"[导入订阅] 发送配置变更通知: {}",
uid
);
handle::Handle::notify_profile_changed(uid.clone());
}
// 异步保存配置文件并发送全局通知
let uid_clone = item.uid.clone();
crate::process::AsyncHandler::spawn(move || async move {
// 使用Send-safe helper函数
if let Err(e) = profiles_save_file_safe().await {
logging!(error, Type::Cmd, true, "[导入订阅] 保存配置文件失败: {}", e);
} 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(())
})
.await;
match import_result {
Ok(Ok(())) => {
logging!(info, Type::Cmd, true, "[导入订阅] 导入完成: {}", url);
Ok(())
}
Ok(Err(e)) => {
logging!(error, Type::Cmd, true, "[导入订阅] 导入失败: {}", e);
Err(format!("导入订阅失败: {e}"))
}
Err(_) => {
logging!(error, Type::Cmd, true, "[导入订阅] 导入超时(60秒): {}", url);
Err("导入订阅超时,请检查网络连接".into())
}
}
}
/// 重新排序配置文件
/// 调整profile的顺序
#[tauri::command]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
wrap_err!(Config::profiles().data().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]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
wrap_err!(Config::profiles().data().append_item(item))
match profiles_append_item_with_filedata_safe(item, file_data).await {
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]
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]
pub async fn delete_profile(index: String) -> CmdResult {
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
// 删除后自动清理冗余文件
let _ = Config::profiles().latest().auto_cleanup();
// 使用Send-safe helper函数
let should_update = wrap_err!(profiles_delete_item_safe(index).await)?;
if should_update {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
}
Err(e) => {
log::error!(target: "app", "{}", e);
return Err(e.to_string());
}
}
}
Ok(())
}
@@ -151,13 +253,40 @@ pub async fn delete_profile(index: String) -> CmdResult {
/// 修改profiles的配置
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 获取互斥锁,防止并发执行
let _guard = PROFILE_UPDATE_MUTEX.lock().await;
if CURRENT_SWITCHING_PROFILE.load(Ordering::SeqCst) {
logging!(info, Type::Cmd, true, "当前正在切换配置,放弃请求");
return Ok(false);
}
CURRENT_SWITCHING_PROFILE.store(true, Ordering::SeqCst);
logging!(info, Type::Cmd, true, "开始修改配置文件");
// 为当前请求分配序列号
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
let target_profile = profiles.current.clone();
logging!(
info,
Type::Cmd,
true,
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
current_sequence,
target_profile
);
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);
}
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().latest().current.clone();
let current_profile = Config::profiles().await.latest_ref().current.clone();
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
@@ -167,8 +296,8 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 获取目标配置文件路径
let config_file_result = {
let profiles_config = Config::profiles();
let profiles_data = profiles_config.latest();
let profiles_config = Config::profiles().await;
let profiles_data = profiles_config.latest_ref();
match profiles_data.get_item(new_profile) {
Ok(item) => {
if let Some(file) = &item.file {
@@ -211,8 +340,8 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
match file_read_result {
Ok(Ok(content)) => {
let yaml_parse_result = tokio::task::spawn_blocking(move || {
serde_yaml::from_str::<serde_yaml::Value>(&content)
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
})
.await;
@@ -221,7 +350,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
}
Ok(Err(err)) => {
let error_msg = format!(" {}", err);
let error_msg = format!(" {err}");
logging!(
error,
Type::Cmd,
@@ -236,7 +365,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
return Ok(false);
}
Err(join_err) => {
let error_msg = format!("YAML解析任务失败: {}", 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",
@@ -247,7 +376,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
}
Ok(Err(err)) => {
let error_msg = format!("无法读取目标配置文件: {}", err);
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
@@ -269,14 +398,56 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
}
// 检查请求有效性
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);
}
// 更新profiles配置
logging!(info, Type::Cmd, true, "正在更新配置草稿");
logging!(
info,
Type::Cmd,
true,
"正在更新配置草稿,序列号: {}",
current_sequence
);
let current_value = profiles.current.clone();
let _ = Config::profiles().draft().patch_config(profiles);
let _ = Config::profiles().await.draft_mut().patch_config(profiles);
// 在调用内核前再次验证请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
true,
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
Config::profiles().await.discard();
return Ok(false);
}
// 为配置更新添加超时保护
logging!(
info,
Type::Cmd,
true,
"开始内核配置更新,序列号: {}",
current_sequence
);
let update_result = tokio::time::timeout(
Duration::from_secs(30), // 30秒超时
CoreManager::global().update_config(),
@@ -286,43 +457,70 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 更新配置并进行验证
match update_result {
Ok(Ok((true, _))) => {
logging!(info, Type::Cmd, true, "配置更新成功");
Config::profiles().apply();
// 内核操作完成后再次检查请求有效性
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
if current_sequence < latest_sequence {
logging!(
info,
Type::Cmd,
true,
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
current_sequence,
latest_sequence
);
Config::profiles().await.discard();
return Ok(false);
}
logging!(
info,
Type::Cmd,
true,
"配置更新成功,序列号: {}",
current_sequence
);
Config::profiles().await.apply();
handle::Handle::refresh_clash();
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = super::proxy::force_refresh_proxies().await {
log::warn!(target: "app", "强制刷新代理缓存失败: {}", e);
log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
}
});
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Tray::global().update_tooltip() {
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
}
if let Err(e) = Tray::global().update_tooltip().await {
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
}
if let Err(e) = Tray::global().update_menu() {
log::warn!(target: "app", "异步更新托盘菜单失败: {}", e);
}
if let Err(e) = Tray::global().update_menu().await {
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
}
// 保存配置文件
if let Err(e) = Config::profiles().data().save_file() {
log::warn!(target: "app", "异步保存配置文件失败: {}", e);
}
});
// 保存配置文件
if let Err(e) = profiles_save_file_safe().await {
log::warn!(target: "app", "异步保存配置文件失败: {e}");
}
// 立即通知前端配置变更
if let Some(current) = &current_value {
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
logging!(
info,
Type::Cmd,
true,
"向前端发送配置变更事件: {}, 序列号: {}",
current,
current_sequence
);
handle::Handle::notify_profile_changed(current.clone());
}
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(true)
}
Ok(Ok((false, error_msg))) => {
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
Config::profiles().discard();
Config::profiles().await.discard();
// 如果验证失败,恢复到之前的配置
if let Some(prev_profile) = current_profile {
logging!(
@@ -337,12 +535,17 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
items: None,
};
// 静默恢复,不触发验证
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
Config::profiles().apply();
wrap_err!({
Config::profiles()
.await
.draft_mut()
.patch_config(restore_profiles)
})?;
Config::profiles().await.apply();
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Config::profiles().data().save_file() {
log::warn!(target: "app", "异步保存恢复配置文件失败: {}", e);
if let Err(e) = profiles_save_file_safe().await {
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
}
});
@@ -351,37 +554,61 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 发送验证错误通知
handle::Handle::notice_message("config_validate::error", &error_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
Ok(Err(e)) => {
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
Config::profiles().discard();
logging!(
warn,
Type::Cmd,
true,
"更新过程发生错误: {}, 序列号: {}",
e,
current_sequence
);
Config::profiles().await.discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
Err(_) => {
// 超时处理
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(error, Type::Cmd, true, "{}", timeout_msg);
Config::profiles().discard();
logging!(
error,
Type::Cmd,
true,
"{}, 序列号: {}",
timeout_msg,
current_sequence
);
Config::profiles().await.discard();
if let Some(prev_profile) = current_profile {
logging!(
info,
Type::Cmd,
true,
"超时后尝试恢复到之前的配置: {}",
prev_profile
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
prev_profile,
current_sequence
);
let restore_profiles = IProfiles {
current: Some(prev_profile),
items: None,
};
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
Config::profiles().apply();
wrap_err!({
Config::profiles()
.await
.draft_mut()
.patch_config(restore_profiles)
})?;
Config::profiles().await.apply();
}
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
CURRENT_SWITCHING_PROFILE.store(false, Ordering::SeqCst);
Ok(false)
}
}
@@ -389,10 +616,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
/// 根据profile name修改profiles
#[tauri::command]
pub async fn patch_profiles_config_by_profile_index(
_app_handle: tauri::AppHandle,
profile_index: String,
) -> CmdResult<bool> {
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
let profiles = IProfiles {
@@ -404,26 +628,26 @@ pub async fn patch_profiles_config_by_profile_index(
/// 修改某个profile item的
#[tauri::command]
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
// 保存修改前检查是否有更新 update_interval
let update_interval_changed =
if let Ok(old_profile) = Config::profiles().latest().get_item(&index) {
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);
old_interval != new_interval
} else {
false
};
let profiles = Config::profiles().await;
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 new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
old_interval != new_interval
} else {
false
};
// 保存修改
wrap_err!(Config::profiles().data().patch_item(index.clone(), profile))?;
wrap_err!(profiles_patch_item_safe(index.clone(), profile).await)?;
// 如果更新间隔变更,异步刷新定时器
if update_interval_changed {
let index_clone = index.clone();
crate::process::AsyncHandler::spawn(move || async move {
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);
} else {
// 刷新成功后发送自定义事件,不触发配置重载
@@ -437,9 +661,10 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
/// 查看配置文件
#[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 file = {
wrap_err!(Config::profiles().latest().get_item(&index))?
wrap_err!(profiles.latest_ref().get_item(&index))?
.file
.clone()
.ok_or("the file field is null")
@@ -450,23 +675,23 @@ pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
ret_err!("the file not found");
}
wrap_err!(help::open_file(app_handle, path))
wrap_err!(help::open_file(path))
}
/// 读取配置文件内容
#[tauri::command]
pub fn read_profile_file(index: String) -> CmdResult<String> {
let profiles = Config::profiles();
let profiles = profiles.latest();
let item = wrap_err!(profiles.get_item(&index))?;
pub async fn read_profile_file(index: String) -> CmdResult<String> {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let item = wrap_err!(profiles_ref.get_item(&index))?;
let data = wrap_err!(item.read_file())?;
Ok(data)
}
/// 获取下一次更新时间
#[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 next_time = timer.get_next_update_time(&uid);
let next_time = timer.get_next_update_time(&uid).await;
Ok(next_time)
}
+102 -80
View File
@@ -1,99 +1,121 @@
use super::CmdResult;
use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState};
use std::{
sync::Mutex,
time::{Duration, Instant},
};
use tauri::Manager;
use tauri::Emitter;
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3);
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
use super::CmdResult;
use crate::{
core::{handle::Handle, tray::Tray},
ipc::IpcManager,
logging,
state::proxy::ProxyRequestCache,
utils::logging::Type,
};
use std::time::Duration;
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global();
let app_handle = handle::Handle::global().app_handle().unwrap();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
let should_refresh = {
let mut state = cmd_proxy_state.lock().unwrap();
let now = Instant::now();
if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL {
state.need_refresh = true;
state.last_refresh_time = now;
}
state.need_refresh
};
if should_refresh {
let proxies = manager.get_refresh_proxies().await?;
{
let mut state = cmd_proxy_state.lock().unwrap();
state.proxies = Box::new(proxies);
state.need_refresh = false;
}
log::debug!(target: "app", "proxies刷新成功");
}
let proxies = {
let state = cmd_proxy_state.lock().unwrap();
state.proxies.clone()
};
Ok(*proxies)
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default");
let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
}
/// 强制刷新代理缓存用于profile切换
#[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global();
let app_handle = handle::Handle::global().app_handle().unwrap();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
log::debug!(target: "app", "强制刷新代理缓存");
let proxies = manager.get_refresh_proxies().await?;
{
let mut state = cmd_proxy_state.lock().unwrap();
state.proxies = Box::new(proxies.clone());
state.need_refresh = false;
state.last_refresh_time = Instant::now();
}
log::debug!(target: "app", "强制刷新代理缓存完成");
Ok(proxies)
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("proxies", "default");
cache.map.remove(&key);
get_proxies().await
}
#[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("providers", "default");
let value = cache
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
let manager = IpcManager::global();
manager.get_providers_proxies().await.unwrap_or_else(|e| {
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
serde_json::Value::Object(serde_json::Map::new())
})
})
.await;
Ok((*value).clone())
}
let should_refresh = {
let mut state = cmd_proxy_state.lock().unwrap();
let now = Instant::now();
if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL {
state.need_refresh = true;
state.last_refresh_time = now;
/// 同步托盘和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())
}
state.need_refresh
};
}
}
/// 更新代理选择并同步托盘和GUI状态
#[tauri::command]
pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> {
match IpcManager::global().update_proxy(&group, &proxy).await {
Ok(_) => {
logging!(
info,
Type::Cmd,
"Proxy updated successfully: {} -> {}",
group,
proxy
);
let cache = crate::state::proxy::ProxyRequestCache::global();
let key = crate::state::proxy::ProxyRequestCache::make_key("proxies", "default");
cache.map.remove(&key);
if should_refresh {
let manager = MihomoManager::global();
let providers = manager.get_providers_proxies().await?;
{
let mut state = cmd_proxy_state.lock().unwrap();
state.providers_proxies = Box::new(providers);
state.need_refresh = false;
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(())
}
log::debug!(target: "app", "providers_proxies刷新成功");
Err(e) => {
logging!(
error,
Type::Cmd,
"Failed to update proxy: {} -> {}, error: {}",
group,
proxy,
e
);
Err(e.to_string())
}
}
let providers_proxies = {
let state = cmd_proxy_state.lock().unwrap();
state.providers_proxies.clone()
};
Ok(*providers_proxies)
}
+11 -11
View File
@@ -1,36 +1,36 @@
use super::CmdResult;
use crate::{config::*, wrap_err};
use anyhow::Context;
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::collections::HashMap;
/// 获取运行时配置
#[tauri::command]
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().latest().config.clone())
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().await.latest_ref().config.clone())
}
/// 获取运行时YAML配置
#[tauri::command]
pub fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime();
let runtime = runtime.latest();
pub async fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_ref();
let config = runtime.config.as_ref();
wrap_err!(config
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
.and_then(
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|config| serde_yaml_ng::to_string(config).context("failed to convert config to yaml")
))
}
/// 获取运行时存在的键
#[tauri::command]
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().latest().exists_keys.clone())
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().await.latest_ref().exists_keys.clone())
}
/// 获取运行时日志
#[tauri::command]
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().latest().chain_logs.clone())
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().await.latest_ref().chain_logs.clone())
}
+38 -19
View File
@@ -6,7 +6,7 @@ use crate::{
utils::{dirs, logging::Type},
wrap_err,
};
use std::fs;
use tokio::fs;
/// 保存profiles的配置
#[tauri::command]
@@ -17,8 +17,8 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
// 在异步操作前完成所有文件操作
let (file_path, original_content, is_merge_file) = {
let profiles = Config::profiles();
let profiles_guard = profiles.latest();
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_ref();
let item = wrap_err!(profiles_guard.get_item(&index))?;
// 确定是否为merge类型文件
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
@@ -29,7 +29,8 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
};
// 保存新的配置文件
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
let file_data = file_data.ok_or("file_data is None")?;
wrap_err!(fs::write(&file_path, &file_data).await)?;
let file_path_str = file_path.to_string_lossy().to_string();
logging!(
@@ -61,14 +62,20 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
"[cmd配置save] merge文件语法验证通过"
);
// 成功后尝试更新整体配置
if let Err(e) = CoreManager::global().update_config().await {
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
match CoreManager::global().update_config().await {
Ok(_) => {
// 配置更新成功,刷新前端
handle::Handle::refresh_clash();
}
Err(e) => {
logging!(
warn,
Type::Config,
true,
"[cmd配置save] 更新整体配置时发生错误: {}",
e
);
}
}
return Ok(());
}
@@ -81,7 +88,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
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());
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
@@ -96,7 +103,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
e
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
wrap_err!(fs::write(&file_path, original_content).await)?;
return Err(e.to_string());
}
}
@@ -120,7 +127,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
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")
@@ -133,17 +140,29 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|| (!file_path_str.ends_with(".js") && !is_script_error)
{
// 普通YAML错误使用YAML通知处理
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
logging!(
info,
Type::Config,
"[cmd配置save] YAML配置文件验证失败,发送通知"
);
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error {
// 脚本错误使用专门的通知处理
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
logging!(
info,
Type::Config,
"[cmd配置save] 脚本文件验证失败,发送通知"
);
let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
logging!(
info,
Type::Config,
"[cmd配置save] 其他类型验证失败,发送一般通知"
);
handle::Handle::notice_message("config_validate::error", &error_msg);
}
@@ -158,7 +177,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
e
);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
wrap_err!(fs::write(&file_path, original_content).await)?;
Err(e.to_string())
}
}
+15 -12
View File
@@ -3,40 +3,43 @@ use crate::{
core::{service, CoreManager},
utils::i18n::t,
};
use anyhow::Result;
async fn execute_service_operation(
service_op: impl std::future::Future<Output = Result<(), impl ToString + std::fmt::Debug>>,
op_type: &str,
) -> CmdResult {
if service_op.await.is_err() {
let emsg = format!("{} {} failed", op_type, "Service");
return Err(t(emsg.as_str()));
async fn execute_service_operation_sync<F, Fut, E>(service_op: F, op_type: &str) -> CmdResult
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<(), E>>,
E: ToString + std::fmt::Debug,
{
if let Err(e) = service_op().await {
let emsg = format!("{} {} failed: {}", op_type, "Service", e.to_string());
return Err(t(emsg.as_str()).await);
}
if CoreManager::global().restart_core().await.is_err() {
let emsg = format!("{} {} failed", "Restart", "Core");
return Err(t(emsg.as_str()));
return Err(t(emsg.as_str()).await);
}
Ok(())
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
execute_service_operation(service::install_service(), "Install").await
execute_service_operation_sync(service::install_service, "Install").await
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
execute_service_operation(service::uninstall_service(), "Uninstall").await
execute_service_operation_sync(service::uninstall_service, "Uninstall").await
}
#[tauri::command]
pub async fn reinstall_service() -> CmdResult {
execute_service_operation(service::reinstall_service(), "Reinstall").await
execute_service_operation_sync(service::reinstall_service, "Reinstall").await
}
#[tauri::command]
pub async fn repair_service() -> CmdResult {
execute_service_operation(service::force_reinstall_service(), "Repair").await
execute_service_operation_sync(service::force_reinstall_service, "Repair").await
}
#[tauri::command]
+11 -7
View File
@@ -1,7 +1,9 @@
use super::CmdResult;
use crate::{
core::{handle, CoreManager},
logging,
module::sysinfo::PlatformSpecification,
utils::logging::Type,
};
use once_cell::sync::Lazy;
use std::{
@@ -23,28 +25,30 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
#[tauri::command]
pub async fn export_diagnostic_info() -> CmdResult<()> {
let sysinfo = PlatformSpecification::new_async().await;
let info = format!("{:?}", sysinfo);
let sysinfo = PlatformSpecification::new_sync();
let info = format!("{sysinfo:?}");
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = handle::Handle::global()
.app_handle()
.ok_or("Failed to get app handle")?;
let cliboard = app_handle.clipboard();
if cliboard.write_text(info).is_err() {
log::error!(target: "app", "Failed to write to clipboard");
logging!(error, Type::System, "Failed to write to clipboard");
}
Ok(())
}
#[tauri::command]
pub async fn get_system_info() -> CmdResult<String> {
let sysinfo = PlatformSpecification::new_async().await;
let info = format!("{:?}", sysinfo);
let sysinfo = PlatformSpecification::new_sync();
let info = format!("{sysinfo:?}");
Ok(info)
}
/// 获取当前内核运行模式
#[tauri::command]
pub async fn get_running_mode() -> Result<String, String> {
Ok(CoreManager::global().get_running_mode().await.to_string())
Ok(CoreManager::global().get_running_mode().to_string())
}
/// 获取应用的运行时间(毫秒)
+4 -4
View File
@@ -6,8 +6,8 @@ mod platform {
use super::CmdResult;
use crate::{core::win_uwp, wrap_err};
pub async fn invoke_uwp_tool() -> CmdResult {
wrap_err!(win_uwp::invoke_uwptools().await)
pub fn invoke_uwp_tool() -> CmdResult {
wrap_err!(win_uwp::invoke_uwptools())
}
}
@@ -16,7 +16,7 @@ mod platform {
mod platform {
use super::CmdResult;
pub async fn invoke_uwp_tool() -> CmdResult {
pub fn invoke_uwp_tool() -> CmdResult {
Ok(())
}
}
@@ -24,5 +24,5 @@ mod platform {
/// Command exposed to Tauri
#[tauri::command]
pub async fn invoke_uwp_tool() -> CmdResult {
platform::invoke_uwp_tool().await
platform::invoke_uwp_tool()
}
+8 -4
View File
@@ -3,10 +3,14 @@ use crate::{config::*, feat, wrap_err};
/// 获取Verge配置
#[tauri::command]
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge();
let verge_data = verge.data().clone();
Ok(IVergeResponse::from(*verge_data))
pub async fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge().await;
let verge_data = {
let ref_data = verge.latest_ref();
ref_data.clone()
};
let verge_response = IVergeResponse::from(*verge_data);
Ok(verge_response)
}
/// 修改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),
..IVerge::default()
};
Config::verge().draft().patch_config(patch.clone());
Config::verge().apply();
Config::verge()
.data()
.await
.draft_mut()
.patch_config(patch.clone());
Config::verge().await.apply();
// 分离数据获取和异步调用
let verge_data = Config::verge().await.latest_ref().clone();
verge_data
.save_file()
.await
.map_err(|err| err.to_string())?;
core::backup::WebDavClient::global().reset();
Ok(())
+74 -24
View File
@@ -1,7 +1,9 @@
use crate::config::Config;
use crate::utils::dirs::{ipc_path, path_to_str};
use crate::utils::{dirs, help};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value};
use serde_yaml_ng::{Mapping, Value};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
str::FromStr,
@@ -11,13 +13,22 @@ use std::{
pub struct IClashTemp(pub Mapping);
impl IClashTemp {
pub fn new() -> Self {
pub async fn new() -> Self {
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) => {
template.0.keys().for_each(|key| {
if !map.contains_key(key) {
map.insert(key.clone(), template.0.get(key).unwrap().clone());
if let Some(value) = template.0.get(key) {
map.insert(key.clone(), value.clone());
}
}
});
// 确保 secret 字段存在且不为空
@@ -57,6 +68,16 @@ impl IClashTemp {
map.insert("ipv6".into(), true.into());
map.insert("mode".into(), "rule".into());
map.insert("external-controller".into(), "127.0.0.1:9097".into());
#[cfg(unix)]
map.insert(
"external-controller-unix".into(),
Self::guard_external_controller_ipc().into(),
);
#[cfg(windows)]
map.insert(
"external-controller-pipe".into(),
Self::guard_external_controller_ipc().into(),
);
cors_map.insert("allow-private-network".into(), true.into());
cors_map.insert(
"allow-origins".into(),
@@ -87,7 +108,12 @@ impl IClashTemp {
let mixed_port = Self::guard_mixed_port(&config);
let socks_port = Self::guard_socks_port(&config);
let port = Self::guard_port(&config);
let ctrl = Self::guard_server_ctrl(&config);
let ctrl = Self::guard_external_controller(&config);
#[cfg(unix)]
let external_controller_unix = Self::guard_external_controller_ipc();
#[cfg(windows)]
let external_controller_pipe = Self::guard_external_controller_ipc();
#[cfg(not(target_os = "windows"))]
config.insert("redir-port".into(), redir_port.into());
#[cfg(target_os = "linux")]
@@ -97,25 +123,16 @@ impl IClashTemp {
config.insert("port".into(), port.into());
config.insert("external-controller".into(), ctrl.into());
// 强制覆盖 external-controller-cors 字段,允许本地和 tauri 前端
let mut cors_map = Mapping::new();
cors_map.insert("allow-private-network".into(), true.into());
cors_map.insert(
"allow-origins".into(),
vec![
"tauri://localhost",
"http://tauri.localhost",
// Only enable this in dev mode
#[cfg(feature = "verge-dev")]
"http://localhost:3000",
"https://yacd.metacubex.one",
"https://metacubex.github.io",
"https://board.zash.run.place",
]
.into(),
#[cfg(unix)]
config.insert(
"external-controller-unix".into(),
external_controller_unix.into(),
);
#[cfg(windows)]
config.insert(
"external-controller-pipe".into(),
external_controller_pipe.into(),
);
config.insert("external-controller-cors".into(), cors_map.into());
config
}
@@ -125,12 +142,13 @@ impl IClashTemp {
}
}
pub fn save_config(&self) -> Result<()> {
pub async fn save_config(&self) -> Result<()> {
help::save_yaml(
&dirs::clash_path()?,
&self.0,
Some("# Generated by Clash Verge"),
)
.await
}
pub fn get_mixed_port(&self) -> u16 {
@@ -264,6 +282,27 @@ impl IClashTemp {
.unwrap_or("127.0.0.1:9097".into())
}
pub fn guard_external_controller(config: &Mapping) -> String {
// 在初始化阶段,直接返回配置中的值,不进行额外检查
// 这样可以避免在配置加载期间的循环依赖
Self::guard_server_ctrl(config)
}
pub async fn guard_external_controller_with_setting(config: &Mapping) -> String {
// 检查 enable_external_controller 设置,用于运行时配置生成
let enable_external_controller = Config::verge()
.await
.latest_ref()
.enable_external_controller
.unwrap_or(false);
if enable_external_controller {
Self::guard_server_ctrl(config)
} else {
"".into()
}
}
pub fn guard_client_ctrl(config: &Mapping) -> String {
let value = Self::guard_server_ctrl(config);
match SocketAddr::from_str(value.as_str()) {
@@ -276,6 +315,17 @@ impl IClashTemp {
Err(_) => "127.0.0.1:9097".into(),
}
}
pub fn guard_external_controller_ipc() -> String {
// 总是使用当前的 IPC 路径,确保配置文件与运行时路径一致
ipc_path()
.ok()
.and_then(|path| path_to_str(&path).ok().map(|s| s.to_string()))
.unwrap_or_else(|| {
log::error!(target: "app", "Failed to get IPC path, using default");
"127.0.0.1:9090".to_string()
})
}
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+45 -41
View File
@@ -1,14 +1,13 @@
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{
config::PrfItem,
config::{profiles_append_item_safe, PrfItem},
core::{handle, CoreManager},
enhance, logging,
process::AsyncHandler,
utils::{dirs, help, logging::Type},
};
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use std::path::PathBuf;
use tokio::sync::OnceCell;
use tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
@@ -22,50 +21,55 @@ pub struct Config {
}
impl Config {
pub fn global() -> &'static Config {
static CONFIG: OnceCell<Config> = OnceCell::new();
CONFIG.get_or_init(|| Config {
clash_config: Draft::from(Box::new(IClashTemp::new())),
verge_config: Draft::from(Box::new(IVerge::new())),
profiles_config: Draft::from(Box::new(IProfiles::new())),
runtime_config: Draft::from(Box::new(IRuntime::new())),
})
pub async fn global() -> &'static Config {
static CONFIG: OnceCell<Config> = OnceCell::const_new();
CONFIG
.get_or_init(|| async {
Config {
clash_config: Draft::from(Box::new(IClashTemp::new().await)),
verge_config: Draft::from(Box::new(IVerge::new().await)),
profiles_config: Draft::from(Box::new(IProfiles::new().await)),
runtime_config: Draft::from(Box::new(IRuntime::new())),
}
})
.await
}
pub fn clash() -> Draft<Box<IClashTemp>> {
Self::global().clash_config.clone()
pub async fn clash() -> Draft<Box<IClashTemp>> {
Self::global().await.clash_config.clone()
}
pub fn verge() -> Draft<Box<IVerge>> {
Self::global().verge_config.clone()
pub async fn verge() -> Draft<Box<IVerge>> {
Self::global().await.verge_config.clone()
}
pub fn profiles() -> Draft<Box<IProfiles>> {
Self::global().profiles_config.clone()
pub async fn profiles() -> Draft<Box<IProfiles>> {
Self::global().await.profiles_config.clone()
}
pub fn runtime() -> Draft<Box<IRuntime>> {
Self::global().runtime_config.clone()
pub async fn runtime() -> Draft<Box<IRuntime>> {
Self::global().await.runtime_config.clone()
}
/// 初始化订阅
pub async fn init_config() -> Result<()> {
if Self::profiles()
.data()
.await
.latest_ref()
.get_item(&"Merge".to_string())
.is_err()
{
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
Self::profiles().data().append_item(merge_item.clone())?;
profiles_append_item_safe(merge_item.clone()).await?;
}
if Self::profiles()
.data()
.await
.latest_ref()
.get_item(&"Script".to_string())
.is_err()
{
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
Self::profiles().data().append_item(script_item.clone())?;
profiles_append_item_safe(script_item.clone()).await?;
}
// 生成运行时配置
if let Err(err) = Self::generate().await {
@@ -75,7 +79,7 @@ impl Config {
}
// 生成运行时配置文件并验证
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() {
// 验证配置文件
@@ -118,30 +122,30 @@ impl Config {
// 在单独的任务中发送通知
if let Some((msg_type, msg_content)) = validation_result {
AsyncHandler::spawn(move || async move {
sleep(Duration::from_secs(2)).await;
handle::Handle::notice_message(msg_type, &msg_content);
});
sleep(Duration::from_secs(2)).await;
handle::Handle::notice_message(msg_type, &msg_content);
}
Ok(())
}
/// 将订阅丢到对应的文件中
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
pub async fn generate_file(typ: ConfigType) -> Result<PathBuf> {
let path = match typ {
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG),
};
let runtime = Config::runtime();
let runtime = runtime.latest();
let runtime = Config::runtime().await;
let config = runtime
.latest_ref()
.config
.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)
}
@@ -149,7 +153,7 @@ impl Config {
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
*Config::runtime().draft() = Box::new(IRuntime {
*Config::runtime().await.draft_mut() = Box::new(IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
@@ -170,33 +174,33 @@ mod tests {
use std::mem;
#[test]
#[allow(unused_variables)]
#[allow(clippy::expect_used)]
fn test_prfitem_from_merge_size() {
let merge_item = PrfItem::from_merge(Some("Merge".to_string())).unwrap();
dbg!(&merge_item);
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))
.expect("Failed to create merge item in test");
let prfitem_size = mem::size_of_val(&merge_item);
dbg!(prfitem_size);
// Boxed version
let boxed_merge_item = Box::new(merge_item);
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
dbg!(box_prfitem_size);
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
assert!(box_prfitem_size < prfitem_size);
}
#[test]
#[allow(unused_variables)]
fn test_draft_size_non_boxed() {
let draft = Draft::from(IRuntime::new());
let iruntime_size = std::mem::size_of_val(&draft);
dbg!(iruntime_size);
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
}
#[test]
#[allow(unused_variables)]
fn test_draft_size_boxed() {
let draft = Draft::from(Box::new(IRuntime::new()));
let box_iruntime_size = std::mem::size_of_val(&draft);
dbg!(box_iruntime_size);
assert_eq!(
box_iruntime_size,
std::mem::size_of::<Draft<Box<IRuntime>>>()
+122 -103
View File
@@ -1,135 +1,154 @@
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
use std::sync::Arc;
use parking_lot::{
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard,
RwLockUpgradableReadGuard, RwLockWriteGuard,
};
#[derive(Debug, Clone)]
pub struct Draft<T: Clone + ToOwned> {
inner: Arc<Mutex<(T, Option<T>)>>,
inner: Arc<RwLock<(T, Option<T>)>>,
}
macro_rules! draft_define {
($id: ident) => {
impl From<$id> for Draft<$id> {
fn from(data: $id) -> Self {
Draft {
inner: Arc::new(Mutex::new((data, None))),
}
}
impl<T: Clone + ToOwned> From<T> for Draft<T> {
fn from(data: T) -> Self {
Self {
inner: Arc::new(RwLock::new((data, None))),
}
impl Draft<Box<$id>> {
#[allow(unused)]
pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
}
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
&mut inner.0
} else {
inner.1.as_mut().unwrap()
}
})
}
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
inner.1 = Some(inner.0.clone());
}
inner.1.as_mut().unwrap()
})
}
pub fn apply(&self) -> Option<Box<$id>> {
let mut inner = self.inner.lock();
match inner.1.take() {
Some(draft) => {
let old_value = inner.0.to_owned();
inner.0 = draft.to_owned();
Some(old_value)
}
None => None,
}
}
pub fn discard(&self) -> Option<Box<$id>> {
let mut inner = self.inner.lock();
inner.1.take()
}
}
impl From<Box<$id>> for Draft<Box<$id>> {
fn from(data: Box<$id>) -> Self {
Draft {
inner: Arc::new(Mutex::new((data, None))),
}
}
}
};
}
}
// draft_define!(IClash);
draft_define!(IClashTemp);
draft_define!(IProfiles);
draft_define!(IRuntime);
draft_define!(IVerge);
/// Implements draft management for `Box<T>`, allowing for safe concurrent editing and committing of draft data.
/// # Type Parameters
/// - `T`: The underlying data type, which must implement `Clone` and `ToOwned`.
///
/// # Methods
/// - `data_mut`: Returns a mutable reference to the committed data.
/// - `data_ref`: Returns an immutable reference to the committed data.
/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists.
/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data.
/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed.
/// - `discard`: Discards the draft data and returns it if it existed.
impl<T: Clone + ToOwned> Draft<Box<T>> {
/// 可写正式数据
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
}
/// 返回正式数据的只读视图(不包含草稿)
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
}
/// 创建或获取草稿并返回可写引用
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
let guard = self.inner.upgradable_read();
if guard.1.is_none() {
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
guard.1 = Some(guard.0.clone());
return RwLockWriteGuard::map(guard, |inner| {
inner.1.as_mut().unwrap_or_else(|| {
unreachable!("Draft was just created above, this should never fail")
})
});
}
// 已存在草稿,升级为写锁映射
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
inner
.1
.as_mut()
.unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()"))
})
}
/// 零拷贝只读视图:返回草稿(若存在)或正式值
pub fn latest_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
RwLockReadGuard::map(self.inner.read(), |inner| {
inner.1.as_ref().unwrap_or(&inner.0)
})
}
/// 提交草稿,返回旧正式数据
pub fn apply(&self) -> Option<Box<T>> {
let mut inner = self.inner.write();
inner
.1
.take()
.map(|draft| std::mem::replace(&mut inner.0, draft))
}
/// 丢弃草稿,返回被丢弃的草稿
pub fn discard(&self) -> Option<Box<T>> {
self.inner.write().1.take()
}
}
#[test]
fn test_draft_box() {
use super::IVerge;
// 1. 创建 Draft<Box<IVerge>>
let verge = Box::new(IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..IVerge::default()
});
let draft = Draft::from(verge);
assert_eq!(draft.data().enable_auto_launch, Some(true));
assert_eq!(draft.data().enable_tun_mode, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert_eq!(draft.draft().enable_tun_mode, Some(false));
// 2. 读取正式数据(data_mut)
{
let mut d = draft.draft();
let data = draft.data_mut();
assert_eq!(data.enable_auto_launch, Some(true));
assert_eq!(data.enable_tun_mode, Some(false));
}
// 3. 初次获取草稿(draft_mut 会自动 clone 一份)
{
let draft_view = draft.draft_mut();
assert_eq!(draft_view.enable_auto_launch, Some(true));
assert_eq!(draft_view.enable_tun_mode, Some(false));
}
// 4. 修改草稿
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
}
assert_eq!(draft.data().enable_auto_launch, Some(true));
assert_eq!(draft.data().enable_tun_mode, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_tun_mode, Some(true));
assert_eq!(draft.latest().enable_auto_launch, Some(false));
assert_eq!(draft.latest().enable_tun_mode, Some(true));
assert!(draft.apply().is_some());
assert!(draft.apply().is_none());
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert_eq!(draft.data().enable_tun_mode, Some(true));
assert_eq!(draft.draft().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_tun_mode, Some(true));
// 正式数据未变
assert_eq!(draft.data_mut().enable_auto_launch, Some(true));
assert_eq!(draft.data_mut().enable_tun_mode, Some(false));
// 草稿已变
{
let mut d = draft.draft();
d.enable_auto_launch = Some(true);
let latest = draft.latest_ref();
assert_eq!(latest.enable_auto_launch, Some(false));
assert_eq!(latest.enable_tun_mode, Some(true));
}
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(true));
// 5. 提交草稿
assert!(draft.apply().is_some()); // 第一次提交应有返回
assert!(draft.apply().is_none()); // 第二次提交返回 None
assert!(draft.discard().is_some());
// 正式数据已更新
{
let data = draft.data_mut();
assert_eq!(data.enable_auto_launch, Some(false));
assert_eq!(data.enable_tun_mode, Some(true));
}
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert!(draft.discard().is_none());
// 6. 新建并修改下一轮草稿
{
let mut d = draft.draft_mut();
d.enable_auto_launch = Some(true);
}
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
assert_eq!(draft.draft().enable_auto_launch, Some(false));
// 7. 丢弃草稿
assert!(draft.discard().is_some()); // 第一次丢弃返回 Some
assert!(draft.discard().is_none()); // 再次丢弃返回 None
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false));
}
+2 -2
View File
@@ -21,7 +21,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
// Encrypt data
let ciphertext = cipher
.encrypt(nonce.as_slice().into(), data.as_bytes())
.map_err(|e| format!("Encryption failed: {}", e))?;
.map_err(|e| format!("Encryption failed: {e}"))?;
// Concatenate nonce and ciphertext and encode them in base64
let mut combined = nonce;
@@ -46,7 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
// Decrypt data
let plaintext = cipher
.decrypt(nonce.into(), ciphertext)
.map_err(|e| format!("Decryption failed: {}", e))?;
.map_err(|e| format!("Decryption failed: {e}"))?;
String::from_utf8(plaintext).map_err(|e| e.into())
}
+1 -1
View File
@@ -4,7 +4,7 @@ mod config;
mod draft;
mod encrypt;
mod prfitem;
mod profiles;
pub mod profiles;
mod runtime;
mod verge;
+33 -39
View File
@@ -4,13 +4,10 @@ use crate::utils::{
tmpl,
};
use anyhow::{bail, Context, Result};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::{fs, time::Duration};
use super::Config;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PrfItem {
pub uid: Option<String>,
@@ -147,12 +144,15 @@ impl PrfItem {
bail!("type should not be null");
}
match item.itype.unwrap().as_str() {
let itype = item
.itype
.ok_or_else(|| anyhow::anyhow!("type should not be null"))?;
match itype.as_str() {
"remote" => {
if item.url.is_none() {
bail!("url should not be null");
}
let url = item.url.as_ref().unwrap().as_str();
let url = item
.url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("url should not be null"))?;
let name = item.name;
let desc = item.desc;
PrfItem::from_url(url, name, desc, item.option).await
@@ -160,7 +160,7 @@ impl PrfItem {
"local" => {
let name = item.name.unwrap_or("Local File".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}\""),
}
@@ -168,7 +168,7 @@ impl PrfItem {
/// ## Local type
/// create a new item from name/desc
pub fn from_local(
pub async fn from_local(
name: String,
desc: String,
file_data: Option<String>,
@@ -186,29 +186,27 @@ impl PrfItem {
if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?;
Config::profiles().data().append_item(merge_item.clone())?;
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = PrfItem::from_script(None)?;
Config::profiles().data().append_item(script_item.clone())?;
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
script = script_item.uid;
}
if rules.is_none() {
let rules_item = PrfItem::from_rules()?;
Config::profiles().data().append_item(rules_item.clone())?;
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
rules = rules_item.uid;
}
if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?;
Config::profiles()
.data()
.append_item(proxies_item.clone())?;
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
proxies = proxies_item.uid;
}
if groups.is_none() {
let groups_item = PrfItem::from_groups()?;
Config::profiles().data().append_item(groups_item.clone())?;
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
groups = groups_item.uid;
}
Ok(PrfItem {
@@ -267,7 +265,7 @@ impl PrfItem {
};
// 使用网络管理器发送请求
let resp = match NetworkManager::global()
let resp = match NetworkManager::new()
.get_with_interrupt(
url,
proxy_type,
@@ -285,7 +283,7 @@ impl PrfItem {
};
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}")
}
@@ -351,13 +349,13 @@ impl PrfItem {
let uid = help::get_uid("R");
let file = format!("{uid}.yaml");
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"
let data = data.trim_start_matches('\u{feff}');
// 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")?;
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
@@ -366,29 +364,27 @@ impl PrfItem {
if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?;
Config::profiles().data().append_item(merge_item.clone())?;
crate::config::profiles::profiles_append_item_safe(merge_item.clone()).await?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = PrfItem::from_script(None)?;
Config::profiles().data().append_item(script_item.clone())?;
crate::config::profiles::profiles_append_item_safe(script_item.clone()).await?;
script = script_item.uid;
}
if rules.is_none() {
let rules_item = PrfItem::from_rules()?;
Config::profiles().data().append_item(rules_item.clone())?;
crate::config::profiles::profiles_append_item_safe(rules_item.clone()).await?;
rules = rules_item.uid;
}
if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?;
Config::profiles()
.data()
.append_item(proxies_item.clone())?;
crate::config::profiles::profiles_append_item_safe(proxies_item.clone()).await?;
proxies = proxies_item.uid;
}
if groups.is_none() {
let groups_item = PrfItem::from_groups()?;
Config::profiles().data().append_item(groups_item.clone())?;
crate::config::profiles::profiles_append_item_safe(groups_item.clone()).await?;
groups = groups_item.uid;
}
@@ -533,22 +529,20 @@ impl PrfItem {
/// get the file data
pub fn read_file(&self) -> Result<String> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let file = self
.file
.clone()
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
let path = dirs::app_profiles_dir()?.join(file);
fs::read_to_string(path).context("failed to read the file")
}
/// save the file data
pub fn save_file(&self, data: String) -> Result<()> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let file = self
.file
.clone()
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
let path = dirs::app_profiles_dir()?.join(file);
fs::write(path, data.as_bytes()).context("failed to save the file")
}
+221 -81
View File
@@ -1,9 +1,14 @@
use super::{prfitem::PrfItem, PrfOption};
use crate::utils::{dirs, help};
use crate::{
logging_error,
process::AsyncHandler,
utils::{dirs, help, logging::Type},
};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::{collections::HashSet, fs, io::Write};
use serde_yaml_ng::Mapping;
use std::collections::HashSet;
use tokio::fs;
/// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -32,22 +37,28 @@ macro_rules! patch {
}
impl IProfiles {
pub fn new() -> Self {
match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) {
Ok(mut profiles) => {
if profiles.items.is_none() {
profiles.items = Some(vec![]);
}
// compatible with the old old old version
if let Some(items) = profiles.items.as_mut() {
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d"));
pub async fn new() -> Self {
match dirs::profiles_path() {
Ok(path) => match help::read_yaml::<Self>(&path).await {
Ok(mut profiles) => {
if profiles.items.is_none() {
profiles.items = Some(vec![]);
}
// compatible with the old old old version
if let Some(items) = profiles.items.as_mut() {
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d"));
}
}
}
profiles
}
profiles
}
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
}
},
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
@@ -62,12 +73,13 @@ impl IProfiles {
}
}
pub fn save_file(&self) -> Result<()> {
pub async fn save_file(&self) -> Result<()> {
help::save_yaml(
&dirs::profiles_path()?,
self,
Some("# Profiles Config for Clash Verge"),
)
.await
}
/// 只修改current,valid和chain
@@ -77,10 +89,11 @@ impl IProfiles {
}
if let Some(current) = patch.current {
let items = self.items.as_ref().unwrap();
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
if let Some(items) = self.items.as_ref() {
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
}
}
@@ -114,7 +127,7 @@ impl IProfiles {
/// append new item
/// if the file_data is some
/// 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() {
bail!("the uid should not be null");
}
@@ -127,13 +140,14 @@ impl IProfiles {
bail!("the file should not be null");
}
let file = item.file.clone().unwrap();
let file = item.file.clone().ok_or_else(|| {
anyhow::anyhow!("file field is required when file_data is provided")
})?;
let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path)
.with_context(|| format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes())
.with_context(|| format!("failed to write to file \"{}\"", file))?;
fs::write(&path, file_data.as_bytes())
.await
.with_context(|| format!("failed to write to file \"{file}\""))?;
}
if self.current.is_none()
@@ -150,11 +164,11 @@ impl IProfiles {
items.push(item)
}
self.save_file()
self.save_file().await
}
/// 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 old_index = None;
let mut new_index = None;
@@ -168,17 +182,18 @@ impl IProfiles {
}
}
if old_index.is_none() || new_index.is_none() {
return Ok(());
}
let item = items.remove(old_index.unwrap());
items.insert(new_index.unwrap(), item);
let (old_idx, new_idx) = match (old_index, new_index) {
(Some(old), Some(new)) => (old, new),
_ => return Ok(()),
};
let item = items.remove(old_idx);
items.insert(new_idx, item);
self.items = Some(items);
self.save_file()
self.save_file().await
}
/// 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();
for each in items.iter_mut() {
@@ -194,7 +209,7 @@ impl IProfiles {
patch!(each, item, option);
self.items = Some(items);
return self.save_file();
return self.save_file().await;
}
}
@@ -204,7 +219,7 @@ impl IProfiles {
/// be used to update the remote item
/// 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() {
self.items = Some(vec![]);
}
@@ -233,10 +248,9 @@ impl IProfiles {
let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path)
.with_context(|| format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes())
.with_context(|| format!("failed to write to file \"{}\"", file))?;
fs::write(&path, file_data.as_bytes())
.await
.with_context(|| format!("failed to write to file \"{file}\""))?;
}
break;
@@ -244,12 +258,12 @@ impl IProfiles {
}
}
self.save_file()
self.save_file().await
}
/// delete item
/// 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 = current.clone();
let item = self.get_item(&uid)?;
@@ -275,10 +289,19 @@ impl IProfiles {
}
if let Some(index) = index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
}
});
}
@@ -292,10 +315,19 @@ impl IProfiles {
}
if let Some(index) = merge_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
}
});
}
@@ -309,10 +341,19 @@ impl IProfiles {
}
if let Some(index) = script_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
}
});
}
@@ -326,10 +367,19 @@ impl IProfiles {
}
if let Some(index) = rules_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
}
});
}
@@ -343,10 +393,19 @@ impl IProfiles {
}
if let Some(index) = proxies_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
}
});
}
@@ -360,10 +419,19 @@ impl IProfiles {
}
if let Some(index) = groups_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let _ = dirs::app_profiles_dir().map(async move |path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
let result = fs::remove_file(path.clone()).await;
if let Err(err) = result {
logging_error!(
Type::Config,
false,
"[配置文件删除] 删除文件 {} 失败: {}",
path.display(),
err
);
}
}
});
}
@@ -382,12 +450,12 @@ impl IProfiles {
}
self.items = Some(items);
self.save_file()?;
self.save_file().await?;
Ok(current == uid)
}
/// 获取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()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
@@ -395,7 +463,7 @@ impl IProfiles {
Some(file) => dirs::app_profiles_dir()?.join(file),
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}\"");
}
@@ -531,7 +599,7 @@ impl IProfiles {
if Self::is_profile_file(file_name) {
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "保护全局扩展配置文件: {}", file_name);
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
continue;
}
@@ -540,11 +608,11 @@ impl IProfiles {
match std::fs::remove_file(&path) {
Ok(_) => {
deleted_files.push(file_name.to_string());
log::info!(target: "app", "已清理冗余文件: {}", file_name);
log::info!(target: "app", "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{}: {}", file_name, e));
log::warn!(target: "app", "清理文件失败: {} - {}", file_name, e);
failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
}
}
@@ -667,23 +735,95 @@ impl IProfiles {
.unwrap_or(false)
})
}
}
pub fn auto_cleanup(&self) -> Result<()> {
match self.cleanup_orphaned_files() {
Ok(result) => {
if !result.deleted_files.is_empty() {
log::info!(
target: "app",
"自动清理完成,删除了 {} 个冗余文件",
result.deleted_files.len()
);
}
Ok(())
}
Err(e) => {
log::warn!(target: "app", "自动清理失败: {}", e);
Ok(())
}
}
}
// 特殊的Send-safe helper函数,完全避免跨await持有guard
use crate::config::Config;
pub async fn profiles_append_item_with_filedata_safe(
item: PrfItem,
file_data: Option<String>,
) -> Result<()> {
AsyncHandler::spawn_blocking(move || {
AsyncHandler::handle().block_on(async {
let item = PrfItem::from(item, file_data).await?;
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_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))?
}
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::enhance::field::use_keys;
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value};
use serde_yaml_ng::{Mapping, Value};
use std::collections::HashMap;
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IRuntime {
+26 -20
View File
@@ -138,9 +138,6 @@ pub struct IVerge {
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
pub auto_log_clean: Option<i32>,
/// 是否启用随机端口
pub enable_random_port: Option<bool>,
/// verge 的各种 port 用于覆盖 clash 的各种 port
#[cfg(not(target_os = "windows"))]
pub verge_redir_port: Option<u16>,
@@ -204,6 +201,9 @@ pub struct IVerge {
/// 启用代理页面自动滚动
pub enable_hover_jump_navigator: Option<bool>,
/// 启用外部控制器
pub enable_external_controller: Option<bool>,
/// 服务状态跟踪
pub service_state: Option<crate::core::service::ServiceState>,
}
@@ -237,9 +237,9 @@ impl IVerge {
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
/// 验证并修正配置文件中的clash_core值
pub fn validate_and_fix_config() -> Result<()> {
pub async fn validate_and_fix_config() -> Result<()> {
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,
Err(_) => Self::template(),
};
@@ -273,7 +273,7 @@ impl IVerge {
// 修正后保存配置
if needs_fix {
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!(
info,
Type::Config,
@@ -281,7 +281,7 @@ impl IVerge {
"配置文件修正完成,需要重新加载配置"
);
Self::reload_config_after_fix(config)?;
Self::reload_config_after_fix(config).await?;
} else {
logging!(
info,
@@ -296,11 +296,11 @@ 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;
let config_draft = Config::verge();
*config_draft.draft() = Box::new(updated_config.clone());
let config_draft = Config::verge().await;
*config_draft.draft_mut() = Box::new(updated_config.clone());
config_draft.apply();
logging!(
@@ -335,9 +335,15 @@ impl IVerge {
}
}
pub fn new() -> Self {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config,
pub async fn new() -> Self {
match dirs::verge_path() {
Ok(path) => match help::read_yaml::<IVerge>(&path).await {
Ok(config) => config,
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
}
},
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
@@ -371,7 +377,6 @@ impl IVerge {
proxy_auto_config: Some(false),
pac_file_content: Some(DEFAULT_PAC.into()),
proxy_host: Some("127.0.0.1".into()),
enable_random_port: Some(false),
#[cfg(not(target_os = "windows"))]
verge_redir_port: Some(7895),
#[cfg(not(target_os = "windows"))]
@@ -391,7 +396,7 @@ impl IVerge {
auto_close_connection: Some(true),
auto_check_update: Some(true),
enable_builtin_enhanced: Some(true),
auto_log_clean: Some(3),
auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天
webdav_url: None,
webdav_username: None,
webdav_password: None,
@@ -403,13 +408,14 @@ impl IVerge {
enable_dns_settings: Some(false),
home_cards: None,
service_state: None,
enable_external_controller: Some(false),
..Self::default()
}
}
/// Save IVerge App Config
pub fn save_file(&self) -> Result<()> {
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
pub async fn save_file(&self) -> Result<()> {
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")).await
}
/// patch verge config
@@ -444,7 +450,6 @@ impl IVerge {
patch!(enable_auto_launch);
patch!(enable_silent_start);
patch!(enable_hover_jump_navigator);
patch!(enable_random_port);
#[cfg(not(target_os = "windows"))]
patch!(verge_redir_port);
#[cfg(not(target_os = "windows"))]
@@ -491,6 +496,7 @@ impl IVerge {
patch!(enable_dns_settings);
patch!(home_cards);
patch!(service_state);
patch!(enable_external_controller);
}
/// 在初始化前尝试拿到单例端口的值
@@ -562,7 +568,6 @@ pub struct IVergeResponse {
pub proxy_layout_column: Option<i32>,
pub test_list: Option<Vec<IVergeTestItem>>,
pub auto_log_clean: Option<i32>,
pub enable_random_port: Option<bool>,
#[cfg(not(target_os = "windows"))]
pub verge_redir_port: Option<u16>,
#[cfg(not(target_os = "windows"))]
@@ -586,6 +591,7 @@ pub struct IVergeResponse {
pub enable_dns_settings: Option<bool>,
pub home_cards: Option<serde_json::Value>,
pub enable_hover_jump_navigator: Option<bool>,
pub enable_external_controller: Option<bool>,
pub service_state: Option<crate::core::service::ServiceState>,
}
@@ -634,7 +640,6 @@ impl From<IVerge> for IVergeResponse {
proxy_layout_column: verge.proxy_layout_column,
test_list: verge.test_list,
auto_log_clean: verge.auto_log_clean,
enable_random_port: verge.enable_random_port,
#[cfg(not(target_os = "windows"))]
verge_redir_port: verge.verge_redir_port,
#[cfg(not(target_os = "windows"))]
@@ -658,6 +663,7 @@ impl From<IVerge> for IVergeResponse {
enable_dns_settings: verge.enable_dns_settings,
home_cards: verge.home_cards,
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
enable_external_controller: verge.enable_external_controller,
service_state: verge.service_state,
}
}
+533
View File
@@ -0,0 +1,533 @@
#[cfg(target_os = "windows")]
use crate::process::AsyncHandler;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::time::{timeout, Duration};
#[cfg(target_os = "linux")]
use anyhow::anyhow;
#[cfg(not(target_os = "windows"))]
use tokio::process::Command;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AsyncAutoproxy {
pub enable: bool,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AsyncSysproxy {
pub enable: bool,
pub host: String,
pub port: u16,
pub bypass: String,
}
impl Default for AsyncSysproxy {
fn default() -> Self {
Self {
enable: false,
host: "127.0.0.1".to_string(),
port: 7897,
bypass: String::new(),
}
}
}
pub struct AsyncProxyQuery;
impl AsyncProxyQuery {
/// 异步获取自动代理配置(PAC)
pub async fn get_auto_proxy() -> AsyncAutoproxy {
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
Ok(Ok(proxy)) => {
log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "异步获取自动代理失败: {e}");
AsyncAutoproxy::default()
}
Err(_) => {
log::warn!(target: "app", "异步获取自动代理超时");
AsyncAutoproxy::default()
}
}
}
/// 异步获取系统代理配置
pub async fn get_system_proxy() -> AsyncSysproxy {
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
Ok(Ok(proxy)) => {
log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "异步获取系统代理失败: {e}");
AsyncSysproxy::default()
}
Err(_) => {
log::warn!(target: "app", "异步获取系统代理超时");
AsyncSysproxy::default()
}
}
}
#[cfg(target_os = "windows")]
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
// Windows: 从注册表读取PAC配置
AsyncHandler::spawn_blocking(move || -> Result<AsyncAutoproxy> {
Self::get_pac_config_from_registry()
})
.await?
}
#[cfg(target_os = "windows")]
fn get_pac_config_from_registry() -> Result<AsyncAutoproxy> {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
.encode_utf16()
.collect::<Vec<u16>>();
let mut hkey: HKEY = ptr::null_mut();
let result =
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
if result != 0 {
log::debug!(target: "app", "无法打开注册表项");
return Ok(AsyncAutoproxy::default());
}
// 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用)
let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::<Vec<u16>>();
let mut url_buffer = vec![0u16; 1024];
let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD;
let mut url_value_type: DWORD = 0;
let url_query_result = RegQueryValueExW(
hkey,
auto_config_url_name.as_ptr(),
ptr::null_mut(),
&mut url_value_type,
url_buffer.as_mut_ptr() as *mut u8,
&mut url_buffer_size,
);
let mut pac_url = String::new();
if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 {
let end_pos = url_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(url_buffer.len());
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
log::debug!(target: "app", "从注册表读取到PAC URL: {pac_url}");
}
// 2. 检查自动检测设置是否启用
let auto_detect_name = "AutoDetect\0".encode_utf16().collect::<Vec<u16>>();
let mut auto_detect: DWORD = 0;
let mut detect_buffer_size: DWORD = 4;
let mut detect_value_type: DWORD = 0;
let detect_query_result = RegQueryValueExW(
hkey,
auto_detect_name.as_ptr(),
ptr::null_mut(),
&mut detect_value_type,
&mut auto_detect as *mut DWORD as *mut u8,
&mut detect_buffer_size,
);
RegCloseKey(hkey);
// PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用
let pac_enabled = !pac_url.is_empty()
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
if pac_enabled {
log::debug!(target: "app", "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}");
if pac_url.is_empty() && auto_detect != 0 {
pac_url = "auto-detect".to_string();
}
Ok(AsyncAutoproxy {
enable: true,
url: pac_url,
})
} else {
log::debug!(target: "app", "PAC配置未启用");
Ok(AsyncAutoproxy::default())
}
}
}
#[cfg(target_os = "macos")]
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
// macOS: 使用 scutil --proxy 命令
let output = Command::new("scutil").args(["--proxy"]).output().await?;
if !output.status.success() {
return Ok(AsyncAutoproxy::default());
}
let stdout = String::from_utf8_lossy(&output.stdout);
log::debug!(target: "app", "scutil output: {stdout}");
let mut pac_enabled = false;
let mut pac_url = String::new();
// 解析 scutil 输出
for line in stdout.lines() {
let line = line.trim();
if line.contains("ProxyAutoConfigEnable") && line.contains("1") {
pac_enabled = true;
} else if line.contains("ProxyAutoConfigURLString") {
// 正确解析包含冒号的URL
// 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac"
if let Some(colon_pos) = line.find(" : ") {
pac_url = line[colon_pos + 3..].trim().to_string();
}
}
}
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
Ok(AsyncAutoproxy {
enable: pac_enabled && !pac_url.is_empty(),
url: pac_url,
})
}
#[cfg(target_os = "linux")]
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
// Linux: 检查环境变量和GNOME设置
// 首先检查环境变量
if let Ok(auto_proxy) = std::env::var("auto_proxy") {
if !auto_proxy.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: auto_proxy,
});
}
}
// 尝试使用 gsettings 获取 GNOME 代理设置
let output = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy", "mode"])
.output()
.await;
if let Ok(output) = output {
if output.status.success() {
let mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
if mode.contains("auto") {
// 获取 PAC URL
let pac_output = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
.output()
.await;
if let Ok(pac_output) = pac_output {
if pac_output.status.success() {
let pac_url = String::from_utf8_lossy(&pac_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
if !pac_url.is_empty() {
return Ok(AsyncAutoproxy {
enable: true,
url: pac_url,
});
}
}
}
}
}
}
Ok(AsyncAutoproxy::default())
}
#[cfg(target_os = "windows")]
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
// Windows: 使用注册表直接读取代理设置
AsyncHandler::spawn_blocking(move || -> Result<AsyncSysproxy> {
Self::get_system_proxy_from_registry()
})
.await?
}
#[cfg(target_os = "windows")]
fn get_system_proxy_from_registry() -> Result<AsyncSysproxy> {
use std::ptr;
use winapi::shared::minwindef::{DWORD, HKEY};
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER};
unsafe {
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
.encode_utf16()
.collect::<Vec<u16>>();
let mut hkey: HKEY = ptr::null_mut();
let result =
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
if result != 0 {
return Ok(AsyncSysproxy::default());
}
// 检查代理是否启用
let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::<Vec<u16>>();
let mut proxy_enable: DWORD = 0;
let mut buffer_size: DWORD = 4;
let mut value_type: DWORD = 0;
let enable_result = RegQueryValueExW(
hkey,
proxy_enable_name.as_ptr(),
ptr::null_mut(),
&mut value_type,
&mut proxy_enable as *mut DWORD as *mut u8,
&mut buffer_size,
);
if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 {
RegCloseKey(hkey);
return Ok(AsyncSysproxy::default());
}
// 读取代理服务器设置
let proxy_server_name = "ProxyServer\0".encode_utf16().collect::<Vec<u16>>();
let mut buffer = vec![0u16; 1024];
let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD;
let mut value_type: DWORD = 0;
let server_result = RegQueryValueExW(
hkey,
proxy_server_name.as_ptr(),
ptr::null_mut(),
&mut value_type,
buffer.as_mut_ptr() as *mut u8,
&mut buffer_size,
);
let mut proxy_server = String::new();
if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
proxy_server = String::from_utf16_lossy(&buffer[..end_pos]);
}
// 读取代理绕过列表
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
let mut bypass_buffer = vec![0u16; 1024];
let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD;
let mut bypass_value_type: DWORD = 0;
let override_result = RegQueryValueExW(
hkey,
proxy_override_name.as_ptr(),
ptr::null_mut(),
&mut bypass_value_type,
bypass_buffer.as_mut_ptr() as *mut u8,
&mut bypass_buffer_size,
);
let mut bypass_list = String::new();
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
let end_pos = bypass_buffer
.iter()
.position(|&x| x == 0)
.unwrap_or(bypass_buffer.len());
bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]);
}
RegCloseKey(hkey);
if !proxy_server.is_empty() {
// 解析服务器地址和端口
let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') {
let host = proxy_server[..colon_pos].to_string();
let port = proxy_server[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
(host, port)
} else {
(proxy_server, 8080)
};
log::debug!(target: "app", "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}");
Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: bypass_list,
})
} else {
Ok(AsyncSysproxy::default())
}
}
}
#[cfg(target_os = "macos")]
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
let output = Command::new("scutil").args(["--proxy"]).output().await?;
if !output.status.success() {
return Ok(AsyncSysproxy::default());
}
let stdout = String::from_utf8_lossy(&output.stdout);
log::debug!(target: "app", "scutil proxy output: {stdout}");
let mut http_enabled = false;
let mut http_host = String::new();
let mut http_port = 8080u16;
let mut exceptions = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.contains("HTTPEnable") && line.contains("1") {
http_enabled = true;
} else if line.contains("HTTPProxy") && !line.contains("Port") {
if let Some(host_part) = line.split(':').nth(1) {
http_host = host_part.trim().to_string();
}
} else if line.contains("HTTPPort") {
if let Some(port_part) = line.split(':').nth(1) {
if let Ok(port) = port_part.trim().parse::<u16>() {
http_port = port;
}
}
} else if line.contains("ExceptionsList") {
// 解析异常列表
if let Some(list_part) = line.split(':').nth(1) {
let list = list_part.trim();
if !list.is_empty() {
exceptions.push(list.to_string());
}
}
}
}
Ok(AsyncSysproxy {
enable: http_enabled && !http_host.is_empty(),
host: http_host,
port: http_port,
bypass: exceptions.join(","),
})
}
#[cfg(target_os = "linux")]
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
// Linux: 检查环境变量和桌面环境设置
// 首先检查环境变量
if let Ok(http_proxy) = std::env::var("http_proxy") {
if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) {
return Ok(proxy_info);
}
}
if let Ok(https_proxy) = std::env::var("https_proxy") {
if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) {
return Ok(proxy_info);
}
}
// 尝试使用 gsettings 获取 GNOME 代理设置
let mode_output = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy", "mode"])
.output()
.await;
if let Ok(mode_output) = mode_output {
if mode_output.status.success() {
let mode = String::from_utf8_lossy(&mode_output.stdout)
.trim()
.to_string();
if mode.contains("manual") {
// 获取HTTP代理设置
let host_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "host"])
.output()
.await;
let port_result = Command::new("gsettings")
.args(["get", "org.gnome.system.proxy.http", "port"])
.output()
.await;
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) {
if host_output.status.success() && port_output.status.success() {
let host = String::from_utf8_lossy(&host_output.stdout)
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
let port = String::from_utf8_lossy(&port_output.stdout)
.trim()
.parse::<u16>()
.unwrap_or(8080);
if !host.is_empty() {
return Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: String::new(),
});
}
}
}
}
}
}
Ok(AsyncSysproxy::default())
}
#[cfg(target_os = "linux")]
fn parse_proxy_url(proxy_url: &str) -> Result<AsyncSysproxy> {
// 解析形如 "http://proxy.example.com:8080" 的URL
let url = proxy_url.trim();
// 移除协议前缀
let url = if let Some(stripped) = url.strip_prefix("http://") {
stripped
} else if let Some(stripped) = url.strip_prefix("https://") {
stripped
} else {
url
};
// 解析主机和端口
let (host, port) = if let Some(colon_pos) = url.rfind(':') {
let host = url[..colon_pos].to_string();
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
(host, port)
} else {
(url.to_string(), 8080)
};
if host.is_empty() {
return Err(anyhow!("无效的代理URL"));
}
Ok(AsyncSysproxy {
enable: true,
host,
port,
bypass: std::env::var("no_proxy").unwrap_or_default(),
})
}
}
+23 -18
View File
@@ -74,11 +74,14 @@ impl WebDavClient {
// 获取或创建配置
let config = {
let mut lock = self.config.lock();
if let Some(cfg) = lock.as_ref() {
cfg.clone()
// 首先检查是否已有配置
let existing_config = self.config.lock().as_ref().cloned();
if let Some(cfg) = existing_config {
cfg
} else {
let verge = Config::verge().latest().clone();
// 释放锁后获取异步配置
let verge = Config::verge().await.latest_ref().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
@@ -97,7 +100,8 @@ impl WebDavClient {
password: verge.webdav_password.unwrap_or_default(),
};
*lock = Some(config.clone());
// 重新获取锁并存储配置
*self.config.lock() = Some(config.clone());
config
}
};
@@ -108,10 +112,7 @@ impl WebDavClient {
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(op.timeout()))
.user_agent(format!(
"clash-verge/{} ({} WebDAV-Client)",
APP_VERSION, OS
))
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
.redirect(reqwest::redirect::Policy::custom(|attempt| {
// 允许所有请求类型的重定向,包括PUT
if attempt.previous().len() >= 5 {
@@ -120,8 +121,7 @@ impl WebDavClient {
attempt.follow()
}
}))
.build()
.unwrap(),
.build()?,
)
.set_host(config.url)
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
@@ -177,7 +177,7 @@ impl WebDavClient {
}
Ok(Err(e)) => {
log::warn!("Upload failed, retrying once: {}", e);
log::warn!("Upload failed, retrying once: {e}");
tokio::time::sleep(Duration::from_millis(500)).await;
timeout(
Duration::from_secs(TIMEOUT_UPLOAD),
@@ -237,7 +237,7 @@ impl WebDavClient {
pub fn create_backup() -> Result<(String, PathBuf), Error> {
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
let zip_file_name = format!("{OS}-backup-{now}.zip");
let zip_path = temp_dir().join(&zip_file_name);
let file = fs::File::create(&zip_path)?;
@@ -246,12 +246,17 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
for entry in entries {
let entry = entry.unwrap();
let entry = entry?;
let path = entry.path();
if path.is_file() {
let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap());
let file_name_os = entry.file_name();
let file_name = file_name_os
.to_str()
.ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?;
let backup_path = format!("profiles/{}", file_name);
zip.start_file(backup_path, options)?;
zip.write_all(fs::read(path).unwrap().as_slice())?;
let file_content = fs::read(&path)?;
zip.write_all(&file_content)?;
}
}
}
@@ -259,14 +264,14 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
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() {
obj.remove("webdav_username");
obj.remove("webdav_password");
obj.remove("webdav_url");
}
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.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
+417 -94
View File
@@ -1,13 +1,13 @@
#[cfg(target_os = "macos")]
use crate::core::tray::Tray;
use crate::{
config::*,
core::{
handle,
service::{self},
},
ipc::IpcManager,
logging, logging_error,
module::mihomo::MihomoManager,
process::AsyncHandler,
singleton_lazy,
utils::{
dirs,
help::{self},
@@ -16,7 +16,7 @@ use crate::{
};
use anyhow::Result;
use chrono::Local;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{
fmt,
fs::{create_dir_all, File},
@@ -25,7 +25,6 @@ use std::{
sync::Arc,
};
use tauri_plugin_shell::{process::CommandChild, ShellExt};
use tokio::sync::Mutex;
#[derive(Debug)]
pub struct CoreManager {
@@ -140,23 +139,23 @@ impl CoreManager {
/// 使用默认配置
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
*Config::runtime().draft() = Box::new(IRuntime {
config: Some(Config::clash().latest().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![],
chain_logs: Default::default(),
});
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
Some("# Clash Verge Runtime"),
)?;
help::save_yaml(&runtime_path, &clash_config, Some("# Clash Verge Runtime")).await?;
handle::Handle::notice_message(msg_type, msg_content);
Ok(())
}
/// 验证运行时配置
pub async fn validate_config(&self) -> Result<(bool, String)> {
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)?;
self.validate_config_internal(config_path).await
}
@@ -174,7 +173,7 @@ impl CoreManager {
// 检查文件是否存在
if !std::path::Path::new(config_path).exists() {
let error_msg = format!("File not found: {}", config_path);
let error_msg = format!("File not found: {config_path}");
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
return Ok((false, error_msg));
}
@@ -188,7 +187,7 @@ impl CoreManager {
"检测到Merge文件,仅进行语法检查: {}",
config_path
);
return self.validate_file_syntax(config_path).await;
return self.validate_file_syntax(config_path);
}
// 检查是否为脚本文件
@@ -220,7 +219,7 @@ impl CoreManager {
"检测到脚本文件,使用JavaScript验证: {}",
config_path
);
return self.validate_script_file(config_path).await;
return self.validate_script_file(config_path);
}
// 对YAML配置文件使用Clash内核验证
@@ -249,10 +248,14 @@ impl CoreManager {
config_path
);
let clash_core = Config::verge().latest().get_valid_clash_core();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
let msg = "Failed to get app handle";
logging!(error, Type::Core, true, "{}", msg);
anyhow::anyhow!(msg)
})?;
let app_dir = dirs::app_home_dir()?;
let app_dir_str = dirs::path_to_str(&app_dir)?;
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
@@ -286,7 +289,7 @@ impl CoreManager {
} else if !stderr.is_empty() {
stderr.to_string()
} else if let Some(code) = output.status.code() {
format!("验证进程异常退出,退出码: {}", code)
format!("验证进程异常退出,退出码: {code}")
} else {
"验证进程被终止".to_string()
};
@@ -300,40 +303,40 @@ impl CoreManager {
}
}
/// 只进行文件语法检查,不进行完整验证
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
// 读取文件内容
let content = match std::fs::read_to_string(config_path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read file: {}", err);
let error_msg = format!("Failed to read file: {err}");
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
return Ok((false, error_msg));
}
};
// 对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(_) => {
logging!(info, Type::Config, true, "YAML语法检查通过");
Ok((true, String::new()))
}
Err(err) => {
// 使用标准化的前缀,以便错误处理函数能正确识别
let error_msg = format!("YAML syntax error: {}", err);
let error_msg = format!("YAML syntax error: {err}");
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
Ok((false, error_msg))
}
}
}
/// 验证脚本文件语法
async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
// 读取脚本内容
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read script file: {}", err);
let error_msg = format!("Failed to read script file: {err}");
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
return Ok((false, error_msg));
@@ -366,7 +369,7 @@ impl CoreManager {
Ok((true, String::new()))
}
Err(err) => {
let error_msg = format!("Script syntax error: {}", err);
let error_msg = format!("Script syntax error: {err}");
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
Ok((false, error_msg))
@@ -393,18 +396,18 @@ impl CoreManager {
logging!(info, Type::Config, true, "配置验证通过");
// 4. 验证通过后,生成正式的运行时配置
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);
Ok((true, "something".into()))
}
Ok((false, error_msg)) => {
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
Config::runtime().discard();
Config::runtime().await.discard();
Ok((false, error_msg))
}
Err(e) => {
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
Config::runtime().discard();
Config::runtime().await.discard();
Err(e)
}
}
@@ -415,18 +418,15 @@ impl CoreManager {
logging_error!(Type::Core, true, "{}", msg);
msg
});
match MihomoManager::global()
.put_configs_force(run_path_str?)
.await
{
match IpcManager::global().put_configs_force(run_path_str?).await {
Ok(_) => {
Config::runtime().apply();
Config::runtime().await.apply();
logging!(info, Type::Core, true, "Configuration updated successfully");
Ok(())
}
Err(e) => {
let msg = e.to_string();
Config::runtime().discard();
Config::runtime().await.discard();
logging_error!(Type::Core, true, "Failed to update configuration: {}", msg);
Err(msg)
}
@@ -435,13 +435,313 @@ impl CoreManager {
}
impl CoreManager {
/// 清理多余的 mihomo 进程
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
logging!(info, Type::Core, true, "开始清理多余的 mihomo 进程");
// 获取当前管理的进程 PID
let current_pid = {
let child_guard = self.child_sidecar.lock();
child_guard.as_ref().map(|child| child.pid())
};
let target_processes = ["verge-mihomo", "verge-mihomo-alpha"];
// 并行查找所有目标进程
let mut process_futures = Vec::new();
for &target in &target_processes {
let process_name = if cfg!(windows) {
format!("{target}.exe")
} else {
target.to_string()
};
process_futures.push(self.find_processes_by_name(process_name, target));
}
let process_results = futures::future::join_all(process_futures).await;
// 收集所有需要终止的进程PID
let mut pids_to_kill = Vec::new();
for result in process_results {
match result {
Ok((pids, process_name)) => {
for pid in pids {
// 跳过当前管理的进程
if let Some(current) = current_pid {
if pid == current {
logging!(
debug,
Type::Core,
true,
"跳过当前管理的进程: {} (PID: {})",
process_name,
pid
);
continue;
}
}
pids_to_kill.push((pid, process_name.clone()));
}
}
Err(e) => {
logging!(debug, Type::Core, true, "查找进程时发生错误: {}", e);
}
}
}
if pids_to_kill.is_empty() {
logging!(debug, Type::Core, true, "未发现多余的 mihomo 进程");
return Ok(());
}
let mut kill_futures = Vec::new();
for (pid, process_name) in &pids_to_kill {
kill_futures.push(self.kill_process_with_verification(*pid, process_name.clone()));
}
let kill_results = futures::future::join_all(kill_futures).await;
let killed_count = kill_results.into_iter().filter(|&success| success).count();
if killed_count > 0 {
logging!(
info,
Type::Core,
true,
"清理完成,共终止了 {} 个多余的 mihomo 进程",
killed_count
);
}
Ok(())
}
/// 根据进程名查找进程PID列
async fn find_processes_by_name(
&self,
process_name: String,
_target: &str,
) -> Result<(Vec<u32>, String)> {
#[cfg(windows)]
{
use std::mem;
use winapi::um::handleapi::CloseHandle;
use winapi::um::tlhelp32::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
};
use winapi::um::winnt::HANDLE;
let process_name_clone = process_name.clone();
let pids = AsyncHandler::spawn_blocking(move || -> Result<Vec<u32>> {
let mut pids = Vec::new();
unsafe {
// 创建进程快照
let snapshot: HANDLE = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
return Err(anyhow::anyhow!("Failed to create process snapshot"));
}
let mut pe32: PROCESSENTRY32W = mem::zeroed();
pe32.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
// 获取第一个进程
if Process32FirstW(snapshot, &mut pe32) != 0 {
loop {
// 将宽字符转换为String
let end_pos = pe32
.szExeFile
.iter()
.position(|&x| x == 0)
.unwrap_or(pe32.szExeFile.len());
let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]);
// 检查进程名是否匹配
if exe_file.eq_ignore_ascii_case(&process_name_clone) {
pids.push(pe32.th32ProcessID);
}
if Process32NextW(snapshot, &mut pe32) == 0 {
break;
}
}
}
// 关闭句柄
CloseHandle(snapshot);
}
Ok(pids)
})
.await??;
Ok((pids, process_name))
}
#[cfg(not(windows))]
{
let output = if cfg!(target_os = "macos") {
tokio::process::Command::new("pgrep")
.arg(&process_name)
.output()
.await?
} else {
// Linux
tokio::process::Command::new("pidof")
.arg(&process_name)
.output()
.await?
};
if !output.status.success() {
return Ok((Vec::new(), process_name));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut pids = Vec::new();
// Unix系统直接解析PID列表
for pid_str in stdout.split_whitespace() {
if let Ok(pid) = pid_str.parse::<u32>() {
pids.push(pid);
}
}
Ok((pids, process_name))
}
}
/// 终止进程并验证结果 - 使用Windows API直接终止,更优雅高效
async fn kill_process_with_verification(&self, pid: u32, process_name: String) -> bool {
logging!(
info,
Type::Core,
true,
"尝试终止进程: {} (PID: {})",
process_name,
pid
);
#[cfg(windows)]
let success = {
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE};
AsyncHandler::spawn_blocking(move || -> bool {
unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid);
if process_handle.is_null() {
return false;
}
let result = TerminateProcess(process_handle, 1);
CloseHandle(process_handle);
result != 0
}
})
.await
.unwrap_or(false)
};
#[cfg(not(windows))]
let success = {
tokio::process::Command::new("kill")
.args(["-9", &pid.to_string()])
.output()
.await
.map(|output| output.status.success())
.unwrap_or(false)
};
if success {
// 短暂等待并验证进程是否真正终止
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let still_running = self.is_process_running(pid).await.unwrap_or(false);
if still_running {
logging!(
warn,
Type::Core,
true,
"进程 {} (PID: {}) 终止命令成功但进程仍在运行",
process_name,
pid
);
false
} else {
logging!(
info,
Type::Core,
true,
"成功终止进程: {} (PID: {})",
process_name,
pid
);
true
}
} else {
logging!(
warn,
Type::Core,
true,
"无法终止进程: {} (PID: {})",
process_name,
pid
);
false
}
}
/// Windows API检查进程
async fn is_process_running(&self, pid: u32) -> Result<bool> {
#[cfg(windows)]
{
use winapi::shared::minwindef::DWORD;
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::GetExitCodeProcess;
use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION};
let result = AsyncHandler::spawn_blocking(move || -> Result<bool> {
unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if process_handle.is_null() {
return Ok(false);
}
let mut exit_code: DWORD = 0;
let result = GetExitCodeProcess(process_handle, &mut exit_code);
CloseHandle(process_handle);
if result == 0 {
return Ok(false);
}
Ok(exit_code == 259)
}
})
.await?;
result
}
#[cfg(not(windows))]
{
let output = tokio::process::Command::new("ps")
.args(["-p", &pid.to_string()])
.output()
.await?;
Ok(output.status.success() && !output.stdout.is_empty())
}
}
async fn start_core_by_sidecar(&self) -> Result<()> {
logging!(trace, 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()
.app_handle()
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
let clash_core = Config::verge().latest().get_valid_clash_core();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
let config_dir = dirs::app_home_dir()?;
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
@@ -450,7 +750,7 @@ impl CoreManager {
let now = Local::now();
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
let log_path = service_log_dir.join(format!("sidecar_{}.log", timestamp));
let log_path = service_log_dir.join(format!("sidecar_{timestamp}.log"));
let mut log_file = File::create(log_path)?;
@@ -465,7 +765,7 @@ impl CoreManager {
])
.spawn()?;
tokio::spawn(async move {
AsyncHandler::spawn(move || async move {
while let Some(event) = rx.recv().await {
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event {
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
@@ -489,14 +789,14 @@ impl CoreManager {
"Started core by sidecar pid: {}",
pid
);
*self.child_sidecar.lock().await = Some(child);
self.set_running_mode(RunningMode::Sidecar).await;
*self.child_sidecar.lock() = Some(child);
self.set_running_mode(RunningMode::Sidecar);
Ok(())
}
async fn stop_core_by_sidecar(&self) -> Result<()> {
fn stop_core_by_sidecar(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by sidecar");
if let Some(child) = self.child_sidecar.lock().await.take() {
if let Some(child) = self.child_sidecar.lock().take() {
let pid = child.pid();
child.kill()?;
logging!(
@@ -507,7 +807,7 @@ impl CoreManager {
pid
);
}
self.set_running_mode(RunningMode::NotRunning).await;
self.set_running_mode(RunningMode::NotRunning);
Ok(())
}
}
@@ -515,27 +815,32 @@ impl CoreManager {
impl CoreManager {
async fn start_core_by_service(&self) -> Result<()> {
logging!(trace, 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?;
self.set_running_mode(RunningMode::Service).await;
self.set_running_mode(RunningMode::Service);
Ok(())
}
async fn stop_core_by_service(&self) -> Result<()> {
logging!(trace, Type::Core, true, "Stopping core by service");
service::stop_core_by_service().await?;
self.set_running_mode(RunningMode::NotRunning).await;
self.set_running_mode(RunningMode::NotRunning);
Ok(())
}
}
impl CoreManager {
pub fn global() -> &'static CoreManager {
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
CORE_MANAGER.get_or_init(|| CoreManager {
impl Default for CoreManager {
fn default() -> Self {
CoreManager {
running: Arc::new(Mutex::new(RunningMode::NotRunning)),
child_sidecar: Arc::new(Mutex::new(None)),
})
}
}
}
// Use simplified singleton_lazy macro
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
impl CoreManager {
// 当服务安装失败时的回退逻辑
async fn attempt_service_init(&self) -> Result<()> {
if service::check_service_needs_reinstall().await {
@@ -563,11 +868,11 @@ impl CoreManager {
e
);
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
let mut state = service::ServiceState::get();
let mut state = service::ServiceState::get().await;
if !state.prefer_sidecar {
state.prefer_sidecar = true;
state.last_error = Some(format!("通过服务启动核心失败: {}", e));
if let Err(save_err) = state.save() {
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
if let Err(save_err) = state.save().await {
logging!(
error,
Type::Core,
@@ -585,6 +890,17 @@ impl CoreManager {
pub async fn init(&self) -> Result<()> {
logging!(trace, Type::Core, "Initializing core");
// 应用启动时先清理任何遗留的 mihomo 进程
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
logging!(
warn,
Type::Core,
true,
"应用初始化时清理多余 mihomo 进程失败: {}",
e
);
}
let mut core_started_successfully = false;
if service::is_service_available().await.is_ok() {
@@ -625,7 +941,7 @@ impl CoreManager {
"核心未通过服务模式启动,执行Sidecar回退或首次安装逻辑"
);
let service_state = service::ServiceState::get();
let service_state = service::ServiceState::get().await;
if service_state.prefer_sidecar {
logging!(
@@ -652,7 +968,7 @@ impl CoreManager {
let mut new_state = service::ServiceState::default();
new_state.record_install();
new_state.prefer_sidecar = false;
new_state.save()?;
new_state.save().await?;
if service::is_service_available().await.is_ok() {
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动");
@@ -665,11 +981,11 @@ impl CoreManager {
true,
"新安装的服务启动失败,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get();
let mut final_state = service::ServiceState::get().await;
final_state.prefer_sidecar = true;
final_state.last_error =
Some("Newly installed service failed to start".to_string());
final_state.save()?;
final_state.save().await?;
self.start_core_by_sidecar().await?;
}
} else {
@@ -679,13 +995,13 @@ impl CoreManager {
true,
"服务安装成功但未能连接/立即可用,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get();
let mut final_state = service::ServiceState::get().await;
final_state.prefer_sidecar = true;
final_state.last_error = Some(
"Newly installed service not immediately available/connectable"
.to_string(),
);
final_state.save()?;
final_state.save().await?;
self.start_core_by_sidecar().await?;
}
}
@@ -696,7 +1012,7 @@ impl CoreManager {
prefer_sidecar: true,
..Default::default()
};
new_state.save()?;
new_state.save().await?;
self.start_core_by_sidecar().await?;
}
}
@@ -710,7 +1026,7 @@ impl CoreManager {
true,
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式"
);
let mut final_state = service::ServiceState::get();
let mut final_state = service::ServiceState::get().await;
if !final_state.prefer_sidecar {
logging!(
warn,
@@ -724,7 +1040,7 @@ impl CoreManager {
"Service startup failed or unavailable before sidecar fallback"
.to_string()
}));
final_state.save()?;
final_state.save().await?;
}
self.start_core_by_sidecar().await?;
}
@@ -732,19 +1048,18 @@ impl CoreManager {
}
logging!(trace, Type::Core, "Initied core logic completed");
#[cfg(target_os = "macos")]
logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
// #[cfg(target_os = "macos")]
// logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await);
Ok(())
}
pub async fn set_running_mode(&self, mode: RunningMode) {
let mut guard = self.running.lock().await;
pub fn set_running_mode(&self, mode: RunningMode) {
let mut guard = self.running.lock();
*guard = mode;
}
pub async fn get_running_mode(&self) -> RunningMode {
let guard = self.running.lock().await;
pub fn get_running_mode(&self) -> RunningMode {
let guard = self.running.lock();
(*guard).clone()
}
@@ -756,30 +1071,31 @@ impl CoreManager {
}
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
self.start_core_by_service().await?;
return Ok(());
};
// 服务不可用,检查用户偏好
let service_state = service::ServiceState::get().await;
if service_state.prefer_sidecar {
logging!(
info,
Type::Core,
true,
"服务不可用,根据用户偏好使用Sidecar模式"
);
self.start_core_by_sidecar().await?;
} else {
// 服务不可用,检查用户偏好
let service_state = service::ServiceState::get();
if service_state.prefer_sidecar {
logging!(
info,
Type::Core,
true,
"服务不可用,根据用户偏好使用Sidecar模式"
);
self.start_core_by_sidecar().await?;
} else {
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
self.start_core_by_sidecar().await?;
}
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
self.start_core_by_sidecar().await?;
}
Ok(())
}
/// 停止核心运行
pub async fn stop_core(&self) -> Result<()> {
match self.get_running_mode().await {
match self.get_running_mode() {
RunningMode::Service => self.stop_core_by_service().await,
RunningMode::Sidecar => self.stop_core_by_sidecar().await,
RunningMode::Sidecar => self.stop_core_by_sidecar(),
RunningMode::NotRunning => Ok(()),
}
}
@@ -798,18 +1114,25 @@ impl CoreManager {
logging!(error, Type::Core, true, "{}", error_message);
return Err(error_message.to_string());
}
let core: &str = &clash_core.clone().unwrap();
if !IVerge::VALID_CLASH_CORES.contains(&core) {
let error_message = format!("Clash core invalid name: {}", core);
let core = clash_core.as_ref().ok_or_else(|| {
let msg = "Clash core should not be None";
logging!(error, Type::Core, true, "{}", msg);
msg.to_string()
})?;
if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) {
let error_message = format!("Clash core invalid name: {core}");
logging!(error, Type::Core, true, "{}", error_message);
return Err(error_message);
}
Config::verge().draft().clash_core = clash_core.clone();
Config::verge().apply();
logging_error!(Type::Core, true, Config::verge().latest().save_file());
Config::verge().await.draft_mut().clash_core = clash_core.clone();
Config::verge().await.apply();
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();
logging_error!(Type::Core, true, "{}", msg);
msg
+585
View File
@@ -0,0 +1,585 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::{mpsc, oneshot};
use tokio::time::{sleep, timeout, Duration};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use crate::config::{Config, IVerge};
use crate::core::async_proxy_query::AsyncProxyQuery;
use crate::logging_error;
use crate::process::AsyncHandler;
use crate::utils::logging::Type;
use once_cell::sync::Lazy;
use sysproxy::{Autoproxy, Sysproxy};
#[derive(Debug, Clone)]
pub enum ProxyEvent {
/// 配置变更事件
ConfigChanged,
/// 强制检查代理状态
#[allow(dead_code)]
ForceCheck,
/// 启用系统代理
#[allow(dead_code)]
EnableProxy,
/// 禁用系统代理
#[allow(dead_code)]
DisableProxy,
/// 切换到PAC模式
#[allow(dead_code)]
SwitchToPac,
/// 切换到HTTP代理模式
#[allow(dead_code)]
SwitchToHttp,
/// 应用启动事件
AppStarted,
/// 应用关闭事件
#[allow(dead_code)]
AppStopping,
}
#[derive(Debug, Clone)]
pub struct ProxyState {
pub sys_enabled: bool,
pub pac_enabled: bool,
pub auto_proxy: Autoproxy,
pub sys_proxy: Sysproxy,
pub last_updated: std::time::Instant,
pub is_healthy: bool,
}
impl Default for ProxyState {
fn default() -> Self {
Self {
sys_enabled: false,
pac_enabled: false,
auto_proxy: Autoproxy {
enable: false,
url: "".to_string(),
},
sys_proxy: Sysproxy {
enable: false,
host: "127.0.0.1".to_string(),
port: 7897,
bypass: "".to_string(),
},
last_updated: std::time::Instant::now(),
is_healthy: true,
}
}
}
pub struct EventDrivenProxyManager {
state: Arc<RwLock<ProxyState>>,
event_sender: mpsc::UnboundedSender<ProxyEvent>,
query_sender: mpsc::UnboundedSender<QueryRequest>,
}
#[derive(Debug)]
pub struct QueryRequest {
response_tx: oneshot::Sender<Autoproxy>,
}
// 配置结构体移到外部
struct ProxyConfig {
sys_enabled: bool,
pac_enabled: bool,
guard_enabled: bool,
}
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
impl EventDrivenProxyManager {
pub fn global() -> &'static EventDrivenProxyManager {
&PROXY_MANAGER
}
fn new() -> Self {
let state = Arc::new(RwLock::new(ProxyState::default()));
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (query_tx, query_rx) = mpsc::unbounded_channel();
let state_clone = Arc::clone(&state);
AsyncHandler::spawn(move || Self::start_event_loop(state_clone, event_rx, query_rx));
Self {
state,
event_sender: event_tx,
query_sender: query_tx,
}
}
/// 获取自动代理配置(缓存)
pub async fn get_auto_proxy_cached(&self) -> Autoproxy {
self.state.read().await.auto_proxy.clone()
}
/// 异步获取最新的自动代理配置
pub async fn get_auto_proxy_async(&self) -> Autoproxy {
let (tx, rx) = oneshot::channel();
let query = QueryRequest { response_tx: tx };
if self.query_sender.send(query).is_err() {
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
return self.get_auto_proxy_cached().await;
}
match timeout(Duration::from_secs(5), rx).await {
Ok(Ok(result)) => result,
_ => {
log::warn!(target: "app", "查询超时,返回缓存数据");
self.get_auto_proxy_cached().await
}
}
}
/// 通知配置变更
pub fn notify_config_changed(&self) {
self.send_event(ProxyEvent::ConfigChanged);
}
/// 通知应用启动
pub fn notify_app_started(&self) {
self.send_event(ProxyEvent::AppStarted);
}
/// 通知应用即将关闭
#[allow(dead_code)]
pub fn notify_app_stopping(&self) {
self.send_event(ProxyEvent::AppStopping);
}
/// 启用系统代理
#[allow(dead_code)]
pub fn enable_proxy(&self) {
self.send_event(ProxyEvent::EnableProxy);
}
/// 禁用系统代理
#[allow(dead_code)]
pub fn disable_proxy(&self) {
self.send_event(ProxyEvent::DisableProxy);
}
/// 强制检查代理状态
#[allow(dead_code)]
pub fn force_check(&self) {
self.send_event(ProxyEvent::ForceCheck);
}
fn send_event(&self, event: ProxyEvent) {
if let Err(e) = self.event_sender.send(event) {
log::error!(target: "app", "发送代理事件失败: {e}");
}
}
pub async fn start_event_loop(
state: Arc<RwLock<ProxyState>>,
event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
) {
log::info!(target: "app", "事件驱动代理管理器启动");
// 将 mpsc 接收器包装成 Stream,避免每次循环创建 future
let mut event_stream = UnboundedReceiverStream::new(event_rx);
let mut query_stream = UnboundedReceiverStream::new(query_rx);
loop {
tokio::select! {
Some(event) = event_stream.next() => {
log::debug!(target: "app", "处理代理事件: {event:?}");
Self::handle_event(&state, event).await;
}
Some(query) = query_stream.next() => {
let result = Self::handle_query(&state).await;
let _ = query.response_tx.send(result);
}
else => {
// 两个通道都关闭时退出
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
break;
}
}
}
}
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
match event {
ProxyEvent::ConfigChanged | ProxyEvent::ForceCheck => {
Self::update_proxy_config(state).await;
}
ProxyEvent::EnableProxy => {
Self::enable_system_proxy(state).await;
}
ProxyEvent::DisableProxy => {
Self::disable_system_proxy(state);
}
ProxyEvent::SwitchToPac => {
Self::switch_proxy_mode(state, true).await;
}
ProxyEvent::SwitchToHttp => {
Self::switch_proxy_mode(state, false).await;
}
ProxyEvent::AppStarted => {
Self::initialize_proxy_state(state).await;
}
ProxyEvent::AppStopping => {
log::info!(target: "app", "清理代理状态");
}
}
}
async fn handle_query(state: &Arc<RwLock<ProxyState>>) -> Autoproxy {
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
Self::update_state_timestamp(state, |s| {
s.auto_proxy = auto_proxy.clone();
})
.await;
auto_proxy
}
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "初始化代理状态");
let config = Self::get_proxy_config().await;
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
let sys_proxy = Self::get_sys_proxy_with_timeout().await;
Self::update_state_timestamp(state, |s| {
s.sys_enabled = config.sys_enabled;
s.pac_enabled = config.pac_enabled;
s.auto_proxy = auto_proxy;
s.sys_proxy = sys_proxy;
s.is_healthy = true;
})
.await;
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
}
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
log::debug!(target: "app", "更新代理配置");
let config = Self::get_proxy_config().await;
Self::update_state_timestamp(state, |s| {
s.sys_enabled = config.sys_enabled;
s.pac_enabled = config.pac_enabled;
})
.await;
if config.guard_enabled && config.sys_enabled {
Self::check_and_restore_proxy(state).await;
}
}
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
let (sys_enabled, pac_enabled) = {
let s = state.read().await;
(s.sys_enabled, s.pac_enabled)
};
if !sys_enabled {
return;
}
log::debug!(target: "app", "检查代理状态");
if pac_enabled {
Self::check_and_restore_pac_proxy(state).await;
} else {
Self::check_and_restore_sys_proxy(state).await;
}
}
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
let current = Self::get_auto_proxy_with_timeout().await;
let expected = Self::get_expected_pac_config().await;
Self::update_state_timestamp(state, |s| {
s.auto_proxy = current.clone();
})
.await;
if !current.enable || current.url != expected.url {
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
log::error!(target: "app", "恢复PAC代理失败: {}", e);
}
sleep(Duration::from_millis(500)).await;
let restored = Self::get_auto_proxy_with_timeout().await;
Self::update_state_timestamp(state, |s| {
s.is_healthy = restored.enable && restored.url == expected.url;
s.auto_proxy = restored;
})
.await;
}
}
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
let current = Self::get_sys_proxy_with_timeout().await;
let expected = Self::get_expected_sys_proxy().await;
Self::update_state_timestamp(state, |s| {
s.sys_proxy = current.clone();
})
.await;
if !current.enable || current.host != expected.host || current.port != expected.port {
log::info!(target: "app", "系统代理设置异常,正在恢复...");
if let Err(e) = Self::restore_sys_proxy(&expected).await {
log::error!(target: "app", "恢复系统代理失败: {}", e);
}
sleep(Duration::from_millis(500)).await;
let restored = Self::get_sys_proxy_with_timeout().await;
Self::update_state_timestamp(state, |s| {
s.is_healthy = restored.enable
&& restored.host == expected.host
&& restored.port == expected.port;
s.sys_proxy = restored;
})
.await;
}
}
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "启用系统代理");
let pac_enabled = state.read().await.pac_enabled;
if pac_enabled {
let expected = Self::get_expected_pac_config().await;
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
log::error!(target: "app", "启用PAC代理失败: {}", e);
}
} else {
let expected = Self::get_expected_sys_proxy().await;
if let Err(e) = Self::restore_sys_proxy(&expected).await {
log::error!(target: "app", "启用系统代理失败: {}", e);
}
}
Self::check_and_restore_proxy(state).await;
}
fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "禁用系统代理");
#[cfg(not(target_os = "windows"))]
{
let disabled_sys = Sysproxy::default();
let disabled_auto = Autoproxy::default();
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
}
}
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
log::info!(target: "app", "切换到{}模式", if to_pac { "PAC" } else { "HTTP代理" });
if to_pac {
let disabled_sys = Sysproxy::default();
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
let expected = Self::get_expected_pac_config().await;
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
log::error!(target: "app", "切换到PAC模式失败: {}", e);
}
} else {
let disabled_auto = Autoproxy::default();
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
let expected = Self::get_expected_sys_proxy().await;
if let Err(e) = Self::restore_sys_proxy(&expected).await {
log::error!(target: "app", "切换到HTTP代理模式失败: {}", e);
}
}
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac).await;
Self::check_and_restore_proxy(state).await;
}
async fn get_auto_proxy_with_timeout() -> Autoproxy {
let async_proxy = AsyncProxyQuery::get_auto_proxy().await;
// 转换为兼容的结构
Autoproxy {
enable: async_proxy.enable,
url: async_proxy.url,
}
}
async fn get_sys_proxy_with_timeout() -> Sysproxy {
let async_proxy = AsyncProxyQuery::get_system_proxy().await;
// 转换为兼容的结构
Sysproxy {
enable: async_proxy.enable,
host: async_proxy.host,
port: async_proxy.port,
bypass: async_proxy.bypass,
}
}
// 统一的状态更新方法
async fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
where
F: FnOnce(&mut ProxyState),
{
let mut state_guard = state.write().await;
update_fn(&mut state_guard);
state_guard.last_updated = std::time::Instant::now();
}
async fn get_proxy_config() -> ProxyConfig {
let (sys_enabled, pac_enabled, guard_enabled) = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_ref();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
verge.enable_proxy_guard.unwrap_or(false),
)
};
ProxyConfig {
sys_enabled,
pac_enabled,
guard_enabled,
}
}
async fn get_expected_pac_config() -> Autoproxy {
let proxy_host = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_ref();
verge
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".to_string())
};
let pac_port = IVerge::get_singleton_port();
Autoproxy {
enable: true,
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
}
}
async fn get_expected_sys_proxy() -> Sysproxy {
let verge_config = Config::verge().await;
let verge_mixed_port = verge_config.latest_ref().verge_mixed_port;
let proxy_host = verge_config.latest_ref().proxy_host.clone();
let port = verge_mixed_port.unwrap_or(Config::clash().await.latest_ref().get_mixed_port());
let proxy_host = proxy_host.unwrap_or_else(|| "127.0.0.1".to_string());
Sysproxy {
enable: true,
host: proxy_host,
port,
bypass: Self::get_bypass_config().await,
}
}
async fn get_bypass_config() -> String {
let (use_default, custom_bypass) = {
let verge_config = Config::verge().await;
let verge = verge_config.latest_ref();
(
verge.use_default_bypass.unwrap_or(true),
verge.system_proxy_bypass.clone().unwrap_or_default(),
)
};
#[cfg(target_os = "windows")]
let default_bypass = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
#[cfg(target_os = "linux")]
let default_bypass =
"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")]
let default_bypass = "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>";
if custom_bypass.is_empty() {
default_bypass.to_string()
} else if use_default {
format!("{default_bypass},{custom_bypass}")
} else {
custom_bypass
}
}
#[cfg(target_os = "windows")]
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
Self::execute_sysproxy_command(&["pac", expected_url]).await
}
#[allow(clippy::unused_async)]
#[cfg(not(target_os = "windows"))]
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
{
let new_autoproxy = Autoproxy {
enable: true,
url: expected_url.to_string(),
};
// logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
new_autoproxy
.set_auto_proxy()
.map_err(|e| anyhow::anyhow!("Failed to set auto proxy: {}", e))
}
}
#[cfg(target_os = "windows")]
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
let address = format!("{}:{}", expected.host, expected.port);
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await
}
#[allow(clippy::unused_async)]
#[cfg(not(target_os = "windows"))]
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
{
// logging_error!(Type::System, true, expected.set_system_proxy());
expected
.set_system_proxy()
.map_err(|e| anyhow::anyhow!("Failed to set system proxy: {}", e))
}
}
#[cfg(target_os = "windows")]
async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> {
use crate::utils::dirs;
#[allow(unused_imports)] // creation_flags必须
use std::os::windows::process::CommandExt;
use tokio::process::Command;
let binary_path = match dirs::service_path() {
Ok(path) => path,
Err(e) => {
log::error!(target: "app", "获取服务路径失败: {e}");
return Err(e);
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe 不存在");
}
anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist");
let _output = Command::new(sysproxy_exe)
.args(args)
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
.output()
.await?;
Ok(())
}
}
+162 -107
View File
@@ -1,4 +1,4 @@
use once_cell::sync::OnceCell;
use crate::singleton;
use parking_lot::RwLock;
use std::{
sync::{
@@ -82,122 +82,123 @@ impl NotificationSystem {
*self.last_emit_time.write() = Instant::now();
self.worker_handle = Some(
thread::Builder::new()
.name("frontend-notifier".into())
.spawn(move || {
let handle = Handle::global();
match thread::Builder::new()
.name("frontend-notifier".into())
.spawn(move || {
let handle = Handle::global();
while !handle.is_exiting() {
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(event) => {
let system_guard = handle.notification_system.read();
if system_guard.as_ref().is_none() {
log::warn!("NotificationSystem not found in handle while processing event.");
continue;
}
let system = system_guard.as_ref().unwrap();
while !handle.is_exiting() {
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(event) => {
let system_guard = handle.notification_system.read();
let Some(system) = system_guard.as_ref() else {
log::warn!("NotificationSystem not found in handle while processing event.");
continue;
};
let is_emergency = *system.emergency_mode.read();
let is_emergency = *system.emergency_mode.read();
if is_emergency {
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
if status == "info" {
log::warn!(
"Emergency mode active, skipping info message"
);
continue;
}
if is_emergency {
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
if status == "info" {
log::warn!(
"Emergency mode active, skipping info message"
);
continue;
}
}
}
if let Some(window) = handle.get_window() {
*system.last_emit_time.write() = Instant::now();
if let Some(window) = handle.get_window() {
*system.last_emit_time.write() = Instant::now();
let (event_name_str, payload_result) = match event {
FrontendEvent::RefreshClash => {
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
}
FrontendEvent::RefreshVerge => {
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
}
FrontendEvent::NoticeMessage { status, message } => {
match serde_json::to_value((status, message)) {
Ok(p) => ("verge://notice-message", Ok(p)),
Err(e) => {
log::error!("Failed to serialize NoticeMessage payload: {}", e);
("verge://notice-message", Err(e))
}
}
}
FrontendEvent::ProfileChanged { current_profile_id } => {
("profile-changed", Ok(serde_json::json!(current_profile_id)))
}
FrontendEvent::TimerUpdated { profile_index } => {
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
}
FrontendEvent::StartupCompleted => {
("verge://startup-completed", Ok(serde_json::json!(null)))
}
FrontendEvent::ProfileUpdateStarted { uid } => {
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
}
FrontendEvent::ProfileUpdateCompleted { uid } => {
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
}
};
if let Ok(payload) = payload_result {
match window.emit(event_name_str, payload) {
Ok(_) => {
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
// 记录成功发送的事件
if log::log_enabled!(log::Level::Debug) {
log::debug!("Successfully emitted event: {}", event_name_str);
}
}
let (event_name_str, payload_result) = match event {
FrontendEvent::RefreshClash => {
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
}
FrontendEvent::RefreshVerge => {
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
}
FrontendEvent::NoticeMessage { status, message } => {
match serde_json::to_value((status, message)) {
Ok(p) => ("verge://notice-message", Ok(p)),
Err(e) => {
log::warn!("Failed to emit event {}: {}", event_name_str, e);
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
log::error!("Failed to serialize NoticeMessage payload: {e}");
("verge://notice-message", Err(e))
}
}
}
FrontendEvent::ProfileChanged { current_profile_id } => {
("profile-changed", Ok(serde_json::json!(current_profile_id)))
}
FrontendEvent::TimerUpdated { profile_index } => {
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
}
FrontendEvent::StartupCompleted => {
("verge://startup-completed", Ok(serde_json::json!(null)))
}
FrontendEvent::ProfileUpdateStarted { uid } => {
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
}
FrontendEvent::ProfileUpdateCompleted { uid } => {
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
}
};
let errors = system.stats.total_errors.load(Ordering::SeqCst);
const EMIT_ERROR_THRESHOLD: u64 = 10;
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
log::warn!(
"Reached {} emit errors, entering emergency mode",
EMIT_ERROR_THRESHOLD
);
*system.emergency_mode.write() = true;
}
if let Ok(payload) = payload_result {
match window.emit(event_name_str, payload) {
Ok(_) => {
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
// 记录成功发送的事件
if log::log_enabled!(log::Level::Debug) {
log::debug!("Successfully emitted event: {event_name_str}");
}
}
Err(e) => {
log::warn!("Failed to emit event {event_name_str}: {e}");
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
let errors = system.stats.total_errors.load(Ordering::SeqCst);
const EMIT_ERROR_THRESHOLD: u64 = 10;
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
log::warn!(
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
);
*system.emergency_mode.write() = true;
}
}
} else {
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
log::warn!("Skipped emitting event due to payload serialization error for {}", event_name_str);
}
} else {
log::warn!("No window found, skipping event emit.");
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*system.stats.last_error_time.write() = Some(Instant::now());
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
}
thread::sleep(Duration::from_millis(20));
}
Err(mpsc::RecvTimeoutError::Timeout) => {
continue;
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
log::info!(
"Notification channel disconnected, exiting worker thread"
);
break;
} else {
log::warn!("No window found, skipping event emit.");
}
thread::sleep(Duration::from_millis(20));
}
Err(mpsc::RecvTimeoutError::Timeout) => {
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
log::info!(
"Notification channel disconnected, exiting worker thread"
);
break;
}
}
}
log::info!("Notification worker thread exiting");
})
.expect("Failed to start notification worker thread"),
);
log::info!("Notification worker thread exiting");
}) {
Ok(handle) => {
self.worker_handle = Some(handle);
}
Err(e) => {
log::error!("Failed to start notification worker thread: {e}");
}
}
}
/// 发送事件到队列
@@ -215,7 +216,7 @@ impl NotificationSystem {
match sender.send(event) {
Ok(_) => true,
Err(e) => {
log::warn!("Failed to send event to notification queue: {:?}", e);
log::warn!("Failed to send event to notification queue: {e:?}");
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
*self.stats.last_error_time.write() = Some(Instant::now());
false
@@ -243,7 +244,7 @@ impl NotificationSystem {
log::info!("NotificationSystem worker thread joined successfully");
}
Err(e) => {
log::error!("NotificationSystem worker thread join failed: {:?}", e);
log::error!("NotificationSystem worker thread join failed: {e:?}");
}
}
}
@@ -273,16 +274,18 @@ impl Default for Handle {
}
}
// Use singleton macro
singleton!(Handle, HANDLE);
impl Handle {
pub fn global() -> &'static Handle {
static HANDLE: OnceCell<Handle> = OnceCell::new();
HANDLE.get_or_init(Handle::default)
pub fn new() -> Self {
Self::default()
}
pub fn init(&self, app_handle: &AppHandle) {
pub fn init(&self, app_handle: AppHandle) {
{
let mut handle = self.app_handle.write();
*handle = Some(app_handle.clone());
*handle = Some(app_handle);
}
let mut system_opt = self.notification_system.write();
@@ -291,6 +294,7 @@ impl Handle {
}
}
/// 获取 AppHandle
pub fn app_handle(&self) -> Option<AppHandle> {
self.app_handle.read().clone()
}
@@ -500,7 +504,7 @@ impl Handle {
});
if let Err(e) = thread_result {
log::error!("Failed to spawn startup errors thread: {}", e);
log::error!("Failed to spawn startup errors thread: {e}");
}
}
@@ -518,3 +522,54 @@ impl Handle {
*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
);
}
}
}
+317 -210
View File
@@ -1,34 +1,297 @@
use crate::process::AsyncHandler;
use crate::utils::notification::{notify_event, NotificationEvent};
use crate::{
config::Config,
core::handle,
feat, logging, logging_error,
module::lightweight::entry_lightweight_mode,
process::AsyncHandler,
utils::{logging::Type, resolve},
config::Config, core::handle, feat, logging, logging_error,
module::lightweight::entry_lightweight_mode, singleton_with_logging, utils::logging::Type,
};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc};
use tauri::Manager;
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
use tauri::{AppHandle, Manager};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
/// Enum representing all available hotkey functions
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HotkeyFunction {
OpenOrCloseDashboard,
ClashModeRule,
ClashModeGlobal,
ClashModeDirect,
ToggleSystemProxy,
ToggleTunMode,
EntryLightweightMode,
Quit,
#[cfg(target_os = "macos")]
Hide,
}
impl fmt::Display for HotkeyFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard",
HotkeyFunction::ClashModeRule => "clash_mode_rule",
HotkeyFunction::ClashModeGlobal => "clash_mode_global",
HotkeyFunction::ClashModeDirect => "clash_mode_direct",
HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy",
HotkeyFunction::ToggleTunMode => "toggle_tun_mode",
HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode",
HotkeyFunction::Quit => "quit",
#[cfg(target_os = "macos")]
HotkeyFunction::Hide => "hide",
};
write!(f, "{s}")
}
}
impl FromStr for HotkeyFunction {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard),
"clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule),
"clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal),
"clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect),
"toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy),
"toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode),
"entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode),
"quit" => Ok(HotkeyFunction::Quit),
#[cfg(target_os = "macos")]
"hide" => Ok(HotkeyFunction::Hide),
_ => bail!("invalid hotkey function: {}", s),
}
}
}
#[cfg(target_os = "macos")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Enum representing predefined system hotkeys
pub enum SystemHotkey {
CmdQ,
CmdW,
}
#[cfg(target_os = "macos")]
impl fmt::Display for SystemHotkey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
SystemHotkey::CmdQ => "CMD+Q",
SystemHotkey::CmdW => "CMD+W",
};
write!(f, "{s}")
}
}
#[cfg(target_os = "macos")]
impl SystemHotkey {
pub fn function(self) -> HotkeyFunction {
match self {
SystemHotkey::CmdQ => HotkeyFunction::Quit,
SystemHotkey::CmdW => HotkeyFunction::Hide,
}
}
}
pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
current: Arc<Mutex<Vec<String>>>,
}
impl Hotkey {
pub fn global() -> &'static Hotkey {
static HOTKEY: OnceCell<Hotkey> = OnceCell::new();
HOTKEY.get_or_init(|| Hotkey {
fn new() -> Self {
Self {
current: Arc::new(Mutex::new(Vec::new())),
})
}
}
pub fn init(&self) -> Result<()> {
let verge = Config::verge();
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
/// Execute the function associated with a hotkey function enum
fn execute_function(function: HotkeyFunction, app_handle: &AppHandle) {
let app_handle = app_handle.clone();
match function {
HotkeyFunction::OpenOrCloseDashboard => {
AsyncHandler::spawn(async move || {
crate::feat::open_or_close_dashboard_hotkey().await;
notify_event(app_handle, NotificationEvent::DashboardToggled).await;
});
}
HotkeyFunction::ClashModeRule => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("rule".into()).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Rule" },
)
.await;
});
}
HotkeyFunction::ClashModeGlobal => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("global".into()).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Global" },
)
.await;
});
}
HotkeyFunction::ClashModeDirect => {
AsyncHandler::spawn(async move || {
feat::change_clash_mode("direct".into()).await;
notify_event(
app_handle,
NotificationEvent::ClashModeChanged { mode: "Direct" },
)
.await;
});
}
HotkeyFunction::ToggleSystemProxy => {
AsyncHandler::spawn(async move || {
feat::toggle_system_proxy().await;
notify_event(app_handle, NotificationEvent::SystemProxyToggled).await;
});
}
HotkeyFunction::ToggleTunMode => {
AsyncHandler::spawn(async move || {
feat::toggle_tun_mode(None).await;
notify_event(app_handle, NotificationEvent::TunModeToggled).await;
});
}
HotkeyFunction::EntryLightweightMode => {
AsyncHandler::spawn(async move || {
entry_lightweight_mode().await;
notify_event(app_handle, NotificationEvent::LightweightModeEntered).await;
});
}
HotkeyFunction::Quit => {
AsyncHandler::spawn(async move || {
notify_event(app_handle, NotificationEvent::AppQuit).await;
feat::quit().await;
});
}
#[cfg(target_os = "macos")]
HotkeyFunction::Hide => {
AsyncHandler::spawn(async move || {
feat::hide().await;
notify_event(app_handle, NotificationEvent::AppHidden).await;
});
}
}
}
#[cfg(target_os = "macos")]
/// Register a system hotkey using enum
pub async fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
let hotkey_str = hotkey.to_string();
let function = hotkey.function();
self.register_hotkey_with_function(&hotkey_str, function)
.await
}
#[cfg(target_os = "macos")]
/// Unregister a system hotkey using enum
pub fn unregister_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
let hotkey_str = hotkey.to_string();
self.unregister(&hotkey_str)
}
/// Register a hotkey with function enum
#[allow(clippy::unused_async)]
pub async fn register_hotkey_with_function(
&self,
hotkey: &str,
function: HotkeyFunction,
) -> Result<()> {
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
let manager = app_handle.global_shortcut();
logging!(
debug,
Type::Hotkey,
"Attempting to register hotkey: {} for function: {}",
hotkey,
function
);
if manager.is_registered(hotkey) {
logging!(
debug,
Type::Hotkey,
"Hotkey {} was already registered, unregistering first",
hotkey
);
manager.unregister(hotkey)?;
}
let is_quit = matches!(function, HotkeyFunction::Quit);
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
let hotkey_event_owned = *hotkey_event;
let event_owned = event;
let function_owned = function;
let is_quit_owned = is_quit;
let app_handle_cloned = app_handle.clone();
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") {
if window.is_focused().unwrap_or(false) {
logging!(debug, Type::Hotkey, "Executing quit function");
Self::execute_function(function_owned, &app_handle_cloned);
}
}
} else {
logging!(debug, Type::Hotkey, "Executing function directly");
let is_enable_global_hotkey = Config::verge()
.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!(
debug,
Type::Hotkey,
"Successfully registered hotkey {} for {}",
hotkey,
function
);
Ok(())
}
}
// Use unified singleton macro
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
impl Hotkey {
pub async fn init(&self) -> Result<()> {
let verge = Config::verge().await;
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
logging!(
debug,
@@ -38,12 +301,14 @@ impl Hotkey {
enable_global_hotkey
);
// 如果全局热键被禁用,则不注册热键
if !enable_global_hotkey {
return Ok(());
}
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
// Extract hotkeys data before async operations
let hotkeys = verge.latest_ref().hotkeys.as_ref().cloned();
if let Some(hotkeys) = hotkeys {
logging!(
debug,
Type::Hotkey,
@@ -67,7 +332,7 @@ impl Hotkey {
key,
func
);
if let Err(e) = self.register(key, func) {
if let Err(e) = self.register(key, func).await {
logging!(
error,
Type::Hotkey,
@@ -101,7 +366,7 @@ impl Hotkey {
}
}
}
self.current.lock().clone_from(hotkeys);
self.current.lock().clone_from(&hotkeys);
} else {
logging!(debug, Type::Hotkey, "No hotkeys configured");
}
@@ -110,203 +375,34 @@ impl Hotkey {
}
pub fn reset(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
let manager = app_handle.global_shortcut();
manager.unregister_all()?;
Ok(())
}
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
logging!(
debug,
Type::Hotkey,
"Attempting to register hotkey: {} for function: {}",
hotkey,
func
);
if manager.is_registered(hotkey) {
logging!(
debug,
Type::Hotkey,
"Hotkey {} was already registered, unregistering first",
hotkey
);
manager.unregister(hotkey)?;
}
let f = match func.trim() {
"open_or_close_dashboard" => {
logging!(
debug,
Type::Hotkey,
"Registering open_or_close_dashboard function"
);
|| {
logging!(
debug,
Type::Hotkey,
true,
"=== Hotkey Dashboard Window Operation Start ==="
);
// 检查是否在轻量模式下,如果是,需要同步处理
if crate::module::lightweight::is_in_lightweight_mode() {
logging!(
info,
Type::Hotkey,
true,
"In lightweight mode, calling open_or_close_dashboard directly"
);
crate::feat::open_or_close_dashboard();
} else {
AsyncHandler::spawn(move || async move {
logging!(
debug,
Type::Hotkey,
true,
"Toggle dashboard window visibility (async)"
);
// 检查窗口是否存在
if let Some(window) = handle::Handle::global().get_window() {
// 如果窗口可见,则隐藏
match window.is_visible() {
Ok(visible) => {
if visible {
logging!(
info,
Type::Window,
true,
"Window is visible, hiding it"
);
let _ = window.hide();
} else {
// 如果窗口不可见,则显示
logging!(
info,
Type::Window,
true,
"Window is hidden, showing it"
);
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
}
}
Err(e) => {
logging!(
warn,
Type::Window,
true,
"Failed to check window visibility: {}",
e
);
let _ = window.show();
let _ = window.set_focus();
}
}
} else {
// 如果窗口不存在,创建一个新窗口
logging!(
info,
Type::Window,
true,
"Window does not exist, creating a new one"
);
resolve::create_window(true);
}
});
}
logging!(
debug,
Type::Hotkey,
"=== Hotkey Dashboard Window Operation End ==="
);
}
}
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
"clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
"toggle_system_proxy" => || feat::toggle_system_proxy(),
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
"entry_lightweight_mode" => || entry_lightweight_mode(),
"quit" => || feat::quit(),
#[cfg(target_os = "macos")]
"hide" => || feat::hide(),
_ => {
logging!(error, Type::Hotkey, "Invalid function: {}", func);
bail!("invalid function \"{func}\"");
}
};
let is_quit = func.trim() == "quit";
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
if event.state == ShortcutState::Pressed {
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey);
if hotkey.key == Code::KeyQ && is_quit {
if let Some(window) = app_handle.get_webview_window("main") {
if window.is_focused().unwrap_or(false) {
logging!(debug, Type::Hotkey, "Executing quit function");
f();
}
}
} else {
// 直接执行函数,不做任何状态检查
logging!(debug, Type::Hotkey, "Executing function directly");
// 获取全局热键状态
let is_enable_global_hotkey = Config::verge()
.latest()
.enable_global_hotkey
.unwrap_or(true);
if is_enable_global_hotkey {
f();
} 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 {
f();
}
}
}
}
});
logging!(
debug,
Type::Hotkey,
"Successfully registered hotkey {} for {}",
hotkey,
func
);
Ok(())
/// Register a hotkey with string-based function (backward compatibility)
pub async fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let function = HotkeyFunction::from_str(func)?;
self.register_hotkey_with_function(hotkey, function).await
}
pub fn unregister(&self, hotkey: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = handle::Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
let manager = app_handle.global_shortcut();
manager.unregister(hotkey)?;
logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey);
Ok(())
}
pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
let mut current = self.current.lock();
let old_map = Self::get_map_from_vec(&current);
pub async fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
// Extract current hotkeys before async operations
let current_hotkeys = self.current.lock().clone();
let old_map = Self::get_map_from_vec(&current_hotkeys);
let new_map = Self::get_map_from_vec(&new_hotkeys);
let (del, add) = Self::get_diff(old_map, new_map);
@@ -315,11 +411,12 @@ impl Hotkey {
let _ = self.unregister(key);
});
add.iter().for_each(|(key, func)| {
logging_error!(Type::Hotkey, self.register(key, func));
});
for (key, func) in add.iter() {
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(())
}
@@ -371,7 +468,17 @@ impl Hotkey {
impl Drop for Hotkey {
fn drop(&mut self) {
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
logging!(
error,
Type::Hotkey,
"Failed to get app handle during hotkey cleanup"
);
return;
}
};
if let Err(e) = app_handle.global_shortcut().unregister_all() {
logging!(
error,
+3 -1
View File
@@ -1,6 +1,8 @@
pub mod async_proxy_query;
pub mod backup;
#[allow(clippy::module_inception)]
mod core;
pub mod event_driven_proxy;
pub mod handle;
pub mod hotkey;
pub mod service;
@@ -10,4 +12,4 @@ pub mod timer;
pub mod tray;
pub mod win_uwp;
pub use self::{core::*, timer::Timer};
pub use self::{core::*, event_driven_proxy::EventDrivenProxyManager, timer::Timer};
+51 -226
View File
@@ -13,7 +13,7 @@ use std::{
time::{SystemTime, UNIX_EPOCH},
};
const REQUIRED_SERVICE_VERSION: &str = "1.1.0"; // 定义所需的服务版本号
const REQUIRED_SERVICE_VERSION: &str = "1.1.2"; // 定义所需的服务版本号
// 限制重装时间和次数的常量
const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期
@@ -31,22 +31,26 @@ pub struct ServiceState {
impl ServiceState {
// 获取当前的服务状态
pub fn get() -> Self {
if let Some(state) = Config::verge().latest().service_state.clone() {
pub async fn get() -> Self {
if let Some(state) = Config::verge().await.latest_ref().service_state.clone() {
return state;
}
Self::default()
}
// 保存服务状态
pub fn save(&self) -> Result<()> {
let config = Config::verge();
let mut latest = config.latest().clone();
pub async fn save(&self) -> Result<()> {
let config = Config::verge().await;
let mut latest = config.latest_ref().clone();
latest.service_state = Some(self.clone());
*config.draft() = latest;
*config.draft_mut() = latest;
config.apply();
let result = config.latest().save_file();
result
// 先获取数据,再异步保存,避免跨await持有锁
let verge_data = config.latest_ref().clone();
drop(config); // 显式释放锁
verge_data.save_file().await
}
// 更新安装信息
@@ -138,7 +142,7 @@ pub async fn uninstall_service() -> Result<()> {
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -172,7 +176,7 @@ pub async fn install_service() -> Result<()> {
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -184,7 +188,7 @@ pub async fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "reinstall service");
// 获取当前服务状态
let mut service_state = ServiceState::get();
let mut service_state = ServiceState::get().await;
// 检查是否允许重装
if !service_state.can_reinstall() {
@@ -214,19 +218,20 @@ pub async fn reinstall_service() -> Result<()> {
// 记录安装信息并保存
service_state.record_install();
service_state.last_error = None;
service_state.save()?;
service_state.save().await?;
Ok(())
}
Err(err) => {
let error = format!("failed to install service: {}", err);
let error = format!("failed to install service: {err}");
service_state.last_error = Some(error.clone());
service_state.prefer_sidecar = true;
service_state.save()?;
service_state.save().await?;
bail!(error)
}
}
}
#[allow(clippy::unused_async)]
#[cfg(target_os = "linux")]
pub async fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, true, "uninstall service");
@@ -254,13 +259,13 @@ pub async fn uninstall_service() -> Result<()> {
Type::Service,
true,
"uninstall status code:{}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -294,13 +299,13 @@ pub async fn install_service() -> Result<()> {
Type::Service,
true,
"install status code:{}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -312,7 +317,7 @@ pub async fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "reinstall service");
// 获取当前服务状态
let mut service_state = ServiceState::get();
let mut service_state = ServiceState::get().await;
// 检查是否允许重装
if !service_state.can_reinstall() {
@@ -342,14 +347,14 @@ pub async fn reinstall_service() -> Result<()> {
// 记录安装信息并保存
service_state.record_install();
service_state.last_error = None;
service_state.save()?;
service_state.save().await?;
Ok(())
}
Err(err) => {
let error = format!("failed to install service: {}", err);
let error = format!("failed to install service: {err}");
service_state.last_error = Some(error.clone());
service_state.prefer_sidecar = true;
service_state.save()?;
service_state.save().await?;
bail!(error)
}
}
@@ -370,7 +375,7 @@ pub async fn uninstall_service() -> Result<()> {
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
let prompt = t("Service Administrator Prompt");
let prompt = t("Service Administrator Prompt").await;
let command = format!(
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
);
@@ -384,7 +389,7 @@ pub async fn uninstall_service() -> Result<()> {
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -406,7 +411,7 @@ pub async fn install_service() -> Result<()> {
let install_shell: String = install_path.to_string_lossy().into_owned();
let prompt = t("Service Administrator Prompt");
let prompt = t("Service Administrator Prompt").await;
let command = format!(
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
);
@@ -420,7 +425,7 @@ pub async fn install_service() -> Result<()> {
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
status.code().unwrap_or(-1)
);
}
@@ -432,7 +437,7 @@ pub async fn reinstall_service() -> Result<()> {
logging!(info, Type::Service, true, "reinstall service");
// 获取当前服务状态
let mut service_state = ServiceState::get();
let mut service_state = ServiceState::get().await;
// 检查是否允许重装
if !service_state.can_reinstall() {
@@ -462,14 +467,14 @@ pub async fn reinstall_service() -> Result<()> {
// 记录安装信息并保存
service_state.record_install();
service_state.last_error = None;
service_state.save()?;
service_state.save().await?;
Ok(())
}
Err(err) => {
let error = format!("failed to install service: {}", err);
let error = format!("failed to install service: {err}");
service_state.last_error = Some(error.clone());
service_state.prefer_sidecar = true;
service_state.save()?;
service_state.save().await?;
bail!(error)
}
}
@@ -676,7 +681,7 @@ pub async fn check_service_version() -> Result<String> {
pub async fn check_service_needs_reinstall() -> bool {
logging!(info, Type::Service, true, "开始检查服务是否需要重装");
let service_state = ServiceState::get();
let service_state = ServiceState::get().await;
if !service_state.can_reinstall() {
log::info!(target: "app", "服务重装检查: 处于冷却期或已达最大尝试次数");
@@ -686,7 +691,7 @@ pub async fn check_service_needs_reinstall() -> bool {
// 检查版本和可用性
match check_service_version().await {
Ok(version) => {
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
log::info!(target: "app", "服务版本检测:当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
/* logging!(
info,
Type::Service,
@@ -698,8 +703,7 @@ pub async fn check_service_needs_reinstall() -> bool {
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
if needs_reinstall {
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
version, REQUIRED_SERVICE_VERSION);
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装");
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
@@ -717,7 +721,7 @@ pub async fn check_service_needs_reinstall() -> bool {
// 检查服务是否可用
match is_service_available().await {
Ok(()) => {
log::info!(target: "app", "服务正在运行但版本检查失败: {}", err);
log::info!(target: "app", "服务正在运行但版本检查失败: {err}");
/* logging!(
info,
Type::Service,
@@ -742,7 +746,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
let clash_core = Config::verge().latest().get_valid_clash_core();
let clash_core = Config::verge().await.latest_ref().get_valid_clash_core();
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}");
@@ -827,8 +831,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
// 先检查服务版本,不受冷却期限制
let version_check = match check_service_version().await {
Ok(version) => {
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
version, REQUIRED_SERVICE_VERSION);
log::info!(target: "app", "检测到服务版本: {version}, 要求版本: {REQUIRED_SERVICE_VERSION}");
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
log::warn!(target: "app", "服务版本不匹配,需要重装");
@@ -839,7 +842,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
}
}
Err(err) => {
log::warn!(target: "app", "无法获取服务版本: {}", err);
log::warn!(target: "app", "无法获取服务版本: {err}");
false
}
};
@@ -852,23 +855,20 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
if !version_check {
log::info!(target: "app", "服务版本不匹配,尝试重装");
let service_state = ServiceState::get();
let service_state = ServiceState::get().await;
if !service_state.can_reinstall() {
log::warn!(target: "app", "由于限制无法重装服务");
if let Ok(()) = start_with_existing_service(config_file).await {
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
return Ok(());
} else {
bail!("服务版本不匹配且无法重装,启动失败");
}
bail!("服务版本不匹配且无法重装,启动失败");
}
log::info!(target: "app", "开始重装服务");
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err);
log::info!(target: "app", "尝试使用现有服务");
return start_with_existing_service(config_file).await;
log::warn!(target: "app", "服务重装失败: {err}");
bail!("Failed to reinstall service: {}", err);
}
log::info!(target: "app", "服务重装成功,尝试启动");
@@ -884,7 +884,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
}
}
Err(err) => {
log::warn!(target: "app", "服务检查失败: {}", err);
log::warn!(target: "app", "服务检查失败: {err}");
}
}
@@ -893,7 +893,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
log::info!(target: "app", "服务需要重装");
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err);
log::warn!(target: "app", "服务重装失败: {err}");
bail!("Failed to reinstall service: {}", err);
}
@@ -976,7 +976,7 @@ pub async fn force_reinstall_service() -> Result<()> {
log::info!(target: "app", "用户请求强制重装服务");
let service_state = ServiceState::default();
service_state.save()?;
service_state.save().await?;
log::info!(target: "app", "已重置服务状态,开始执行重装");
@@ -986,183 +986,8 @@ pub async fn force_reinstall_service() -> Result<()> {
Ok(())
}
Err(err) => {
log::error!(target: "app", "强制重装服务失败: {}", err);
log::error!(target: "app", "强制重装服务失败: {err}");
bail!("强制重装服务失败: {}", err)
}
}
}
/*
/// 彻底诊断服务状态,检查安装状态、IPC通信和服务版本
pub async fn diagnose_service() -> Result<()> {
logging!(info, Type::Service, true, "============= 开始服务诊断 =============");
// 1. 检查服务文件是否存在
let service_path = dirs::service_path();
match service_path {
Ok(path) => {
let service_exists = path.exists();
logging!(info, Type::Service, true, "服务可执行文件路径: {:?}, 存在: {}", path, service_exists);
if !service_exists {
logging!(error, Type::Service, true, "服务可执行文件不存在,需要重新安装");
bail!("服务可执行文件不存在,需要重新安装");
}
// 检查服务版本文件
let version_file = path.with_file_name("version.txt");
if version_file.exists() {
match std::fs::read_to_string(&version_file) {
Ok(content) => {
logging!(info, Type::Service, true, "服务版本文件内容: {}", content.trim());
}
Err(e) => {
logging!(warn, Type::Service, true, "读取服务版本文件失败: {}", e);
}
}
} else {
logging!(warn, Type::Service, true, "服务版本文件不存在: {:?}", version_file);
}
}
Err(e) => {
logging!(error, Type::Service, true, "获取服务路径失败: {}", e);
bail!("获取服务路径失败: {}", e);
}
}
// 2. 检查IPC通信 - 命名管道/Unix套接字
let socket_path = if cfg!(windows) {
r"\\.\pipe\clash-verge-service"
} else {
"/tmp/clash-verge-service.sock"
};
logging!(info, Type::Service, true, "IPC通信路径: {}", socket_path);
if !cfg!(windows) {
// Unix系统检查套接字文件是否存在
let socket_exists = std::path::Path::new(socket_path).exists();
logging!(info, Type::Service, true, "Unix套接字文件是否存在: {}", socket_exists);
if !socket_exists {
logging!(warn, Type::Service, true, "Unix套接字文件不存在,服务可能未运行");
}
}
// 3. 尝试通过IPC检查服务状态
logging!(info, Type::Service, true, "尝试通过IPC通信检查服务状态...");
match check_service().await {
Ok(resp) => {
logging!(info, Type::Service, true, "服务状态检查成功: code={}, msg={}", resp.code, resp.msg);
// 4. 检查服务版本
match check_service_version().await {
Ok(version) => {
logging!(info, Type::Service, true, "服务版本: {}, 要求版本: {}",
version, REQUIRED_SERVICE_VERSION);
if version != REQUIRED_SERVICE_VERSION {
logging!(warn, Type::Service, true, "服务版本不匹配,建议重装服务");
} else {
logging!(info, Type::Service, true, "服务版本匹配");
}
}
Err(err) => {
logging!(error, Type::Service, true, "检查服务版本失败: {}", err);
}
}
}
Err(err) => {
logging!(error, Type::Service, true, "服务状态检查失败: {}", err);
// 5. 检查系统服务状态 - Windows专用
#[cfg(windows)]
{
use std::process::Command;
logging!(info, Type::Service, true, "尝试检查Windows服务状态...");
let output = Command::new("sc")
.args(["query", "clash_verge_service"])
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
let contains_running = stdout.contains("RUNNING");
logging!(info, Type::Service, true, "Windows服务查询结果: {}",
if contains_running { "正在运行" } else { "未运行" });
if !contains_running {
logging!(info, Type::Service, true, "服务输出: {}", stdout);
}
}
Err(e) => {
logging!(error, Type::Service, true, "检查Windows服务状态失败: {}", e);
}
}
}
// macOS专用
#[cfg(target_os = "macos")]
{
use std::process::Command;
logging!(info, Type::Service, true, "尝试检查macOS服务状态...");
let output = Command::new("launchctl")
.args(["list", "io.github.clash-verge-rev.clash-verge-rev.service"])
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
if out.status.success() {
logging!(info, Type::Service, true, "macOS服务正在运行");
logging!(debug, Type::Service, true, "服务详情: {}", stdout);
} else {
logging!(warn, Type::Service, true, "macOS服务未运行");
if !stderr.is_empty() {
logging!(info, Type::Service, true, "错误信息: {}", stderr);
}
}
}
Err(e) => {
logging!(error, Type::Service, true, "检查macOS服务状态失败: {}", e);
}
}
}
// Linux专用
#[cfg(target_os = "linux")]
{
use std::process::Command;
logging!(info, Type::Service, true, "尝试检查Linux服务状态...");
let output = Command::new("systemctl")
.args(["status", "clash_verge_service"])
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
let is_active = stdout.contains("Active: active (running)");
logging!(info, Type::Service, true, "Linux服务状态: {}",
if is_active { "活跃运行中" } else { "未运行" });
if !is_active {
logging!(info, Type::Service, true, "服务状态详情: {}", stdout);
}
}
Err(e) => {
logging!(error, Type::Service, true, "检查Linux服务状态失败: {}", e);
}
}
}
}
}
logging!(info, Type::Service, true, "============= 服务诊断完成 =============");
Ok(())
} */
+5 -3
View File
@@ -1,3 +1,5 @@
#[cfg(target_os = "windows")]
use crate::process::AsyncHandler;
use crate::{logging, utils::logging::Type};
use anyhow::{bail, Context, Result};
use hmac::{Hmac, Mac};
@@ -131,7 +133,7 @@ pub async fn send_ipc_request(
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
let command_type = format!("{:?}", command);
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
@@ -143,7 +145,7 @@ pub async fn send_ipc_request(
let request_json = serde_json::to_string(&request)?;
let result = tokio::task::spawn_blocking(move || -> Result<IpcResponse> {
let result = AsyncHandler::spawn_blocking(move || -> Result<IpcResponse> {
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
Ok(name) => name,
Err(e) => {
@@ -257,7 +259,7 @@ pub async fn send_ipc_request(
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
let command_type = format!("{:?}", command);
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
+103 -221
View File
@@ -2,25 +2,20 @@
use crate::utils::autostart as startup_shortcut;
use crate::{
config::{Config, IVerge},
core::handle::Handle,
logging, logging_error,
process::AsyncHandler,
core::{handle::Handle, EventDrivenProxyManager},
logging, logging_error, singleton_lazy,
utils::logging::Type,
};
use anyhow::Result;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::sync::Arc;
#[cfg(not(target_os = "windows"))]
use sysproxy::{Autoproxy, Sysproxy};
use tauri::async_runtime::Mutex as TokioMutex;
use tauri_plugin_autostart::ManagerExt;
use tokio::time::{sleep, Duration};
pub struct Sysopt {
update_sysproxy: Arc<TokioMutex<bool>>,
reset_sysproxy: Arc<TokioMutex<bool>>,
/// record whether the guard async is running or not
guard_state: Arc<Mutex<bool>>,
}
#[cfg(target_os = "windows")]
@@ -32,11 +27,15 @@ 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>";
fn get_bypass() -> String {
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true);
async fn get_bypass() -> String {
let use_default = Config::verge()
.await
.latest_ref()
.use_default_bypass
.unwrap_or(true);
let res = {
let verge = Config::verge();
let verge = verge.latest();
let verge = Config::verge().await;
let verge = verge.latest_ref();
verge.system_proxy_bypass.clone()
};
let custom_bypass = match res {
@@ -47,24 +46,31 @@ fn get_bypass() -> String {
if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string()
} else if use_default {
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
format!("{DEFAULT_BYPASS},{custom_bypass}")
} else {
custom_bypass
}
}
impl Sysopt {
pub fn global() -> &'static Sysopt {
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
SYSOPT.get_or_init(|| Sysopt {
impl Default for Sysopt {
fn default() -> Self {
Sysopt {
update_sysproxy: Arc::new(TokioMutex::new(false)),
reset_sysproxy: Arc::new(TokioMutex::new(false)),
guard_state: Arc::new(false.into()),
})
}
}
}
// Use simplified singleton_lazy macro
singleton_lazy!(Sysopt, SYSOPT, Sysopt::default);
impl Sysopt {
pub fn init_guard_sysproxy(&self) -> Result<()> {
self.guard_proxy();
// 使用事件驱动代理管理器
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_app_started();
log::info!(target: "app", "已启用事件驱动代理守卫");
Ok(())
}
@@ -72,15 +78,18 @@ impl Sysopt {
pub async fn update_sysproxy(&self) -> Result<()> {
let _lock = self.update_sysproxy.lock().await;
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let port = {
let verge_port = Config::verge().await.latest_ref().verge_mixed_port;
match verge_port {
Some(port) => port,
None => Config::clash().await.latest_ref().get_mixed_port(),
}
};
let pac_port = IVerge::get_singleton_port();
let (sys_enable, pac_enable, proxy_host) = {
let verge = Config::verge();
let verge = verge.latest();
let verge = Config::verge().await;
let verge = verge.latest_ref();
(
verge.enable_system_proxy.unwrap_or(false),
verge.proxy_auto_config.unwrap_or(false),
@@ -97,16 +106,18 @@ impl Sysopt {
enable: false,
host: proxy_host.clone(),
port,
bypass: get_bypass(),
bypass: get_bypass().await,
};
let mut auto = Autoproxy {
enable: false,
url: format!("http://{}:{}/commands/pac", proxy_host, pac_port),
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
};
if !sys_enable {
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return Ok(());
}
@@ -115,6 +126,8 @@ impl Sysopt {
auto.enable = true;
sys.set_system_proxy()?;
auto.set_auto_proxy()?;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return Ok(());
}
@@ -123,19 +136,26 @@ impl Sysopt {
sys.enable = true;
auto.set_auto_proxy()?;
sys.set_system_proxy()?;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return Ok(());
}
}
#[cfg(target_os = "windows")]
{
if !sys_enable {
return self.reset_sysproxy().await;
let result = self.reset_sysproxy().await;
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
return result;
}
use crate::{core::handle::Handle, utils::dirs};
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let app_handle = Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
@@ -145,30 +165,36 @@ impl Sysopt {
let shell = app_handle.shell();
let output = if pac_enable {
let address = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
let sysproxy_str = sysproxy_exe
.as_path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
shell
.command(sysproxy_str)
.args(["pac", address.as_str()])
.output()
.await
.unwrap();
output
.await?
} else {
let address = format!("{}:{}", proxy_host, port);
let bypass = get_bypass();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
let address = format!("{proxy_host}:{port}");
let bypass = get_bypass().await;
let sysproxy_str = sysproxy_exe
.as_path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
shell
.command(sysproxy_str)
.args(["global", address.as_str(), bypass.as_ref()])
.output()
.await
.unwrap();
output
.await?
};
if !output.status.success() {
bail!("sysproxy exe run failed");
}
}
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_config_changed();
Ok(())
}
@@ -180,7 +206,16 @@ impl Sysopt {
#[cfg(not(target_os = "windows"))]
{
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
let mut autoproxy = Autoproxy::get_auto_proxy()?;
let mut autoproxy = match Autoproxy::get_auto_proxy() {
Ok(ap) => ap,
Err(e) => {
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
Autoproxy {
enable: false,
url: "".to_string(),
}
}
};
sysproxy.enable = false;
autoproxy.enable = false;
autoproxy.set_auto_proxy()?;
@@ -193,7 +228,9 @@ impl Sysopt {
use anyhow::bail;
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let app_handle = Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
let binary_path = dirs::service_path()?;
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
@@ -203,12 +240,15 @@ impl Sysopt {
}
let shell = app_handle.shell();
let sysproxy_str = sysproxy_exe
.as_path()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.command(sysproxy_str)
.args(["set", "1"])
.output()
.await
.unwrap();
.await?;
if !output.status.success() {
bail!("sysproxy exe run failed");
@@ -219,8 +259,8 @@ impl Sysopt {
}
/// update the startup
pub fn update_launch(&self) -> Result<()> {
let enable_auto_launch = { Config::verge().latest().enable_auto_launch };
pub async fn update_launch(&self) -> Result<()> {
let enable_auto_launch = { Config::verge().await.latest_ref().enable_auto_launch };
let is_enable = enable_auto_launch.unwrap_or(false);
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
@@ -229,14 +269,14 @@ impl Sysopt {
{
if is_enable {
if let Err(e) = startup_shortcut::create_shortcut() {
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
log::error!(target: "app", "创建启动快捷方式失败: {e}");
// 如果快捷方式创建失败,回退到原来的方法
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
}
} else if let Err(e) = startup_shortcut::remove_shortcut() {
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
log::error!(target: "app", "删除启动快捷方式失败: {e}");
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
@@ -254,7 +294,10 @@ impl Sysopt {
/// 尝试使用原来的自启动方法
fn try_original_autostart_method(&self, is_enable: bool) {
let app_handle = Handle::global().app_handle().unwrap();
let Some(app_handle) = Handle::global().app_handle() else {
log::error!(target: "app", "App handle not available for autostart");
return;
};
let autostart_manager = app_handle.autolaunch();
if is_enable {
@@ -271,191 +314,30 @@ impl Sysopt {
{
match startup_shortcut::is_shortcut_enabled() {
Ok(enabled) => {
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
log::info!(target: "app", "快捷方式自启动状态: {enabled}");
return Ok(enabled);
}
Err(e) => {
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {e}");
}
}
}
// 回退到原来的方法
let app_handle = Handle::global().app_handle().unwrap();
let app_handle = Handle::global()
.app_handle()
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
let autostart_manager = app_handle.autolaunch();
match autostart_manager.is_enabled() {
Ok(status) => {
log::info!(target: "app", "Auto launch status: {}", status);
log::info!(target: "app", "Auto launch status: {status}");
Ok(status)
}
Err(e) => {
log::error!(target: "app", "Failed to get auto launch status: {}", e);
log::error!(target: "app", "Failed to get auto launch status: {e}");
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
}
}
}
fn guard_proxy(&self) {
let _lock = self.guard_state.lock();
AsyncHandler::spawn(move || async move {
// default duration is 10s
let mut wait_secs = 10u64;
loop {
sleep(Duration::from_secs(wait_secs)).await;
let (enable, guard, guard_duration, pac, proxy_host) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.enable_proxy_guard.unwrap_or(false),
verge.proxy_guard_duration.unwrap_or(10),
verge.proxy_auto_config.unwrap_or(false),
verge
.proxy_host
.clone()
.unwrap_or_else(|| String::from("127.0.0.1")),
)
};
// stop loop
if !enable || !guard {
continue;
}
// update duration
wait_secs = guard_duration;
log::debug!(target: "app", "try to guard the system proxy");
// 获取期望的代理端口
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let pac_port = IVerge::get_singleton_port();
let bypass = get_bypass();
// 检查系统代理配置
if pac {
// 检查 PAC 代理设置
let expected_url = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
let autoproxy = match Autoproxy::get_auto_proxy() {
Ok(ap) => ap,
Err(e) => {
log::error!(target: "app", "failed to get the auto proxy: {}", e);
continue;
}
};
// 检查自动代理是否启用且URL是否正确
if !autoproxy.enable || autoproxy.url != expected_url {
log::info!(target: "app", "auto proxy settings changed, restoring...");
#[cfg(not(target_os = "windows"))]
{
let new_autoproxy = Autoproxy {
enable: true,
url: expected_url,
};
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
}
#[cfg(target_os = "windows")]
{
use crate::{core::handle::Handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let binary_path = match dirs::service_path() {
Ok(path) => path,
Err(e) => {
log::error!(target: "app", "failed to get service path: {}", e);
continue;
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe not found");
continue;
}
let shell = app_handle.shell();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["pac", expected_url.as_str()])
.output()
.await
.unwrap();
if !output.status.success() {
log::error!(target: "app", "failed to set auto proxy");
}
}
}
} else {
// 检查常规系统代理设置
let sysproxy = match Sysproxy::get_system_proxy() {
Ok(sp) => sp,
Err(e) => {
log::error!(target: "app", "failed to get the system proxy: {}", e);
continue;
}
};
// 检查系统代理是否启用且配置是否匹配
if !sysproxy.enable || sysproxy.host != proxy_host || sysproxy.port != port {
log::info!(target: "app", "system proxy settings changed, restoring...");
#[cfg(not(target_os = "windows"))]
{
let new_sysproxy = Sysproxy {
enable: true,
host: proxy_host.clone(),
port,
bypass: bypass.clone(),
};
logging_error!(Type::System, true, new_sysproxy.set_system_proxy());
}
#[cfg(target_os = "windows")]
{
use crate::{core::handle::Handle, utils::dirs};
use tauri_plugin_shell::ShellExt;
let app_handle = Handle::global().app_handle().unwrap();
let binary_path = match dirs::service_path() {
Ok(path) => path,
Err(e) => {
log::error!(target: "app", "failed to get service path: {}", e);
continue;
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe not found");
continue;
}
let address = format!("{}:{}", proxy_host, port);
let shell = app_handle.shell();
let output = shell
.command(sysproxy_exe.as_path().to_str().unwrap())
.args(["global", address.as_str(), bypass.as_ref()])
.output()
.await
.unwrap();
if !output.status.success() {
log::error!(target: "app", "failed to set system proxy");
}
}
}
}
}
});
}
}
+157 -143
View File
@@ -1,9 +1,15 @@
use crate::{config::Config, feat, logging, logging_error, utils::logging::Type};
use crate::{config::Config, feat, logging, logging_error, singleton, utils::logging::Type};
use anyhow::{Context, Result};
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock};
use std::{collections::HashMap, sync::Arc};
use parking_lot::RwLock;
use std::{
collections::HashMap,
pin::Pin,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
};
type TaskID = u64;
@@ -22,94 +28,91 @@ pub struct Timer {
/// save the current state - using RwLock for better read concurrency
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
/// increment id - kept as mutex since it's just a counter
pub timer_count: Arc<Mutex<TaskID>>,
/// increment id - atomic counter for better performance
pub timer_count: AtomicU64,
/// Flag to mark if timer is initialized - atomic for better performance
pub initialized: Arc<std::sync::atomic::AtomicBool>,
pub initialized: AtomicBool,
}
impl Timer {
pub fn global() -> &'static Timer {
static TIMER: OnceCell<Timer> = OnceCell::new();
// Use singleton macro
singleton!(Timer, TIMER_INSTANCE);
TIMER.get_or_init(|| Timer {
impl Timer {
fn new() -> Self {
Timer {
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
timer_map: Arc::new(RwLock::new(HashMap::new())),
timer_count: Arc::new(Mutex::new(1)),
initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)),
})
timer_count: AtomicU64::new(1),
initialized: AtomicBool::new(false),
}
}
/// 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
if self
.initialized
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
logging!(debug, Type::Timer, "Timer already initialized, skipping...");
return Ok(());
}
logging!(info, Type::Timer, true, "Initializing timer...");
// Initialize timer tasks
if let Err(e) = self.refresh() {
if let Err(e) = self.refresh().await {
// Reset initialization flag on error
self.initialized
.store(false, std::sync::atomic::Ordering::SeqCst);
self.initialized.store(false, Ordering::SeqCst);
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
return Err(e);
}
let timer_map = self.timer_map.read();
logging!(
info,
Type::Timer,
"已注册的定时任务数量: {}",
timer_map.len()
);
for (uid, task) in timer_map.iter() {
// Log timer info first
{
let timer_map = self.timer_map.read();
logging!(
info,
Type::Timer,
"注册了定时任务 - uid={}, interval={}min, task_id={}",
uid,
task.interval_minutes,
task.task_id
"已注册的定时任务数量: {}",
timer_map.len()
);
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();
// Collect profiles that need immediate update
let profiles_to_update = if let Some(items) = Config::profiles().latest().get_items() {
items
.iter()
.filter_map(|item| {
let interval = item.option.as_ref()?.update_interval? as i64;
let updated = item.updated? as i64;
let uid = item.uid.as_ref()?;
let profiles_to_update =
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
items
.iter()
.filter_map(|item| {
let interval = item.option.as_ref()?.update_interval? as i64;
let updated = item.updated? as i64;
let uid = item.uid.as_ref()?;
if interval > 0 && cur_timestamp - updated >= interval * 60 {
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
Some(uid.clone())
} else {
None
}
})
.collect::<Vec<String>>()
} else {
Vec::new()
};
if interval > 0 && cur_timestamp - updated >= interval * 60 {
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
Some(uid.clone())
} else {
None
}
})
.collect::<Vec<String>>()
} else {
Vec::new()
};
// Advance tasks outside of locks to minimize lock contention
if !profiles_to_update.is_empty() {
@@ -137,9 +140,9 @@ impl Timer {
}
/// 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
let diff_map = self.gen_diff();
let diff_map = self.gen_diff().await;
if diff_map.is_empty() {
logging!(debug, Type::Timer, "No timer changes needed");
@@ -153,72 +156,80 @@ impl Timer {
diff_map.len()
);
// Apply changes while holding locks
let mut timer_map = self.timer_map.write();
let mut delay_timer = self.delay_timer.write();
// Apply changes - first collect operations to perform without holding locks
let mut operations_to_add: Vec<(String, TaskID, u64)> = Vec::new();
let _operations_to_remove: Vec<String> = Vec::new();
for (uid, diff) in diff_map {
match diff {
DiffFlag::Del(tid) => {
timer_map.remove(&uid);
if let Err(e) = delay_timer.remove_task(tid) {
logging!(
warn,
Type::Timer,
"Failed to remove task {} for uid {}: {}",
tid,
uid,
e
);
} else {
logging!(debug, Type::Timer, "Removed task {} for uid {}", tid, uid);
// Perform sync operations while holding locks
{
let mut timer_map = self.timer_map.write();
let delay_timer = self.delay_timer.write();
for (uid, diff) in diff_map {
match diff {
DiffFlag::Del(tid) => {
timer_map.remove(&uid);
if let Err(e) = delay_timer.remove_task(tid) {
logging!(
warn,
Type::Timer,
"Failed to remove task {} for 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 {
task_id: tid,
interval_minutes: interval,
last_run: chrono::Local::now().timestamp(),
};
}
} // Locks are dropped here
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) {
logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e);
timer_map.remove(&uid); // Rollback on failure
} else {
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);
}
}
// Rollback on failure - remove from timer_map
self.timer_map.write().remove(&uid);
} else {
logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid);
}
}
@@ -226,10 +237,10 @@ impl Timer {
}
/// 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();
if let Some(items) = Config::profiles().latest().get_items() {
if let Some(items) = Config::profiles().await.latest_ref().get_items() {
for item in items.iter() {
if let Some(option) = item.option.as_ref() {
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
@@ -258,9 +269,9 @@ impl Timer {
}
/// 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 new_map = self.gen_map();
let new_map = self.gen_map().await;
// Read lock for comparing current state
let timer_map = self.timer_map.read();
@@ -299,7 +310,8 @@ impl Timer {
}
// Find new tasks to add
let mut next_id = *self.timer_count.lock();
let mut next_id = self.timer_count.load(Ordering::Relaxed);
let original_id = next_id;
for (uid, &interval) in new_map.iter() {
if !timer_map.contains_key(uid) {
@@ -316,8 +328,8 @@ impl Timer {
}
// Update counter only if we added new tasks
if next_id > *self.timer_count.lock() {
*self.timer_count.lock() = next_id;
if next_id > original_id {
self.timer_count.store(next_id, Ordering::Relaxed);
}
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
@@ -348,9 +360,9 @@ impl Timer {
.set_frequency_repeated_by_minutes(minutes)
.spawn_async_routine(move || {
let uid = uid.clone();
async move {
Box::pin(async move {
Self::async_task(uid).await;
}
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
})
.context("failed to create timer task")?;
@@ -362,21 +374,23 @@ impl Timer {
}
/// 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);
let timer_map = self.timer_map.read();
let task = match timer_map.get(uid) {
Some(t) => t,
None => {
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
return None;
// First extract timer task data without holding the lock across await
let task_interval = {
let timer_map = self.timer_map.read();
match timer_map.get(uid) {
Some(t) => t.interval_minutes,
None => {
logging!(warn, Type::Timer, "找不到对应的定时任务,uid={}", uid);
return None;
}
}
};
// Get the profile updated timestamp
let profiles_config = Config::profiles();
let profiles = profiles_config.latest();
// Get the profile updated timestamp - now safe to await
let profiles = { Config::profiles().await.clone().data_ref() }.clone();
let items = match profiles.get_items() {
Some(i) => i,
None => {
@@ -396,8 +410,8 @@ impl Timer {
let updated = profile.updated.unwrap_or(0) as i64;
// Calculate next update time
if updated > 0 && task.interval_minutes > 0 {
let next_time = updated + (task.interval_minutes as i64 * 60);
if updated > 0 && task_interval > 0 {
let next_time = updated + (task_interval as i64 * 60);
logging!(
info,
Type::Timer,
@@ -412,7 +426,7 @@ impl Timer {
Type::Timer,
"更新时间或间隔无效,updated={}, interval={}",
updated,
task.interval_minutes
task_interval
);
None
}
@@ -438,7 +452,7 @@ impl Timer {
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
Self::emit_update_event(&uid, true);
let is_current = Config::profiles().latest().current.as_ref() == Some(&uid);
let is_current = Config::profiles().await.latest_ref().current.as_ref() == Some(&uid);
logging!(
info,
Type::Timer,
File diff suppressed because it is too large Load Diff
-335
View File
@@ -1,336 +1 @@
use crate::{
module::mihomo::{MihomoManager, Rate},
utils::help::format_bytes_speed,
};
use ab_glyph::FontArc;
use anyhow::Result;
use futures::Stream;
use image::{GenericImageView, Rgba, RgbaImage};
use imageproc::drawing::draw_text_mut;
use parking_lot::Mutex;
use std::{io::Cursor, sync::Arc};
use tokio_tungstenite::tungstenite::http;
use tungstenite::client::IntoClientRequest;
#[derive(Debug, Clone)]
pub struct SpeedRate {
rate: Arc<Mutex<(Rate, Rate)>>,
last_update: Arc<Mutex<std::time::Instant>>,
}
impl SpeedRate {
pub fn new() -> Self {
Self {
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
}
}
pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option<Rate> {
let mut rates = self.rate.lock();
let mut last_update = self.last_update.lock();
let now = std::time::Instant::now();
// 限制更新频率为每秒最多2次(500ms)
if now.duration_since(*last_update).as_millis() < 500 {
return None;
}
let (current, previous) = &mut *rates;
// Avoid unnecessary float conversions for small value checks
let should_update = if current.up < 1000 && down < 1000 {
// For small values, always update to ensure accuracy
current.up != up || current.down != down
} else {
// For larger values, use integer math to check for >5% change
// Multiply by 20 instead of dividing by 0.05 to avoid floating point
let up_threshold = current.up / 20;
let down_threshold = current.down / 20;
(up > current.up && up - current.up > up_threshold)
|| (up < current.up && current.up - up > up_threshold)
|| (down > current.down && down - current.down > down_threshold)
|| (down < current.down && current.down - down > down_threshold)
};
if !should_update {
return None;
}
*previous = current.clone();
current.up = up;
current.down = down;
*last_update = now;
if previous != current {
Some(current.clone())
} else {
None
}
}
pub fn get_curent_rate(&self) -> Option<Rate> {
let rates = self.rate.lock();
let (current, _) = &*rates;
Some(current.clone())
}
// 分离图标加载和速率渲染
pub fn add_speed_text(
is_custom_icon: bool,
icon_bytes: Option<Vec<u8>>,
rate: Option<&Rate>,
) -> Result<Vec<u8>> {
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
let (mut icon_width, mut icon_height) = (0, 256);
let icon_image = if let Some(bytes) = icon_bytes.clone() {
let icon_image = image::load_from_memory(&bytes)?;
icon_width = icon_image.width();
icon_height = icon_image.height();
icon_image
} else {
// 返回一个空的 RGBA 图像
image::DynamicImage::new_rgba8(0, 0)
};
let total_width = match (is_custom_icon, icon_bytes.is_some()) {
(true, true) => 510,
(true, false) => 740,
(false, false) => 740,
(false, true) => icon_width + 740,
};
// println!(
// "icon_height: {}, icon_wight: {}, total_width: {}",
// icon_height, icon_width, total_width
// );
// 创建新的透明画布
let mut combined_image = RgbaImage::new(total_width, icon_height);
// 将原始图标绘制到新画布的左侧
if icon_bytes.is_some() {
for y in 0..icon_height {
for x in 0..icon_width {
let pixel = icon_image.get_pixel(x, y);
combined_image.put_pixel(x, y, pixel);
}
}
}
let is_colorful = if let Some(bytes) = icon_bytes.clone() {
!crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false)
} else {
false
};
// 选择文本颜色
let (text_color, shadow_color) = if is_colorful {
(
Rgba([144u8, 144u8, 144u8, 255u8]),
// Rgba([255u8, 255u8, 255u8, 128u8]),
Rgba([0u8, 0u8, 0u8, 128u8]),
)
// (
// Rgba([160u8, 160u8, 160u8, 255u8]),
// // Rgba([255u8, 255u8, 255u8, 128u8]),
// Rgba([0u8, 0u8, 0u8, 255u8]),
// )
} else {
(
Rgba([255u8, 255u8, 255u8, 255u8]),
Rgba([0u8, 0u8, 0u8, 128u8]),
)
};
// 减小字体大小以适应文本区域
let font_data = include_bytes!("../../../assets/fonts/SF-Pro.ttf");
let font = FontArc::try_from_vec(font_data.to_vec()).unwrap();
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
let scale = ab_glyph::PxScale::from(font_size);
// 使用更简洁的速率格式
let up_text = format!("↑ {}", format_bytes_speed(rate.up));
let down_text = format!("↓ {}", format_bytes_speed(rate.down));
// For test rate display
// let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024));
// 计算文本位置,确保垂直间距合适
// 修改文本位置为居右显示
// 计算右对齐的文本位置
// let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
// let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
// let up_text_x = total_width - up_text_width;
// let down_text_x = total_width - down_text_width;
// 计算左对齐的文本位置
let (up_text_x, down_text_x) = {
if is_custom_icon || icon_bytes.is_some() {
let text_left_offset = 30;
let left_begin = icon_width + text_left_offset;
(left_begin, left_begin)
} else {
(icon_width, icon_width)
}
};
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
let text_height = font_size as i32;
let total_text_height = text_height * 2;
let up_y = (icon_height as i32 - total_text_height) / 2;
let down_y = up_y + text_height;
// 绘制速率文本(先阴影后文字)
let shadow_offset = 1;
// 绘制上行速率
draw_text_mut(
&mut combined_image,
shadow_color,
up_text_x as i32 + shadow_offset,
up_y + shadow_offset,
scale,
&font,
&up_text,
);
draw_text_mut(
&mut combined_image,
text_color,
up_text_x as i32,
up_y,
scale,
&font,
&up_text,
);
// 绘制下行速率
draw_text_mut(
&mut combined_image,
shadow_color,
down_text_x as i32 + shadow_offset,
down_y + shadow_offset,
scale,
&font,
&down_text,
);
draw_text_mut(
&mut combined_image,
text_color,
down_text_x as i32,
down_y,
scale,
&font,
&down_text,
);
// 将结果转换为 PNG 数据
let mut bytes = Vec::new();
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
Ok(bytes)
}
}
#[derive(Debug, Clone)]
pub struct Traffic {
pub up: u64,
pub down: u64,
}
impl Traffic {
pub async fn get_traffic_stream() -> Result<impl Stream<Item = Result<Traffic, anyhow::Error>>>
{
use futures::{
future::FutureExt,
stream::{self, StreamExt},
};
use std::time::Duration;
// 先处理错误和超时情况
let stream = Box::pin(
stream::unfold((), move |_| async move {
'retry: loop {
log::info!(target: "app", "establishing traffic websocket connection");
let (url, token) = MihomoManager::get_traffic_ws_url();
let mut request = match url.into_client_request() {
Ok(req) => req,
Err(e) => {
log::error!(target: "app", "failed to create websocket request: {}", e);
tokio::time::sleep(Duration::from_secs(2)).await;
continue 'retry;
}
};
request.headers_mut().insert(http::header::AUTHORIZATION, token);
match tokio::time::timeout(Duration::from_secs(3),
tokio_tungstenite::connect_async(request)
).await {
Ok(Ok((ws_stream, _))) => {
log::info!(target: "app", "traffic websocket connection established");
// 设置流超时控制
let traffic_stream = ws_stream
.take_while(|msg| {
let continue_stream = msg.is_ok();
async move { continue_stream }.boxed()
})
.filter_map(|msg| async move {
match msg {
Ok(msg) => {
if !msg.is_text() {
return None;
}
match tokio::time::timeout(
Duration::from_millis(200),
async { msg.into_text() }
).await {
Ok(Ok(text)) => {
match serde_json::from_str::<serde_json::Value>(&text) {
Ok(json) => {
let up = json["up"].as_u64().unwrap_or(0);
let down = json["down"].as_u64().unwrap_or(0);
Some(Ok(Traffic { up, down }))
},
Err(e) => {
log::warn!(target: "app", "traffic json parse error: {} for {}", e, text);
None
}
}
},
Ok(Err(e)) => {
log::warn!(target: "app", "traffic text conversion error: {}", e);
None
},
Err(_) => {
log::warn!(target: "app", "traffic text processing timeout");
None
}
}
},
Err(e) => {
log::error!(target: "app", "traffic websocket error: {}", e);
None
}
}
});
return Some((traffic_stream, ()));
},
Ok(Err(e)) => {
log::error!(target: "app", "traffic websocket connection failed: {}", e);
},
Err(_) => {
log::error!(target: "app", "traffic websocket connection timed out");
}
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
})
.flatten(),
);
Ok(stream)
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::process::Command as StdCommand;
pub async fn invoke_uwptools() -> Result<()> {
pub fn invoke_uwptools() -> Result<()> {
let resource_dir = dirs::app_resources_dir()?;
let tool_path = resource_dir.join("enableLoopback.exe");
@@ -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) {
if (config.mode === "script") {
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) {
if (Array.isArray(config.proxies)) {
config.proxies.forEach((p, i) => {
+67 -17
View File
@@ -3,7 +3,7 @@ use crate::{
config::PrfItem,
utils::{dirs, help},
};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::fs;
#[derive(Debug, Clone)]
@@ -22,6 +22,7 @@ pub enum ChainType {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum ChainSupport {
Clash,
ClashMeta,
@@ -29,8 +30,49 @@ pub enum ChainSupport {
All,
}
impl From<&PrfItem> for Option<ChainItem> {
fn from(item: &PrfItem) -> Self {
// impl From<&PrfItem> for Option<ChainItem> {
// 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 file = item.file.clone()?;
let uid = item.uid.clone().unwrap_or("".into());
@@ -47,25 +89,33 @@ impl From<&PrfItem> for Option<ChainItem> {
}),
"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()?),
data: ChainType::Merge(help::read_mapping(&path).await.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,
}
}
}
impl 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;
pub const HANDLE_FIELDS: [&str; 12] = [
+8 -5
View File
@@ -1,5 +1,5 @@
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) {
match (a, b) {
@@ -18,7 +18,10 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
deep_merge(&mut config, &Value::from(merge));
let config = config.as_mapping().unwrap().clone();
let config = config.as_mapping().cloned().unwrap_or_else(|| {
log::error!("Failed to convert merged config to mapping, using empty mapping");
Mapping::new()
});
config
}
@@ -51,10 +54,10 @@ fn test_merge() -> anyhow::Result<()> {
script1: test
";
let merge = serde_yaml::from_str::<Mapping>(merge)?;
let config = serde_yaml::from_str::<Mapping>(config)?;
let merge = serde_yaml_ng::from_str::<Mapping>(merge)?;
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(())
}
+198 -76
View File
@@ -7,7 +7,7 @@ mod tun;
use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*};
use crate::{config::Config, utils::tmpl};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
use std::collections::{HashMap, HashSet};
type ResultLog = Vec<(String, String)>;
@@ -16,11 +16,11 @@ type ResultLog = Vec<(String, String)>;
/// 返回最终订阅、该订阅包含的键、和script执行的结果
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的订阅
let clash_config = { Config::clash().latest().0.clone() };
let clash_config = { Config::clash().await.latest_ref().0.clone() };
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
let verge = Config::verge();
let verge = verge.latest();
let verge = Config::verge().await;
let verge = verge.latest_ref();
(
Some(verge.get_valid_clash_core()),
verge.enable_tun_mode.unwrap_or(false),
@@ -32,18 +32,18 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
};
#[cfg(not(target_os = "windows"))]
let redir_enabled = {
let verge = Config::verge();
let verge = verge.latest();
let verge = Config::verge().await;
let verge = verge.latest_ref();
verge.verge_redir_enabled.unwrap_or(false)
};
#[cfg(target_os = "linux")]
let tproxy_enabled = {
let verge = Config::verge();
let verge = verge.latest();
let verge = Config::verge().await;
let verge = verge.latest_ref();
verge.verge_tproxy_enabled.unwrap_or(false)
};
// 从profiles里拿东西
// 从profiles里拿东西 - 先收集需要的数据,然后释放锁
let (
mut config,
merge_item,
@@ -55,74 +55,172 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
global_script,
profile_name,
) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
// 收集所有需要的数据,然后释放profiles锁
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
.get_item(&profiles.current_merge().unwrap_or_default())
.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 profiles = Config::profiles().await;
let profiles_ref = profiles.latest_ref();
let global_merge = profiles
.get_item(&"Merge".to_string())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()),
});
let merge_uid = profiles_ref.current_merge().unwrap_or_default();
let script_uid = profiles_ref.current_script().unwrap_or_default();
let rules_uid = profiles_ref.current_rules().unwrap_or_default();
let proxies_uid = profiles_ref.current_proxies().unwrap_or_default();
let groups_uid = profiles_ref.current_groups().unwrap_or_default();
let current_profile_uid = profiles_ref.get_current().unwrap_or_default();
let global_script = profiles
.get_item(&"Script".to_string())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let name = profiles_ref
.get_item(&current_profile_uid)
.ok()
.and_then(|item| item.name.clone())
.unwrap_or_default();
let name = profiles
.get_item(&profiles.get_current().unwrap_or_default())
.ok()
.and_then(|item| item.name.clone())
.unwrap_or_default();
(
current,
merge_uid,
script_uid,
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,
@@ -214,6 +312,12 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
config.remove("port");
continue;
}
#[cfg(target_os = "windows")]
{
if key.as_str() == Some("redir-port") || key.as_str() == Some("tproxy-port") {
continue;
}
}
#[cfg(not(target_os = "windows"))]
{
if key.as_str() == Some("redir-port") && !redir_enabled {
@@ -228,7 +332,23 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
continue;
}
}
config.insert(key, value);
// 处理 external-controller 键的开关逻辑
if key.as_str() == Some("external-controller") {
let enable_external_controller = Config::verge()
.await
.latest_ref()
.enable_external_controller
.unwrap_or(false);
if enable_external_controller {
config.insert(key, value);
} else {
// 如果禁用了外部控制器,设置为空字符串
config.insert(key, "".into());
}
} else {
config.insert(key, value);
}
}
}
@@ -253,7 +373,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
});
}
config = use_tun(config, enable_tun).await;
config = use_tun(config, enable_tun);
config = use_sort(config);
// 应用独立的DNS配置(如果启用)
@@ -266,7 +386,9 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if dns_path.exists() {
if let Ok(dns_yaml) = fs::read_to_string(&dns_path) {
if let Ok(dns_config) = serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
if 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") {
if hosts_value.is_mapping() {
+52 -27
View File
@@ -1,30 +1,49 @@
use super::use_lowercase;
use anyhow::{Error, Result};
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
pub fn use_script(
script: String,
config: Mapping,
name: String,
) -> Result<(Mapping, Vec<(String, String)>)> {
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
use std::sync::{Arc, Mutex};
use boa_engine::{native_function::NativeFunction, Context, JsString, JsValue, Source};
use std::{cell::RefCell, rc::Rc};
let mut context = Context::default();
let outputs = Arc::new(Mutex::new(vec![]));
let outputs = Rc::new(RefCell::new(vec![]));
let copy_outputs = outputs.clone();
let copy_outputs = Rc::clone(&outputs);
unsafe {
let _ = context.register_global_builtin_callable(
"__verge_log__".into(),
2,
NativeFunction::from_closure(
move |_: &JsValue, args: &[JsValue], context: &mut Context| {
let level = args.first().unwrap().to_string(context)?;
let level = level.to_std_string().unwrap();
let data = args.get(1).unwrap().to_string(context)?;
let data = data.to_std_string().unwrap();
let mut out = copy_outputs.lock().unwrap();
let level = args.first().ok_or_else(|| {
boa_engine::JsError::from_opaque(
JsString::from("Missing level argument").into(),
)
})?;
let level = level.to_string(context)?;
let level = level.to_std_string().map_err(|_| {
boa_engine::JsError::from_opaque(
JsString::from("Failed to convert level to string").into(),
)
})?;
let data = args.get(1).ok_or_else(|| {
boa_engine::JsError::from_opaque(
JsString::from("Missing data argument").into(),
)
})?;
let data = data.to_string(context)?;
let data = data.to_std_string().map_err(|_| {
boa_engine::JsError::from_opaque(
JsString::from("Failed to convert data to string").into(),
)
})?;
let mut out = copy_outputs.borrow_mut();
out.push((level, data));
Ok(JsValue::undefined())
},
@@ -49,25 +68,29 @@ pub fn use_script(
let safe_name = escape_js_string_for_single_quote(&name);
let code = format!(
r#"try{{
r"try{{
{script};
JSON.stringify(main({config_str},'{safe_name}')||'')
}} catch(err) {{
`__error_flag__ ${{err.toString()}}`
}}"#
}}"
);
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
if !result.is_string() {
anyhow::bail!("main function should return object");
}
let result = result.to_string(&mut context).unwrap();
let result = result.to_std_string().unwrap();
let result = result
.to_string(&mut context)
.map_err(|e| anyhow::anyhow!("Failed to convert JS result to string: {}", e))?;
let result = result
.to_std_string()
.map_err(|_| anyhow::anyhow!("Failed to convert JS string to std string"))?;
// 直接解析JSON结果,不做其他解析
let res: Result<Mapping, Error> = parse_json_safely(&result);
let mut out = outputs.lock().unwrap();
let mut out = outputs.borrow_mut();
match res {
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
Err(err) => {
@@ -102,6 +125,8 @@ fn escape_js_string_for_single_quote(s: &str) -> String {
}
#[test]
#[allow(unused_variables)]
#[allow(clippy::expect_used)]
fn test_script() {
let script = r#"
function main(config) {
@@ -114,7 +139,7 @@ fn test_script() {
}
"#;
let config = r#"
let config = r"
rules:
- 111
- 222
@@ -122,36 +147,36 @@ fn test_script() {
enable: false
dns:
enable: false
"#;
";
let config = serde_yaml::from_str(config).unwrap();
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
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())
.expect("Script execution should succeed in test");
let _ = serde_yaml::to_string(&config).unwrap();
let _ = serde_yaml_ng::to_string(&config).expect("Failed to serialize config to YAML");
let yaml_config_size = std::mem::size_of_val(&config);
dbg!(yaml_config_size);
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
dbg!(box_yaml_config_size);
dbg!(results);
assert!(box_yaml_config_size < yaml_config_size);
}
// 测试特殊字符转义功能
#[test]
#[allow(clippy::expect_used)]
fn test_escape_unescape() {
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
let escaped = escape_js_string_for_single_quote(test_string);
println!("Original: {}", test_string);
println!("Escaped: {}", escaped);
println!("Original: {test_string}");
println!("Escaped: {escaped}");
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
let parsed = parse_json_safely(json_str).unwrap();
let parsed = parse_json_safely(json_str).expect("Failed to parse test JSON safely");
assert!(parsed.contains_key("key"));
assert!(parsed.contains_key("nested"));
let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#;
let parsed_quoted = parse_json_safely(quoted_json_str).unwrap();
let parsed_quoted =
parse_json_safely(quoted_json_str).expect("Failed to parse quoted test JSON safely");
assert!(parsed_quoted.contains_key("key"));
assert!(parsed_quoted.contains_key("nested"));
+31 -15
View File
@@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Sequence, Value};
use serde_yaml_ng::{Mapping, Sequence, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap {
@@ -86,9 +86,11 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
mod tests {
use super::*;
#[allow(unused_imports)]
use serde_yaml::Value;
use serde_yaml_ng::Value;
#[test]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
fn test_delete_proxy_and_references() {
let config_str = r#"
proxies:
@@ -107,7 +109,8 @@ proxy-groups:
proxies:
- "proxy1"
"#;
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
let mut config: Mapping =
serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML");
let seq = SeqMap {
prepend: Sequence::new(),
@@ -118,38 +121,51 @@ proxy-groups:
config = use_seq(seq, config, "proxies");
// Check if proxy1 is removed from proxies
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
let proxies = config
.get("proxies")
.expect("proxies field should exist")
.as_sequence()
.expect("proxies should be a sequence");
assert_eq!(proxies.len(), 1);
assert_eq!(
proxies[0]
.as_mapping()
.unwrap()
.expect("proxy should be a mapping")
.get("name")
.unwrap()
.expect("proxy should have name")
.as_str()
.unwrap(),
.expect("name should be string"),
"proxy2"
);
// Check if proxy1 is removed from all groups
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
let groups = config
.get("proxy-groups")
.expect("proxy-groups field should exist")
.as_sequence()
.expect("proxy-groups should be a sequence");
let group1_proxies = groups[0]
.as_mapping()
.unwrap()
.expect("group should be a mapping")
.get("proxies")
.unwrap()
.expect("group should have proxies")
.as_sequence()
.unwrap();
.expect("group proxies should be a sequence");
let group2_proxies = groups[1]
.as_mapping()
.unwrap()
.expect("group should be a mapping")
.get("proxies")
.unwrap()
.expect("group should have proxies")
.as_sequence()
.unwrap();
.expect("group proxies should be a sequence");
assert_eq!(group1_proxies.len(), 1);
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
assert_eq!(
group1_proxies[0]
.as_str()
.expect("proxy name should be string"),
"proxy2"
);
assert_eq!(group2_proxies.len(), 0);
}
}
+12 -5
View File
@@ -1,4 +1,7 @@
use serde_yaml::{Mapping, Value};
use serde_yaml_ng::{Mapping, Value};
#[cfg(target_os = "macos")]
use crate::process::AsyncHandler;
macro_rules! revise {
($map: expr, $key: expr, $val: expr) => {
@@ -18,7 +21,7 @@ macro_rules! append {
};
}
pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key);
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
@@ -59,8 +62,10 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
#[cfg(target_os = "macos")]
{
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
AsyncHandler::spawn(move || async move {
crate::utils::resolve::dns::restore_public_dns().await;
crate::utils::resolve::dns::set_public_dns("223.6.6.6".to_string()).await;
});
}
}
@@ -69,7 +74,9 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
} else {
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
#[cfg(target_os = "macos")]
crate::utils::resolve::restore_public_dns().await;
AsyncHandler::spawn(move || async move {
crate::utils::resolve::dns::restore_public_dns().await;
});
}
// 更新TUN配置
+11 -9
View File
@@ -11,7 +11,7 @@ use std::fs;
/// Create a backup and upload to WebDAV
pub async fn create_backup_and_upload_webdav() -> Result<()> {
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
log::error!(target: "app", "Failed to create backup: {:#?}", err);
log::error!(target: "app", "Failed to create backup: {err:#?}");
err
})?;
@@ -19,12 +19,12 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
.upload(temp_file_path.clone(), file_name)
.await
{
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
log::error!(target: "app", "Failed to upload to WebDAV: {err:#?}");
return Err(err);
}
if let Err(err) = std::fs::remove_file(&temp_file_path) {
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
log::warn!(target: "app", "Failed to remove temp file: {err:#?}");
}
Ok(())
@@ -33,7 +33,7 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
/// List WebDAV backups
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
backup::WebDavClient::global().list().await.map_err(|err| {
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
log::error!(target: "app", "Failed to list WebDAV backup files: {err:#?}");
err
})
}
@@ -44,25 +44,27 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
.delete(filename)
.await
.map_err(|err| {
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
log::error!(target: "app", "Failed to delete WebDAV backup file: {err:#?}");
err
})
}
/// Restore WebDAV backup
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge();
let verge_data = verge.data().clone();
let verge = Config::verge().await;
let verge_data = verge.latest_ref().clone();
let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
let backup_storage_path = app_home_dir().unwrap().join(&filename);
let backup_storage_path = app_home_dir()
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
.join(&filename);
backup::WebDavClient::global()
.download(filename, backup_storage_path.clone())
.await
.map_err(|err| {
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
log::error!(target: "app", "Failed to download WebDAV backup file: {err:#?}");
err
})?;
+66 -51
View File
@@ -1,92 +1,103 @@
use crate::{
config::Config,
core::{handle, tray, CoreManager},
ipc::IpcManager,
logging_error,
module::mihomo::MihomoManager,
process::AsyncHandler,
utils::{logging::Type, resolve},
};
use serde_yaml::{Mapping, Value};
use tauri::Manager;
use serde_yaml_ng::{Mapping, Value};
/// Restart the Clash core
pub fn restart_clash_core() {
AsyncHandler::spawn(move || async move {
match CoreManager::global().restart_core().await {
Ok(_) => {
handle::Handle::refresh_clash();
handle::Handle::notice_message("set_config::ok", "ok");
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target:"app", "{err}");
}
pub async fn restart_clash_core() {
match CoreManager::global().restart_core().await {
Ok(_) => {
handle::Handle::refresh_clash();
handle::Handle::notice_message("set_config::ok", "ok");
}
});
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target:"app", "{err}");
}
}
}
/// Restart the application
pub fn restart_app() {
AsyncHandler::spawn(move || async move {
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
resolve::resolve_reset_async().await;
let app_handle = handle::Handle::global().app_handle().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
tauri::process::restart(&app_handle.env());
});
pub async fn restart_app() {
// logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
resolve::resolve_reset_async().await;
handle::Handle::global()
.app_handle()
.map(|app_handle| {
app_handle.restart();
})
.unwrap_or_else(|| {
logging_error!(
Type::System,
false,
"{}",
"Failed to get app handle for restart"
);
});
}
fn after_change_clash_mode() {
AsyncHandler::spawn(move || async {
match MihomoManager::global().get_connections().await {
match IpcManager::global().get_connections().await {
Ok(connections) => {
if let Some(connections_array) = connections["connections"].as_array() {
for connection in connections_array {
if let Some(id) = connection["id"].as_str() {
let _ = MihomoManager::global().delete_connection(id).await;
let _ = IpcManager::global().delete_connection(id).await;
}
}
}
}
Err(err) => {
log::error!(target: "app", "Failed to get connections: {}", err);
log::error!(target: "app", "Failed to get connections: {err}");
}
}
});
}
/// 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();
mapping.insert(Value::from("mode"), mode.clone().into());
// Convert YAML mapping to JSON Value
let json_value = serde_json::json!({
"mode": mode
});
AsyncHandler::spawn(move || async move {
log::debug!(target: "app", "change clash mode to {mode}");
match MihomoManager::global().patch_configs(json_value).await {
Ok(_) => {
// 更新订阅
Config::clash().data().patch_config(mapping);
log::debug!(target: "app", "change clash mode to {mode}");
match IpcManager::global().patch_configs(json_value).await {
Ok(_) => {
// 更新订阅
Config::clash().await.data_mut().patch_config(mapping);
if Config::clash().data().save_config().is_ok() {
handle::Handle::refresh_clash();
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
}
// 分离数据获取和异步调用
let clash_data = Config::clash().await.data_mut().clone();
if clash_data.save_config().await.is_ok() {
handle::Handle::refresh_clash();
logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
logging_error!(
Type::Tray,
true,
tray::Tray::global().update_icon(None).await
);
}
let is_auto_close_connection = Config::verge()
.data()
.auto_close_connection
.unwrap_or(false);
if is_auto_close_connection {
after_change_clash_mode();
}
let is_auto_close_connection = Config::verge()
.await
.data_mut()
.auto_close_connection
.unwrap_or(false);
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
@@ -94,7 +105,11 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
use crate::utils::network::{NetworkManager, ProxyType};
use tokio::time::Instant;
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
let tun_mode = Config::verge()
.await
.latest_ref()
.enable_tun_mode
.unwrap_or(false);
// 如果是TUN模式,不使用代理,否则使用自身代理
let proxy_type = if !tun_mode {
@@ -107,13 +122,13 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
let start = Instant::now();
let response = NetworkManager::global()
let response = NetworkManager::new()
.get_with_interrupt(&url, proxy_type, Some(10), user_agent, false)
.await;
match response {
Ok(response) => {
log::trace!(target: "app", "test_delay response: {:#?}", response);
log::trace!(target: "app", "test_delay response: {response:#?}");
if response.status().is_success() {
Ok(start.elapsed().as_millis() as u32)
} else {
@@ -121,7 +136,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
}
}
Err(err) => {
log::trace!(target: "app", "test_delay error: {:#?}", err);
log::trace!(target: "app", "test_delay error: {err:#?}");
Err(err)
}
}
+43 -21
View File
@@ -6,11 +6,14 @@ use crate::{
utils::logging::Type,
};
use anyhow::Result;
use serde_yaml::Mapping;
use serde_yaml_ng::Mapping;
/// Patch Clash configuration
pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone());
Config::clash()
.await
.draft_mut()
.patch_config(patch.clone());
let res = {
// 激活订阅
@@ -19,10 +22,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
CoreManager::global().restart_core().await?;
} else {
if patch.get("mode").is_some() {
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await);
logging_error!(
Type::Tray,
true,
tray::Tray::global().update_icon(None).await
);
}
Config::runtime().latest().patch_config(patch);
Config::runtime().await.draft_mut().patch_config(patch);
CoreManager::global().update_config().await?;
}
handle::Handle::refresh_clash();
@@ -30,12 +37,14 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
};
match res {
Ok(()) => {
Config::clash().apply();
Config::clash().data().save_config()?;
Config::clash().await.apply();
// 分离数据获取和异步调用
let clash_data = Config::clash().await.data_mut().clone();
clash_data.save_config().await?;
Ok(())
}
Err(err) => {
Config::clash().discard();
Config::clash().await.discard();
Err(err)
}
}
@@ -60,7 +69,10 @@ enum UpdateFlags {
/// Patch Verge configuration
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge().draft().patch_config(patch.clone());
Config::verge()
.await
.draft_mut()
.patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch;
@@ -95,6 +107,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
let tray_event = patch.tray_event;
let home_cards = patch.home_cards.clone();
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
let enable_external_controller = patch.enable_external_controller;
let res: std::result::Result<(), anyhow::Error> = {
// Initialize with no flags set
let mut update_flags: i32 = UpdateFlags::None as i32;
@@ -165,6 +178,11 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
update_flags |= UpdateFlags::LighteWeight as i32;
}
// 处理 external-controller 的开关
if enable_external_controller.is_some() {
update_flags |= UpdateFlags::RestartCore as i32;
}
// Process updates based on flags
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
Config::generate().await?;
@@ -175,33 +193,35 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
handle::Handle::refresh_clash();
}
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
Config::verge().await.draft_mut().enable_global_hotkey = enable_global_hotkey;
handle::Handle::refresh_verge();
}
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 {
sysopt::Sysopt::global().update_sysproxy().await?;
}
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
if let Some(hotkeys) = patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys).await?;
}
}
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 {
tray::Tray::global().update_icon(None)?;
tray::Tray::global().update_icon(None).await?;
}
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 {
tray::Tray::global().update_click_behavior()?;
tray::Tray::global().update_click_behavior().await?;
}
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
if enable_auto_light_weight.unwrap() {
lightweight::enable_auto_light_weight_mode();
if enable_auto_light_weight.unwrap_or(false) {
lightweight::enable_auto_light_weight_mode().await;
} else {
lightweight::disable_auto_light_weight_mode();
}
@@ -211,15 +231,17 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
};
match res {
Ok(()) => {
Config::verge().apply();
Config::verge().await.apply();
if !not_save_file {
Config::verge().data().save_file()?;
// 分离数据获取和异步调用
let verge_data = Config::verge().await.data_mut().clone();
verge_data.save_file().await?;
}
Ok(())
}
Err(err) => {
Config::verge().discard();
Config::verge().await.discard();
Err(err)
}
}
+48 -32
View File
@@ -1,26 +1,25 @@
use crate::{
cmd,
config::{Config, PrfItem, PrfOption},
core::{handle, CoreManager, *},
config::{profiles::profiles_draft_update_item_safe, Config, PrfItem, PrfOption},
core::{handle, tray, CoreManager},
logging,
process::AsyncHandler,
utils::logging::Type,
};
use anyhow::{bail, Result};
/// Toggle proxy profile
pub fn toggle_proxy_profile(profile_index: String) {
AsyncHandler::spawn(|| async move {
let app_handle = handle::Handle::global().app_handle().unwrap();
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}");
pub async fn toggle_proxy_profile(profile_index: String) {
match cmd::patch_profiles_config_by_profile_index(profile_index).await {
Ok(_) => {
let result = tray::Tray::global().update_menu().await;
if let Err(err) = result {
logging!(error, Type::Tray, true, "更新菜单失败: {}", err);
}
}
});
Err(err) => {
log::error!(target: "app", "{err}");
}
}
}
/// Update a profile
@@ -35,24 +34,29 @@ pub async fn update_profile(
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
let url_opt = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let profiles = Config::profiles().await;
let profiles = profiles.latest_ref();
let item = profiles.get_item(&uid)?;
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote {
log::info!(target: "app", "[订阅更新] {} 不是远程订阅,跳过更新", uid);
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
None // 非远程订阅直接更新
} else if item.url.is_none() {
log::warn!(target: "app", "[订阅更新] {} 缺少URL,无法更新", uid);
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL,无法更新");
bail!("failed to get the profile item url");
} else {
log::info!(target: "app",
"[订阅更新] {} 是远程订阅,URL: {}",
uid,
item.url.clone().unwrap()
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
);
Some((item.url.clone().unwrap(), item.option.clone()))
Some((
item.url
.clone()
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
item.option.clone(),
))
}
};
@@ -65,17 +69,19 @@ pub async fn update_profile(
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => {
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?;
let profiles = Config::profiles().await;
let is_current = Some(uid.clone()) == profiles.get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_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}");
is_current && auto_refresh
}
Err(err) => {
// 首次更新失败,尝试使用Clash代理
log::warn!(target: "app", "[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err},尝试使用Clash代理更新");
// 发送通知
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
@@ -101,9 +107,10 @@ pub async fn update_profile(
}
// 更新到配置
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item.clone())?;
let profiles = Config::profiles().await;
// 使用 Send-safe 方法进行数据操作
profiles_draft_update_item_safe(uid.clone(), item.clone()).await?;
// 获取配置名称用于通知
let profile_name = item.name.clone().unwrap_or_else(|| uid.clone());
@@ -111,15 +118,15 @@ pub async fn update_profile(
// 发送通知告知用户自动更新使用了回退机制
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {}", is_current);
let is_current = Some(uid.clone()) == profiles.data_ref().get_current();
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
is_current && auto_refresh
}
Err(retry_err) => {
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{}", retry_err),
format!("{retry_err}"),
);
return Err(retry_err);
}
@@ -136,6 +143,15 @@ pub async fn update_profile(
Ok(_) => {
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
handle::Handle::refresh_clash();
if let Err(err) = cmd::proxy::force_refresh_proxies().await {
logging!(
error,
Type::Config,
true,
"[订阅更新] 代理组刷新失败: {}",
err
);
}
}
Err(err) => {
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
+74 -57
View File
@@ -1,85 +1,102 @@
use crate::{
config::{Config, IVerge},
core::handle,
process::AsyncHandler,
ipc::IpcManager,
logging,
utils::logging::Type,
};
use std::env;
use tauri_plugin_clipboard_manager::ClipboardExt;
/// Toggle system proxy on/off
pub fn toggle_system_proxy() {
let enable = Config::verge().draft().enable_system_proxy;
let enable = enable.unwrap_or(false);
let auto_close_connection = Config::verge()
.data()
.auto_close_connection
.unwrap_or(false);
pub async fn toggle_system_proxy() {
// 获取当前系统代理状态
let enable = {
let verge = Config::verge().await;
let enable = verge.latest_ref().enable_system_proxy.unwrap_or(false);
enable
};
// 获取自动关闭连接设置
let auto_close_connection = {
let verge = Config::verge().await;
let auto_close = verge.latest_ref().auto_close_connection.unwrap_or(false);
auto_close
};
AsyncHandler::spawn(move || async move {
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
if enable && auto_close_connection {
if let Err(err) = crate::module::mihomo::MihomoManager::global()
.close_all_connections()
.await
{
log::error!(target: "app", "Failed to close all connections: {}", err);
}
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
if enable && auto_close_connection {
if let Err(err) = IpcManager::global().close_all_connections().await {
log::error!(target: "app", "Failed to close all connections: {err}");
}
}
match super::patch_verge(
IVerge {
enable_system_proxy: Some(!enable),
..IVerge::default()
},
false,
)
.await
{
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
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().data().enable_tun_mode;
pub async fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().await.data_mut().enable_tun_mode;
let enable = enable.unwrap_or(false);
AsyncHandler::spawn(async move || {
match super::patch_verge(
IVerge {
enable_tun_mode: Some(!enable),
..IVerge::default()
},
not_save_file.unwrap_or(false),
)
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
});
match super::patch_verge(
IVerge {
enable_tun_mode: Some(!enable),
..IVerge::default()
},
not_save_file.unwrap_or(false),
)
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
}
/// Copy proxy environment variables to clipboard
pub fn copy_clash_env() {
pub async fn copy_clash_env() {
// 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| {
Config::verge()
.latest()
let clash_verge_rev_ip = match env::var("CLASH_VERGE_REV_IP") {
Ok(ip) => ip,
Err(_) => Config::verge()
.await
.latest_ref()
.proxy_host
.clone()
.unwrap_or_else(|| "127.0.0.1".to_string())
});
.unwrap_or_else(|| "127.0.0.1".to_string()),
};
let app_handle = handle::Handle::global().app_handle().unwrap();
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
let Some(app_handle) = handle::Handle::global().app_handle() else {
logging!(
error,
Type::System,
"Failed to get app handle for proxy operation"
);
return;
};
let port = {
Config::verge()
.await
.latest_ref()
.verge_mixed_port
.unwrap_or(7897)
};
let http_proxy = format!("http://{clash_verge_rev_ip}:{port}");
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
let cliboard = app_handle.clipboard();
let env_type = { Config::verge().latest().env_type.clone() };
let env_type = { Config::verge().await.latest_ref().env_type.clone() };
let env_type = match env_type {
Some(env_type) => env_type,
None => {
+95 -60
View File
@@ -1,42 +1,74 @@
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use crate::{
config::Config,
core::{handle, sysopt, CoreManager},
ipc::IpcManager,
logging,
module::mihomo::MihomoManager,
utils::logging::Type,
};
/// Open or close the dashboard window
#[allow(dead_code)]
pub fn open_or_close_dashboard() {
pub async fn open_or_close_dashboard() {
open_or_close_dashboard_internal(false).await
}
/// Open or close the dashboard window (hotkey call, dispatched to main thread)
#[allow(dead_code)]
pub async fn open_or_close_dashboard_hotkey() {
open_or_close_dashboard_internal(true).await
}
/// Internal implementation for opening/closing dashboard
async fn open_or_close_dashboard_internal(bypass_debounce: bool) {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Attempting to open/close dashboard");
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
// 检查是否在轻量模式下
// 热键调用调度到主线程执行,避免 WebView 创建死锁
if bypass_debounce {
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
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().await;
log::info!(target: "app", "Creating new window after exiting lightweight mode");
let result = WindowManager::show_main_window().await;
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
let result = WindowManager::toggle_main_window().await;
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();
crate::module::lightweight::exit_lightweight_mode().await;
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);
let result = WindowManager::show_main_window().await;
log::info!(target: "app", "Window operation result: {result:?}");
return;
}
// 使用统一的窗口管理器切换窗口状态
let result = WindowManager::toggle_main_window();
log::info!(target: "app", "Window toggle result: {:?}", result);
let result = WindowManager::toggle_main_window().await;
log::info!(target: "app", "Window toggle result: {result:?}");
}
/// 异步优化的应用退出函数
pub fn quit() {
use crate::process::AsyncHandler;
pub async fn quit() {
logging!(debug, Type::System, true, "启动退出流程");
// 获取应用句柄并设置退出标志
let app_handle = handle::Handle::global().app_handle().unwrap();
let Some(app_handle) = handle::Handle::global().app_handle() else {
logging!(
error,
Type::System,
"Failed to get app handle for quit operation"
);
return;
};
handle::Handle::global().set_is_exiting();
// 优先关闭窗口,提供立即反馈
@@ -46,19 +78,17 @@ pub fn quit() {
}
// 使用异步任务处理资源清理,避免阻塞
AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "开始异步清理资源");
let cleanup_result = clean_async().await;
logging!(info, Type::System, true, "开始异步清理资源");
let cleanup_result = clean_async().await;
logging!(
info,
Type::System,
true,
"资源清理完成,退出代码: {}",
if cleanup_result { 0 } else { 1 }
);
app_handle.exit(if cleanup_result { 0 } else { 1 });
});
logging!(
info,
Type::System,
true,
"资源清理完成,退出代码: {}",
if cleanup_result { 0 } else { 1 }
);
app_handle.exit(if cleanup_result { 0 } else { 1 });
}
async fn clean_async() -> bool {
@@ -67,31 +97,35 @@ async fn clean_async() -> bool {
logging!(info, Type::System, true, "开始执行异步清理操作...");
// 1. 处理TUN模式
let tun_task = async {
if Config::verge().data().enable_tun_mode.unwrap_or(false) {
let disable_tun = serde_json::json!({
"tun": {
"enable": false
}
});
match timeout(
Duration::from_secs(2),
MihomoManager::global().patch_configs(disable_tun),
)
.await
{
Ok(_) => {
log::info!(target: "app", "TUN模式已禁用");
true
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时");
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}});
match timeout(
Duration::from_secs(3),
IpcManager::global().patch_configs(disable_tun),
)
.await
{
Ok(Ok(_)) => {
log::info!(target: "app", "TUN模式已禁用");
tokio::time::sleep(Duration::from_millis(300)).await;
true
}
Ok(Err(e)) => {
log::warn!(target: "app", "禁用TUN模式失败: {e}");
false
}
Err(_) => {
log::warn!(target: "app", "禁用TUN模式超时");
false
}
} else {
true
}
} else {
true
};
// 2. 系统代理重置
@@ -132,7 +166,7 @@ async fn clean_async() -> bool {
let dns_task = async {
match timeout(
Duration::from_millis(1000),
crate::utils::resolve::restore_public_dns(),
crate::utils::resolve::dns::restore_public_dns(),
)
.await
{
@@ -147,8 +181,8 @@ async fn clean_async() -> bool {
}
};
// 并行执行所有清理任务
let (tun_success, proxy_success, core_success) = tokio::join!(tun_task, proxy_task, core_task);
// 并行执行剩余清理任务
let (proxy_success, core_success) = tokio::join!(proxy_task, core_task);
#[cfg(target_os = "macos")]
let dns_success = dns_task.await;
@@ -161,7 +195,7 @@ async fn clean_async() -> bool {
info,
Type::System,
true,
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
"异步关闭操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
tun_success,
proxy_success,
core_success,
@@ -178,7 +212,7 @@ pub fn clean() -> bool {
let (tx, rx) = std::sync::mpsc::channel();
AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "开始执行清理操作...");
logging!(info, Type::System, true, "开始执行关闭操作...");
// 使用已有的异步清理函数
let cleanup_result = clean_async().await;
@@ -189,7 +223,7 @@ pub fn clean() -> bool {
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
Ok(result) => {
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
logging!(info, Type::System, true, "关闭操作完成,结果: {}", result);
result
}
Err(_) => {
@@ -205,16 +239,17 @@ pub fn clean() -> bool {
}
#[cfg(target_os = "macos")]
pub fn hide() {
pub async fn hide() {
use crate::module::lightweight::add_light_weight_timer;
let enable_auto_light_weight_mode = Config::verge()
.data()
.await
.data_mut()
.enable_auto_light_weight_mode
.unwrap_or(false);
if enable_auto_light_weight_mode {
add_light_weight_timer();
add_light_weight_timer().await;
}
if let Some(window) = handle::Handle::global().get_window() {
@@ -222,5 +257,5 @@ pub fn hide() {
let _ = window.hide();
}
}
AppHandleManager::global().set_activation_policy_accessory();
handle::Handle::global().set_activation_policy_accessory();
}
+372
View File
@@ -0,0 +1,372 @@
use std::time::Duration;
use kode_bridge::{
errors::{AnyError, AnyResult},
ClientConfig, IpcHttpClient, LegacyResponse,
};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use crate::{
logging, singleton_with_logging,
utils::{dirs::ipc_path, logging::Type},
};
// 定义用于URL路径的编码集合,只编码真正必要的字符
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ') // 空格
.add(b'/') // 斜杠
.add(b'?') // 问号
.add(b'#') // 井号
.add(b'&') // 和号
.add(b'%'); // 百分号
// Helper function to create AnyError from string
fn create_error(msg: impl Into<String>) -> AnyError {
Box::new(std::io::Error::other(msg.into()))
}
pub struct IpcManager {
client: IpcHttpClient,
}
impl IpcManager {
fn new() -> Self {
let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
});
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
let config = ClientConfig {
default_timeout: Duration::from_secs(5),
enable_pooling: false,
max_retries: 4,
retry_delay: Duration::from_millis(125),
max_concurrent_requests: 16,
max_requests_per_second: Some(64.0),
..Default::default()
};
#[allow(clippy::unwrap_used)]
let client = IpcHttpClient::with_config(ipc_path, config).unwrap();
Self { client }
}
}
// Use singleton macro with logging
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
impl IpcManager {
pub async fn request(
&self,
method: &str,
path: &str,
body: Option<&serde_json::Value>,
) -> AnyResult<LegacyResponse> {
self.client.request(method, path, body).await
}
}
impl IpcManager {
pub async fn send_request(
&self,
method: &str,
path: &str,
body: Option<&serde_json::Value>,
) -> AnyResult<serde_json::Value> {
let response = IpcManager::global().request(method, path, body).await?;
match method {
"GET" => Ok(response.json()?),
"PATCH" => {
if response.status == 204 {
Ok(serde_json::json!({"code": 204}))
} else {
Ok(response.json()?)
}
}
"PUT" | "DELETE" => {
if response.status == 204 {
Ok(serde_json::json!({"code": 204}))
} else {
match response.json() {
Ok(json) => Ok(json),
Err(_) => Ok(serde_json::json!({
"code": response.status,
"message": response.body,
"error": "failed to parse response as JSON"
})),
}
}
}
_ => match response.json() {
Ok(json) => Ok(json),
Err(_) => Ok(serde_json::json!({
"code": response.status,
"message": response.body,
"error": "failed to parse response as JSON"
})),
},
}
}
// 基础代理信息获取
pub async fn get_proxies(&self) -> AnyResult<serde_json::Value> {
let url = "/proxies";
self.send_request("GET", url, None).await
}
// 代理提供者信息获取
pub async fn get_providers_proxies(&self) -> AnyResult<serde_json::Value> {
let url = "/providers/proxies";
self.send_request("GET", url, None).await
}
// 连接管理
pub async fn get_connections(&self) -> AnyResult<serde_json::Value> {
let url = "/connections";
self.send_request("GET", url, None).await
}
pub async fn delete_connection(&self, id: &str) -> AnyResult<()> {
let encoded_id = utf8_percent_encode(id, URL_PATH_ENCODE_SET).to_string();
let url = format!("/connections/{encoded_id}");
let response = self.send_request("DELETE", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"].as_str().unwrap_or("unknown error"),
))
}
}
pub async fn close_all_connections(&self) -> AnyResult<()> {
let url = "/connections";
let response = self.send_request("DELETE", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_owned(),
))
}
}
}
impl IpcManager {
#[allow(dead_code)]
pub async fn is_mihomo_running(&self) -> AnyResult<()> {
let url = "/version";
let _response = self.send_request("GET", url, None).await?;
Ok(())
}
pub async fn put_configs_force(&self, clash_config_path: &str) -> AnyResult<()> {
let url = "/configs?force=true";
let payload = serde_json::json!({
"path": clash_config_path,
});
let _response = self.send_request("PUT", url, Some(&payload)).await?;
Ok(())
}
pub async fn patch_configs(&self, config: serde_json::Value) -> AnyResult<()> {
let url = "/configs";
let response = self.send_request("PATCH", url, Some(&config)).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_owned(),
))
}
}
pub async fn test_proxy_delay(
&self,
name: &str,
test_url: Option<String>,
timeout: i32,
) -> AnyResult<serde_json::Value> {
let test_url =
test_url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
// 测速URL不再编码,直接传递
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
}
// 版本和配置相关
pub async fn get_version(&self) -> AnyResult<serde_json::Value> {
let url = "/version";
self.send_request("GET", url, None).await
}
pub async fn get_config(&self) -> AnyResult<serde_json::Value> {
let url = "/configs";
self.send_request("GET", url, None).await
}
pub async fn update_geo_data(&self) -> AnyResult<()> {
let url = "/configs/geo";
let response = self.send_request("POST", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
pub async fn upgrade_core(&self) -> AnyResult<()> {
let url = "/upgrade";
let response = self.send_request("POST", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 规则相关
pub async fn get_rules(&self) -> AnyResult<serde_json::Value> {
let url = "/rules";
self.send_request("GET", url, None).await
}
pub async fn get_rule_providers(&self) -> AnyResult<serde_json::Value> {
let url = "/providers/rules";
self.send_request("GET", url, None).await
}
pub async fn update_rule_provider(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/rules/{encoded_name}");
let response = self.send_request("PUT", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 代理相关
pub async fn update_proxy(&self, group: &str, proxy: &str) -> AnyResult<()> {
// 使用 percent-encoding 进行正确的 URL 编码
let encoded_group = utf8_percent_encode(group, URL_PATH_ENCODE_SET).to_string();
let url = format!("/proxies/{encoded_group}");
let payload = serde_json::json!({
"name": proxy
});
match self.send_request("PUT", &url, Some(&payload)).await {
Ok(_) => Ok(()),
Err(e) => {
logging!(
error,
crate::utils::logging::Type::Ipc,
true,
"IPC: updateProxy encountered error: {} (ignored, always returning true)",
e
);
Ok(())
}
}
}
pub async fn proxy_provider_health_check(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/proxies/{encoded_name}/healthcheck");
let response = self.send_request("GET", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
pub async fn update_proxy_provider(&self, name: &str) -> AnyResult<()> {
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
let url = format!("/providers/proxies/{encoded_name}");
let response = self.send_request("PUT", &url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 延迟测试相关
pub async fn get_group_proxy_delays(
&self,
group_name: &str,
url: Option<String>,
timeout: i32,
) -> AnyResult<serde_json::Value> {
let test_url = url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
let encoded_group_name = utf8_percent_encode(group_name, URL_PATH_ENCODE_SET).to_string();
// 测速URL不再编码,直接传递
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
let response = self.send_request("GET", &url, None).await;
response
}
// 调试相关
pub async fn is_debug_enabled(&self) -> AnyResult<bool> {
let url = "/debug/pprof";
match self.send_request("GET", url, None).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
pub async fn gc(&self) -> AnyResult<()> {
let url = "/debug/gc";
let response = self.send_request("PUT", url, None).await?;
if response["code"] == 204 {
Ok(())
} else {
Err(create_error(
response["message"]
.as_str()
.unwrap_or("unknown error")
.to_string(),
))
}
}
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理
}
+330
View File
@@ -0,0 +1,330 @@
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, sync::Arc, time::Instant};
use tauri::async_runtime::JoinHandle;
use tokio::{sync::RwLock, time::Duration};
use crate::{
ipc::monitor::MonitorData,
logging,
process::AsyncHandler,
singleton_with_logging,
utils::{dirs::ipc_path, logging::Type},
};
const MAX_LOGS: usize = 1000; // Maximum number of logs to keep in memory
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LogData {
#[serde(rename = "type")]
pub log_type: String,
pub payload: String,
}
#[derive(Debug, Clone)]
pub struct LogItem {
pub log_type: String,
pub payload: String,
pub time: String,
}
impl LogItem {
fn new(log_type: String, payload: String) -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs();
// Simple time formatting (HH:MM:SS)
let hours = (now / 3600) % 24;
let minutes = (now / 60) % 60;
let seconds = now % 60;
let time_str = format!("{hours:02}:{minutes:02}:{seconds:02}");
Self {
log_type,
payload,
time: time_str,
}
}
}
#[derive(Debug, Clone)]
pub struct CurrentLogs {
pub logs: VecDeque<LogItem>,
// pub level: String,
pub last_updated: Instant,
}
impl Default for CurrentLogs {
fn default() -> Self {
Self {
logs: VecDeque::with_capacity(MAX_LOGS),
// level: "info".to_string(),
last_updated: Instant::now(),
}
}
}
impl MonitorData for CurrentLogs {
fn mark_fresh(&mut self) {
self.last_updated = Instant::now();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.last_updated.elapsed() < duration
}
}
// Logs monitor with streaming support
pub struct LogsMonitor {
current: Arc<RwLock<CurrentLogs>>,
task_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
current_monitoring_level: Arc<RwLock<Option<String>>>,
}
// Use singleton_with_logging macro
singleton_with_logging!(LogsMonitor, INSTANCE, "LogsMonitor");
impl LogsMonitor {
fn new() -> Self {
let current = Arc::new(RwLock::new(CurrentLogs::default()));
Self {
current,
task_handle: Arc::new(RwLock::new(None)),
current_monitoring_level: Arc::new(RwLock::new(None)),
}
}
pub async fn start_monitoring(&self, level: Option<String>) {
let filter_level = level.clone().unwrap_or_else(|| "info".to_string());
// Check if we're already monitoring the same level
// let level_changed = {
// let current_level = self.current_monitoring_level.read().await;
// if let Some(existing_level) = current_level.as_ref() {
// if existing_level == &filter_level {
// logging!(
// info,
// Type::Ipc,
// true,
// "LogsMonitor: Already monitoring level '{}', skipping duplicate request",
// filter_level
// );
// return;
// }
// true // Level changed
// } else {
// true // First time or was stopped
// }
// };
// Stop existing monitoring task if level changed or first time
{
let mut handle = self.task_handle.write().await;
if let Some(task) = handle.take() {
task.abort();
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Stopped previous monitoring task (level changed)"
);
}
}
// We want to keep the logs cache even if the level changes,
// so we don't clear it here. The cache will be cleared only when the level changes
// and a new task is started. This allows us to keep logs from previous levels
// even if the level changes during monitoring.
// Clear logs cache when level changes to ensure fresh data
// if level_changed {
// let mut current = self.current.write().await;
// current.logs.clear();
// current.level = filter_level.clone();
// current.mark_fresh();
// logging!(
// info,
// Type::Ipc,
// true,
// "LogsMonitor: Cleared logs cache due to level change to '{}'",
// filter_level
// );
// }
// Update current monitoring level
{
let mut current_level = self.current_monitoring_level.write().await;
*current_level = Some(filter_level.clone());
}
let monitor_current = Arc::clone(&self.current);
let task = AsyncHandler::spawn(move || async move {
loop {
// Get fresh IPC path and client for each connection attempt
let (_ipc_path_buf, client) = match Self::create_ipc_client() {
Ok((path, client)) => (path, client),
Err(e) => {
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
}
};
let url = if filter_level == "all" {
"/logs".to_string()
} else {
format!("/logs?level={filter_level}")
};
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Starting stream for {}",
url
);
let _ = client
.get(&url)
.timeout(Duration::from_secs(30))
.process_lines(|line| {
Self::process_log_line(line, Arc::clone(&monitor_current))
})
.await;
// Wait before retrying
tokio::time::sleep(Duration::from_secs(2)).await;
}
});
// Store the task handle
{
let mut handle = self.task_handle.write().await;
*handle = Some(task);
}
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Started new monitoring task for level: {:?}",
level
);
}
pub async fn stop_monitoring(&self) {
// Stop monitoring task but keep logs
{
let mut handle = self.task_handle.write().await;
if let Some(task) = handle.take() {
task.abort();
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Stopped monitoring task"
);
}
}
// Reset monitoring level
{
let mut monitoring_level = self.current_monitoring_level.write().await;
*monitoring_level = None;
}
}
fn create_ipc_client() -> Result<
(std::path::PathBuf, kode_bridge::IpcStreamClient),
Box<dyn std::error::Error + Send + Sync>,
> {
use kode_bridge::IpcStreamClient;
let ipc_path_buf = ipc_path()?;
let ipc_path = ipc_path_buf.to_str().ok_or("Invalid IPC path")?;
let client = IpcStreamClient::new(ipc_path)?;
Ok((ipc_path_buf, client))
}
fn process_log_line(
line: &str,
current: Arc<RwLock<CurrentLogs>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(log_data) = serde_json::from_str::<LogData>(line.trim()) {
// Server-side filtering via query parameters handles the level filtering
// We only need to accept all logs since filtering is done at the endpoint level
let log_item = LogItem::new(log_data.log_type, log_data.payload);
AsyncHandler::spawn(move || async move {
let mut logs = current.write().await;
// Add new log
logs.logs.push_back(log_item);
// Keep only the last 1000 logs
if logs.logs.len() > 1000 {
logs.logs.pop_front();
}
logs.mark_fresh();
});
}
Ok(())
}
pub async fn current(&self) -> CurrentLogs {
self.current.read().await.clone()
}
pub async fn clear_logs(&self) {
let mut current = self.current.write().await;
current.logs.clear();
current.mark_fresh();
logging!(
info,
Type::Ipc,
true,
"LogsMonitor: Cleared frontend logs (monitoring continues)"
);
}
pub async fn get_logs_as_json(&self) -> serde_json::Value {
let current = self.current().await;
// Simply return all cached logs since filtering is handled by start_monitoring
// and the cache is cleared when level changes
let logs: Vec<serde_json::Value> = current
.logs
.iter()
.map(|log| {
serde_json::json!({
"type": log.log_type,
"payload": log.payload,
"time": log.time
})
})
.collect();
serde_json::Value::Array(logs)
}
}
pub async fn start_logs_monitoring(level: Option<String>) {
LogsMonitor::global().start_monitoring(level).await;
}
pub async fn stop_logs_monitoring() {
LogsMonitor::global().stop_monitoring().await;
}
pub async fn clear_logs() {
LogsMonitor::global().clear_logs().await;
}
pub async fn get_logs_json() -> serde_json::Value {
LogsMonitor::global().get_logs_as_json().await
}
+119
View File
@@ -0,0 +1,119 @@
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Instant};
use tokio::{sync::RwLock, time::Duration};
use crate::{
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
process::AsyncHandler,
singleton_lazy_with_logging,
utils::format::fmt_bytes,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MemoryData {
pub inuse: u64,
pub oslimit: u64,
}
#[derive(Debug, Clone)]
pub struct CurrentMemory {
pub inuse: u64,
pub oslimit: u64,
pub last_updated: Instant,
}
impl Default for CurrentMemory {
fn default() -> Self {
Self {
inuse: 0,
oslimit: 0,
last_updated: Instant::now(),
}
}
}
impl MonitorData for CurrentMemory {
fn mark_fresh(&mut self) {
self.last_updated = Instant::now();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.last_updated.elapsed() < duration
}
}
impl StreamingParser for CurrentMemory {
fn parse_and_update(
line: &str,
current: Arc<RwLock<Self>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(memory) = serde_json::from_str::<MemoryData>(line.trim()) {
AsyncHandler::spawn(move || async move {
let mut current_guard = current.write().await;
current_guard.inuse = memory.inuse;
current_guard.oslimit = memory.oslimit;
current_guard.mark_fresh();
});
}
Ok(())
}
}
// Minimal memory monitor using the new architecture
pub struct MemoryMonitor {
monitor: IpcStreamMonitor<CurrentMemory>,
}
impl Default for MemoryMonitor {
fn default() -> Self {
MemoryMonitor {
monitor: IpcStreamMonitor::new(
"/memory".to_string(),
Duration::from_secs(10),
Duration::from_secs(2),
Duration::from_secs(10),
),
}
}
}
// Use simplified singleton_lazy_with_logging macro
singleton_lazy_with_logging!(
MemoryMonitor,
INSTANCE,
"MemoryMonitor",
MemoryMonitor::default
);
impl MemoryMonitor {
pub async fn current(&self) -> CurrentMemory {
self.monitor.current().await
}
pub async fn is_fresh(&self) -> bool {
self.monitor.is_fresh().await
}
}
pub async fn get_current_memory() -> CurrentMemory {
MemoryMonitor::global().current().await
}
pub async fn get_formatted_memory() -> (String, String, f64, bool) {
let monitor = MemoryMonitor::global();
let memory = monitor.current().await;
let is_fresh = monitor.is_fresh().await;
let usage_percent = if memory.oslimit > 0 {
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
} else {
0.0
};
(
fmt_bytes(memory.inuse),
fmt_bytes(memory.oslimit),
usage_percent,
is_fresh,
)
}
+15
View File
@@ -0,0 +1,15 @@
pub mod general;
pub mod logs;
pub mod memory;
pub mod monitor;
pub mod traffic;
pub use general::IpcManager;
pub use logs::{clear_logs, get_logs_json, start_logs_monitoring, stop_logs_monitoring};
pub use memory::{get_current_memory, get_formatted_memory};
pub use traffic::{get_current_traffic, get_formatted_traffic};
pub struct Rate {
// pub up: usize,
// pub down: usize,
}
+120
View File
@@ -0,0 +1,120 @@
use kode_bridge::IpcStreamClient;
use std::sync::Arc;
use tokio::{sync::RwLock, time::Duration};
use crate::{
logging,
process::AsyncHandler,
utils::{dirs::ipc_path, logging::Type},
};
/// Generic base structure for IPC monitoring data with freshness tracking
pub trait MonitorData: Clone + Send + Sync + 'static {
/// Update the last_updated timestamp to now
fn mark_fresh(&mut self);
/// Check if data is fresh based on the given duration
fn is_fresh_within(&self, duration: Duration) -> bool;
}
/// Trait for parsing streaming data and updating monitor state
pub trait StreamingParser: MonitorData {
/// Parse a line of streaming data and update the current state
fn parse_and_update(
line: &str,
current: Arc<RwLock<Self>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
/// Generic IPC stream monitor that handles the common streaming pattern
pub struct IpcStreamMonitor<T>
where
T: MonitorData + StreamingParser + Default,
{
current: Arc<RwLock<T>>,
#[allow(dead_code)]
endpoint: String,
#[allow(dead_code)]
timeout: Duration,
#[allow(dead_code)]
retry_interval: Duration,
freshness_duration: Duration,
}
impl<T> IpcStreamMonitor<T>
where
T: MonitorData + StreamingParser + Default,
{
pub fn new(
endpoint: String,
timeout: Duration,
retry_interval: Duration,
freshness_duration: Duration,
) -> Self {
let current = Arc::new(RwLock::new(T::default()));
let monitor_current = Arc::clone(&current);
let endpoint_clone = endpoint.clone();
// Start the monitoring task
AsyncHandler::spawn(move || async move {
Self::streaming_task(monitor_current, endpoint_clone, timeout, retry_interval).await;
});
Self {
current,
endpoint,
timeout,
retry_interval,
freshness_duration,
}
}
pub async fn current(&self) -> T {
self.current.read().await.clone()
}
pub async fn is_fresh(&self) -> bool {
self.current
.read()
.await
.is_fresh_within(self.freshness_duration)
}
/// The core streaming task that can be specialized per monitor type
async fn streaming_task(
current: Arc<RwLock<T>>,
endpoint: String,
timeout: Duration,
retry_interval: Duration,
) {
loop {
let ipc_path_buf = match ipc_path() {
Ok(path) => path,
Err(e) => {
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
tokio::time::sleep(retry_interval).await;
continue;
}
};
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
let client = match IpcStreamClient::new(ipc_path) {
Ok(client) => client,
Err(e) => {
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
tokio::time::sleep(retry_interval).await;
continue;
}
};
let _ = client
.get(&endpoint)
.timeout(timeout)
.process_lines(|line| T::parse_and_update(line, Arc::clone(&current)))
.await;
tokio::time::sleep(retry_interval).await;
}
}
}
+153
View File
@@ -0,0 +1,153 @@
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Instant};
use tokio::{sync::RwLock, time::Duration};
use crate::{
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
process::AsyncHandler,
singleton_lazy_with_logging,
utils::format::fmt_bytes,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TrafficData {
pub up: u64,
pub down: u64,
}
#[derive(Debug, Clone)]
pub struct CurrentTraffic {
pub up_rate: u64,
pub down_rate: u64,
pub total_up: u64,
pub total_down: u64,
pub last_updated: Instant,
}
impl Default for CurrentTraffic {
fn default() -> Self {
Self {
up_rate: 0,
down_rate: 0,
total_up: 0,
total_down: 0,
last_updated: Instant::now(),
}
}
}
impl MonitorData for CurrentTraffic {
fn mark_fresh(&mut self) {
self.last_updated = Instant::now();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.last_updated.elapsed() < duration
}
}
// Traffic monitoring state for calculating rates
#[derive(Debug, Clone, Default)]
pub struct TrafficMonitorState {
pub current: CurrentTraffic,
pub last_traffic: Option<TrafficData>,
}
impl MonitorData for TrafficMonitorState {
fn mark_fresh(&mut self) {
self.current.mark_fresh();
}
fn is_fresh_within(&self, duration: Duration) -> bool {
self.current.is_fresh_within(duration)
}
}
impl StreamingParser for TrafficMonitorState {
fn parse_and_update(
line: &str,
current: Arc<RwLock<Self>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(traffic) = serde_json::from_str::<TrafficData>(line.trim()) {
AsyncHandler::spawn(move || async move {
let mut state_guard = current.write().await;
let (up_rate, down_rate) = state_guard
.last_traffic
.as_ref()
.map(|l| {
(
traffic.up.saturating_sub(l.up),
traffic.down.saturating_sub(l.down),
)
})
.unwrap_or((0, 0));
state_guard.current = CurrentTraffic {
up_rate,
down_rate,
total_up: traffic.up,
total_down: traffic.down,
last_updated: Instant::now(),
};
state_guard.last_traffic = Some(traffic);
});
}
Ok(())
}
}
// Minimal traffic monitor using the new architecture
pub struct TrafficMonitor {
monitor: IpcStreamMonitor<TrafficMonitorState>,
}
impl Default for TrafficMonitor {
fn default() -> Self {
TrafficMonitor {
monitor: IpcStreamMonitor::new(
"/traffic".to_string(),
Duration::from_secs(10),
Duration::from_secs(1),
Duration::from_secs(5),
),
}
}
}
// Use simplified singleton_lazy_with_logging macro
singleton_lazy_with_logging!(
TrafficMonitor,
INSTANCE,
"TrafficMonitor",
TrafficMonitor::default
);
impl TrafficMonitor {
pub async fn current(&self) -> CurrentTraffic {
self.monitor.current().await.current
}
pub async fn is_fresh(&self) -> bool {
self.monitor.is_fresh().await
}
}
pub async fn get_current_traffic() -> CurrentTraffic {
TrafficMonitor::global().current().await
}
pub async fn get_formatted_traffic() -> (String, String, String, String, bool) {
let monitor = TrafficMonitor::global();
let traffic = monitor.current().await;
let is_fresh = monitor.is_fresh().await;
(
fmt_bytes(traffic.up_rate),
fmt_bytes(traffic.down_rate),
fmt_bytes(traffic.total_up),
fmt_bytes(traffic.total_down),
is_fresh,
)
}
+432 -313
View File
@@ -1,20 +1,27 @@
#![allow(non_snake_case)]
#![recursion_limit = "512"]
mod cmd;
mod config;
pub mod config;
mod core;
mod enhance;
mod feat;
mod ipc;
mod module;
mod process;
mod state;
mod utils;
#[cfg(target_os = "macos")]
use crate::utils::window_manager::WindowManager;
use crate::{
core::handle,
core::hotkey,
process::AsyncHandler,
utils::{resolve, resolve::resolve_scheme, server},
utils::{resolve, server},
};
use config::Config;
use std::sync::{Mutex, Once};
use tauri::AppHandle;
#[cfg(target_os = "macos")]
use tauri::Manager;
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher;
@@ -22,204 +29,115 @@ use tauri_plugin_deep_link::DeepLinkExt;
use tokio::time::{timeout, Duration};
use utils::logging::Type;
/// A global singleton handle to the application.
pub struct AppHandleManager {
inner: Mutex<Option<AppHandle>>,
init: Once,
}
/// Application initialization helper functions
mod app_init {
use super::*;
impl AppHandleManager {
/// Get the global instance of the app handle manager.
pub fn global() -> &'static Self {
static INSTANCE: AppHandleManager = AppHandleManager {
inner: Mutex::new(None),
init: Once::new(),
};
&INSTANCE
}
/// Initialize the app handle manager with an app handle.
pub fn init(&self, handle: AppHandle) {
self.init.call_once(|| {
let mut app_handle = self.inner.lock().unwrap();
*app_handle = Some(handle);
/// Initialize singleton monitoring for other instances
pub fn init_singleton_check() {
AsyncHandler::spawn_blocking(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_millis(500), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if let Some(app_handle) = handle::Handle::global().app_handle() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
}
}
Err(_) => {
logging!(
warn,
Type::Setup,
true,
"单例检查超时,假定没有其他实例运行"
);
}
}
});
}
/// Get the app handle if it has been initialized.
pub fn get(&self) -> Option<AppHandle> {
self.inner.lock().unwrap().clone()
}
/// Setup plugins for the Tauri builder
pub fn setup_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
#[allow(unused_mut)]
let mut builder = builder
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init());
/// Get the app handle, panics if it hasn't been initialized.
pub fn get_handle(&self) -> AppHandle {
self.get().expect("AppHandle not initialized")
}
pub fn set_activation_policy_regular(&self) {
#[cfg(target_os = "macos")]
#[cfg(all(debug_assertions, not(feature = "tokio-trace")))]
{
let app_handle = self.inner.lock().unwrap();
let app_handle = app_handle.as_ref().unwrap();
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
builder = builder.plugin(tauri_plugin_devtools::init());
}
builder
}
pub fn set_activation_policy_accessory(&self) {
#[cfg(target_os = "macos")]
/// Setup deep link handling
pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
let app_handle = self.inner.lock().unwrap();
let app_handle = app_handle.as_ref().unwrap();
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
logging!(info, Type::Setup, true, "注册深层链接...");
app.deep_link().register_all()?;
}
}
pub fn set_activation_policy_prohibited(&self) {
#[cfg(target_os = "macos")]
{
let app_handle = self.inner.lock().unwrap();
let app_handle = app_handle.as_ref().unwrap();
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited);
}
}
}
#[allow(clippy::panic)]
pub fn run() {
utils::network::NetworkManager::global().init();
let _ = utils::dirs::init_portable_flag();
// 异步单例检测
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if let Some(app_handle) = AppHandleManager::global().get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
}
}
Err(_) => {
logging!(
warn,
Type::Setup,
true,
"单例检查超时,假定没有其他实例运行"
);
}
}
});
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
#[cfg(debug_assertions)]
let devtools = tauri_plugin_devtools::init();
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
logging!(info, Type::Setup, true, "开始应用初始化...");
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
#[cfg(target_os = "macos")]
{
auto_start_plugin_builder = auto_start_plugin_builder
.macos_launcher(MacosLauncher::LaunchAgent)
.app_name(app.config().identifier.clone());
}
let _ = app.handle().plugin(auto_start_plugin_builder.build());
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
use tauri_plugin_deep_link::DeepLinkExt;
logging!(info, Type::Setup, true, "注册深层链接...");
logging_error!(Type::System, true, app.deep_link().register_all());
}
app.deep_link().on_open_url(|event| {
AsyncHandler::spawn(move || {
let url = event.urls().first().map(|u| u.to_string());
async move {
if let Some(url) = url {
logging_error!(Type::Setup, true, resolve_scheme(url).await);
}
app.deep_link().on_open_url(|event| {
let url = event.urls().first().map(|u| u.to_string());
if let Some(url) = url {
AsyncHandler::spawn(|| async {
if let Err(e) = resolve::resolve_scheme(url).await {
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
}
});
});
// 窗口管理
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
.build();
let _ = app.handle().plugin(window_state_plugin);
// 异步处理
let app_handle = app.handle().clone();
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秒),继续执行后续流程"
);
}
}
});
logging!(info, Type::Setup, true, "执行主要设置操作...");
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, "初始化配置...");
if let Err(e) = utils::init::init_config() {
logging!(error, Type::Setup, true, "初始化配置失败: {}", e);
}
});
logging!(info, Type::Setup, true, "初始化资源...");
if let Err(e) = utils::init::init_resources() {
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
}
Ok(())
}
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
/// Setup autostart plugin
pub fn setup_autostart(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(target_os = "macos")]
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
#[cfg(not(target_os = "macos"))]
let auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(())
})
.invoke_handler(tauri::generate_handler![
// common
#[cfg(target_os = "macos")]
{
auto_start_plugin_builder = auto_start_plugin_builder
.macos_launcher(MacosLauncher::LaunchAgent)
.app_name(app.config().identifier.clone());
}
app.handle().plugin(auto_start_plugin_builder.build())?;
Ok(())
}
/// Setup window state management
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
.build();
app.handle().plugin(window_state_plugin)?;
Ok(())
}
/// Generate all command handlers for the application
pub fn generate_handlers(
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
tauri::generate_handler![
// Common commands
cmd::get_sys_proxy,
cmd::get_auto_proxy,
cmd::open_app_dir,
@@ -230,11 +148,11 @@ pub fn run() {
cmd::get_network_interfaces,
cmd::get_system_hostname,
cmd::restart_app,
// 内核管理
// Core management
cmd::start_core,
cmd::stop_core,
cmd::restart_core,
// 启动命令
// Application lifecycle
cmd::notify_ui_ready,
cmd::update_ui_stage,
cmd::reset_ui_ready_state,
@@ -242,16 +160,16 @@ pub fn run() {
cmd::get_app_uptime,
cmd::get_auto_launch_status,
cmd::is_admin,
// 添加轻量模式相关命令
// Lightweight mode
cmd::entry_lightweight_mode,
cmd::exit_lightweight_mode,
// service 管理
// Service management
cmd::install_service,
cmd::uninstall_service,
cmd::reinstall_service,
cmd::repair_service,
cmd::is_service_available,
// clash
// Clash core commands
cmd::get_clash_info,
cmd::patch_clash_config,
cmd::patch_clash_mode,
@@ -265,12 +183,44 @@ pub fn run() {
cmd::get_proxies,
cmd::force_refresh_proxies,
cmd::get_providers_proxies,
cmd::sync_tray_proxy_selection,
cmd::update_proxy_and_sync,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::check_dns_config_exists,
cmd::get_dns_config_content,
cmd::validate_dns_config,
// verge
cmd::get_clash_version,
cmd::get_clash_config,
cmd::force_refresh_clash_config,
cmd::update_geo_data,
cmd::upgrade_clash_core,
cmd::get_clash_rules,
cmd::update_proxy_choice,
cmd::get_proxy_providers,
cmd::get_rule_providers,
cmd::proxy_provider_health_check,
cmd::update_proxy_provider,
cmd::update_rule_provider,
cmd::get_clash_connections,
cmd::delete_clash_connection,
cmd::close_all_clash_connections,
cmd::get_group_proxy_delays,
cmd::is_clash_debug_enabled,
cmd::clash_gc,
// Logging and monitoring
cmd::get_clash_logs,
cmd::start_logs_monitoring,
cmd::stop_logs_monitoring,
cmd::clear_logs,
cmd::get_traffic_data,
cmd::get_memory_data,
cmd::get_formatted_traffic_data,
cmd::get_formatted_memory_data,
cmd::get_system_monitor_overview,
cmd::start_traffic_service,
cmd::stop_traffic_service,
// Verge configuration
cmd::get_verge_config,
cmd::patch_verge_config,
cmd::test_delay,
@@ -280,7 +230,7 @@ pub fn run() {
cmd::open_devtools,
cmd::exit_app,
cmd::get_network_interfaces_info,
// profile
// Profile management
cmd::get_profiles,
cmd::enhance_profiles,
cmd::patch_profiles_config,
@@ -294,164 +244,333 @@ pub fn run() {
cmd::read_profile_file,
cmd::save_profile_file,
cmd::get_next_update_time,
// script validation
// Script validation
cmd::script_validate_notice,
cmd::validate_script_file,
// clash api
// Clash API
cmd::clash_api_get_proxy_delay,
// backup
// Backup and WebDAV
cmd::create_webdav_backup,
cmd::save_webdav_config,
cmd::list_webdav_backup,
cmd::delete_webdav_backup,
cmd::restore_webdav_backup,
// export diagnostic info for issue reporting
// Diagnostics and system info
cmd::export_diagnostic_info,
// get system info for display
cmd::get_system_info,
// media unlock checker
// Media unlock checker
cmd::get_unlock_items,
cmd::check_media_unlock,
// light-weight model
cmd::entry_lightweight_mode,
]);
]
}
}
#[cfg(debug_assertions)]
pub fn run() {
// Setup singleton check
app_init::init_singleton_check();
// We don't need init_portable_flag here anymore due to the init_config will do the things
// let _ = utils::dirs::init_portable_flag();
// Set Linux environment variable
#[cfg(target_os = "linux")]
{
builder = builder.plugin(devtools);
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.to_uppercase();
let is_kde_desktop = desktop_env.contains("KDE");
let is_plasma_desktop = desktop_env.contains("PLASMA");
if is_kde_desktop || is_plasma_desktop {
std::env::set_var("GTK_CSD", "0");
logging!(
info,
Type::Setup,
true,
"KDE detected: Disabled GTK CSD for better titlebar stability."
);
}
}
// Macos Application Menu
#[cfg(target_os = "macos")]
{
// Temporary Achived due to cannot CMD+C/V/A
}
// Create and configure the Tauri builder
let builder = app_init::setup_plugins(tauri::Builder::default())
.setup(|app| {
logging!(info, Type::Setup, true, "开始应用初始化...");
let app = builder
.build(tauri::generate_context!())
.expect("error while running tauri application");
// Setup autostart plugin
if let Err(e) = app_init::setup_autostart(app) {
logging!(error, Type::Setup, true, "Failed to setup autostart: {}", e);
}
app.run(|app_handle, e| match e {
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
// Setup deep links
if let Err(e) = app_init::setup_deep_links(app) {
logging!(
error,
Type::Setup,
true,
"Failed to setup deep links: {}",
e
);
}
// Setup window state management
if let Err(e) = app_init::setup_window_state(app) {
logging!(
error,
Type::Setup,
true,
"Failed to setup window state: {}",
e
);
}
let app_handle = app.handle().clone();
logging!(info, Type::Setup, true, "执行主要设置操作...");
resolve::resolve_setup_handle(app_handle);
resolve::resolve_setup_async();
resolve::resolve_setup_sync();
logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(())
})
.invoke_handler(app_init::generate_handlers());
/// Event handling helper functions
mod event_handlers {
use crate::core::handle;
use super::*;
/// Handle application ready/resumed events
pub fn handle_ready_resumed(app_handle: &AppHandle) {
logging!(info, Type::System, true, "应用就绪或恢复");
AppHandleManager::global().init(app_handle.clone());
handle::Handle::global().init(app_handle.clone());
#[cfg(target_os = "macos")]
{
if let Some(window) = AppHandleManager::global()
.get_handle()
.get_webview_window("main")
{
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "设置macOS窗口标题");
let _ = window.set_title("Clash Verge");
}
}
}
/// Handle application reopen events (macOS)
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen {
has_visible_windows,
..
} => {
pub async fn handle_reopen(app_handle: &AppHandle, has_visible_windows: bool) {
logging!(
info,
Type::System,
true,
"处理 macOS 应用重新打开事件: has_visible_windows={}",
has_visible_windows
);
handle::Handle::global().init(app_handle.clone());
if !has_visible_windows {
AppHandleManager::global().set_activation_policy_regular();
}
AppHandleManager::global().init(app_handle.clone());
}
tauri::RunEvent::ExitRequested { api, code, .. } => {
if code.is_none() {
api.prevent_exit();
// 当没有可见窗口时,设置为 regular 模式并显示主窗口
handle::Handle::global().set_activation_policy_regular();
logging!(info, Type::System, true, "没有可见窗口,尝试显示主窗口");
let result = WindowManager::show_main_window().await;
logging!(
info,
Type::System,
true,
"窗口显示操作完成,结果: {:?}",
result
);
} else {
logging!(info, Type::System, true, "已有可见窗口,无需额外操作");
}
}
tauri::RunEvent::Exit => {
// avoid duplicate cleanup
/// Handle window close requests
pub fn handle_window_close(api: &tauri::WindowEvent) {
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_accessory();
if core::handle::Handle::global().is_exiting() {
return;
}
feat::clean();
}
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory();
if core::handle::Handle::global().is_exiting() {
return;
}
log::info!(target: "app", "closing window...");
api.prevent_close();
if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide();
} else {
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
}
}
tauri::WindowEvent::Focused(true) => {
#[cfg(target_os = "macos")]
{
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().register("CMD+Q", "quit")
);
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().register("CMD+W", "hide")
);
}
{
let is_enable_global_hotkey = Config::verge()
.latest()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().init())
}
}
}
tauri::WindowEvent::Focused(false) => {
#[cfg(target_os = "macos")]
{
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().unregister("CMD+Q")
);
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().unregister("CMD+W")
);
}
{
let is_enable_global_hotkey = Config::verge()
.latest()
.enable_global_hotkey
.unwrap_or(true);
if !is_enable_global_hotkey {
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().reset())
}
}
}
tauri::WindowEvent::Destroyed => {
#[cfg(target_os = "macos")]
{
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().unregister("CMD+Q")
);
logging_error!(
Type::Hotkey,
true,
hotkey::Hotkey::global().unregister("CMD+W")
);
}
}
_ => {}
log::info!(target: "app", "closing window...");
if let tauri::WindowEvent::CloseRequested { api, .. } = api {
api.prevent_close();
if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide();
} else {
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
}
}
}
_ => {}
/// Handle window focus events
pub fn handle_window_focus(focused: bool) {
AsyncHandler::spawn(move || async move {
let is_enable_global_hotkey = Config::verge()
.await
.latest_ref()
.enable_global_hotkey
.unwrap_or(true);
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 {
if 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")]
{
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);
}
}
});
}
/// Handle window destroyed events
pub fn handle_window_destroyed() {
#[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 on destroy: {}",
e
);
}
if let Err(e) =
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
{
logging!(
error,
Type::Hotkey,
true,
"Failed to unregister CMD+W on destroy: {}",
e
);
}
}
}
}
// Build the application
let app = builder
.build(tauri::generate_context!())
.unwrap_or_else(|e| {
logging!(
error,
Type::Setup,
true,
"Failed to build Tauri application: {}",
e
);
std::process::exit(1);
});
app.run(|app_handle, e| {
match e {
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
event_handlers::handle_ready_resumed(app_handle);
}
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen {
has_visible_windows,
..
} => {
let app_handle = app_handle.clone();
AsyncHandler::spawn(move || async move {
event_handlers::handle_reopen(&app_handle, has_visible_windows).await;
});
}
tauri::RunEvent::ExitRequested { api, code, .. } => {
if code.is_none() {
api.prevent_exit();
}
}
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();
}
_ => {}
}
}
}
_ => {}
}
});
}
+2
View File
@@ -1,4 +1,6 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
#[cfg(feature = "tokio-trace")]
console_subscriber::init();
app_lib::run();
}
+134 -87
View File
@@ -2,95 +2,120 @@ use crate::{
config::Config,
core::{handle, timer::Timer, tray::Tray},
log_err, logging,
state::lightweight::LightWeightState,
utils::logging::Type,
process::AsyncHandler,
state::proxy::ProxyRequestCache,
utils::{logging::Type, window_manager::WindowManager},
};
#[cfg(target_os = "macos")]
use crate::logging_error;
#[cfg(target_os = "macos")]
use crate::AppHandleManager;
use anyhow::{Context, Result};
use delay_timer::prelude::TaskBuilder;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{Listener, Manager};
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
fn with_lightweight_status<F, R>(f: F) -> R
where
F: FnOnce(&mut LightWeightState) -> R,
{
let app_handle = handle::Handle::global().app_handle().unwrap();
let state = app_handle.state::<Mutex<LightWeightState>>();
let mut guard = state.lock().unwrap();
f(&mut guard)
}
// 添加退出轻量模式的锁,防止并发调用
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
pub fn run_once_auto_lightweight() {
LightWeightState::default().run_once_time(|| {
let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true);
let enable_auto = Config::verge()
.data()
.enable_auto_light_weight_mode
.unwrap_or(true);
if enable_auto && is_silent_start {
logging!(
info,
Type::Lightweight,
true,
"正常创建窗口和添加定时器监听器"
);
set_lightweight_mode(false);
disable_auto_light_weight_mode();
static IS_IN_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
// 触发托盘更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {}", e);
}
}
});
}
pub fn auto_lightweight_mode_init() {
if let Some(app_handle) = handle::Handle::global().app_handle() {
let _ = app_handle.state::<Mutex<LightWeightState>>();
let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false);
let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false);
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 inner_set_lightweight_mode(value: bool) -> bool {
if value {
logging!(info, Type::Lightweight, true, "轻量模式已开启");
} else {
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
}
IS_IN_LIGHTWEIGHT.store(value, Ordering::SeqCst);
value
}
fn inner_get_lightweight_mode() -> bool {
IS_IN_LIGHTWEIGHT.load(Ordering::SeqCst) || !WindowManager::is_main_window_exists()
}
pub async fn run_once_auto_lightweight() {
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;
}
logging!(
info,
Type::Lightweight,
true,
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
);
enable_auto_light_weight_mode().await;
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
}
}
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_lightweight_mode(true).await;
enable_auto_light_weight_mode().await;
}
Ok(())
}
// 检查是否处于轻量模式
pub fn is_in_lightweight_mode() -> bool {
with_lightweight_status(|state| state.is_lightweight)
IS_IN_LIGHTWEIGHT.load(Ordering::SeqCst)
}
// 设置轻量模式状态
fn set_lightweight_mode(value: bool) {
with_lightweight_status(|state| {
state.set_lightweight_mode(value);
});
// 触发托盘更新
if let Err(e) = Tray::global().update_part() {
log::warn!("Failed to update tray: {}", e);
pub async fn set_lightweight_mode(value: bool) {
if inner_get_lightweight_mode() != value {
inner_set_lightweight_mode(value);
// 只有在状态可用时才触发托盘更新
if let Err(e) = Tray::global().update_part().await {
log::warn!("Failed to update tray: {e}");
}
}
}
pub fn enable_auto_light_weight_mode() {
Timer::global().init().unwrap();
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}");
return;
}
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
setup_window_close_listener();
setup_webview_focus_listener();
@@ -102,7 +127,7 @@ pub fn disable_auto_light_weight_mode() {
cancel_window_close_listener();
}
pub fn entry_lightweight_mode() {
pub async fn entry_lightweight_mode() {
use crate::utils::window_manager::WindowManager;
let result = WindowManager::hide_main_window();
@@ -119,47 +144,69 @@ pub fn entry_lightweight_mode() {
let _ = webview.destroy();
}
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory();
logging!(info, Type::Lightweight, true, "轻量模式已开启");
handle::Handle::global().set_activation_policy_accessory();
}
set_lightweight_mode(true);
set_lightweight_mode(true).await;
let _ = cancel_light_weight_timer();
ProxyRequestCache::global().clean_default_keys();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
logging_error!(Type::Lightweight, true, Tray::global().update_part().await);
}
// 添加从轻量模式恢复的函数
pub fn exit_lightweight_mode() {
pub async fn exit_lightweight_mode() {
// 使用原子操作检查是否已经在退出过程中,防止并发调用
if EXITING_LIGHTWEIGHT
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
logging!(
info,
Type::Lightweight,
true,
"轻量模式退出操作已在进行中,跳过重复调用"
);
return;
}
// 使用defer确保无论如何都会重置标志
let _guard = scopeguard::guard((), |_| {
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
});
// 确保当前确实处于轻量模式才执行退出操作
if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
return;
}
set_lightweight_mode(false);
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
set_lightweight_mode(false).await;
// macOS激活策略
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_regular();
handle::Handle::global().set_activation_policy_regular();
// 重置UI就绪状态
crate::utils::resolve::reset_ui_ready();
crate::utils::resolve::ui::reset_ui_ready();
// 更新托盘显示
let _tray = crate::core::tray::Tray::global();
}
#[cfg(target_os = "macos")]
pub fn add_light_weight_timer() {
logging_error!(Type::Lightweight, setup_light_weight_timer());
pub async fn add_light_weight_timer() {
logging_error!(Type::Lightweight, setup_light_weight_timer().await);
}
fn setup_window_close_listener() -> u32 {
if let Some(window) = handle::Handle::global().get_window() {
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!(
info,
Type::Lightweight,
@@ -194,19 +241,19 @@ fn cancel_window_close_listener() {
}
}
fn setup_light_weight_timer() -> Result<()> {
Timer::global().init()?;
async fn setup_light_weight_timer() -> Result<()> {
Timer::global().init().await?;
let once_by_minutes = Config::verge()
.latest()
.await
.latest_ref()
.auto_light_weight_minutes
.unwrap_or(10);
// 获取task_id
let task_id = {
let mut timer_count = Timer::global().timer_count.lock();
let id = *timer_count;
*timer_count += 1;
id
Timer::global()
.timer_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
};
// 创建任务
@@ -216,7 +263,7 @@ fn setup_light_weight_timer() -> Result<()> {
.set_frequency_once_by_minutes(once_by_minutes)
.spawn_async_routine(move || async move {
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
entry_lightweight_mode();
entry_lightweight_mode().await;
})
.context("failed to create timer task")?;
-136
View File
@@ -1,136 +0,0 @@
use crate::config::Config;
use mihomo_api;
use once_cell::sync::Lazy;
use parking_lot::{Mutex, RwLock};
use std::time::{Duration, Instant};
use tauri::http::HeaderMap;
#[cfg(target_os = "macos")]
use tauri::http::HeaderValue;
// 缓存的最大有效期(5秒)
const CACHE_TTL: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Rate {
pub up: u64,
pub down: u64,
}
// 缓存MihomoManager实例
struct MihomoCache {
manager: mihomo_api::MihomoManager,
created_at: Instant,
server: String,
}
// 使用RwLock替代Mutex,允许多个读取操作并发进行
pub struct MihomoManager {
mihomo_cache: RwLock<Option<MihomoCache>>,
create_lock: Mutex<()>,
}
impl MihomoManager {
fn __global() -> &'static MihomoManager {
static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager {
mihomo_cache: RwLock::new(None),
create_lock: Mutex::new(()),
});
&INSTANCE
}
pub fn global() -> mihomo_api::MihomoManager {
let instance = MihomoManager::__global();
// 尝试从缓存读取(只需读锁)
{
let cache = instance.mihomo_cache.read();
if let Some(cache_entry) = &*cache {
let (current_server, _) = MihomoManager::get_clash_client_info()
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
// 检查缓存是否有效
if cache_entry.server == current_server
&& cache_entry.created_at.elapsed() < CACHE_TTL
{
return cache_entry.manager.clone();
}
}
}
// 缓存无效,获取创建锁
let _create_guard = instance.create_lock.lock();
// 再次检查缓存(双重检查锁定模式)
{
let cache = instance.mihomo_cache.read();
if let Some(cache_entry) = &*cache {
let (current_server, _) = MihomoManager::get_clash_client_info()
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
if cache_entry.server == current_server
&& cache_entry.created_at.elapsed() < CACHE_TTL
{
return cache_entry.manager.clone();
}
}
}
// 创建新实例
let (current_server, headers) = MihomoManager::get_clash_client_info()
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
let manager = mihomo_api::MihomoManager::new(current_server.clone(), headers);
// 更新缓存
{
let mut cache = instance.mihomo_cache.write();
*cache = Some(MihomoCache {
manager: manager.clone(),
created_at: Instant::now(),
server: current_server,
});
}
manager
}
}
impl MihomoManager {
pub fn get_clash_client_info() -> Option<(String, HeaderMap)> {
let client = { Config::clash().data().get_client_info() };
let server = format!("http://{}", client.server);
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
if let Some(secret) = client.secret {
let secret = format!("Bearer {}", secret).parse().unwrap();
headers.insert("Authorization", secret);
}
Some((server, headers))
}
// 提供默认值的版本,避免在connection_info为None时panic
#[cfg(target_os = "macos")]
fn get_clash_client_info_or_default() -> (String, HeaderMap) {
Self::get_clash_client_info().unwrap_or_else(|| {
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
("http://127.0.0.1:9090".to_string(), headers)
})
}
#[cfg(target_os = "macos")]
pub fn get_traffic_ws_url() -> (String, HeaderValue) {
let (url, headers) = MihomoManager::get_clash_client_info_or_default();
let ws_url = url.replace("http://", "ws://") + "/traffic";
let auth = headers
.get("Authorization")
.map(|val| val.to_str().unwrap_or("").to_string())
.unwrap_or_default();
// 创建默认的空HeaderValue而不是使用unwrap_or_default
let token = match HeaderValue::from_str(&auth) {
Ok(v) => v,
Err(_) => HeaderValue::from_static(""),
};
(ws_url, token)
}
}
-1
View File
@@ -1,3 +1,2 @@
pub mod lightweight;
pub mod mihomo;
pub mod sysinfo;
+13 -3
View File
@@ -32,7 +32,17 @@ impl PlatformSpecification {
let system_kernel_version = System::kernel_version().unwrap_or("Null".into());
let system_arch = System::cpu_arch();
let handler = handle::Handle::global().app_handle().unwrap();
let Some(handler) = handle::Handle::global().app_handle() else {
return Self {
system_name,
system_version,
system_kernel_version,
system_arch,
verge_version: "unknown".into(),
running_mode: "NotRunning".to_string(),
is_admin: false,
};
};
let verge_version = handler.package_info().version.to_string();
// 使用默认值避免在同步上下文中执行异步操作
@@ -52,10 +62,10 @@ impl PlatformSpecification {
}
// 异步方法来获取完整的系统信息
pub async fn new_async() -> Self {
pub fn new_sync() -> Self {
let mut info = Self::new();
let running_mode = CoreManager::global().get_running_mode().await;
let running_mode = CoreManager::global().get_running_mode();
info.running_mode = running_mode.to_string();
info

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