rust_supervisor/dashboard/
protocol.rs1use crate::dashboard::error::DashboardError;
7use crate::dashboard::model::{
8 ControlCommandRequest, ControlCommandResult, DashboardState, EventRecord, LogRecord,
9 TargetProcessRegistration,
10};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14pub const DASHBOARD_IPC_PROTOCOL_VERSION: &str = "dashboard-ipc.v1";
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct IpcRequest {
20 pub request_id: String,
22 pub method: String,
24 #[serde(default)]
26 pub params: Value,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum IpcMethod {
32 Hello,
34 CurrentState,
36 EventsSubscribe,
38 LogsTail,
40 CommandRestartChild,
42 CommandPauseChild,
44 CommandResumeChild,
46 CommandQuarantineChild,
48 CommandRemoveChild,
50 CommandAddChild,
52 CommandShutdownTree,
54}
55
56impl IpcMethod {
57 pub fn parse(method: &str) -> Result<Self, DashboardError> {
67 match method {
68 "hello" => Ok(Self::Hello),
69 "state" => Ok(Self::CurrentState),
70 "events.subscribe" => Ok(Self::EventsSubscribe),
71 "logs.tail" => Ok(Self::LogsTail),
72 "command.restart_child" => Ok(Self::CommandRestartChild),
73 "command.pause_child" => Ok(Self::CommandPauseChild),
74 "command.resume_child" => Ok(Self::CommandResumeChild),
75 "command.quarantine_child" => Ok(Self::CommandQuarantineChild),
76 "command.remove_child" => Ok(Self::CommandRemoveChild),
77 "command.add_child" => Ok(Self::CommandAddChild),
78 "command.shutdown_tree" => Ok(Self::CommandShutdownTree),
79 _ => Err(DashboardError::unsupported_method(method)),
80 }
81 }
82
83 pub fn as_str(&self) -> &'static str {
93 match self {
94 Self::Hello => "hello",
95 Self::CurrentState => "state",
96 Self::EventsSubscribe => "events.subscribe",
97 Self::LogsTail => "logs.tail",
98 Self::CommandRestartChild => "command.restart_child",
99 Self::CommandPauseChild => "command.pause_child",
100 Self::CommandResumeChild => "command.resume_child",
101 Self::CommandQuarantineChild => "command.quarantine_child",
102 Self::CommandRemoveChild => "command.remove_child",
103 Self::CommandAddChild => "command.add_child",
104 Self::CommandShutdownTree => "command.shutdown_tree",
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111#[serde(tag = "type", rename_all = "snake_case")]
112pub enum IpcResult {
113 Hello {
115 protocol_version: String,
117 registration: TargetProcessRegistration,
119 },
120 State {
122 target_id: String,
124 state: Box<DashboardState>,
126 },
127 Subscription {
129 target_id: String,
131 subscription: String,
133 },
134 CommandResult {
136 target_id: String,
138 result: ControlCommandResult,
140 },
141}
142
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct IpcResponse {
146 pub request_id: String,
148 pub ok: bool,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub result: Option<IpcResult>,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub error: Option<DashboardError>,
156}
157
158impl IpcResponse {
159 pub fn ok(request_id: impl Into<String>, result: IpcResult) -> Self {
170 Self {
171 request_id: request_id.into(),
172 ok: true,
173 result: Some(result),
174 error: None,
175 }
176 }
177
178 pub fn error(request_id: impl Into<String>, error: DashboardError) -> Self {
189 Self {
190 request_id: request_id.into(),
191 ok: false,
192 result: None,
193 error: Some(error),
194 }
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200#[serde(tag = "type", rename_all = "snake_case")]
201pub enum IpcServerPush {
202 Event {
204 target_id: String,
206 event: EventRecord,
208 },
209 Log {
211 target_id: String,
213 log: LogRecord,
215 },
216 StateDelta {
218 target_id: String,
220 delta: Value,
222 },
223 Error {
225 error: DashboardError,
227 },
228}
229
230pub fn parse_request_line(line: &str) -> Result<IpcRequest, DashboardError> {
240 let request: IpcRequest = serde_json::from_str(line).map_err(|error| {
241 DashboardError::new(
242 "invalid_json",
243 "protocol_parse",
244 None,
245 format!("failed to parse IPC JSON request: {error}"),
246 false,
247 )
248 })?;
249 IpcMethod::parse(&request.method)?;
250 Ok(request)
251}
252
253pub fn response_to_line(response: &IpcResponse) -> Result<String, DashboardError> {
263 let mut line = serde_json::to_string(response).map_err(|error| {
264 DashboardError::new(
265 "serialization_failed",
266 "protocol_write",
267 response
268 .error
269 .as_ref()
270 .and_then(|error| error.target_id.clone()),
271 format!("failed to serialize IPC response: {error}"),
272 false,
273 )
274 })?;
275 line.push('\n');
276 Ok(line)
277}
278
279pub fn decode_command_params(
289 request: &IpcRequest,
290) -> Result<ControlCommandRequest, DashboardError> {
291 serde_json::from_value(request.params.clone()).map_err(|error| {
292 DashboardError::new(
293 "invalid_command_params",
294 "protocol_parse",
295 None,
296 format!("failed to parse command params: {error}"),
297 false,
298 )
299 })
300}