//! SSE 流式输出端点。`GET /api/exec/stream?task_id=X`。 //! //! 鉴权 + 限流已在 [`super::execute::start_exec_stream`] 完成(校验链),此处只 //! 校验 task 存在。task_id 是 UUID,不可枚举;只有过了校验的调用者才知道 task_id。 //! //! 前端用原生 EventSource 连接本端点(`web_sys::EventSource`),按 event 类型 //! 分发:`stdout` → 终端 writeStdout,`stderr` → writeStderr,`done` → 终态收尾。 //! keep-alive comment 每 15s 一次,防反向代理超时关闭空闲连接。 use std::convert::Infallible; use std::time::Duration; use axum::extract::Query; use axum::http::StatusCode; use axum::response::sse::{Event, KeepAlive, Sse}; use futures::StreamExt; use tokio_stream::wrappers::ReceiverStream; use crate::api::code_runner::progress::EXEC_STREAMS; use crate::infra::docker::OutputChunk; /// SSE 查询参数:`?task_id=X`。 #[derive(serde::Deserialize)] pub struct StreamQuery { pub task_id: String, } /// done 事件的 JSON payload。 #[derive(serde::Serialize)] struct DonePayload { exit_code: Option, oom_killed: bool, timed_out: bool, duration_ms: u64, } /// SSE handler:从 EXEC_STREAMS 取出 receiver(取出即移除,防重复连接), /// 包成 ReceiverStream,映射成 SSE Event 流。 pub async fn exec_stream( Query(q): Query, ) -> Result>>, (StatusCode, String)> { // 取出 rx(取出即移除):同一 task_id 只能连一次 SSE, // 防止多客户端或重连导致 receiver 被多次消费。 let (_, entry) = EXEC_STREAMS .remove(&q.task_id) .ok_or((StatusCode::NOT_FOUND, "任务不存在或已结束".to_string()))?; let stream = ReceiverStream::new(entry.rx).map(|chunk| { Ok::<_, Infallible>(match chunk { OutputChunk::Stdout(s) => Event::default().event("stdout").data(s), OutputChunk::Stderr(s) => Event::default().event("stderr").data(s), OutputChunk::Done { exit_code, oom_killed, timed_out, duration_ms, } => Event::default() .event("done") .json_data(DonePayload { exit_code, oom_killed, timed_out, duration_ms, }) .unwrap_or_else(|_| Event::default().event("done").data("{}")), }) }); Ok(Sse::new(stream).keep_alive( KeepAlive::new() .interval(Duration::from_secs(15)) .text("keep-alive"), )) }