use crate::server::app::AppState;
use crate::server::body::read_json_object;
use crate::server::errors::{error, method_not_allowed};
use crate::server::routes::query::query_value;
use axum::body::Bytes;
use axum::extract::{Path, State};
use axum::http::{StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use nomoreide_core::log_volume::bucket_log_volume;
use nomoreide_core::metrics_store::RunningService;
use nomoreide_core::system_processes::{terminate_system_process, ManagedRoot};
use serde_json::{json, Value};
const LOG_TAIL: usize = 500;
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/api/metrics", get(activity))
.route(
"/api/services/:name/metrics",
get(service_series).fallback(method_not_allowed),
)
.route("/api/processes/terminate", post(terminate))
}
fn running_services(state: &AppState) -> Vec<RunningService> {
state
.runtime
.status()
.into_iter()
.filter(|status| {
serde_json::to_value(status.state)
.ok()
.and_then(|value| value.as_str().map(str::to_string))
.as_deref()
== Some("running")
})
.map(|status| RunningService {
name: status.name,
pid: status.pid.map(i64::from),
started_at: status.started_at,
})
.collect()
}
async fn activity(State(state): State<AppState>, uri: Uri) -> Response {
let running = running_services(&state);
let mut metrics = state.metrics.read_activity(&running);
if query_value(&uri, "includeProcesses").as_deref() != Some("1") {
if let Some(object) = metrics.as_object_mut() {
object.shift_remove("systemProcesses");
}
}
Json(json!({ "ok": true, "metrics": metrics })).into_response()
}
async fn service_series(State(state): State<AppState>, Path(name): Path<String>) -> Response {
let mut series = state.metrics.read(&name);
let sample_times: Vec<f64> = series
.get("samples")
.and_then(Value::as_array)
.map(|samples| {
samples
.iter()
.filter_map(|sample| sample.get("t").and_then(Value::as_f64))
.collect()
})
.unwrap_or_default();
let lines: Vec<(String, String)> = state
.runtime
.logs(&name, LOG_TAIL)
.into_iter()
.map(|line| (line.timestamp, line.text))
.collect();
if let Some(object) = series.as_object_mut() {
object.insert(
"logVolume".into(),
Value::Array(bucket_log_volume(&lines, &sample_times)),
);
}
Json(json!({ "ok": true, "metrics": series })).into_response()
}
async fn terminate(State(state): State<AppState>, body: Bytes) -> Response {
let payload = read_json_object(&body);
let pid = payload
.get("pid")
.and_then(Value::as_f64)
.unwrap_or(f64::NAN);
let expected = payload
.get("expectedCommand")
.and_then(Value::as_str)
.unwrap_or_default();
if !is_safe_integer(pid) || expected.is_empty() {
return error(
StatusCode::BAD_REQUEST,
"pid and expectedCommand are required",
);
}
let roots: Vec<ManagedRoot> = running_services(&state)
.into_iter()
.filter_map(|service| {
Some(ManagedRoot {
pid: service.pid?,
service: service.name,
})
})
.collect();
match terminate_system_process(pid, expected, &roots).await {
Ok(()) => Json(json!({ "ok": true })).into_response(),
Err(reason) => error(StatusCode::CONFLICT, &reason),
}
}
fn is_safe_integer(value: f64) -> bool {
value.is_finite() && value.fract() == 0.0 && value.abs() <= 9_007_199_254_740_991.0
}