1use std::fmt;
2
3use base64::Engine;
4use base64::engine::general_purpose::STANDARD as BASE64;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7use crate::{CommandNumber, SandboxId, Timestamp};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
12#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
13#[serde(transparent)]
14pub struct RunId(i64);
15
16impl RunId {
17 pub fn new(id: i64) -> Self {
18 Self(id)
19 }
20
21 pub fn get(self) -> i64 {
22 self.0
23 }
24}
25
26impl fmt::Display for RunId {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 self.0.fmt(f)
29 }
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
35#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
36pub struct Run {
37 pub id: RunId,
38 pub replay_of: Option<RunId>,
40 pub state: RunState,
41 pub runner: Option<String>,
43 pub created_at: Timestamp,
45 pub started_at: Option<Timestamp>,
46 pub ended_at: Option<Timestamp>,
47 pub log_bytes: Option<u64>,
50 pub log_outdated: bool,
53 pub verdict: Option<ReplayVerdict>,
55 pub version: u64,
58}
59
60impl Run {
61 pub fn supersedes(&self, shown: &Run) -> bool {
65 self.id == shown.id && self.version >= shown.version
66 }
67
68 pub fn sandbox(&self) -> SandboxId {
70 SandboxId::new(self.replay_of.unwrap_or(self.id).get())
71 }
72
73 pub(crate) fn replayable_if_whole(&self, whole: bool) -> Result<(), String> {
78 if let Some(recorded) = self.replay_of {
79 return Err(format!("run {} is a replay of run {recorded}", self.id));
80 }
81 if !whole {
82 return Err(format!(
83 "run {} didn't exit, so its log is cut short",
84 self.id
85 ));
86 }
87 if self.log_bytes.is_none() {
88 return Err(format!("run {} has no log", self.id));
89 }
90 if self.log_outdated {
91 return Err(format!(
92 "run {}'s log was made by another dvm than its runner's now: run it again",
93 self.id
94 ));
95 }
96 Ok(())
97 }
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
106#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
107#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
108#[serde(tag = "kind", rename_all = "snake_case")]
109pub enum ReplayVerdict {
110 Same,
112 OtherExit {
114 recorded: i32,
115 replayed: i32,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 command: Option<CommandNumber>,
118 },
119 OtherOutput {
122 stream: OutputStream,
123 at: u64,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 command: Option<CommandNumber>,
126 },
127 OtherCommand { command: CommandNumber },
130}
131
132impl ReplayVerdict {
133 pub fn is_same(&self) -> bool {
135 matches!(self, Self::Same)
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
141#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
142#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
143#[serde(rename_all = "snake_case")]
144pub enum OutputStream {
145 Stdout,
146 Stderr,
147}
148
149impl OutputStream {
150 pub fn name(self) -> &'static str {
152 match self {
153 Self::Stdout => "stdout",
154 Self::Stderr => "stderr",
155 }
156 }
157}
158
159#[derive(Clone, Debug, Default, PartialEq, Eq)]
162#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
163#[cfg_attr(
164 feature = "typescript",
165 ts(export_to = "controller-api.ts", type = "string")
166)]
167pub struct ByteChunk(Vec<u8>);
168
169impl ByteChunk {
170 pub fn new(bytes: Vec<u8>) -> Self {
171 Self(bytes)
172 }
173
174 pub fn bytes(&self) -> &[u8] {
175 &self.0
176 }
177}
178
179impl Serialize for ByteChunk {
180 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
181 serializer.serialize_str(&BASE64.encode(&self.0))
182 }
183}
184
185impl<'de> Deserialize<'de> for ByteChunk {
186 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
187 let encoded = String::deserialize(deserializer)?;
188 BASE64
189 .decode(encoded)
190 .map(Self)
191 .map_err(serde::de::Error::custom)
192 }
193}
194
195#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
198#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
199#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
200pub struct RunOutput {
201 pub stdout: String,
202 pub stderr: String,
203}
204
205#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
212#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
213#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
214#[serde(tag = "kind", rename_all = "snake_case")]
215pub enum RunState {
216 Queued,
219 Running,
220 Paused,
221 Exited {
223 code: i32,
224 },
225 Stopped,
227 Failed {
229 reason: String,
230 },
231}
232
233impl RunState {
234 pub fn kind(&self) -> &'static str {
236 match self {
237 Self::Queued => "queued",
238 Self::Running => "running",
239 Self::Paused => "paused",
240 Self::Exited { .. } => "exited",
241 Self::Stopped => "stopped",
242 Self::Failed { .. } => "failed",
243 }
244 }
245
246 pub fn is_active(&self) -> bool {
248 matches!(self, Self::Queued | Self::Running | Self::Paused)
249 }
250
251 pub fn after(&self, action: RunAction) -> Result<Self, InvalidRunAction> {
254 match (self, action) {
255 (Self::Running, RunAction::Pause) => Ok(Self::Paused),
256 (Self::Paused, RunAction::Resume) => Ok(Self::Running),
257 (Self::Queued | Self::Running | Self::Paused, RunAction::Stop) => Ok(Self::Stopped),
258 _ => Err(InvalidRunAction {
259 action,
260 state: self.kind(),
261 }),
262 }
263 }
264
265 pub fn allows(&self, action: RunAction) -> bool {
266 self.after(action).is_ok()
267 }
268}
269
270#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
274#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
275#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
276#[serde(rename_all = "snake_case")]
277pub enum RunAction {
278 Pause,
279 Resume,
280 Stop,
281}
282
283impl fmt::Display for RunAction {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 f.write_str(match self {
286 Self::Pause => "pause",
287 Self::Resume => "resume",
288 Self::Stop => "stop",
289 })
290 }
291}
292
293#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
295#[error("can't {action} a run that is {state}")]
296pub struct InvalidRunAction {
297 action: RunAction,
298 state: &'static str,
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn actions_follow_the_state_machine() {
307 use RunAction::*;
308 let exited = RunState::Exited { code: 0 };
309 let failed = RunState::Failed {
310 reason: "unserved syscall".to_owned(),
311 };
312 let cases = [
313 (RunState::Queued, Pause, None),
314 (RunState::Queued, Resume, None),
315 (RunState::Queued, Stop, Some(RunState::Stopped)),
316 (RunState::Running, Pause, Some(RunState::Paused)),
317 (RunState::Running, Resume, None),
318 (RunState::Running, Stop, Some(RunState::Stopped)),
319 (RunState::Paused, Pause, None),
320 (RunState::Paused, Resume, Some(RunState::Running)),
321 (RunState::Paused, Stop, Some(RunState::Stopped)),
322 (exited.clone(), Stop, None),
323 (RunState::Stopped, Stop, None),
324 (failed.clone(), Resume, None),
325 ];
326 for (state, action, after) in cases {
327 assert_eq!(state.after(action).ok(), after, "{action} from {state:?}");
328 }
329 assert!(!exited.is_active() && !failed.is_active());
330 }
331
332 #[test]
333 fn a_refusal_says_why() {
334 let error = RunState::Queued.after(RunAction::Pause).unwrap_err();
335 assert_eq!(error.to_string(), "can't pause a run that is queued");
336 }
337
338 #[test]
339 fn serializes_tagged() {
340 let json = serde_json::to_string(&RunState::Exited { code: 3 }).unwrap();
341 assert_eq!(json, r#"{"kind":"exited","code":3}"#);
342 assert_eq!(
343 serde_json::to_string(&RunAction::Resume).unwrap(),
344 r#""resume""#
345 );
346 }
347}