From 21878ea93616410981fb2f7df16ea9146ee89630 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Fri, 22 May 2026 12:30:48 +0800 Subject: [PATCH] Refactor timer management and remove delay_timer dependency (#7075) * Refactor timer management and remove delay_timer dependency The delay_timer spawned tokio threads of cores, which not controled by expected. * refactor: improve logging messages in Timer implementation * fix(timer): prevent potential panic by using saturating_mul for interval calculations * refactor timer scheduler * chore: translate timer logs to english * fix timer clippy warnings * fix profile auto update refresh * fix duplicate profile update notification * fix: prevent potential panic in lightweight timer setup * fix profile update listener cleanup * Revert "fix duplicate profile update notification" This reverts commit 4aafa94d3e9292bea997bbf1250c14c1bf033b57. --- Cargo.lock | 350 ++------------ src-tauri/Cargo.toml | 2 +- src-tauri/src/cmd/profile.rs | 4 +- src-tauri/src/core/timer.rs | 602 +++++++++++------------- src-tauri/src/module/lightweight.rs | 121 ++--- src/components/profile/profile-item.tsx | 85 ++-- 6 files changed, 403 insertions(+), 761 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5d39c75d..4617a7731 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,23 +186,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener 5.3.0", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - [[package]] name = "async-channel" version = "2.5.0" @@ -223,44 +212,12 @@ checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.4.1", - "futures-lite 2.6.1", + "fastrand", + "futures-lite", "pin-project-lite", "slab", ] -[[package]] -name = "async-fs" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "blocking", - "futures-lite 1.13.0", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.28", - "slab", - "socket2 0.4.10", - "waker-fn", -] - [[package]] name = "async-io" version = "2.6.0" @@ -271,77 +228,40 @@ dependencies = [ "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.6.1", + "futures-lite", "parking", - "polling 3.11.0", + "polling", "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.3.0", + "event-listener", "event-listener-strategy", "pin-project-lite", ] -[[package]] -name = "async-net" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0434b1ed18ce1cf5769b8ac540e33f01fa9471058b5e89da9e06f3c882a8c12f" -dependencies = [ - "async-io 1.13.0", - "blocking", - "futures-lite 1.13.0", -] - -[[package]] -name = "async-process" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" -dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", - "async-signal", - "blocking", - "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.44", - "windows-sys 0.48.0", -] - [[package]] name = "async-process" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel 2.5.0", - "async-io 2.6.0", - "async-lock 3.4.2", + "async-channel", + "async-io", + "async-lock", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener 5.3.0", - "futures-lite 2.6.1", + "event-listener", + "futures-lite", "rustix 1.1.4", ] @@ -362,8 +282,8 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ - "async-io 2.6.0", - "async-lock 3.4.2", + "async-io", + "async-lock", "atomic-waker", "cfg-if", "futures-core", @@ -575,7 +495,7 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ - "fastrand 2.4.1", + "fastrand", "gloo-timers", "tokio", ] @@ -683,10 +603,10 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.5.0", + "async-channel", "async-task", "futures-io", - "futures-lite 2.6.1", + "futures-lite", "piper", ] @@ -723,13 +643,13 @@ dependencies = [ "bytemuck", "cfg-if", "cow-utils", - "dashmap 6.1.0", + "dashmap", "dynify", "fast-float2", "float16", "futures-channel", "futures-concurrency", - "futures-lite 2.6.1", + "futures-lite", "hashbrown 0.16.1", "icu_normalizer", "indexmap 2.14.0", @@ -1174,7 +1094,6 @@ dependencies = [ "criterion", "dark-light", "deelevate", - "delay_timer", "dunce", "flexi_logger", "futures", @@ -1222,6 +1141,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-stream", + "tokio-util", "warp", "webpki-roots", "windows 0.62.2", @@ -1390,16 +1310,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "concat-idents" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d" -dependencies = [ - "quote", - "syn 2.0.117", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1654,17 +1564,6 @@ dependencies = [ "itertools 0.13.0", ] -[[package]] -name = "cron_clock" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8699d8ed16e3db689f8ae04d8dc3c6666a4ba7e724e5a157884b7cc385d16b" -dependencies = [ - "chrono", - "nom 7.1.3", - "once_cell", -] - [[package]] name = "crossbeam" version = "0.8.4" @@ -1888,19 +1787,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -1958,31 +1844,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" -[[package]] -name = "delay_timer" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20eba879b7ee5f1d255d5666e16646fe384f899f71a4c0b4b0644e2e074964fe" -dependencies = [ - "anyhow", - "async-trait", - "autocfg", - "concat-idents", - "cron_clock", - "dashmap 5.5.3", - "event-listener 5.3.0", - "futures", - "log", - "lru", - "once_cell", - "rs-snowflake", - "rustc_version 0.2.3", - "smol", - "thiserror 1.0.69", - "tokio", - "tracing", -] - [[package]] name = "deranged" version = "0.5.8" @@ -2470,23 +2331,6 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "event-listener" version = "5.3.0" @@ -2504,7 +2348,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.3.0", + "event-listener", "pin-project-lite", ] @@ -2514,15 +2358,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.4.1" @@ -2725,7 +2560,7 @@ checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" dependencies = [ "fixedbitset", "futures-core", - "futures-lite 2.6.1", + "futures-lite", "pin-project", "smallvec", ] @@ -2753,28 +2588,13 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - [[package]] name = "futures-lite" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ - "fastrand 2.4.1", + "fastrand", "futures-core", "futures-io", "parking", @@ -3282,8 +3102,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -3353,12 +3171,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hermit-abi" version = "0.5.2" @@ -3853,15 +3665,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "interprocess" version = "2.4.2" @@ -3886,17 +3689,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -3941,7 +3733,7 @@ version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi", "libc", "windows-sys 0.61.2", ] @@ -4292,12 +4084,6 @@ dependencies = [ "redox_syscall 0.7.4", ] -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -4349,15 +4135,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -4721,7 +4498,7 @@ version = "4.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bdaf6120b9df005d37e58f6b75329be6255450453fbeba9ce4192324f921fb9" dependencies = [ - "futures-lite 2.6.1", + "futures-lite", "log", "mac-notification-sys", "serde", @@ -5421,7 +5198,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ - "fastrand 2.4.1", + "fastrand", "phf_shared 0.13.1", ] @@ -5534,7 +5311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", - "fastrand 2.4.1", + "fastrand", "futures-io", ] @@ -5617,22 +5394,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - [[package]] name = "polling" version = "3.11.0" @@ -5641,7 +5402,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.5.2", + "hermit-abi", "pin-project-lite", "rustix 1.1.4", "windows-sys 0.61.2", @@ -6463,12 +6224,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "rs-snowflake" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" - [[package]] name = "runas" version = "1.2.0" @@ -6578,20 +6333,6 @@ dependencies = [ "semver 1.0.28", ] -[[package]] -name = "rustix" -version = "0.37.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "0.38.44" @@ -7304,23 +7045,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "smol" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" -dependencies = [ - "async-channel 1.9.0", - "async-executor", - "async-fs", - "async-io 1.13.0", - "async-lock 2.8.0", - "async-net", - "async-process 1.8.1", - "blocking", - "futures-lite 1.13.0", -] - [[package]] name = "socket2" version = "0.4.10" @@ -8245,7 +7969,7 @@ version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "fastrand 2.4.1", + "fastrand", "getrandom 0.4.2", "once_cell", "rustix 1.1.4", @@ -8504,7 +8228,6 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.6.3", @@ -8577,6 +8300,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project-lite", + "slab", "tokio", ] @@ -9298,12 +9022,6 @@ dependencies = [ "utf8parse", ] -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" @@ -10598,17 +10316,17 @@ checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" dependencies = [ "async-broadcast", "async-executor", - "async-io 2.6.0", - "async-lock 3.4.2", - "async-process 2.5.0", + "async-io", + "async-lock", + "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", - "event-listener 5.3.0", + "event-listener", "futures-core", - "futures-lite 2.6.1", + "futures-lite", "hex", "libc", "ordered-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fe783ace4..f52e1469b 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -62,7 +62,6 @@ nanoid = "0.5" chrono = "0.4.44" boa_engine = "0.21.1" once_cell = { version = "1.21.4", features = ["parking_lot"] } -delay_timer = "0.11.6" percent-encoding = "2.3.2" reqwest = { version = "0.13.3", features = [ "json", @@ -109,6 +108,7 @@ rust_iso3166 = "0.1.14" # Use the git repo until the next release after v2.0.0. dark-light = { git = "https://github.com/rust-dark-light/dark-light" } bytes = "1.11.1" +tokio-util = { version = "0.7.18", features = ["time"] } [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.4" diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 5d7de63bb..00ac2b679 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -349,9 +349,9 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { // 如果更新间隔或允许自动更新变更,异步刷新定时器 if should_refresh_timer { crate::process::AsyncHandler::spawn(move || async move { - logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器..."); + logging!(info, Type::Timer, "Timer update settings changed, refreshing timer..."); if let Err(e) = crate::core::Timer::global().refresh().await { - logging!(error, Type::Timer, "刷新定时器失败: {}", e); + logging!(error, Type::Timer, "Failed to refresh timer: {}", e); } else { // 刷新成功后发送自定义事件,不触发配置重载 crate::core::handle::Handle::notify_timer_updated(&index); diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index 243dedf5e..173eddd61 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -1,60 +1,66 @@ -use crate::{config::Config, feat, singleton, utils::resolve::is_resolve_done}; -use anyhow::{Context as _, Result}; +use crate::{config::Config, core::handle, feat, process::AsyncHandler, singleton, utils::resolve::is_resolve_done}; +use anyhow::Result; use clash_verge_logging::{Type, logging, logging_error}; -use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use smartstring::alias::String; use std::{ collections::HashMap, - pin::Pin, sync::{ Arc, - atomic::{AtomicBool, AtomicU64, Ordering}, + atomic::{AtomicBool, Ordering}, }, time::Duration, }; -use tokio::time::{sleep, timeout}; +use tokio::{ + sync::mpsc, + time::{sleep, timeout}, +}; +use tokio_stream::StreamExt as _; +use tokio_util::time::{DelayQueue, delay_queue::Key}; -type TaskID = u64; +enum TimerCommand { + Apply(HashMap), + RunNow(String), + TaskFinished(String), +} -#[derive(Debug, Clone)] -pub struct TimerTask { - pub task_id: TaskID, - pub interval_minutes: u64, - #[allow(unused)] - pub last_run: i64, // Timestamp of last execution +struct TaskState { + key: Option, + interval_minutes: u64, + running: bool, +} + +impl TaskState { + const fn new(key: Key, interval_minutes: u64) -> Self { + Self { + key: Some(key), + interval_minutes, + running: false, + } + } } pub struct Timer { - /// cron manager - pub delay_timer: Arc>, - - /// save the current state - using RwLock for better read concurrency - pub timer_map: Arc>>, - - /// increment id - atomic counter for better performance - pub timer_count: AtomicU64, - - /// Flag to mark if timer is initialized - atomic for better performance + command_tx: mpsc::UnboundedSender, + command_rx: Mutex>>, + pub timer_map: Arc>>, pub initialized: AtomicBool, } -// Use singleton macro singleton!(Timer, TIMER_INSTANCE); impl Timer { fn new() -> Self { + let (command_tx, command_rx) = mpsc::unbounded_channel(); Self { - delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())), + command_tx, + command_rx: Mutex::new(Some(command_rx)), timer_map: Arc::new(RwLock::new(HashMap::new())), - timer_count: AtomicU64::new(1), initialized: AtomicBool::new(false), } } - /// Initialize timer with better error handling and atomic operations pub async fn init(&self) -> Result<()> { - // Use compare_exchange for thread-safe initialization check if self .initialized .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) @@ -64,76 +70,48 @@ impl Timer { return Ok(()); } - // Initialize timer tasks + let command_rx = { self.command_rx.lock().take() }; + if let Some(command_rx) = command_rx { + let command_tx = self.command_tx.clone(); + AsyncHandler::spawn(move || async move { + Self::run_scheduler(command_rx, command_tx).await; + }); + } + if let Err(e) = self.refresh().await { - // Reset initialization flag on error self.initialized.store(false, Ordering::SeqCst); logging_error!(Type::Timer, "Failed to initialize timer: {}", e); return Err(e); } - // Log timer info first { let timer_map = self.timer_map.read(); - logging!(info, Type::Timer, "已注册的定时任务数量: {}", timer_map.len()); - - for (uid, task) in timer_map.iter() { + logging!(debug, Type::Timer, "Registered timer task count: {}", timer_map.len()); + for (uid, interval) in timer_map.iter() { logging!( - info, + debug, Type::Timer, - "注册了定时任务 - uid={}, interval={}min, task_id={}", + "Registered timer task: uid={}, interval={}min", uid, - task.interval_minutes, - task.task_id + interval ); } } let cur_timestamp = chrono::Local::now().timestamp(); - - // Collect profiles that need immediate update - let profiles_to_update = if let Some(items) = Config::profiles().await.latest_arc().get_items() { - items - .iter() - .filter_map(|item| { - let allow_auto_update = item.option.as_ref()?.allow_auto_update.unwrap_or_default(); - if !allow_auto_update { - return None; - } - - 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::>() - } else { - Vec::new() - }; - - // Advance tasks outside of locks to minimize lock contention - if !profiles_to_update.is_empty() { - logging!( - info, - Type::Timer, - "需要立即更新的配置数量: {}", - profiles_to_update.len() - ); - let timer_map = self.timer_map.read(); - - for uid in profiles_to_update { - if let Some(task) = timer_map.get(&uid) { - logging!(info, Type::Timer, "立即执行任务: uid={}", uid); - let delay_timer = self.delay_timer.write(); - if let Err(e) = delay_timer.advance_task(task.task_id) { - logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e); - } + if let Some(items) = Config::profiles().await.latest_arc().get_items() { + for item in items.iter() { + if let Some(option) = item.option.as_ref() + && let Some(allow_auto_update) = option.allow_auto_update + && allow_auto_update + && let Some(interval) = option.update_interval + && interval > 0 + && let Some(uid) = item.uid.as_ref() + && let Some(updated) = item.updated + && cur_timestamp - (updated as i64) >= (interval as i64) * 60 + { + logging!(info, Type::Timer, "Running overdue timer task immediately: uid={}", uid); + let _ = self.command_tx.send(TimerCommand::RunNow(uid.clone())); } } } @@ -142,96 +120,29 @@ impl Timer { Ok(()) } - /// Refresh timer tasks with better error handling pub async fn refresh(&self) -> Result<()> { - // Generate diff outside of lock to minimize lock contention - let diff_map = self.gen_diff().await; + let new_map = self.gen_map().await; - if diff_map.is_empty() { + let mut cache = self.timer_map.write(); + if *cache == new_map { logging!(debug, Type::Timer, "No timer changes needed"); return Ok(()); } - logging!(info, Type::Timer, "Refreshing {} timer tasks", diff_map.len()); + logging!( + info, + Type::Timer, + "Refreshing timer tasks map, count: {}", + new_map.len() + ); + *cache = new_map.clone(); + drop(cache); - // 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 = Vec::new(); - - // Perform sync operations while holding locks - { - for (uid, diff) in diff_map { - match diff { - DiffFlag::Del(tid) => { - self.timer_map.write().remove(&uid); - let value = self.delay_timer.write().remove_task(tid); - if let Err(e) = value { - 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(), - }; - - self.timer_map.write().insert(uid.clone(), task); - operations_to_add.push((uid, tid, interval)); - } - DiffFlag::Mod(tid, interval) => { - // Remove old task first - let value = self.delay_timer.write().remove_task(tid); - if let Err(e) = value { - 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(), - }; - - self.timer_map.write().insert(uid.clone(), task); - operations_to_add.push((uid, tid, interval)); - } - } - } - } // Locks are dropped here - - // Now perform async operations without holding locks - let delay_timer = self.delay_timer.write(); - for (uid, tid, interval) in operations_to_add { - if let Err(e) = self.add_task(&delay_timer, uid.clone(), tid, interval) { - logging_error!(Type::Timer, "Failed to add task for uid {}: {}", uid, e); - // Rollback on failure - remove from timer_map - self.timer_map.write().remove(&uid); - } else { - logging!(debug, Type::Timer, "Added task {} for uid {}", tid, uid); - } - } + let _ = self.command_tx.send(TimerCommand::Apply(new_map)); Ok(()) } - /// Generate map of profile UIDs to update intervals async fn gen_map(&self) -> HashMap { let mut new_map = HashMap::new(); @@ -243,218 +154,254 @@ impl Timer { && allow_auto_update && interval > 0 { - logging!( - debug, - Type::Timer, - "找到定时更新配置: uid={}, interval={}min", - uid, - interval - ); new_map.insert(uid.clone(), interval); } } } - - logging!(debug, Type::Timer, "生成的定时更新配置数量: {}", new_map.len()); new_map } - /// Generate differences between current and new timer configuration - async fn gen_diff(&self) -> HashMap { - let mut diff_map = HashMap::new(); - let new_map = self.gen_map().await; + async fn run_scheduler( + mut command_rx: mpsc::UnboundedReceiver, + command_tx: mpsc::UnboundedSender, + ) { + let mut queue = DelayQueue::new(); + let mut tasks = HashMap::new(); - // Read lock for comparing current state - let timer_map = self.timer_map.read(); - logging!(debug, Type::Timer, "当前 timer_map 大小: {}", timer_map.len()); + loop { + tokio::select! { + Some(expired) = queue.next() => { + let uid = expired.into_inner(); + Self::run_expired_task(&mut tasks, uid, command_tx.clone()); + } - // Find tasks to modify or delete - for (uid, task) in timer_map.iter() { - match new_map.get(uid) { - Some(&interval) if interval != task.interval_minutes => { - // Task exists but interval changed - logging!( - debug, - Type::Timer, - "定时任务间隔变更: uid={}, 旧={}, 新={}", - uid, - task.interval_minutes, - interval - ); - diff_map.insert(uid.clone(), DiffFlag::Mod(task.task_id, interval)); - } - None => { - // Task no longer needed - logging!(debug, Type::Timer, "定时任务已删除: uid={}", uid); - diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id)); - } - _ => { - // Task exists with same interval, no change needed - logging!(debug, Type::Timer, "定时任务保持不变: uid={}", uid); + command = command_rx.recv() => { + match command { + Some(TimerCommand::Apply(new_map)) => { + Self::apply_timer_map(&mut queue, &mut tasks, new_map); + } + Some(TimerCommand::RunNow(uid)) => { + Self::run_task_now(&mut queue, &mut tasks, uid, command_tx.clone()); + } + Some(TimerCommand::TaskFinished(uid)) => { + Self::finish_task(&mut queue, &mut tasks, uid); + } + None => break, + } } } } - - // Find new tasks to add - 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) { - logging!( - debug, - Type::Timer, - "新增定时任务: uid={}, interval={}min", - uid, - interval - ); - diff_map.insert(uid.clone(), DiffFlag::Add(next_id, interval)); - next_id += 1; - } - } - - // Update counter only if we added new tasks - if next_id > original_id { - self.timer_count.store(next_id, Ordering::Relaxed); - } - - logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len()); - diff_map } - /// Add a timer task with better error handling - fn add_task(&self, delay_timer: &DelayTimer, uid: String, tid: TaskID, minutes: u64) -> Result<()> { + fn apply_timer_map( + queue: &mut DelayQueue, + tasks: &mut HashMap, + new_map: HashMap, + ) { + tasks.retain(|uid, state| { + if new_map.contains_key(uid) { + return true; + } + + if let Some(key) = state.key.take() { + queue.remove(&key); + } + logging!(debug, Type::Timer, "Removed timer task for uid={}", uid); + false + }); + + for (uid, interval_minutes) in new_map { + let Some(state) = tasks.get_mut(&uid) else { + Self::insert_task(queue, tasks, uid, interval_minutes); + continue; + }; + + if state.interval_minutes == interval_minutes { + continue; + } + + Self::update_task_interval(queue, &uid, state, interval_minutes); + } + } + + fn insert_task( + queue: &mut DelayQueue, + tasks: &mut HashMap, + uid: String, + interval_minutes: u64, + ) { + let key = Self::schedule_task(queue, &uid, interval_minutes); logging!( - info, + debug, Type::Timer, - "Adding task: uid={}, id={}, interval={}min", + "Added timer task: uid={}, interval={}min", uid, - tid, - minutes + interval_minutes ); + tasks.insert(uid, TaskState::new(key, interval_minutes)); + } - // Create a task with reasonable retries and backoff - let task = TaskBuilder::default() - .set_task_id(tid) - .set_maximum_parallel_runnable_num(1) - .set_frequency_repeated_by_minutes(minutes) - .spawn_async_routine(move || { - let uid = uid.clone(); - Box::pin(async move { - Self::wait_until_resolve_done(Duration::from_millis(5000)).await; - Self::async_task(&uid).await; - }) as Pin + Send>> - }) - .context("failed to create timer task")?; - - delay_timer.add_task(task).context("failed to add timer task")?; - - Ok(()) + fn update_task_interval(queue: &mut DelayQueue, uid: &str, state: &mut TaskState, interval_minutes: u64) { + state.interval_minutes = interval_minutes; + + if let Some(key) = state.key.as_ref() { + queue.reset(key, Self::interval_duration(interval_minutes)); + } else if !state.running { + state.key = Some(Self::schedule_task(queue, uid, interval_minutes)); + } + + logging!( + debug, + Type::Timer, + "Updated timer task interval: uid={}, interval={}min", + uid, + interval_minutes + ); + } + + fn run_expired_task( + tasks: &mut HashMap, + uid: String, + command_tx: mpsc::UnboundedSender, + ) { + let Some(state) = tasks.get_mut(&uid) else { + return; + }; + + state.key = None; + if !Self::mark_task_running(state, &uid, false) { + return; + } + + Self::spawn_update_task(uid, command_tx); + } + + fn run_task_now( + queue: &mut DelayQueue, + tasks: &mut HashMap, + uid: String, + command_tx: mpsc::UnboundedSender, + ) { + let Some(state) = tasks.get_mut(&uid) else { + return; + }; + + if !Self::mark_task_running(state, &uid, true) { + return; + } + + if let Some(key) = state.key.take() { + queue.remove(&key); + } + Self::spawn_update_task(uid, command_tx); + } + + fn mark_task_running(state: &mut TaskState, uid: &str, immediate: bool) -> bool { + if !state.running { + state.running = true; + return true; + } + + if immediate { + logging!( + debug, + Type::Timer, + "Timer task already running, skip immediate uid={}", + uid + ); + } else { + logging!(debug, Type::Timer, "Timer task already running, skip uid={}", uid); + } + false + } + + fn finish_task(queue: &mut DelayQueue, tasks: &mut HashMap, uid: String) { + let Some(state) = tasks.get_mut(&uid) else { + return; + }; + + state.running = false; + let key = Self::schedule_task(queue, &uid, state.interval_minutes); + state.key = Some(key); + } + + fn spawn_update_task(uid: String, command_tx: mpsc::UnboundedSender) { + logging!(info, Type::Timer, "Starting timer task: uid={}", uid); + AsyncHandler::spawn(move || async move { + Self::wait_until_resolve_done(Duration::from_millis(5000)).await; + Self::async_task(&uid).await; + let _ = command_tx.send(TimerCommand::TaskFinished(uid)); + }); + } + + const fn interval_duration(interval_minutes: u64) -> Duration { + Duration::from_secs(interval_minutes.saturating_mul(60)) + } + + fn schedule_task(queue: &mut DelayQueue, uid: &str, interval_minutes: u64) -> Key { + queue.insert(String::from(uid), Self::interval_duration(interval_minutes)) } - /// Get next update time for a profile pub async fn get_next_update_time(&self, uid: &str) -> Option { - logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid); + logging!(debug, Type::Timer, "Getting next update time, uid={}", uid); - // 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 - now safe to await - let items = { - let profiles = Config::profiles().await; - let profiles_guard = profiles.latest_arc(); - match profiles_guard.get_items() { - Some(i) => i.clone(), - None => { - logging!(warn, Type::Timer, "获取配置列表失败"); - return None; - } - } - }; - - let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) { - Some(p) => p, - None => { - logging!(warn, Type::Timer, "找不到对应的配置,uid={}", uid); - return None; - } - }; + let task_interval = *self.timer_map.read().get(uid)?; + let profiles = Config::profiles().await; + let profiles_guard = profiles.latest_arc(); + let items = profiles_guard.get_items()?; + let profile = items.iter().find(|item| item.uid.as_deref() == Some(uid))?; let updated = profile.updated.unwrap_or(0) as i64; - // Calculate next update time - if updated > 0 && task_interval > 0 { - let next_time = updated + (task_interval as i64 * 60); - logging!(info, Type::Timer, "计算得到下次更新时间: {}, uid={}", next_time, uid); - Some(next_time) + if updated > 0 { + Some(updated + (task_interval as i64 * 60)) } else { - logging!( - warn, - Type::Timer, - "更新时间或间隔无效,updated={}, interval={}", - updated, - task_interval - ); None } } - /// Emit update events for frontend notification fn emit_update_event(uid: &String, is_start: bool) { - { - if is_start { - super::handle::Handle::notify_profile_update_started(uid); - } else { - super::handle::Handle::notify_profile_update_completed(uid); - } + if is_start { + super::handle::Handle::notify_profile_update_started(uid); + } else { + super::handle::Handle::notify_profile_update_completed(uid); } } - /// Async task with better error handling and logging async fn async_task(uid: &String) { let task_start = std::time::Instant::now(); - logging!(info, Type::Timer, "Running timer task for profile: {}", uid); + logging!(debug, Type::Timer, "Running timer task for profile: {}", uid); match tokio::time::timeout(std::time::Duration::from_secs(40), async { Self::emit_update_event(uid, true); let is_current = Config::profiles().await.latest_arc().current.as_ref() == Some(uid); - logging!(info, Type::Timer, "配置 {} 是否为当前激活配置: {}", uid, is_current); + logging!( + debug, + Type::Timer, + "Profile {} is current active profile: {}", + uid, + is_current + ); feat::update_profile(uid, None, is_current, false, false).await }) .await { - Ok(result) => match result { - Ok(_) => { - let duration = task_start.elapsed().as_millis(); - logging!( - info, - Type::Timer, - "Timer task completed successfully for uid: {} (took {}ms)", - uid, - duration - ); - } - Err(e) => { - logging_error!(Type::Timer, "Failed to update profile uid {}: {}", uid, e); - } - }, - Err(_) => { - logging_error!(Type::Timer, "Timer task timed out for uid: {}", uid); + Ok(Ok(_)) => { + logging!( + info, + Type::Timer, + "Timer task completed for uid: {} (took {}ms)", + uid, + task_start.elapsed().as_millis() + ); + handle::Handle::notify_profile_update_completed(uid); } + Ok(Err(e)) => logging_error!(Type::Timer, "Failed to update profile uid {}: {}", uid, e), + Err(_) => logging_error!(Type::Timer, "Timer task timed out for uid: {}", uid), } - // Emit completed event Self::emit_update_event(uid, false); } @@ -468,10 +415,3 @@ impl Timer { .await; } } - -#[derive(Debug)] -enum DiffFlag { - Del(TaskID), - Add(TaskID, u64), - Mod(TaskID, u64), -} diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index 29d576aa4..9b4cb0041 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -4,15 +4,15 @@ use crate::{ process::AsyncHandler, }; -use clash_verge_logging::{Type, logging, logging_error}; +use clash_verge_logging::{Type, logging}; use crate::utils::window_manager::WindowManager; -use anyhow::{Context as _, Result}; -use delay_timer::prelude::TaskBuilder; +use anyhow::Result; +use parking_lot::Mutex; use std::sync::atomic::{AtomicU8, AtomicU32, Ordering}; use tauri::Listener as _; - -const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; +use tokio::sync::oneshot; +use tokio::time::{Duration, sleep}; #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -43,6 +43,8 @@ static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0); static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0); +static CANCEL_TX: Mutex>> = Mutex::new(None); + #[inline] fn get_state() -> LightweightState { LIGHTWEIGHT_STATE.load(Ordering::Acquire).into() @@ -101,7 +103,7 @@ pub async fn enable_auto_light_weight_mode() { pub fn disable_auto_light_weight_mode() { logging!(info, Type::Lightweight, "关闭自动轻量模式"); - let _ = cancel_light_weight_timer(); + cancel_light_weight_timer(); cancel_window_close_listener(); cancel_webview_focus_listener(); } @@ -114,7 +116,7 @@ pub async fn entry_lightweight_mode() -> bool { } record_state_and_log(LightweightState::In); WindowManager::destroy_main_window(); - let _ = cancel_light_weight_timer(); + cancel_light_weight_timer(); refresh_lightweight_tray_state().await; true } @@ -140,7 +142,7 @@ pub async fn exit_lightweight_mode() -> bool { setup_window_close_listener(); setup_webview_focus_listener(); } - let _ = cancel_light_weight_timer(); + cancel_light_weight_timer(); record_state_and_log(LightweightState::Normal); refresh_lightweight_tray_state().await; true @@ -148,7 +150,7 @@ pub async fn exit_lightweight_mode() -> bool { #[cfg(target_os = "macos")] pub async fn add_light_weight_timer() { - logging_error!(Type::Lightweight, setup_light_weight_timer().await); + setup_light_weight_timer().await; } fn setup_window_close_listener() { @@ -160,13 +162,7 @@ fn setup_window_close_listener() { } let handler_id = window.listen("tauri://close-requested", move |_event| { std::mem::drop(AsyncHandler::spawn(|| async { - if let Err(e) = setup_light_weight_timer().await { - logging!( - warn, - Type::Lightweight, - "Warning: Failed to setup light weight timer: {e}" - ); - } + setup_light_weight_timer().await; })); logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时"); }); @@ -192,7 +188,7 @@ fn setup_webview_focus_listener() { logging!(debug, Type::Lightweight, "覆盖旧的窗口焦点监听"); } let handler_id = window.listen("tauri://focus", move |_event| { - logging_error!(Type::Lightweight, cancel_light_weight_timer()); + cancel_light_weight_timer(); logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时"); }); WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release); @@ -209,72 +205,51 @@ fn cancel_webview_focus_listener() { } } -async fn setup_light_weight_timer() -> Result<()> { - if let Err(e) = Timer::global().init().await { - return Err(e).context("failed to initialize timer"); - } - +async fn setup_light_weight_timer() { let once_by_minutes = Config::verge().await.data_arc().auto_light_weight_minutes.unwrap_or(10); - { - let timer_map = Timer::global().timer_map.read(); - if timer_map.contains_key(LIGHT_WEIGHT_TASK_UID) { - logging!(debug, Type::Timer, "轻量模式计时器已存在,跳过创建"); - return Ok(()); + let mut cancel_tx_guard = CANCEL_TX.lock(); + if cancel_tx_guard.is_some() { + logging!( + debug, + Type::Timer, + "Lightweight mode timer already exists, skipping setup" + ); + return; + } + + let (tx, rx) = oneshot::channel::<()>(); + *cancel_tx_guard = Some(tx); + drop(cancel_tx_guard); + + AsyncHandler::spawn(move || async move { + tokio::select! { + _ = sleep(Duration::from_secs(once_by_minutes.saturating_mul(60))) => { + logging!(info, Type::Timer, "Lightweight mode timer expired, entering lightweight mode"); + { + let mut guard = CANCEL_TX.lock(); + *guard = None; + } + entry_lightweight_mode().await; + } + _ = rx => { + logging!(debug, Type::Timer, "Received cancel signal, stopping lightweight mode timer"); + } } - } - - let task_id = { - Timer::global() - .timer_count - .fetch_add(1, std::sync::atomic::Ordering::Relaxed) - }; - - let task = TaskBuilder::default() - .set_task_id(task_id) - .set_maximum_parallel_runnable_num(1) - .set_frequency_once_by_minutes(once_by_minutes) - .spawn_async_routine(move || async move { - logging!(info, Type::Timer, "计时器到期,开始进入轻量模式"); - entry_lightweight_mode().await; - }) - .context("failed to create timer task")?; - - { - let delay_timer = Timer::global().delay_timer.write(); - delay_timer.add_task(task).context("failed to add timer task")?; - } - - { - let mut timer_map = Timer::global().timer_map.write(); - let timer_task = crate::core::timer::TimerTask { - task_id, - interval_minutes: once_by_minutes, - last_run: chrono::Local::now().timestamp(), - }; - timer_map.insert(LIGHT_WEIGHT_TASK_UID.into(), timer_task); - } + }); logging!( info, Type::Timer, - "计时器已设置,{} 分钟后将自动进入轻量模式", + "Lightweight mode timer set, entering lightweight mode in {} minutes", once_by_minutes ); - - Ok(()) } -fn cancel_light_weight_timer() -> Result<()> { - let value = Timer::global().timer_map.write().remove(LIGHT_WEIGHT_TASK_UID); - if let Some(task) = value { - Timer::global() - .delay_timer - .write() - .remove_task(task.task_id) - .context("failed to remove timer task")?; - logging!(debug, Type::Timer, "计时器已取消"); +fn cancel_light_weight_timer() { + let mut cancel_tx_guard = CANCEL_TX.lock(); + if let Some(tx) = cancel_tx_guard.take() { + let _ = tx.send(()); + logging!(debug, Type::Timer, "Timer cancelled"); } - - Ok(()) } diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 4438831c1..44b45d7ef 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -16,6 +16,7 @@ import { MenuItem, Typography, } from '@mui/material' +import { listen } from '@tauri-apps/api/event' import { open } from '@tauri-apps/plugin-shell' import { useLockFn } from 'ahooks' import dayjs from 'dayjs' @@ -201,11 +202,10 @@ export const ProfileItem = (props: Props) => { // 订阅定时器更新事件 useEffect(() => { - // 处理定时器更新事件 - 这个事件专门用于通知定时器变更 - const handleTimerUpdate = (event: Event) => { - const source = event as CustomEvent & { payload?: string } - const updatedUid = source.detail ?? source.payload + let disposed = false + let unlistenTimerUpdate: (() => void) | undefined + listen('verge://timer-updated', ({ payload: updatedUid }) => { // 只有当更新的是当前配置时才刷新显示 if (updatedUid === itemData.uid && showNextUpdateRef.current) { debugLog(`收到定时器更新事件: uid=${updatedUid}`) @@ -216,17 +216,22 @@ export const ProfileItem = (props: Props) => { fetchNextUpdateTime(true) }, 1000) } - } - - // 只注册定时器更新事件监听 - window.addEventListener('verge://timer-updated', handleTimerUpdate) + }) + .then((unlisten) => { + if (disposed) { + unlisten() + return + } + unlistenTimerUpdate = unlisten + }) + .catch(console.error) return () => { + disposed = true if (refreshTimeoutRef.current !== undefined) { clearTimeout(refreshTimeoutRef.current) } - // 清理事件监听 - window.removeEventListener('verge://timer-updated', handleTimerUpdate) + unlistenTimerUpdate?.() } }, [fetchNextUpdateTime, itemData.uid]) @@ -588,45 +593,49 @@ export const ProfileItem = (props: Props) => { // 监听自动更新事件 useEffect(() => { - const handleUpdateStarted = (event: Event) => { - const customEvent = event as CustomEvent<{ uid?: string }> - if (customEvent.detail?.uid === itemData.uid) { - setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true })) - } - } + let disposed = false + let unlisteners: Array<() => void> = [] + + Promise.allSettled([ + listen<{ uid?: string }>('profile-update-started', ({ payload }) => { + if (payload.uid === itemData.uid) { + setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true })) + } + }), + listen<{ uid?: string }>('profile-update-completed', ({ payload }) => { + if (payload.uid !== itemData.uid) { + return + } - const handleUpdateCompleted = (event: Event) => { - const customEvent = event as CustomEvent<{ uid?: string }> - if (customEvent.detail?.uid === itemData.uid) { setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false })) // 刷新 profile 数据以获取最新的 updated 时间戳 void mutateProfiles() // 更新完成后刷新显示 - if (showNextUpdate) { + if (showNextUpdateRef.current) { fetchNextUpdateTime() } - } - } + }), + ]).then((results) => { + const registeredUnlisteners = results.flatMap((result) => + result.status === 'fulfilled' ? [result.value] : [], + ) - // 注册事件监听 - window.addEventListener('profile-update-started', handleUpdateStarted) - window.addEventListener('profile-update-completed', handleUpdateCompleted) + if (disposed || results.some((result) => result.status === 'rejected')) { + registeredUnlisteners.forEach((unlisten) => unlisten()) + results.forEach((result) => { + if (result.status === 'rejected') console.error(result.reason) + }) + return + } + + unlisteners = registeredUnlisteners + }) return () => { - // 清理事件监听 - window.removeEventListener('profile-update-started', handleUpdateStarted) - window.removeEventListener( - 'profile-update-completed', - handleUpdateCompleted, - ) + disposed = true + unlisteners.forEach((unlisten) => unlisten()) } - }, [ - fetchNextUpdateTime, - itemData.uid, - mutateProfiles, - setLoadingCache, - showNextUpdate, - ]) + }, [fetchNextUpdateTime, itemData.uid, mutateProfiles, setLoadingCache]) const handleSaveProfileDocument = useLockFn(async () => { const currentValue = profileDocument.value