feat(script): convert script execution to async and add timeout handling

This commit is contained in:
Tunglies 2026-04-12 11:20:28 +08:00
parent e6a88cf9c9
commit 749b6c9e30
No known key found for this signature in database
GPG Key ID: B9B01B389469B3E8
2 changed files with 43 additions and 24 deletions

View File

@ -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<String>,
mut result_map: HashMap<String, ResultLog>,
@ -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,16 +455,17 @@ async fn merge_default_config(
config
}
fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable_builtin: bool) -> Mapping {
async fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, 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| {
.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, &String::from("")) {
match use_script(script, config.clone(), String::from("")).await {
Ok((res_config, _)) => {
config = res_config;
}
@ -473,7 +474,7 @@ fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable
}
}
}
});
}
}
config
@ -621,7 +622,8 @@ pub async fn enhance() -> (Mapping, HashSet<String>, HashMap<String, ResultLog>)
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<String>, HashMap<String, ResultLog>)
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<String>, HashMap<String, ResultLog>)
.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);

View File

@ -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
}