From 749b6c9e30ad2a517013fffff589435831e2457e Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:20:28 +0800 Subject: [PATCH] feat(script): convert script execution to async and add timeout handling --- src-tauri/src/enhance/mod.rs | 43 ++++++++++++++++++--------------- src-tauri/src/enhance/script.rs | 24 +++++++++++++++--- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index f74981ff0..8c3b46f98 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -303,7 +303,7 @@ async fn collect_profile_items() -> ProfileItems { } } -fn process_global_items( +async fn process_global_items( mut config: Mapping, global_merge: ChainItem, global_script: ChainItem, @@ -319,7 +319,7 @@ fn process_global_items( if let ChainType::Script(script) = global_script.data { let mut logs = vec![]; - match use_script(script, &config, profile_name) { + match use_script(script, config.clone(), profile_name.clone()).await { Ok((res_config, res_logs)) => { exists_keys.extend(use_keys(&res_config)); config = res_config; @@ -334,7 +334,7 @@ fn process_global_items( } #[allow(clippy::too_many_arguments)] -fn process_profile_items( +async fn process_profile_items( mut config: Mapping, mut exists_keys: Vec, mut result_map: HashMap, @@ -364,7 +364,7 @@ fn process_profile_items( if let ChainType::Script(script) = script_item.data { let mut logs = vec![]; - match use_script(script, &config, profile_name) { + match use_script(script, config.clone(), profile_name.clone()).await { Ok((res_config, res_logs)) => { exists_keys.extend(use_keys(&res_config)); config = res_config; @@ -455,25 +455,26 @@ async fn merge_default_config( config } -fn apply_builtin_scripts(mut config: Mapping, clash_core: Option, enable_builtin: bool) -> Mapping { +async fn apply_builtin_scripts(mut config: Mapping, clash_core: Option, enable_builtin: bool) -> Mapping { if enable_builtin { - ChainItem::builtin() + let items: Vec<_> = ChainItem::builtin() .into_iter() .filter(|(s, _)| s.is_support(clash_core.as_ref())) .map(|(_, c)| c) - .for_each(|item| { - logging!(debug, Type::Core, "run builtin script {}", item.uid); - if let ChainType::Script(script) = item.data { - match use_script(script, &config, &String::from("")) { - Ok((res_config, _)) => { - config = res_config; - } - Err(err) => { - logging!(error, Type::Core, "builtin script error `{err}`"); - } + .collect(); + for item in items { + logging!(debug, Type::Core, "run builtin script {}", item.uid); + if let ChainType::Script(script) = item.data { + match use_script(script, config.clone(), String::from("")).await { + Ok((res_config, _)) => { + config = res_config; + } + Err(err) => { + logging!(error, Type::Core, "builtin script error `{err}`"); } } - }); + } + } } config @@ -621,7 +622,8 @@ pub async fn enhance() -> (Mapping, HashSet, HashMap) let profile_name = profile.profile_name; // process globals - let (config, exists_keys, result_map) = process_global_items(config, global_merge, global_script, &profile_name); + let (config, exists_keys, result_map) = + process_global_items(config, global_merge, global_script, &profile_name).await; // process profile-specific items let (config, exists_keys, result_map) = process_profile_items( @@ -634,7 +636,8 @@ pub async fn enhance() -> (Mapping, HashSet, HashMap) merge_item, script_item, &profile_name, - ); + ) + .await; // merge default clash config let config = merge_default_config( @@ -650,7 +653,7 @@ pub async fn enhance() -> (Mapping, HashSet, HashMap) .await; // builtin scripts - let mut config = apply_builtin_scripts(config, clash_core, enable_builtin); + let mut config = apply_builtin_scripts(config, clash_core, enable_builtin).await; config = cleanup_proxy_groups(config); diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs index df8d3f638..1e44c3769 100644 --- a/src-tauri/src/enhance/script.rs +++ b/src-tauri/src/enhance/script.rs @@ -1,3 +1,5 @@ +use crate::process::AsyncHandler; + use super::use_lowercase; use anyhow::{Error, Result}; use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction}; @@ -10,11 +12,25 @@ use std::sync::Arc; const MAX_OUTPUTS: usize = 1000; const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB const MAX_JSON_SIZE: usize = 10 * 1024 * 1024; // 10MB +const MAX_LOOP_ITERATIONS: u64 = 10_000_000; +const SCRIPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); -// TODO 使用引用改进上下相关处理,避免不必要 Clone -pub fn use_script(script: String, config: &Mapping, name: &String) -> Result<(Mapping, Vec<(String, String)>)> { +pub async fn use_script(script: String, config: Mapping, name: String) -> Result<(Mapping, Vec<(String, String)>)> { + let handle = AsyncHandler::spawn_blocking(move || use_script_sync(script, &config, &name)); + match tokio::time::timeout(SCRIPT_TIMEOUT, handle).await { + Ok(Ok(result)) => result, + Ok(Err(join_err)) => Err(anyhow::anyhow!("script task panicked: {join_err}")), + Err(_elapsed) => Err(anyhow::anyhow!("script execution timed out after {:?}", SCRIPT_TIMEOUT)), + } +} + +fn use_script_sync(script: String, config: &Mapping, name: &String) -> Result<(Mapping, Vec<(String, String)>)> { let mut context = Context::default(); + context + .runtime_limits_mut() + .set_loop_iteration_limit(MAX_LOOP_ITERATIONS); + let outputs = Arc::new(Mutex::new(vec![])); let total_size = Arc::new(Mutex::new(0usize)); @@ -189,7 +205,7 @@ fn test_script() { let config = &serde_yaml_ng::from_str(config).expect("Failed to parse test config YAML"); let (config, results) = - use_script(script.into(), config, &String::from("")).expect("Script execution should succeed in test"); + use_script_sync(script.into(), config, &String::from("")).expect("Script execution should succeed in test"); let _ = serde_yaml_ng::to_string(&config).expect("Failed to serialize config to YAML"); let yaml_config_size = std::mem::size_of_val(&config); @@ -243,7 +259,7 @@ fn test_memory_limits() { #[allow(clippy::expect_used)] let config = &serde_yaml_ng::from_str("test: value").expect("Failed to parse test YAML"); - let result = use_script(script.into(), config, &String::from("")); + let result = use_script_sync(script.into(), config, &String::from("")); // 应该失败或被限制 assert!(result.is_ok()); // 会被限制但不会 panic }