yggdrasil/api/code_runner/
execute.rs1#![allow(clippy::unused_unit, deprecated)]
16
17use dioxus::prelude::*;
18
19use crate::api::code_runner::{ExecRequest, ExecTask};
21#[cfg(feature = "server")]
23use crate::api::code_runner::{ExecResult, ExecStatus};
24
25#[cfg(feature = "server")]
27use crate::api::auth::get_current_admin_user;
28#[cfg(feature = "server")]
29use crate::api::code_runner::languages::{is_supported_lang, normalize_lang, LANGUAGES};
30#[cfg(feature = "server")]
31use crate::api::code_runner::progress::{
32 gc_old_tasks, insert_task, update_task_result, update_task_stage, StreamEntry, EXEC_STREAMS,
33 EXEC_TASKS,
34};
35#[cfg(feature = "server")]
36use crate::api::rate_limit::{check_code_exec_limit, get_client_ip};
37#[cfg(feature = "server")]
38use crate::infra::docker::{run_in_container, run_in_container_stream, OutputChunk};
39#[cfg(feature = "server")]
40use crate::infra::runner_config::{clamp_limits, RUNNER_CONFIG};
41#[cfg(feature = "server")]
42use std::sync::{Arc, LazyLock};
43#[cfg(feature = "server")]
44use std::time::Duration;
45#[cfg(feature = "server")]
46use tokio::sync::Semaphore;
47
48#[cfg(feature = "server")]
50pub static RUNNER_SEMAPHORE: LazyLock<Arc<Semaphore>> =
51 LazyLock::new(|| Arc::new(Semaphore::new(RUNNER_CONFIG.max_concurrent)));
52
53#[cfg(feature = "server")]
55fn client_ip() -> String {
56 match dioxus::fullstack::FullstackContext::current() {
57 Some(ctx) => {
58 let parts = ctx.parts_mut();
59 get_client_ip(&parts.headers)
60 }
61 None => "unknown".to_string(),
62 }
63}
64
65#[cfg(feature = "server")]
70fn validate_exec_request(req: &ExecRequest) -> Result<(), ServerFnError> {
71 if !is_supported_lang(&req.language) {
73 return Err(ServerFnError::new("不支持该执行语言".to_string()));
74 }
75
76 if req.source.len() > RUNNER_CONFIG.max_source_bytes as usize {
78 return Err(ServerFnError::new("源代码过大".to_string()));
79 }
80
81 Ok(())
82}
83
84#[cfg(feature = "server")]
86async fn check_rate_limit_for_user() -> Result<(), ServerFnError> {
87 let is_admin = get_current_admin_user().await.is_ok();
88 if !is_admin {
89 let ip = client_ip();
90 if let Err(msg) = check_code_exec_limit(&ip) {
91 return Err(ServerFnError::new(msg));
92 }
93 }
94 Ok(())
95}
96
97#[cfg(feature = "server")]
107fn spawn_exec_task(
108 task_id: String,
109 req: ExecRequest,
110 stream_tx: Option<tokio::sync::mpsc::Sender<OutputChunk>>,
111) {
112 let lang_key = normalize_lang(&req.language);
115 tokio::spawn(async move {
116 let sem = &*RUNNER_SEMAPHORE;
117
118 let ticket = match tokio::time::timeout(
120 Duration::from_secs(RUNNER_CONFIG.queue_timeout_secs),
121 sem.acquire(),
122 )
123 .await
124 {
125 Ok(Ok(t)) => t,
126 _ => {
127 update_task_stage(&task_id, ExecStatus::Failed, "系统繁忙,排队超时");
128 return;
129 }
130 };
131
132 update_task_stage(&task_id, ExecStatus::Running, "启动容器");
133
134 let lang_def = match LANGUAGES.get(&lang_key) {
135 Some(d) => d,
136 None => {
137 update_task_stage(&task_id, ExecStatus::Failed, "语言未注册");
139 return;
140 }
141 };
142
143 let base_limits = req
145 .overrides
146 .unwrap_or_else(|| lang_def.default_limits.clone());
147 let final_limits = clamp_limits(base_limits, lang_def.allow_network);
148
149 let start_time = chrono::Utc::now();
150 let stream_suffix = if stream_tx.is_some() { " (stream)" } else { "" };
152 let res = match stream_tx {
153 Some(tx) => run_in_container_stream(
154 &lang_def.image,
155 &lang_def.run_cmd,
156 &req.source,
157 &lang_def.extension,
158 final_limits,
159 tx,
160 )
161 .await
162 .map(|(exit_code, stdout, stderr, oom_killed, _)| {
163 (exit_code, stdout, stderr, oom_killed)
164 }),
165 None => {
166 run_in_container(
167 &lang_def.image,
168 &lang_def.run_cmd,
169 &req.source,
170 &lang_def.extension,
171 final_limits,
172 )
173 .await
174 }
175 };
176 let duration_ms = (chrono::Utc::now() - start_time).num_milliseconds().max(0) as u64;
177
178 drop(ticket); match res {
181 Ok((exit_code, stdout, stderr, oom_killed)) => {
182 let status = if oom_killed {
183 ExecStatus::OomKilled
184 } else if exit_code == Some(0) {
185 ExecStatus::Success
186 } else {
187 ExecStatus::Error
188 };
189 let exec_res = ExecResult {
190 status: status.clone(),
191 stdout,
192 stderr,
193 exit_code,
194 duration_ms,
195 language: lang_key.clone(),
196 };
197 update_task_result(&task_id, status, exec_res);
198 }
199 Err(e) => {
200 let s = e.to_string();
202 let is_timeout = s.contains("TimedOut");
203 tracing::error!(error = ?e, task_id = %task_id, "container execution failed{}", stream_suffix);
204 let status = if is_timeout {
205 ExecStatus::Timeout
206 } else {
207 ExecStatus::Failed
208 };
209 let exec_res = ExecResult {
210 status: status.clone(),
211 stdout: String::new(),
212 stderr: if is_timeout {
213 "执行超时".to_string()
214 } else {
215 "系统暂时不可用".to_string()
216 },
217 exit_code: None,
218 duration_ms,
219 language: lang_key.clone(),
220 };
221 update_task_result(&task_id, status, exec_res);
222 }
223 }
224 });
225}
226
227#[server(StartExec, "/api")]
235pub async fn start_exec(req: ExecRequest) -> Result<String, ServerFnError> {
236 check_rate_limit_for_user().await?;
237 validate_exec_request(&req)?;
238
239 let task_id = uuid::Uuid::new_v4().to_string();
241 insert_task(task_id.clone());
242
243 gc_old_tasks();
245
246 spawn_exec_task(task_id.clone(), req, None);
249
250 Ok(task_id)
251}
252
253#[server(StartExecStream, "/api")]
263pub async fn start_exec_stream(req: ExecRequest) -> Result<String, ServerFnError> {
264 check_rate_limit_for_user().await?;
265 validate_exec_request(&req)?;
266
267 let task_id = uuid::Uuid::new_v4().to_string();
268 insert_task(task_id.clone());
270
271 let (tx, rx) = tokio::sync::mpsc::channel(64);
273 EXEC_STREAMS.insert(
274 task_id.clone(),
275 StreamEntry {
276 rx,
277 created_at: chrono::Utc::now(),
278 },
279 );
280
281 gc_old_tasks();
282
283 spawn_exec_task(task_id.clone(), req, Some(tx));
285
286 Ok(task_id)
287}
288
289#[server(GetExecResult, "/api")]
291pub async fn get_exec_result(task_id: String) -> Result<ExecTask, ServerFnError> {
292 if let Some(task) = EXEC_TASKS.get(&task_id) {
293 Ok(task.clone())
294 } else {
295 Err(ServerFnError::new("找不到指定的任务".to_string()))
296 }
297}
298
299#[cfg(all(test, feature = "server"))]
300mod tests {
301 use super::*;
302
303 fn req(language: &str, source: &str) -> ExecRequest {
304 ExecRequest {
305 language: language.to_string(),
306 source: source.to_string(),
307 overrides: None,
308 }
309 }
310
311 #[test]
312 fn validate_accepts_registered_language() {
313 for lang in ["python", "node", "go", "rust", "bun"] {
315 assert!(
316 validate_exec_request(&req(lang, "x")).is_ok(),
317 "{lang} 应被支持"
318 );
319 }
320 }
321
322 #[test]
323 fn validate_accepts_aliases() {
324 for lang in ["js", "javascript", "rs", "ts", "typescript"] {
328 assert!(
329 validate_exec_request(&req(lang, "x")).is_ok(),
330 "别名 {lang} 应被支持"
331 );
332 }
333 }
334
335 #[test]
336 fn validate_language_is_case_and_whitespace_insensitive() {
337 assert!(validate_exec_request(&req("Python", "x")).is_ok());
339 assert!(validate_exec_request(&req(" RUST ", "x")).is_ok());
340 assert!(validate_exec_request(&req("Go", "x")).is_ok());
341 }
342
343 #[test]
344 fn validate_rejects_unregistered_language() {
345 assert!(validate_exec_request(&req("c", "x")).is_err());
347 assert!(validate_exec_request(&req("bash", "x")).is_err());
348 assert!(validate_exec_request(&req("python2", "x")).is_err());
349 assert!(validate_exec_request(&req("", "x")).is_err());
350 }
351
352 #[test]
353 fn validate_rejects_multi_token_language() {
354 assert!(validate_exec_request(&req("python; rm -rf /", "x")).is_err());
357 assert!(validate_exec_request(&req("python node", "x")).is_err());
358 assert!(validate_exec_request(&req("python$(whoami)", "x")).is_err());
359 }
360
361 #[test]
362 fn validate_language_tolerates_surrounding_whitespace() {
363 assert!(validate_exec_request(&req("python\n", "x")).is_ok());
366 assert!(validate_exec_request(&req("\tpython\t", "x")).is_ok());
367 }
368
369 #[test]
370 fn validate_rejects_source_exceeding_max_bytes() {
371 let max = RUNNER_CONFIG.max_source_bytes as usize;
372 let exactly = "a".repeat(max);
374 assert!(
375 validate_exec_request(&req("python", &exactly)).is_ok(),
376 "源码恰好等于上限应放行"
377 );
378 let over = "a".repeat(max + 1);
379 let err = validate_exec_request(&req("python", &over)).expect_err("超出上限应拒绝");
380 assert!(
381 err.to_string().contains("过大"),
382 "错误信息应提及大小: {err}"
383 );
384 }
385
386 #[test]
387 fn validate_empty_source_accepted_for_supported_lang() {
388 assert!(validate_exec_request(&req("python", "")).is_ok());
391 }
392
393 #[test]
394 fn validate_checks_language_before_size() {
395 let huge = "a".repeat((RUNNER_CONFIG.max_source_bytes as usize) + 100);
397 let err = validate_exec_request(&req("brainfuck", &huge)).unwrap_err();
398 assert!(err.to_string().contains("语言"), "应先报语言错误: {err}");
399 }
400}