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)]
30struct DonePayload {
31 exit_code: Option<i64>,
32 oom_killed: bool,
33 timed_out: bool,
34 duration_ms: u64,
35}
36
37pub async fn exec_stream(
40 Query(q): Query<StreamQuery>,
41) -> Result<Sse<impl futures::Stream<Item = Result<Event, Infallible>>>, (StatusCode, String)> {
42 let (_, entry) = EXEC_STREAMS
45 .remove(&q.task_id)
46 .ok_or((StatusCode::NOT_FOUND, "任务不存在或已结束".to_string()))?;
47
48 let stream = ReceiverStream::new(entry.rx).map(|chunk| {
49 Ok::<_, Infallible>(match chunk {
50 OutputChunk::Stdout(s) => Event::default().event("stdout").data(s),
51 OutputChunk::Stderr(s) => Event::default().event("stderr").data(s),
52 OutputChunk::Done {
53 exit_code,
54 oom_killed,
55 timed_out,
56 duration_ms,
57 } => Event::default()
58 .event("done")
59 .json_data(DonePayload {
60 exit_code,
61 oom_killed,
62 timed_out,
63 duration_ms,
64 })
65 .unwrap_or_else(|_| Event::default().event("done").data("{}")),
66 })
67 });
68
69 Ok(Sse::new(stream).keep_alive(
70 KeepAlive::new()
71 .interval(Duration::from_secs(15))
72 .text("keep-alive"),
73 ))
74}