use std::sync::Arc;
use chio_kernel::{ChioKernel, KernelError};
use serde::{Deserialize, Serialize};
use crate::routes::{
EMERGENCY_ADMIN_TOKEN_HEADER, EMERGENCY_RESUME_PATH, EMERGENCY_STATUS_PATH, EMERGENCY_STOP_PATH,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmergencyStopRequest {
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmergencyStopResponse {
pub stopped: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmergencyResumeResponse {
pub stopped: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmergencyStatusResponse {
pub stopped: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EmergencyHandlerError {
Unauthorized,
BadRequest(String),
Kernel(String),
}
impl EmergencyHandlerError {
#[must_use]
pub fn status(&self) -> u16 {
match self {
Self::Unauthorized => 401,
Self::BadRequest(_) => 400,
Self::Kernel(_) => 500,
}
}
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::Unauthorized => "unauthorized",
Self::BadRequest(_) => "bad_request",
Self::Kernel(_) => "internal_error",
}
}
#[must_use]
pub fn message(&self) -> String {
match self {
Self::Unauthorized => "missing or invalid X-Admin-Token header".to_string(),
Self::BadRequest(reason) | Self::Kernel(reason) => reason.clone(),
}
}
#[must_use]
pub fn body(&self) -> serde_json::Value {
serde_json::json!({
"error": self.code(),
"message": self.message(),
})
}
}
impl From<KernelError> for EmergencyHandlerError {
fn from(error: KernelError) -> Self {
Self::Kernel(error.to_string())
}
}
#[derive(Clone)]
pub struct EmergencyAdmin {
kernel: Arc<ChioKernel>,
expected_admin_token: String,
}
impl std::fmt::Debug for EmergencyAdmin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EmergencyAdmin")
.field("admin_token_len", &self.expected_admin_token.len())
.finish_non_exhaustive()
}
}
impl EmergencyAdmin {
#[must_use]
pub fn new(kernel: Arc<ChioKernel>, expected_admin_token: String) -> Self {
Self {
kernel,
expected_admin_token,
}
}
#[must_use]
pub fn kernel(&self) -> &Arc<ChioKernel> {
&self.kernel
}
fn authorize(&self, admin_token: Option<&str>) -> Result<(), EmergencyHandlerError> {
match admin_token {
Some(token) if token == self.expected_admin_token => Ok(()),
_ => Err(EmergencyHandlerError::Unauthorized),
}
}
}
pub fn handle_emergency_stop(
admin: &EmergencyAdmin,
admin_token: Option<&str>,
body: &[u8],
) -> Result<EmergencyStopResponse, EmergencyHandlerError> {
admin.authorize(admin_token)?;
let parsed: EmergencyStopRequest = serde_json::from_slice(body).map_err(|error| {
EmergencyHandlerError::BadRequest(format!("invalid emergency-stop request body: {error}"))
})?;
admin.kernel.emergency_stop(&parsed.reason)?;
Ok(EmergencyStopResponse { stopped: true })
}
pub fn handle_emergency_resume(
admin: &EmergencyAdmin,
admin_token: Option<&str>,
_body: &[u8],
) -> Result<EmergencyResumeResponse, EmergencyHandlerError> {
admin.authorize(admin_token)?;
admin.kernel.emergency_resume()?;
Ok(EmergencyResumeResponse { stopped: false })
}
pub fn handle_emergency_status(
admin: &EmergencyAdmin,
admin_token: Option<&str>,
) -> Result<EmergencyStatusResponse, EmergencyHandlerError> {
admin.authorize(admin_token)?;
let stopped = admin.kernel.is_emergency_stopped();
let since = admin
.kernel
.emergency_stopped_since()
.and_then(|unix_secs| {
let secs = i64::try_from(unix_secs).ok()?;
chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0).map(|dt| dt.to_rfc3339())
});
let reason = admin.kernel.emergency_stop_reason();
Ok(EmergencyStatusResponse {
stopped,
since,
reason,
})
}
pub use crate::routes::EMERGENCY_ADMIN_TOKEN_HEADER as ADMIN_TOKEN_HEADER;
pub use crate::routes::EMERGENCY_RESUME_PATH as RESUME_PATH;
pub use crate::routes::EMERGENCY_STATUS_PATH as STATUS_PATH;
pub use crate::routes::EMERGENCY_STOP_PATH as STOP_PATH;
const _: &str = EMERGENCY_STOP_PATH;
const _: &str = EMERGENCY_RESUME_PATH;
const _: &str = EMERGENCY_STATUS_PATH;
const _: &str = EMERGENCY_ADMIN_TOKEN_HEADER;