use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::watched::WatchKinds;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Moment {
Event { thread: u64, operation: u64 },
Time { at_us: u64 },
}
impl Moment {
pub const START: Self = Self::Event {
thread: 1,
operation: 1,
};
}
impl fmt::Display for Moment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Event { thread, operation } => write!(f, "{thread}:{operation}"),
Self::Time { at_us } => write!(f, "@{}.{:03}", at_us / 1_000, at_us % 1_000),
}
}
}
impl FromStr for Moment {
type Err = String;
fn from_str(moment: &str) -> Result<Self, String> {
let bad = || format!("a moment is <thread>:<operation> or @<milliseconds>: {moment}");
if let Some(milliseconds) = moment.strip_prefix('@') {
let (whole, fraction) = milliseconds.split_once('.').unwrap_or((milliseconds, ""));
let whole: u64 = whole.parse().map_err(|_| bad())?;
let digits = fraction.get(..fraction.len().min(3)).ok_or_else(bad)?;
let micros = match digits {
"" => 0,
digits => {
digits.parse::<u64>().map_err(|_| bad())? * 10_u64.pow(3 - digits.len() as u32)
}
};
let at_us = whole
.checked_mul(1_000)
.and_then(|us| us.checked_add(micros));
return at_us.map(|at_us| Self::Time { at_us }).ok_or_else(bad);
}
let (thread, operation) = moment.split_once(':').ok_or_else(bad)?;
let thread = thread.parse().map_err(|_| bad())?;
let operation = operation.parse().map_err(|_| bad())?;
Ok(Self::Event { thread, operation })
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct NewReplay {
#[serde(default)]
pub held: bool,
#[serde(default)]
pub watch: WatchKinds,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct Seek {
pub to: Moment,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct ReplayStop {
pub moment: Moment,
pub threads: Vec<StoppedThread>,
pub machine: MachineState,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct StoppedThread {
pub thread: u64,
pub operation: u64,
pub before: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct MachineState {
pub processes: Vec<ProcessState>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct ProcessState {
pub id: u64,
pub parent: u64,
#[serde(default)]
pub name: String,
#[serde(default)]
pub program: String,
#[serde(default)]
pub directory: String,
pub threads: Vec<ThreadState>,
#[serde(default)]
pub files: Vec<OpenFile>,
#[serde(default)]
pub memory: Vec<MemoryRegion>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct ThreadState {
pub id: u64,
pub name: String,
pub state: String,
pub waits_in: Option<ThreadWait>,
pub futex_holds: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct ThreadWait {
pub syscall: String,
pub number: u64,
pub first: String,
}
impl ThreadState {
pub fn waiting_in(&self) -> Option<&ThreadWait> {
self.waits_in.as_ref().filter(|_| self.state == "Waiting")
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct OpenFile {
pub fd: u64,
pub to: Option<String>,
pub offset: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[cfg_attr(feature = "typescript", ts(export_to = "controller-api.ts"))]
pub struct MemoryRegion {
pub start: String,
pub end: String,
pub protection: String,
pub name: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_moment_is_spelled_as_dvm_reads_it() {
let event = Moment::Event {
thread: 3,
operation: 20,
};
assert_eq!(event.to_string(), "3:20");
assert_eq!(Moment::Time { at_us: 1_500_250 }.to_string(), "@1500.250");
assert_eq!(Moment::Time { at_us: 7 }.to_string(), "@0.007");
for moment in [event, Moment::Time { at_us: 1_500_250 }, Moment::START] {
assert_eq!(moment.to_string().parse(), Ok(moment));
}
assert_eq!("@1500".parse(), Ok(Moment::Time { at_us: 1_500_000 }));
assert_eq!("@2.5".parse(), Ok(Moment::Time { at_us: 2_500 }));
assert!("@x".parse::<Moment>().is_err() && "3".parse::<Moment>().is_err());
}
#[test]
fn a_description_reads_as_dvm_writes_it() {
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":[]}]}"#;
let machine: MachineState = serde_json::from_str(described).unwrap();
let sleep = &machine.processes[0];
assert_eq!(
(sleep.id, sleep.parent, sleep.name.as_str()),
(3, 1, "sleep")
);
assert_eq!(sleep.files[1].to, None);
assert_eq!(
sleep.threads[0]
.waiting_in()
.map(|wait| wait.syscall.as_str()),
Some("nanosleep")
);
assert_eq!(machine.processes[1].name, "");
}
}