Skip to main content

steamroom_cli/daemon/proto/
mod.rs

1//! Wire types for the daemon RPC. Owned, rkyv-archivable; never contain
2//! `PathBuf`, `Regex`, or other types that rkyv cannot archive directly.
3
4mod event;
5mod frame;
6mod params;
7mod request;
8mod response;
9mod status;
10
11pub use event::Event;
12pub use frame::Frame;
13pub use frame::PROTO_VERSION;
14pub use params::*;
15pub use request::Request;
16pub use response::ErrorKind;
17pub use response::Response;
18pub use status::JobRecord;
19pub use status::StatusSnapshot;
20
21use rkyv::Archive;
22use rkyv::Deserialize;
23use rkyv::Serialize;
24
25/// Monotonically increasing identifier minted by the daemon. Stable for
26/// the daemon's lifetime; zero is reserved as "not a job".
27#[derive(
28    Archive,
29    Serialize,
30    Deserialize,
31    Debug,
32    Clone,
33    Copy,
34    PartialEq,
35    Eq,
36    Hash,
37    serde::Serialize,
38    serde::Deserialize,
39)]
40#[rkyv(derive(Debug))]
41pub struct JobId(pub u64);
42
43impl std::fmt::Display for JobId {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "#{}", self.0)
46    }
47}
48
49/// Discriminator for what kind of work a job represents. Used in
50/// `StatusSnapshot` rendering and the TUI's queue/active panes.
51#[derive(
52    Archive,
53    Serialize,
54    Deserialize,
55    Debug,
56    Clone,
57    Copy,
58    PartialEq,
59    Eq,
60    serde::Serialize,
61    serde::Deserialize,
62)]
63#[rkyv(derive(Debug))]
64pub enum JobKind {
65    Download,
66    Info,
67    Files,
68    Manifests,
69    Diff,
70    Packages,
71    SaveManifest,
72    Workshop,
73    LocalInfo,
74}
75
76/// Output format selector. The clap-derived variant in `cli.rs` is the
77/// CLI's source of truth; this is the wire-format mirror. Convert with
78/// `From<crate::cli::OutputFormat>` (defined here below).
79#[derive(Archive, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
80#[rkyv(derive(Debug))]
81pub enum OutputFormat {
82    Table,
83    Json,
84    Plain,
85}
86
87impl From<crate::cli::OutputFormat> for OutputFormat {
88    fn from(v: crate::cli::OutputFormat) -> Self {
89        match v {
90            crate::cli::OutputFormat::Table => Self::Table,
91            crate::cli::OutputFormat::Json => Self::Json,
92            crate::cli::OutputFormat::Plain => Self::Plain,
93        }
94    }
95}
96
97impl From<OutputFormat> for crate::cli::OutputFormat {
98    fn from(v: OutputFormat) -> Self {
99        match v {
100            OutputFormat::Table => Self::Table,
101            OutputFormat::Json => Self::Json,
102            OutputFormat::Plain => Self::Plain,
103        }
104    }
105}
106
107/// Mirror of `tracing::Level`. Used so attached clients can run the
108/// daemon's tracing events through their own subscriber.
109#[derive(Archive, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
110#[rkyv(derive(Debug))]
111pub enum LogLevel {
112    Error,
113    Warn,
114    Info,
115    Debug,
116    Trace,
117}
118
119impl From<tracing::Level> for LogLevel {
120    fn from(l: tracing::Level) -> Self {
121        match l {
122            tracing::Level::ERROR => Self::Error,
123            tracing::Level::WARN => Self::Warn,
124            tracing::Level::INFO => Self::Info,
125            tracing::Level::DEBUG => Self::Debug,
126            tracing::Level::TRACE => Self::Trace,
127        }
128    }
129}
130
131/// Per-job progress snapshot, emitted by the worker as `Event::Progress`.
132#[derive(Archive, Serialize, Deserialize, Debug, Clone, serde::Serialize, serde::Deserialize)]
133#[rkyv(derive(Debug))]
134pub struct ProgressUpdate {
135    pub bytes_done: u64,
136    pub bytes_total: u64,
137    pub files_done: u32,
138    pub files_total: u32,
139    pub rate_bytes_per_sec: u64,
140    pub eta_seconds: u32,
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn job_id_displays_with_hash_prefix() {
149        assert_eq!(format!("{}", JobId(42)), "#42");
150    }
151
152    #[test]
153    fn output_format_round_trips_through_cli_enum() {
154        for w in [OutputFormat::Table, OutputFormat::Json, OutputFormat::Plain] {
155            let cli: crate::cli::OutputFormat = w.into();
156            let back: OutputFormat = cli.into();
157            assert_eq!(w, back);
158        }
159    }
160
161    #[test]
162    fn log_level_maps_from_tracing() {
163        assert_eq!(LogLevel::from(tracing::Level::ERROR), LogLevel::Error);
164        assert_eq!(LogLevel::from(tracing::Level::TRACE), LogLevel::Trace);
165    }
166
167    #[test]
168    fn frame_round_trips_response_jobaccepted() {
169        let f = Frame::Response(Response::JobAccepted {
170            job_id: JobId(7),
171            position: 0,
172        });
173        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&f).unwrap();
174        let back = rkyv::from_bytes::<Frame, rkyv::rancor::Error>(&bytes).unwrap();
175        match back {
176            Frame::Response(Response::JobAccepted { job_id, position }) => {
177                assert_eq!(job_id, JobId(7));
178                assert_eq!(position, 0);
179            }
180            other => panic!("wrong frame: {other:?}"),
181        }
182    }
183
184    #[test]
185    fn frame_round_trips_event_log() {
186        let f = Frame::Event(Event::Log {
187            job_id: Some(JobId(3)),
188            level: LogLevel::Warn,
189            target: "steamroom_cli".into(),
190            message: "stale".into(),
191        });
192        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&f).unwrap();
193        let _back = rkyv::from_bytes::<Frame, rkyv::rancor::Error>(&bytes).unwrap();
194    }
195
196    #[test]
197    fn event_job_id_routes_correctly() {
198        let scoped = Event::Stdout {
199            job_id: JobId(9),
200            line: "x".into(),
201        };
202        assert_eq!(scoped.job_id(), Some(JobId(9)));
203        let qc = Event::QueueChanged {
204            snapshot: StatusSnapshot {
205                daemon_pid: 1,
206                daemon_started_at: 0,
207                account: None,
208                active: None,
209                queue: vec![],
210                recent: vec![],
211            },
212        };
213        assert_eq!(qc.job_id(), None);
214    }
215}