xfy e50941ef24 fix(runner): propagate duration_ms through SSE done event
The SSE streaming path hardcoded '耗时: -' because duration_ms was
never threaded through:

- docker.rs: OutputChunk::Done now carries duration_ms, computed from
  start_container to wait completion via Instant::elapsed
- sse.rs: DonePayload gains duration_ms, serialized into the done event
- runner.rs: WASM done callback formats the real duration instead of '-'

The polling fallback path already had correct duration via ExecResult;
only the SSE path was missing it.
2026-07-13 10:09:15 +08:00

75 lines
2.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<i64>,
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<StreamQuery>,
) -> Result<Sse<impl futures::Stream<Item = Result<Event, Infallible>>>, (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"),
))
}