Skip to main content

dvm_api/
run.rs

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/// A run's id, the controller's.
10#[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/// A sandbox's run, of its spec, recorded; or a replay of its recording.
33#[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    /// The run whose recording this one replays, if it's a replay.
39    pub replay_of: Option<RunId>,
40    pub state: RunState,
41    /// The runner it was sent to, once it was.
42    pub runner: Option<String>,
43    /// When it was queued.
44    pub created_at: Timestamp,
45    pub started_at: Option<Timestamp>,
46    pub ended_at: Option<Timestamp>,
47    /// The size of its log, which its runner keeps, once it ended; none
48    /// for a replay, or a run that left no log.
49    pub log_bytes: Option<u64>,
50    /// Whether its log was made by another dvm than its runner's now: a log
51    /// replays only on the dvm that made it.
52    pub log_outdated: bool,
53    /// A replay's, once it exited: whether it was the run it replayed.
54    pub verdict: Option<ReplayVerdict>,
55    /// How many times it changed: of two copies of a run, the one with the
56    /// higher version is the newer.
57    pub version: u64,
58}
59
60impl Run {
61    /// Whether this copy is the run as it is in `shown`'s place, or newer:
62    /// the same run, and changed no fewer times. An answer and an event may
63    /// come out of order; the newer stays.
64    pub fn supersedes(&self, shown: &Run) -> bool {
65        self.id == shown.id && self.version >= shown.version
66    }
67
68    /// The sandbox it's of: its own, or the one it replays.
69    pub fn sandbox(&self) -> SandboxId {
70        SandboxId::new(self.replay_of.unwrap_or(self.id).get())
71    }
72
73    /// Whether it may be replayed, and why not: it's no replay itself, its
74    /// log is `whole` (one cut short replays only up to its cut), its runner
75    /// keeps it, and its runner's dvm made it. Its sandbox says
76    /// whether it's whole (`Sandbox::replayable`).
77    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/// Whether a replay was the run it replayed, as far as can be seen from
101/// outside: its exit code, and its output, byte for byte; a sandbox's that
102/// took commands, each command's, in their order. A replay that isn't the
103/// run, where dvm sees it, fails instead (`RunState::Failed`, dvm's
104/// reason).
105#[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    /// The same exit code, and the same output.
111    Same,
112    /// Another exit code: the run's, or the command's.
113    OtherExit {
114        recorded: i32,
115        replayed: i32,
116        #[serde(default, skip_serializing_if = "Option::is_none")]
117        command: Option<CommandNumber>,
118    },
119    /// Another output: the stream it parted in first, and at which byte;
120    /// the run's, or the command's.
121    OtherOutput {
122        stream: OutputStream,
123        at: u64,
124        #[serde(default, skip_serializing_if = "Option::is_none")]
125        command: Option<CommandNumber>,
126    },
127    /// A command that ran in one and not in the other, or ended in one and
128    /// not in the other.
129    OtherCommand { command: CommandNumber },
130}
131
132impl ReplayVerdict {
133    /// Whether the replay was its execution, as far as can be seen.
134    pub fn is_same(&self) -> bool {
135        matches!(self, Self::Same)
136    }
137}
138
139/// One of a program's output streams.
140#[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    /// Its name, as a file of a run's output is named.
151    pub fn name(self) -> &'static str {
152        match self {
153            Self::Stdout => "stdout",
154            Self::Stderr => "stderr",
155        }
156    }
157}
158
159/// Bytes as a program wrote them, or as it's given them (its stdin, a file
160/// written into its sandbox): base64 in JSON, since they needn't be UTF-8.
161#[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/// What a run's program has written so far, as text: bytes that aren't
196/// UTF-8 are replaced.
197#[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/// Where a run is:
206///
207/// ```text
208/// Queued → Running ⇄ Paused
209/// Queued | Running | Paused → Exited | Stopped | Failed
210/// ```
211#[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    /// Waiting for a runner to take it, or for the one it was sent to to
217    /// start it.
218    Queued,
219    Running,
220    Paused,
221    /// The program exited, with its code.
222    Exited {
223        code: i32,
224    },
225    /// Stopped by its owner.
226    Stopped,
227    /// Ended by dvm or the platform, not by the program, and why.
228    Failed {
229        reason: String,
230    },
231}
232
233impl RunState {
234    /// The state's name, as the API and the controller's database spell it.
235    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    /// Whether the run hasn't ended.
247    pub fn is_active(&self) -> bool {
248        matches!(self, Self::Queued | Self::Running | Self::Paused)
249    }
250
251    /// The state an owner's action takes the run to, if the action may be
252    /// taken: a queued run can only be stopped, before it starts.
253    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/// What an owner may do to a sandbox's run, besides starting it, as it's
271/// made: the last segment of its path, `POST /api/sandboxes/{id}/{action}`;
272/// a replay may be stopped, `POST /api/replays/{id}/stop`.
273#[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/// An action the run's state doesn't allow.
294#[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}