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/// 启动时用 `CODE_RUNNER_*` 环境变量播种代码运行器配置。
20#[cfg(feature = "server")]
21pub(crate) async fn seed_runner_settings_from_env(
22    client: &tokio_postgres::Client,
23) -> Result<(), AppError> {
24    use crate::models::settings as m;
25
26    let mut seeds: Vec<(&'static str, String)> = Vec::new();
27
28    if let Ok(v) = std::env::var("CODE_RUNNER_ALLOW_NETWORK") {
29        let l = v.to_lowercase();
30        let b = l == "true" || l == "1" || l == "yes";
31        seeds.push(("runner_allow_network", b.to_string()));
32    }
33    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_CONCURRENT") {
34        match v.trim().parse::<u32>() {
35            Ok(n) => seeds.push((
36                "runner_max_concurrent",
37                m::RunnerSettings::clamp_max_concurrent(n).to_string(),
38            )),
39            Err(_) => tracing::warn!("CODE_RUNNER_MAX_CONCURRENT={v:?} 非法,跳过"),
40        }
41    }
42    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_CPU_CORES") {
43        match v.trim().parse::<f64>() {
44            Ok(n) => seeds.push((
45                "runner_max_cpu_cores",
46                m::RunnerSettings::clamp_max_cpu_cores(n).to_string(),
47            )),
48            Err(_) => tracing::warn!("CODE_RUNNER_MAX_CPU_CORES={v:?} 非法,跳过"),
49        }
50    }
51    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_MEMORY_MB") {
52        match v.trim().parse::<u32>() {
53            Ok(n) => seeds.push((
54                "runner_max_memory_mb",
55                m::RunnerSettings::clamp_max_memory_mb(n).to_string(),
56            )),
57            Err(_) => tracing::warn!("CODE_RUNNER_MAX_MEMORY_MB={v:?} 非法,跳过"),
58        }
59    }
60    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_TIMEOUT_SECS") {
61        match v.trim().parse::<u32>() {
62            Ok(n) => seeds.push((
63                "runner_max_timeout_secs",
64                m::RunnerSettings::clamp_max_timeout_secs(n).to_string(),
65            )),
66            Err(_) => tracing::warn!("CODE_RUNNER_MAX_TIMEOUT_SECS={v:?} 非法,跳过"),
67        }
68    }
69    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_OUTPUT_BYTES") {
70        match v.trim().parse::<u64>() {
71            Ok(n) => seeds.push((
72                "runner_max_output_bytes",
73                m::RunnerSettings::clamp_max_output_bytes(n).to_string(),
74            )),
75            Err(_) => tracing::warn!("CODE_RUNNER_MAX_OUTPUT_BYTES={v:?} 非法,跳过"),
76        }
77    }
78    if let Ok(v) = std::env::var("CODE_RUNNER_MAX_SOURCE_BYTES") {
79        match v.trim().parse::<u64>() {
80            Ok(n) => seeds.push((
81                "runner_max_source_bytes",
82                m::RunnerSettings::clamp_max_source_bytes(n).to_string(),
83            )),
84            Err(_) => tracing::warn!("CODE_RUNNER_MAX_SOURCE_BYTES={v:?} 非法,跳过"),
85        }
86    }
87    if let Ok(v) = std::env::var("CODE_RUNNER_QUEUE_TIMEOUT_SECS") {
88        match v.trim().parse::<u32>() {
89            Ok(n) => seeds.push((
90                "runner_queue_timeout_secs",
91                m::RunnerSettings::clamp_queue_timeout_secs(n).to_string(),
92            )),
93            Err(_) => tracing::warn!("CODE_RUNNER_QUEUE_TIMEOUT_SECS={v:?} 非法,跳过"),
94        }
95    }
96    if let Ok(v) = std::env::var("CODE_RUNNER_TASK_TTL_SECS") {
97        match v.trim().parse::<u32>() {
98            Ok(n) => seeds.push((
99                "runner_task_ttl_secs",
100                m::RunnerSettings::clamp_task_ttl_secs(n).to_string(),
101            )),
102            Err(_) => tracing::warn!("CODE_RUNNER_TASK_TTL_SECS={v:?} 非法,跳过"),
103        }
104    }
105    if let Ok(v) = std::env::var("CODE_RUNNER_LANGUAGES") {
106        if let Some(norm) = m::RunnerSettings::normalize_languages(&v) {
107            seeds.push(("runner_languages", norm));
108        }
109    }
110
111    for (key, value) in seeds {
112        client
113            .execute(
114                "INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING",
115                &[&key, &value],
116            )
117            .await
118            .map_err(AppError::query)?;
119        tracing::info!("运行器配置已从环境变量播种: {key}={value}(仅键缺失时生效)");
120    }
121    Ok(())
122}
123
124/// 从 settings 表读取代码运行器配置(缺键回退默认值)。
125#[cfg(feature = "server")]
126pub(crate) async fn load_runner_settings(
127    client: &tokio_postgres::Client,
128) -> Result<RunnerSettings, AppError> {
129    use crate::models::settings as m;
130
131    async fn read_key(
132        client: &tokio_postgres::Client,
133        key: &str,
134    ) -> Result<Option<String>, AppError> {
135        let row = client
136            .query_opt("SELECT value FROM settings WHERE key = $1", &[&key])
137            .await
138            .map_err(AppError::query)?;
139        Ok(row.map(|r| r.get::<_, String>("value")))
140    }
141
142    let allow_network = read_key(client, "runner_allow_network")
143        .await?
144        .and_then(|v| v.parse().ok())
145        .unwrap_or(m::DEFAULT_RUNNER_ALLOW_NETWORK);
146    let max_concurrent = read_key(client, "runner_max_concurrent")
147        .await?
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 = read_key(client, "runner_max_cpu_cores")
152        .await?
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 = read_key(client, "runner_max_memory_mb")
157        .await?
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 = read_key(client, "runner_max_timeout_secs")
162        .await?
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 = read_key(client, "runner_max_output_bytes")
167        .await?
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 = read_key(client, "runner_max_source_bytes")
172        .await?
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 = read_key(client, "runner_queue_timeout_secs")
177        .await?
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 = read_key(client, "runner_task_ttl_secs")
182        .await?
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 = read_key(client, "runner_languages")
187        .await?
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, "/api")]
206pub async fn get_runner_settings() -> Result<RunnerSettings, ServerFnError> {
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(ServerFnError::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, "/api")]
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, ServerFnError> {
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        for (key, value) in [
258            ("runner_allow_network", allow_network.to_string()),
259            ("runner_max_concurrent", max_concurrent.to_string()),
260            ("runner_max_cpu_cores", max_cpu_cores.to_string()),
261            ("runner_max_memory_mb", max_memory_mb.to_string()),
262            ("runner_max_timeout_secs", max_timeout_secs.to_string()),
263            ("runner_max_output_bytes", max_output_bytes.to_string()),
264            ("runner_max_source_bytes", max_source_bytes.to_string()),
265            ("runner_queue_timeout_secs", queue_timeout_secs.to_string()),
266            ("runner_task_ttl_secs", task_ttl_secs.to_string()),
267            ("runner_languages", lang_str),
268        ] {
269            client
270                .execute(
271                    "INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
272                     ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()",
273                    &[&key, &value],
274                )
275                .await
276                .map_err(AppError::query)?;
277        }
278
279        tracing::info!(
280            "Runner settings updated (需重启生效): allow_network={}, max_concurrent={}, \
281             cpu={}, mem={}MB, timeout={}s, languages={:?}",
282            allow_network,
283            max_concurrent,
284            max_cpu_cores,
285            max_memory_mb,
286            max_timeout_secs,
287            languages
288        );
289    }
290
291    Ok(RunnerSettings {
292        allow_network,
293        max_concurrent,
294        max_cpu_cores,
295        max_memory_mb,
296        max_timeout_secs,
297        max_output_bytes,
298        max_source_bytes,
299        queue_timeout_secs,
300        task_ttl_secs,
301        languages,
302    })
303}