use crate::dashboard::error::DashboardError;
use crate::dashboard::model::{
ControlCommandRequest, ControlCommandResult, DashboardState, EventRecord, LogRecord,
TargetProcessRegistration,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const DASHBOARD_IPC_PROTOCOL_VERSION: &str = "dashboard-ipc.v1";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IpcRequest {
pub request_id: String,
pub method: String,
#[serde(default)]
pub params: Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IpcMethod {
Hello,
CurrentState,
EventsSubscribe,
LogsTail,
CommandRestartChild,
CommandPauseChild,
CommandResumeChild,
CommandQuarantineChild,
CommandRemoveChild,
CommandAddChild,
CommandShutdownTree,
}
impl IpcMethod {
pub fn parse(method: &str) -> Result<Self, DashboardError> {
match method {
"hello" => Ok(Self::Hello),
"state" => Ok(Self::CurrentState),
"events.subscribe" => Ok(Self::EventsSubscribe),
"logs.tail" => Ok(Self::LogsTail),
"command.restart_child" => Ok(Self::CommandRestartChild),
"command.pause_child" => Ok(Self::CommandPauseChild),
"command.resume_child" => Ok(Self::CommandResumeChild),
"command.quarantine_child" => Ok(Self::CommandQuarantineChild),
"command.remove_child" => Ok(Self::CommandRemoveChild),
"command.add_child" => Ok(Self::CommandAddChild),
"command.shutdown_tree" => Ok(Self::CommandShutdownTree),
_ => Err(DashboardError::unsupported_method(method)),
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Hello => "hello",
Self::CurrentState => "state",
Self::EventsSubscribe => "events.subscribe",
Self::LogsTail => "logs.tail",
Self::CommandRestartChild => "command.restart_child",
Self::CommandPauseChild => "command.pause_child",
Self::CommandResumeChild => "command.resume_child",
Self::CommandQuarantineChild => "command.quarantine_child",
Self::CommandRemoveChild => "command.remove_child",
Self::CommandAddChild => "command.add_child",
Self::CommandShutdownTree => "command.shutdown_tree",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum IpcResult {
Hello {
protocol_version: String,
registration: TargetProcessRegistration,
},
State {
target_id: String,
state: Box<DashboardState>,
},
Subscription {
target_id: String,
subscription: String,
},
CommandResult {
target_id: String,
result: ControlCommandResult,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IpcResponse {
pub request_id: String,
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<IpcResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<DashboardError>,
}
impl IpcResponse {
pub fn ok(request_id: impl Into<String>, result: IpcResult) -> Self {
Self {
request_id: request_id.into(),
ok: true,
result: Some(result),
error: None,
}
}
pub fn error(request_id: impl Into<String>, error: DashboardError) -> Self {
Self {
request_id: request_id.into(),
ok: false,
result: None,
error: Some(error),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum IpcServerPush {
Event {
target_id: String,
event: EventRecord,
},
Log {
target_id: String,
log: LogRecord,
},
StateDelta {
target_id: String,
delta: Value,
},
Error {
error: DashboardError,
},
}
pub fn parse_request_line(line: &str) -> Result<IpcRequest, DashboardError> {
let request: IpcRequest = serde_json::from_str(line).map_err(|error| {
DashboardError::new(
"invalid_json",
"protocol_parse",
None,
format!("failed to parse IPC JSON request: {error}"),
false,
)
})?;
IpcMethod::parse(&request.method)?;
Ok(request)
}
pub fn response_to_line(response: &IpcResponse) -> Result<String, DashboardError> {
let mut line = serde_json::to_string(response).map_err(|error| {
DashboardError::new(
"serialization_failed",
"protocol_write",
response
.error
.as_ref()
.and_then(|error| error.target_id.clone()),
format!("failed to serialize IPC response: {error}"),
false,
)
})?;
line.push('\n');
Ok(line)
}
pub fn decode_command_params(
request: &IpcRequest,
) -> Result<ControlCommandRequest, DashboardError> {
serde_json::from_value(request.params.clone()).map_err(|error| {
DashboardError::new(
"invalid_command_params",
"protocol_parse",
None,
format!("failed to parse command params: {error}"),
false,
)
})
}