Skip to main content

yggdrasil/api/settings/
runner.rs

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