Skip to main content

omni_dev/daemon/
protocol.rs

1//! Wire types for the daemon's Unix-domain control socket.
2//!
3//! The control plane speaks newline-delimited JSON (NDJSON): one
4//! [`DaemonEnvelope`] request per line, one [`DaemonReply`] response per line.
5//! New optional fields use `#[serde(default, skip_serializing_if = ...)]` so
6//! older peers stay byte-compatible on the wire.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use super::service::ServiceStatus;
12
13/// The reserved service name for the daemon's own built-in operations
14/// (`ping`, `status`, `shutdown`). A `None` `service` targets the same.
15pub const DAEMON_SERVICE: &str = "daemon";
16
17/// Maximum length, in bytes, of a single NDJSON line on the control socket.
18///
19/// Applies to both requests and replies, capping the per-connection read buffer
20/// so a peer that never sends a newline can't exhaust memory. 1 MiB is far above
21/// any real envelope.
22pub const MAX_LINE_BYTES: usize = 1024 * 1024;
23
24/// A request sent to the daemon over the control socket.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DaemonEnvelope {
27    /// Target service [`name`](super::service::DaemonService::name). `None`
28    /// (or `"daemon"`) routes to the built-in daemon ops.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub service: Option<String>,
31    /// Operation name, interpreted by the target service (or the daemon).
32    pub op: String,
33    /// Operation payload; `null` when the op takes no arguments.
34    #[serde(default, skip_serializing_if = "Value::is_null")]
35    pub payload: Value,
36}
37
38impl DaemonEnvelope {
39    /// Builds an envelope targeting a named service.
40    pub fn service(name: impl Into<String>, op: impl Into<String>, payload: Value) -> Self {
41        Self {
42            service: Some(name.into()),
43            op: op.into(),
44            payload,
45        }
46    }
47
48    /// Builds an envelope targeting the built-in daemon ops.
49    pub fn builtin(op: impl Into<String>) -> Self {
50        Self {
51            service: None,
52            op: op.into(),
53            payload: Value::Null,
54        }
55    }
56}
57
58/// A response returned by the daemon over the control socket.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct DaemonReply {
61    /// Whether the operation succeeded.
62    pub ok: bool,
63    /// Success payload; `null` for ops that return nothing.
64    #[serde(default, skip_serializing_if = "Value::is_null")]
65    pub payload: Value,
66    /// Error message when [`ok`](Self::ok) is `false`.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub error: Option<String>,
69}
70
71impl DaemonReply {
72    /// Builds a successful reply carrying `payload`.
73    pub fn ok(payload: Value) -> Self {
74        Self {
75            ok: true,
76            payload,
77            error: None,
78        }
79    }
80
81    /// Builds a failure reply carrying an error message.
82    pub fn err(message: impl Into<String>) -> Self {
83        Self {
84            ok: false,
85            payload: Value::Null,
86            error: Some(message.into()),
87        }
88    }
89}
90
91/// The payload of a built-in `status` reply: per-service status snapshots.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct StatusReport {
94    /// One entry per registered service, in registration order.
95    pub services: Vec<ServiceStatus>,
96}