1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::watched::WatchKinds;
7
8#[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 Event { thread: u64, operation: u64 },
18 Time { at_us: u64 },
21}
22
23impl Moment {
24 pub const START: Self = Self::Event {
27 thread: 1,
28 operation: 1,
29 };
30}
31
32impl fmt::Display for Moment {
33 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 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#[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#[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#[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 pub moment: Moment,
102 pub threads: Vec<StoppedThread>,
104 pub machine: MachineState,
105}
106
107#[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#[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#[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#[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 pub futex_holds: Option<String>,
162}
163
164#[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 pub fn waiting_in(&self) -> Option<&ThreadWait> {
178 self.waits_in.as_ref().filter(|_| self.state == "Waiting")
179 }
180}
181
182#[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#[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 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 assert_eq!(machine.processes[1].name, "");
246 }
247}