Skip to main content

actl_core/
display.rs

1//! Versioned display lease and per-action presentation acknowledgements.
2use crate::state::{SignalPaths, unix_ms};
3use serde::{Deserialize, Serialize};
4
5pub const DISPLAY_PROTOCOL: u32 = 9;
6#[derive(Clone, Debug, Serialize, Deserialize)]
7pub struct DisplayLease {
8    pub protocol: u32,
9    pub build: String,
10    pub pid: u32,
11    pub epoch: String,
12    pub state: String,
13    pub ts_ms: u64,
14}
15impl DisplayLease {
16    pub fn compatible(&self, now: u64) -> bool {
17        self.protocol == DISPLAY_PROTOCOL
18            && self.build == env!("CARGO_PKG_VERSION")
19            && self.ts_ms <= now
20            && now - self.ts_ms < 1500
21    }
22    pub fn ready(&self, now: u64) -> bool {
23        self.compatible(now) && self.state == "ready"
24    }
25}
26#[derive(Clone, Debug, Serialize, Deserialize)]
27pub struct DisplayAck {
28    pub epoch: String,
29    pub call_id: String,
30    pub action_id: u64,
31    pub ts_ms: u64,
32}
33impl SignalPaths {
34    pub fn display_lease(&self) -> Option<DisplayLease> {
35        serde_json::from_slice(&std::fs::read(self.dir.join("display.json")).ok()?).ok()
36    }
37    pub fn write_display<T: Serialize>(&self, name: &str, value: &T) -> std::io::Result<()> {
38        std::fs::create_dir_all(&self.dir)?;
39        let target = self.dir.join(name);
40        let tmp = target.with_extension(format!("{}.tmp", std::process::id()));
41        std::fs::write(&tmp, serde_json::to_vec(value)?)?;
42        std::fs::rename(tmp, target)
43    }
44    pub fn acknowledged(&self, call: &str, action: u64, epoch: &str, since: u64) -> bool {
45        let ack = std::fs::read(self.dir.join(format!("ack-{call}.json")))
46            .ok()
47            .and_then(|v| serde_json::from_slice::<DisplayAck>(&v).ok());
48        ack.is_some_and(|a| {
49            a.call_id == call
50                && a.action_id == action
51                && a.epoch == epoch
52                && a.ts_ms >= since
53                && a.ts_ms <= unix_ms()
54                && unix_ms().saturating_sub(a.ts_ms) < 1500
55        })
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    #[test]
63    fn lease_requires_current_version_fresh_visible_frame() {
64        let mut l = DisplayLease {
65            protocol: DISPLAY_PROTOCOL,
66            build: env!("CARGO_PKG_VERSION").into(),
67            pid: 1,
68            epoch: "test".into(),
69            state: "ready".into(),
70            ts_ms: 100,
71        };
72        assert!(l.ready(101));
73        assert!(!l.ready(99));
74        assert!(!l.ready(1600));
75        l.state = "capture".into();
76        assert!(!l.ready(101));
77        assert!(l.compatible(101));
78        l.state = "closed".into();
79        assert!(!l.ready(101));
80        l.protocol = 2;
81        assert!(!l.compatible(101));
82    }
83}