Skip to main content

shep_core/protocol/
frame.rs

1//! Anything the daemon writes to a connected client
2//!
3//! The server sends two kinds of frames on one socket: [`Reply`] (answers to
4//! requests) and [`BusEvent`] (broadcast events). This type decodes either,
5//! untagged on the wire, because their JSON key sets are disjoint
6//! (`id`/`result` vs `event`/`data`), at zero cost to the wire.
7//!
8//! Decoding reads keys until one names a frame kind, then hands the rest of
9//! the object to that kind's own decoder. Serde's `untagged` would buffer every
10//! payload into an owned tree first, which this avoids.
11
12use core::fmt;
13
14use serde::de::value::MapAccessDeserializer;
15use serde::de::{DeserializeSeed, Error as _, IgnoredAny, IntoDeserializer, MapAccess, Visitor};
16use serde::{Deserialize, Deserializer, Serialize};
17
18use crate::protocol::{BusEvent, Reply};
19
20/// Anything the daemon writes to a connected client
21///
22/// Round-trips to byte-identical output, since the daemon serializes
23/// `Reply`/`BusEvent` directly (pinned by `server_frame_is_byte_identical`).
24// wire format: changing existing variants is a breaking change
25#[derive(Debug, Clone, PartialEq, Serialize)]
26#[serde(untagged)]
27// Growth is anticipated (progress, flow control), and it is additive for this
28// enum only, never for the wire. `Kind::of` decides a frame's kind by key, so
29// a kind keyed by none of `id`, `result`, `event`, `data` fails to decode
30// here, and what a peer then does is `route_frame`'s rule in
31// `shep-client/src/actor.rs`: it drops an undecodable frame unless `reply_id`
32// can name the request it answers, and `reply_id` matches a frame only when it
33// carries `id` and `result` together. A new kind is therefore additive to an
34// older client when it carries neither half of that pair, one reply key alone
35// included: `id` without `result` (pinned by
36// `reply_id_is_none_for_a_future_progress_frame`) and `result` without `id`
37// are both dropped with the connection intact. A kind that carries both is
38// read as an undecodable reply and fails the waiting request with
39// `RequestError::Undecodable`. Pinned by `a_frame_with_no_known_key_is_refused`
40// below, and in the client by
41// `a_frame_of_a_kind_this_build_does_not_know_is_dropped_without_disturbing_the_connection`
42// and `a_frame_with_one_half_of_the_reply_pair_is_dropped_too`.
43#[non_exhaustive]
44pub enum ServerFrame {
45    /// An answer to one request (from [`Envelope`](crate::protocol::Envelope))
46    Reply(Reply),
47    /// One subscribed bus event
48    Event(BusEvent),
49}
50
51/// The frame kind a JSON key belongs to
52///
53/// A new frame kind adds a variant here, a row in [`Kind::of`] and an arm in
54/// [`FrameVisitor::visit_map`], which the compiler flags.
55enum Kind {
56    Reply,
57    Event,
58}
59
60impl Kind {
61    /// The kind owning `key`, with the key as a static string to replay
62    fn of(key: &str) -> Option<(Self, &'static str)> {
63        match key {
64            "id" => Some((Self::Reply, "id")),
65            "result" => Some((Self::Reply, "result")),
66            "event" => Some((Self::Event, "event")),
67            "data" => Some((Self::Event, "data")),
68            _ => None,
69        }
70    }
71}
72
73/// One top-level key: a known one costs no allocation, an unknown one owns its text
74enum Key {
75    Known(Kind, &'static str),
76    Other(String),
77}
78
79struct KeySeed;
80
81impl<'de> DeserializeSeed<'de> for KeySeed {
82    type Value = Key;
83
84    fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Key, D::Error> {
85        struct KeyVisitor;
86
87        impl Visitor<'_> for KeyVisitor {
88            type Value = Key;
89
90            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91                f.write_str("a string key")
92            }
93
94            fn visit_str<E>(self, key: &str) -> Result<Key, E> {
95                Ok(match Kind::of(key) {
96                    Some((kind, name)) => Key::Known(kind, name),
97                    None => Key::Other(key.to_owned()),
98                })
99            }
100        }
101
102        deserializer.deserialize_identifier(KeyVisitor)
103    }
104}
105
106/// A map handed to one kind's decoder: yields the key already read first, then
107/// the rest, and notes whether a reply key went by
108///
109/// A frame with a reply key after an event key is neither kind, and decoding
110/// it as an event would leave the request that `id` answers waiting.
111struct Keys<A> {
112    first: Option<&'static str>,
113    rest: A,
114    reply_key_seen: bool,
115}
116
117impl<'de, A: MapAccess<'de>> MapAccess<'de> for Keys<A> {
118    type Error = A::Error;
119
120    fn next_key_seed<K: DeserializeSeed<'de>>(
121        &mut self,
122        seed: K,
123    ) -> Result<Option<K::Value>, Self::Error> {
124        if let Some(key) = self.first.take() {
125            return seed.deserialize(key.into_deserializer()).map(Some);
126        }
127        match self.rest.next_key_seed(KeySeed)? {
128            None => Ok(None),
129            Some(Key::Known(kind, name)) => {
130                self.reply_key_seen |= matches!(kind, Kind::Reply);
131                seed.deserialize(name.into_deserializer()).map(Some)
132            }
133            Some(Key::Other(name)) => seed.deserialize(name.into_deserializer()).map(Some),
134        }
135    }
136
137    fn next_value_seed<V: DeserializeSeed<'de>>(
138        &mut self,
139        seed: V,
140    ) -> Result<V::Value, Self::Error> {
141        self.rest.next_value_seed(seed)
142    }
143}
144
145struct FrameVisitor;
146
147impl<'de> Visitor<'de> for FrameVisitor {
148    type Value = ServerFrame;
149
150    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.write_str("a reply (`id`, `result`) or a bus event (`event`, `data`)")
152    }
153
154    // A key no frame kind owns is skipped: a Reply has always tolerated those.
155    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<ServerFrame, A::Error> {
156        let (kind, key) = loop {
157            match map.next_key_seed(KeySeed)? {
158                Some(Key::Known(kind, name)) => break (kind, name),
159                Some(Key::Other(_)) => {
160                    map.next_value::<IgnoredAny>()?;
161                }
162                None => return Err(A::Error::custom("missing `id`/`result` or `event`/`data`")),
163            }
164        };
165        let mut keys = Keys {
166            first: Some(key),
167            rest: map,
168            reply_key_seen: false,
169        };
170        match kind {
171            Kind::Reply => {
172                Reply::deserialize(MapAccessDeserializer::new(keys)).map(ServerFrame::Reply)
173            }
174            Kind::Event => {
175                let event = BusEvent::deserialize(MapAccessDeserializer::new(&mut keys))?;
176                if keys.reply_key_seen {
177                    return Err(A::Error::custom("an event frame carries `id` or `result`"));
178                }
179                Ok(ServerFrame::Event(event))
180            }
181        }
182    }
183}
184
185impl<'de> Deserialize<'de> for ServerFrame {
186    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
187        deserializer.deserialize_map(FrameVisitor)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::protocol::{
195        BusEvent, ProcessEventKind, ProcessInfo, Reply, Response, RpcError, RpcErrorCode,
196        encode_frame,
197    };
198    use crate::status::ProcStatus;
199
200    fn sample_reply() -> Reply {
201        Reply {
202            id: 7,
203            result: Ok(Response::Pong),
204        }
205    }
206
207    fn sample_reply_with_id(id: u64) -> Reply {
208        Reply {
209            id,
210            ..sample_reply()
211        }
212    }
213
214    fn sample_event() -> BusEvent {
215        BusEvent::Process {
216            event: ProcessEventKind::Online,
217            info: ProcessInfo {
218                id: 3,
219                name: "web".to_string(),
220                status: ProcStatus::Online,
221                pid: Some(4242),
222                restarts: 0,
223                uptime_ms: 0,
224                fold: None,
225                depends_on: Vec::new(),
226                out_file: Some("/home/ada/.shep/logs/web-0-out.log".to_string()),
227                err_file: Some("/home/ada/.shep/logs/web-0-err.log".to_string()),
228                cpu_percent: None,
229                memory_bytes: None,
230                cpu_ms: None,
231                dog: None,
232                lambs: None,
233                last_exit: None,
234                smit: None,
235                instance: None,
236                handshook: None,
237                dog_stale: None,
238                pending: None,
239                overridden: None,
240                max_memory: None,
241                level_rules: Vec::new(),
242                reload_deadline_ms: None,
243                disabled: None,
244                log_timestamps: None,
245            },
246            manually: false,
247            at_ms: 1_700_000_000_000,
248        }
249    }
250
251    #[test]
252    fn server_frame_decodes_both_directions_of_the_stream() {
253        // The two shapes are disjoint: a Reply has no `event` key and an
254        // event has no `id`/`result` pair, so untagged never guesses wrong.
255        let reply = r#"{"id":1,"result":{"Ok":{"kind":"pong"}}}"#;
256        assert!(matches!(
257            serde_json::from_str::<ServerFrame>(reply).unwrap(),
258            ServerFrame::Reply(Reply { id: 1, .. })
259        ));
260        let event = r#"{"event":"log_out","data":{"id":3,"line":"ready"}}"#;
261        assert!(matches!(
262            serde_json::from_str::<ServerFrame>(event).unwrap(),
263            ServerFrame::Event(BusEvent::LogOut { id: 3, .. })
264        ));
265    }
266
267    #[test]
268    fn a_whole_payload_survives_the_trip_through_the_frame() {
269        let event = serde_json::to_string(&sample_event()).unwrap();
270        assert_eq!(
271            serde_json::from_str::<ServerFrame>(&event).unwrap(),
272            ServerFrame::Event(sample_event())
273        );
274        let reply = serde_json::to_string(&sample_reply()).unwrap();
275        assert_eq!(
276            serde_json::from_str::<ServerFrame>(&reply).unwrap(),
277            ServerFrame::Reply(sample_reply())
278        );
279    }
280
281    #[test]
282    fn a_malformed_reply_reports_its_own_failure() {
283        // Untagged buffering would swallow this into "did not match any variant".
284        let err =
285            serde_json::from_str::<ServerFrame>(r#"{"id":1,"result":{"Ok":{"kind":"bogus"}}}"#)
286                .unwrap_err()
287                .to_string();
288        assert!(!err.contains("untagged"), "{err}");
289        assert!(err.contains("bogus"), "{err}");
290    }
291
292    #[test]
293    fn a_frame_with_no_known_key_is_refused() {
294        let err = serde_json::from_str::<ServerFrame>(r#"{"unrelated":1}"#)
295            .unwrap_err()
296            .to_string();
297        assert!(err.contains("event"), "{err}");
298        assert!(err.contains("result"), "{err}");
299    }
300
301    #[test]
302    fn key_order_does_not_change_which_frame_decodes() {
303        let reply = r#"{"result":{"Ok":{"kind":"pong"}},"id":1}"#;
304        assert_eq!(
305            serde_json::from_str::<ServerFrame>(reply).unwrap(),
306            ServerFrame::Reply(sample_reply_with_id(1))
307        );
308        let event = r#"{"data":{"id":3,"line":"ready"},"event":"log_out"}"#;
309        assert!(matches!(
310            serde_json::from_str::<ServerFrame>(event).unwrap(),
311            ServerFrame::Event(BusEvent::LogOut { id: 3, .. })
312        ));
313    }
314
315    #[test]
316    fn a_frame_carrying_both_kinds_of_key_is_never_taken_for_an_event() {
317        // Untagged tried Reply first. The first key decides here, so an event
318        // key leading a reply key fails instead of routing the reply as an event.
319        let event_first = r#"{"event":"log_out","data":{"id":3,"line":"ready"},"id":1,"result":{"Ok":{"kind":"pong"}}}"#;
320        let decoded = serde_json::from_str::<ServerFrame>(event_first);
321        assert!(decoded.is_err(), "{decoded:?}");
322
323        let reply_first = r#"{"id":1,"result":{"Ok":{"kind":"pong"}},"event":"log_out","data":{"id":3,"line":"ready"}}"#;
324        assert_eq!(
325            serde_json::from_str::<ServerFrame>(reply_first).unwrap(),
326            ServerFrame::Reply(sample_reply_with_id(1))
327        );
328    }
329
330    #[test]
331    fn an_unknown_leading_key_does_not_hide_a_reply() {
332        // A Reply has always tolerated keys it does not know.
333        let reply = r#"{"extra":{"a":[1,2]},"id":1,"result":{"Ok":{"kind":"pong"}}}"#;
334        assert_eq!(
335            serde_json::from_str::<ServerFrame>(reply).unwrap(),
336            ServerFrame::Reply(sample_reply_with_id(1))
337        );
338    }
339
340    #[test]
341    fn server_frame_is_byte_identical_to_its_payload() {
342        // The daemon encodes Reply/BusEvent directly; if wrapping ever
343        // started adding bytes, every client would break at once.
344        let reply = sample_reply();
345        assert_eq!(
346            encode_frame(&ServerFrame::Reply(reply.clone())).unwrap(),
347            encode_frame(&reply).unwrap()
348        );
349        let event = sample_event();
350        assert_eq!(
351            encode_frame(&ServerFrame::Event(event.clone())).unwrap(),
352            encode_frame(&event).unwrap()
353        );
354    }
355
356    #[test]
357    fn an_error_reply_still_decodes_as_a_reply_frame() {
358        let err = Reply {
359            id: 2,
360            result: Err(RpcError {
361                code: RpcErrorCode::DeadlineExceeded,
362                message: "request deadline of 5000 ms expired".to_string(),
363                daemon_version: None,
364            }),
365        };
366        let json = serde_json::to_string(&err).unwrap();
367        assert_eq!(
368            serde_json::from_str::<ServerFrame>(&json).unwrap(),
369            ServerFrame::Reply(err)
370        );
371    }
372}