Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I5cf55cc4cb558c3f9f764c71224e87176a6a6964
139 lines
4 KiB
Rust
139 lines
4 KiB
Rust
use axum::{
|
|
Router,
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::{
|
|
IntoResponse,
|
|
Response,
|
|
Sse,
|
|
sse::{Event, KeepAlive},
|
|
},
|
|
routing::get,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
use crate::{error::ApiError, state::AppState};
|
|
|
|
async fn get_build_log(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Response, ApiError> {
|
|
// Verify build exists
|
|
let _build = fc_common::repo::builds::get(&state.pool, id)
|
|
.await
|
|
.map_err(ApiError)?;
|
|
|
|
let log_storage =
|
|
fc_common::log_storage::LogStorage::new(state.config.logs.log_dir.clone())
|
|
.map_err(|e| ApiError(fc_common::CiError::Io(e)))?;
|
|
|
|
match log_storage.read_log(&id) {
|
|
Ok(Some(content)) => {
|
|
Ok(
|
|
(
|
|
StatusCode::OK,
|
|
[("content-type", "text/plain; charset=utf-8")],
|
|
content,
|
|
)
|
|
.into_response(),
|
|
)
|
|
},
|
|
Ok(None) => {
|
|
Ok(
|
|
(StatusCode::NOT_FOUND, "No log available for this build")
|
|
.into_response(),
|
|
)
|
|
},
|
|
Err(e) => Err(ApiError(fc_common::CiError::Io(e))),
|
|
}
|
|
}
|
|
|
|
async fn stream_build_log(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<
|
|
Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>>,
|
|
ApiError,
|
|
> {
|
|
let build = fc_common::repo::builds::get(&state.pool, id)
|
|
.await
|
|
.map_err(ApiError)?;
|
|
|
|
let log_storage =
|
|
fc_common::log_storage::LogStorage::new(state.config.logs.log_dir.clone())
|
|
.map_err(|e| ApiError(fc_common::CiError::Io(e)))?;
|
|
|
|
let active_path = log_storage.log_path_for_active(&id);
|
|
let final_path = log_storage.log_path(&id);
|
|
let pool = state.pool.clone();
|
|
let build_id = build.id;
|
|
|
|
let stream = async_stream::stream! {
|
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
|
|
|
// Determine which file to read
|
|
let path = if active_path.exists() {
|
|
active_path.clone()
|
|
} else if final_path.exists() {
|
|
final_path.clone()
|
|
} else {
|
|
// Wait for the file to appear
|
|
let mut found = false;
|
|
for _ in 0..30 {
|
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
if active_path.exists() || final_path.exists() {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if !found {
|
|
yield Ok(Event::default().data("No log file available"));
|
|
return;
|
|
}
|
|
if active_path.exists() { active_path.clone() } else { final_path.clone() }
|
|
};
|
|
|
|
let Ok(file) = tokio::fs::File::open(&path).await else {
|
|
yield Ok(Event::default().data("Failed to open log file"));
|
|
return;
|
|
};
|
|
|
|
let mut reader = BufReader::new(file);
|
|
let mut line = String::new();
|
|
let mut consecutive_empty = 0u32;
|
|
|
|
loop {
|
|
line.clear();
|
|
match reader.read_line(&mut line).await {
|
|
Ok(0) => {
|
|
// EOF - check if build is still running
|
|
consecutive_empty += 1;
|
|
if consecutive_empty > 5 {
|
|
// Check build status
|
|
if let Ok(b) = fc_common::repo::builds::get(&pool, build_id).await
|
|
&& b.status != fc_common::models::BuildStatus::Running
|
|
&& b.status != fc_common::models::BuildStatus::Pending {
|
|
yield Ok(Event::default().event("done").data("Build completed"));
|
|
return;
|
|
}
|
|
consecutive_empty = 0;
|
|
}
|
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
|
}
|
|
Ok(_) => {
|
|
consecutive_empty = 0;
|
|
yield Ok(Event::default().data(line.trim_end()));
|
|
}
|
|
Err(_) => return,
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
|
|
}
|
|
|
|
pub fn router() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/builds/{id}/log", get(get_build_log))
|
|
.route("/builds/{id}/log/stream", get(stream_build_log))
|
|
}
|