yggdrasil/api/code_runner/
sse.rs1use std::convert::Infallible;
11use std::time::Duration;
12
13use axum::extract::Query;
14use axum::http::StatusCode;
15use axum::response::sse::{Event, KeepAlive, Sse};
16use futures::StreamExt;
17use tokio_stream::wrappers::ReceiverStream;
18
19use crate::api::code_runner::progress::EXEC_STREAMS;
20use crate::infra::docker::OutputChunk;
21
22#[derive(serde::Deserialize)]
24pub struct StreamQuery {
25 pub task_id: String,
26}
27
28#[derive(serde::Serialize)]
29struct DonePayload {
30 exit_code: Option<i64>,
31 oom_killed: bool,
32 timed_out: bool,
33 duration_ms: u64,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 error: Option<String>,
37}
38
39pub async fn exec_stream(
42 Query(q): Query<StreamQuery>,
43) -> Result<Sse<impl futures::Stream<Item = Result<Event, Infallible>>>, (StatusCode, String)> {
44 let (_, entry) = EXEC_STREAMS
47 .remove(&q.task_id)
48 .ok_or((StatusCode::NOT_FOUND, "任务不存在或已结束".to_string()))?;
49
50 let stream = ReceiverStream::new(entry.rx).map(|chunk| {
51 Ok::<_, Infallible>(match chunk {
52 OutputChunk::Stdout(s) => Event::default().event("stdout").data(s),
53 OutputChunk::Stderr(s) => Event::default().event("stderr").data(s),
54 OutputChunk::Done {
55 exit_code,
56 oom_killed,
57 timed_out,
58 duration_ms,
59 error,
60 } => Event::default()
61 .event("done")
62 .json_data(DonePayload {
63 exit_code,
64 oom_killed,
65 timed_out,
66 duration_ms,
67 error,
68 })
69 .unwrap_or_else(|_| Event::default().event("done").data("{}")),
70 })
71 });
72
73 Ok(Sse::new(stream).keep_alive(
74 KeepAlive::new()
75 .interval(Duration::from_secs(15))
76 .text("keep-alive"),
77 ))
78}