Skip to main content

yggdrasil/api/settings/
runner.rs

1// 与 posts 模块一致:Leptos `#[server]` 宏触发 deprecated/unit/too_many_arguments
2// 提示,按项目惯例放行(限流/运行器等配置项天然参数多)。
3#![allow(clippy::unused_unit, deprecated, clippy::too_many_arguments)]
4
5use crate::api::error::ApiError;
6use leptos::prelude::*;
7
8#[cfg(feature = "server")]
9use crate::api::auth::get_current_admin_user;
10#[cfg(feature = "server")]
11use crate::api::error::AppError;
12#[cfg(feature = "server")]
13use crate::db::pool::get_conn;
14use crate::models::settings::RunnerSettings;
15
16// ============================================================================
17// 代码运行器配置(需重启生效)
18// ============================================================================
19
20#[cfg(feature = "server")]
21fn parse_allow_network(value: &str) -> bool {
22    let value = value.to_lowercase();
23    value == "true" || value == "1" || value == "yes"
24}
25
26/// 启动时用 `CODE_RUNNER_*` 环境变量播种代码运行器配置。
27#[cfg(feature = "server")]
28pub(crate) async fn seed_runner_settings_from_env(
29    client: &tokio_postgres::Client,
30) -> Result<(), AppError> {
31    use crate::models::settings as m;
32
33    let mut seeds: Vec<(&'static str, String)> = Vec::new();
34
35    if let Ok(v) = std::env::var("CODE_RUNNER_ALLOW_NETWORK") {
36        seeds.push(("runner_allow_network", parse_allow_network(&v).to_string()));
37    }
38    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_CONCURRENT") {
39        match v.trim().parse::<u32>() {
40            Ok(n) => seeds.push((
41                "runner_max_concurrent",
42                m::RunnerSettings::clamp_max_concurrent(n).to_string(),
43            )),
44            Err(_) => tracing::warn!("CODE_RUNNER_MAX_CONCURRENT={v:?} 非法,跳过"),
45        }
46    }
47    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_CPU_CORES") {
48        match v.trim().parse::<f64>() {
49            Ok(n) => seeds.push((
50                "runner_max_cpu_cores",
51                m::RunnerSettings::clamp_max_cpu_cores(n).to_string(),
52            )),
53            Err(_) => tracing::warn!("CODE_RUNNER_MAX_CPU_CORES={v:?} 非法,跳过"),
54        }
55    }
56    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_MEMORY_MB") {
57        match v.trim().parse::<u32>() {
58            Ok(n) => seeds.push((
59                "runner_max_memory_mb",
60                m::RunnerSettings::clamp_max_memory_mb(n).to_string(),
61            )),
62            Err(_) => tracing::warn!("CODE_RUNNER_MAX_MEMORY_MB={v:?} 非法,跳过"),
63        }
64    }
65    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_TIMEOUT_SECS") {
66        match v.trim().parse::<u32>() {
67            Ok(n) => seeds.push((
68                "runner_max_timeout_secs",
69                m::RunnerSettings::clamp_max_timeout_secs(n).to_string(),
70            )),
71            Err(_) => tracing::warn!("CODE_RUNNER_MAX_TIMEOUT_SECS={v:?} 非法,跳过"),
72        }
73    }
74    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_OUTPUT_BYTES") {
75        match v.trim().parse::<u64>() {
76            Ok(n) => seeds.push((
77                "runner_max_output_bytes",
78                m::RunnerSettings::clamp_max_output_bytes(n).to_string(),
79            )),
80            Err(_) => tracing::warn!("CODE_RUNNER_MAX_OUTPUT_BYTES={v:?} 非法,跳过"),
81        }
82    }
83    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_SOURCE_BYTES") {
84        match v.trim().parse::<u64>() {
85            Ok(n) => seeds.push((
86                "runner_max_source_bytes",
87                m::RunnerSettings::clamp_max_source_bytes(n).to_string(),
88            )),
89            Err(_) => tracing::warn!("CODE_RUNNER_MAX_SOURCE_BYTES={v:?} 非法,跳过"),
90        }
91    }
92    if let Ok(v) = std::env::var("CODE_RUNNER_QUEUE_TIMEOUT_SECS") {
93        match v.trim().parse::<u32>() {
94            Ok(n) => seeds.push((
95                "runner_queue_timeout_secs",
96                m::RunnerSettings::clamp_queue_timeout_secs(n).to_string(),
97            )),
98            Err(_) => tracing::warn!("CODE_RUNNER_QUEUE_TIMEOUT_SECS={v:?} 非法,跳过"),
99        }
100    }
101    if let Ok(v) = std::env::var("CODE_RUNNER_TASK_TTL_SECS") {
102        match v.trim().parse::<u32>() {
103            Ok(n) => seeds.push((
104                "runner_task_ttl_secs",
105                m::RunnerSettings::clamp_task_ttl_secs(n).to_string(),
106            )),
107            Err(_) => tracing::warn!("CODE_RUNNER_TASK_TTL_SECS={v:?} 非法,跳过"),
108        }
109    }
110    if let Ok(v) = std::env::var("CODE_RUNNER_LANGUAGES") {
111        if let Some(norm) = m::RunnerSettings::normalize_languages(&v) {
112            seeds.push(("runner_languages", norm));
113        }
114    }
115
116    super::insert_env_seeds(client, seeds).await
117}
118
119/// 从 settings 表读取代码运行器配置(缺键回退默认值)。
120#[cfg(feature = "server")]
121pub(crate) async fn load_runner_settings(
122    client: &tokio_postgres::Client,
123) -> Result<RunnerSettings, AppError> {
124    use crate::models::settings as m;
125
126    let values = super::load_setting_values(
127        client,
128        &[
129            "runner_allow_network",
130            "runner_max_concurrent",
131            "runner_max_cpu_cores",
132            "runner_max_memory_mb",
133            "runner_max_timeout_secs",
134            "runner_max_output_bytes",
135            "runner_max_source_bytes",
136            "runner_queue_timeout_secs",
137            "runner_task_ttl_secs",
138            "runner_languages",
139        ],
140    )
141    .await?;
142    let allow_network = values
143        .get("runner_allow_network")
144        .and_then(|v| v.parse().ok())
145        .unwrap_or(m::DEFAULT_RUNNER_ALLOW_NETWORK);
146    let max_concurrent = values
147        .get("runner_max_concurrent")
148        .and_then(|v| v.parse().ok())
149        .map(m::RunnerSettings::clamp_max_concurrent)
150        .unwrap_or(m::DEFAULT_RUNNER_MAX_CONCURRENT);
151    let max_cpu_cores = values
152        .get("runner_max_cpu_cores")
153        .and_then(|v| v.parse().ok())
154        .map(m::RunnerSettings::clamp_max_cpu_cores)
155        .unwrap_or(m::DEFAULT_RUNNER_MAX_CPU_CORES);
156    let max_memory_mb = values
157        .get("runner_max_memory_mb")
158        .and_then(|v| v.parse().ok())
159        .map(m::RunnerSettings::clamp_max_memory_mb)
160        .unwrap_or(m::DEFAULT_RUNNER_MAX_MEMORY_MB);
161    let max_timeout_secs = values
162        .get("runner_max_timeout_secs")
163        .and_then(|v| v.parse().ok())
164        .map(m::RunnerSettings::clamp_max_timeout_secs)
165        .unwrap_or(m::DEFAULT_RUNNER_MAX_TIMEOUT_SECS);
166    let max_output_bytes = values
167        .get("runner_max_output_bytes")
168        .and_then(|v| v.parse().ok())
169        .map(m::RunnerSettings::clamp_max_output_bytes)
170        .unwrap_or(m::DEFAULT_RUNNER_MAX_OUTPUT_BYTES);
171    let max_source_bytes = values
172        .get("runner_max_source_bytes")
173        .and_then(|v| v.parse().ok())
174        .map(m::RunnerSettings::clamp_max_source_bytes)
175        .unwrap_or(m::DEFAULT_RUNNER_MAX_SOURCE_BYTES);
176    let queue_timeout_secs = values
177        .get("runner_queue_timeout_secs")
178        .and_then(|v| v.parse().ok())
179        .map(m::RunnerSettings::clamp_queue_timeout_secs)
180        .unwrap_or(m::DEFAULT_RUNNER_QUEUE_TIMEOUT_SECS);
181    let task_ttl_secs = values
182        .get("runner_task_ttl_secs")
183        .and_then(|v| v.parse().ok())
184        .map(m::RunnerSettings::clamp_task_ttl_secs)
185        .unwrap_or(m::DEFAULT_RUNNER_TASK_TTL_SECS);
186    let languages = values
187        .get("runner_languages")
188        .and_then(|v| m::RunnerSettings::normalize_languages(v));
189
190    Ok(RunnerSettings {
191        allow_network,
192        max_concurrent,
193        max_cpu_cores,
194        max_memory_mb,
195        max_timeout_secs,
196        max_output_bytes,
197        max_source_bytes,
198        queue_timeout_secs,
199        task_ttl_secs,
200        languages,
201    })
202}
203
204/// 读取代码运行器配置(面板用)。
205#[server(GetRunnerSettings, endpoint = "get_runner_settings", input = leptos::server_fn::codec::Json)]
206pub async fn get_runner_settings() -> Result<RunnerSettings, ApiError> {
207    let _user = get_current_admin_user().await?;
208
209    #[cfg(feature = "server")]
210    {
211        let client = get_conn().await.map_err(AppError::db_conn)?;
212        let s = load_runner_settings(&client)
213            .await
214            .map_err(<ApiError>::from)?;
215        Ok(s)
216    }
217
218    #[cfg(not(feature = "server"))]
219    {
220        Ok(RunnerSettings::default())
221    }
222}
223
224/// 更新代码运行器配置。
225///
226/// 字段会被 clamp / 规范化后写入 DB。配置烘焙进 LazyLock,修改后需**重启进程**生效。
227#[server(UpdateRunnerSettings, endpoint = "update_runner_settings", input = leptos::server_fn::codec::Json)]
228pub async fn update_runner_settings(
229    allow_network: bool,
230    max_concurrent: u32,
231    max_cpu_cores: f64,
232    max_memory_mb: u32,
233    max_timeout_secs: u32,
234    max_output_bytes: u64,
235    max_source_bytes: u64,
236    queue_timeout_secs: u32,
237    task_ttl_secs: u32,
238    languages: Option<String>,
239) -> Result<RunnerSettings, ApiError> {
240    let _user = get_current_admin_user().await?;
241
242    let max_concurrent = RunnerSettings::clamp_max_concurrent(max_concurrent);
243    let max_cpu_cores = RunnerSettings::clamp_max_cpu_cores(max_cpu_cores);
244    let max_memory_mb = RunnerSettings::clamp_max_memory_mb(max_memory_mb);
245    let max_timeout_secs = RunnerSettings::clamp_max_timeout_secs(max_timeout_secs);
246    let max_output_bytes = RunnerSettings::clamp_max_output_bytes(max_output_bytes);
247    let max_source_bytes = RunnerSettings::clamp_max_source_bytes(max_source_bytes);
248    let queue_timeout_secs = RunnerSettings::clamp_queue_timeout_secs(queue_timeout_secs);
249    let task_ttl_secs = RunnerSettings::clamp_task_ttl_secs(task_ttl_secs);
250    let languages = languages.and_then(|s| RunnerSettings::normalize_languages(&s));
251
252    #[cfg(feature = "server")]
253    {
254        let client = get_conn().await.map_err(AppError::db_conn)?;
255
256        let lang_str = languages.clone().unwrap_or_default();
257        super::upsert_settings(
258            &**client,
259            &[
260                ("runner_allow_network", allow_network.to_string()),
261                ("runner_max_concurrent", max_concurrent.to_string()),
262                ("runner_max_cpu_cores", max_cpu_cores.to_string()),
263                ("runner_max_memory_mb", max_memory_mb.to_string()),
264                ("runner_max_timeout_secs", max_timeout_secs.to_string()),
265                ("runner_max_output_bytes", max_output_bytes.to_string()),
266                ("runner_max_source_bytes", max_source_bytes.to_string()),
267                ("runner_queue_timeout_secs", queue_timeout_secs.to_string()),
268                ("runner_task_ttl_secs", task_ttl_secs.to_string()),
269                ("runner_languages", lang_str),
270            ],
271        )
272        .await?;
273
274        tracing::info!(
275            "Runner settings updated (需重启生效): allow_network={}, max_concurrent={}, \
276             cpu={}, mem={}MB, timeout={}s, languages={:?}",
277            allow_network,
278            max_concurrent,
279            max_cpu_cores,
280            max_memory_mb,
281            max_timeout_secs,
282            languages
283        );
284    }
285
286    Ok(RunnerSettings {
287        allow_network,
288        max_concurrent,
289        max_cpu_cores,
290        max_memory_mb,
291        max_timeout_secs,
292        max_output_bytes,
293        max_source_bytes,
294        queue_timeout_secs,
295        task_ttl_secs,
296        languages,
297    })
298}
299
300#[cfg(all(test, feature = "server"))]
301mod tests {
302    use super::parse_allow_network;
303
304    #[test]
305    fn allow_network_env_requires_explicit_opt_in() {
306        for value in ["true", "TRUE", "True", "1", "yes", "YES", "Yes"] {
307            assert!(parse_allow_network(value), "{value:?}");
308        }
309        for value in ["false", "0", "no", "", "maybe", "on", " true "] {
310            assert!(!parse_allow_network(value), "{value:?}");
311        }
312    }
313}