Skip to main content

dvm_api/
replay.rs

1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::watched::WatchKinds;
7
8/// Where a replay stops, as dvm names it (its `docs/05-replay.md`, A
9/// moment): before one thread's operation exactly, or at a time of the
10/// recorded run.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
12#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
13#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
14#[serde(tag = "kind", rename_all = "snake_case")]
15pub enum Moment {
16    /// Before thread `thread`'s operation `operation`, its first being 1.
17    Event { thread: u64, operation: u64 },
18    /// `at_us` microseconds into the recorded run: each thread before its
19    /// first operation stamped after it.
20    Time { at_us: u64 },
21}
22
23impl Moment {
24    /// Where a replay stops first: before its first thread's first
25    /// operation.
26    pub const START: Self = Self::Event {
27        thread: 1,
28        operation: 1,
29    };
30}
31
32impl fmt::Display for Moment {
33    /// As dvm reads it: `<thread>:<operation>`, or `@<milliseconds>`.
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match *self {
36            Self::Event { thread, operation } => write!(f, "{thread}:{operation}"),
37            Self::Time { at_us } => write!(f, "@{}.{:03}", at_us / 1_000, at_us % 1_000),
38        }
39    }
40}
41
42impl FromStr for Moment {
43    type Err = String;
44
45    /// As dvm writes it: `<thread>:<operation>`, or `@<milliseconds>`, a
46    /// fraction to the microsecond.
47    fn from_str(moment: &str) -> Result<Self, String> {
48        let bad = || format!("a moment is <thread>:<operation> or @<milliseconds>: {moment}");
49        if let Some(milliseconds) = moment.strip_prefix('@') {
50            let (whole, fraction) = milliseconds.split_once('.').unwrap_or((milliseconds, ""));
51            let whole: u64 = whole.parse().map_err(|_| bad())?;
52            let digits = fraction.get(..fraction.len().min(3)).ok_or_else(bad)?;
53            let micros = match digits {
54                "" => 0,
55                digits => {
56                    digits.parse::<u64>().map_err(|_| bad())? * 10_u64.pow(3 - digits.len() as u32)
57                }
58            };
59            let at_us = whole
60                .checked_mul(1_000)
61                .and_then(|us| us.checked_add(micros));
62            return at_us.map(|at_us| Self::Time { at_us }).ok_or_else(bad);
63        }
64        let (thread, operation) = moment.split_once(':').ok_or_else(bad)?;
65        let thread = thread.parse().map_err(|_| bad())?;
66        let operation = operation.parse().map_err(|_| bad())?;
67        Ok(Self::Event { thread, operation })
68    }
69}
70
71/// How to start a replay, `POST /api/executions/{id}/replays`' body, which may be
72/// left out: played to its end, watching the events `watch` asks for, if
73/// any (`WatchedEvents`); or held at its start, waiting for a seek, which
74/// watches nothing.
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
76#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
77#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
78pub struct NewReplay {
79    #[serde(default)]
80    pub held: bool,
81    #[serde(default)]
82    pub watch: WatchKinds,
83}
84
85/// Where to take a replay, `POST /api/replays/{id}/seek`'s body.
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
87#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
88#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
89pub struct Seek {
90    pub to: Moment,
91}
92
93/// A replay held at a moment, and the machine there: what a seek answers.
94/// Seeking back starts the replay again, and goes forward to the moment.
95#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
96#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
97#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
98pub struct ReplayStop {
99    /// The moment it's at: the one asked for, or its start, for a time
100    /// before its first stamped operation.
101    pub moment: Moment,
102    /// The threads at their stops, and what each does next.
103    pub threads: Vec<StoppedThread>,
104    pub machine: MachineState,
105}
106
107/// A thread at its stop: the operation it stops before, and what that does,
108/// in dvm's words (`syscall 263`, `a wait's end, by time`).
109#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
110#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
111#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
112pub struct StoppedThread {
113    pub thread: u64,
114    pub operation: u64,
115    pub before: String,
116}
117
118/// What dvm's kernel holds at a replay's stop: its processes, as dvm
119/// describes them.
120#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
121#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
122#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
123pub struct MachineState {
124    pub processes: Vec<ProcessState>,
125}
126
127/// A process at a stop: its id and its parent's (0 for the first's), its
128/// name, the program it runs, its directory, its threads, its open files,
129/// and its memory's regions.
130#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
131#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
132#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
133pub struct ProcessState {
134    pub id: u64,
135    pub parent: u64,
136    #[serde(default)]
137    pub name: String,
138    #[serde(default)]
139    pub program: String,
140    #[serde(default)]
141    pub directory: String,
142    pub threads: Vec<ThreadState>,
143    #[serde(default)]
144    pub files: Vec<OpenFile>,
145    #[serde(default)]
146    pub memory: Vec<MemoryRegion>,
147}
148
149/// A thread at a stop: its id, its name, its state (`Running`, `Ready`,
150/// `Waiting`, as dvm's kernel names them), and the syscall it waits in, or
151/// last waited in, which its state tells apart.
152#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
153#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
154#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
155pub struct ThreadState {
156    pub id: u64,
157    pub name: String,
158    pub state: String,
159    pub waits_in: Option<ThreadWait>,
160    /// For a wait on a futex, what its word holds now, in hex.
161    pub futex_holds: Option<String>,
162}
163
164/// A syscall a thread waits in: its name and number, and its first
165/// argument (a futex's address, a file's descriptor), in hex.
166#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
167#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
168#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
169pub struct ThreadWait {
170    pub syscall: String,
171    pub number: u64,
172    pub first: String,
173}
174
175impl ThreadState {
176    /// The syscall it waits in now, if it waits.
177    pub fn waiting_in(&self) -> Option<&ThreadWait> {
178        self.waits_in.as_ref().filter(|_| self.state == "Waiting")
179    }
180}
181
182/// An open file of a process: its descriptor, where it leads, as
183/// `/proc/<pid>/fd/<n>` says (a path, `pipe:[n]`, `socket:[n]`), and its
184/// offset.
185#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
186#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
187#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
188pub struct OpenFile {
189    pub fd: u64,
190    pub to: Option<String>,
191    pub offset: u64,
192}
193
194/// A region of a process's memory, as `/proc/<pid>/maps` lists it: its
195/// addresses in hex, its protection (`r-x`), and what it maps, if named.
196#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
197#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
198#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
199pub struct MemoryRegion {
200    pub start: String,
201    pub end: String,
202    pub protection: String,
203    pub name: String,
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn a_moment_is_spelled_as_dvm_reads_it() {
212        let event = Moment::Event {
213            thread: 3,
214            operation: 20,
215        };
216        assert_eq!(event.to_string(), "3:20");
217        assert_eq!(Moment::Time { at_us: 1_500_250 }.to_string(), "@1500.250");
218        assert_eq!(Moment::Time { at_us: 7 }.to_string(), "@0.007");
219        for moment in [event, Moment::Time { at_us: 1_500_250 }, Moment::START] {
220            assert_eq!(moment.to_string().parse(), Ok(moment));
221        }
222        // dvm writes a time it was given as a float of milliseconds.
223        assert_eq!("@1500".parse(), Ok(Moment::Time { at_us: 1_500_000 }));
224        assert_eq!("@2.5".parse(), Ok(Moment::Time { at_us: 2_500 }));
225        assert!("@x".parse::<Moment>().is_err() && "3".parse::<Moment>().is_err());
226    }
227
228    #[test]
229    fn a_description_reads_as_dvm_writes_it() {
230        let described = r#"{"processes":[{"directory":"/","files":[{"fd":0,"offset":0,"to":"pipe:[1]"},{"fd":9,"offset":4,"to":null}],"id":3,"memory":[{"end":"0x7ffffffff000","name":"[stack]","protection":"rw-","start":"0x7fffff7ff000"}],"name":"sleep","parent":1,"program":"/bin/busybox","threads":[{"futex_holds":null,"id":3,"name":"sleep","state":"Waiting","waits_in":{"first":"0x7fffffffece8","number":35,"syscall":"nanosleep"}}]},{"id":4,"parent":1,"threads":[]}]}"#;
231        let machine: MachineState = serde_json::from_str(described).unwrap();
232        let sleep = &machine.processes[0];
233        assert_eq!(
234            (sleep.id, sleep.parent, sleep.name.as_str()),
235            (3, 1, "sleep")
236        );
237        assert_eq!(sleep.files[1].to, None);
238        assert_eq!(
239            sleep.threads[0]
240                .waiting_in()
241                .map(|wait| wait.syscall.as_str()),
242            Some("nanosleep")
243        );
244        // A process whose files are gone says only its id, parent and threads.
245        assert_eq!(machine.processes[1].name, "");
246    }
247}