Skip to main content

pfx_editor_shell/
pgpu.rs

1use std::io::Read;
2use std::process::{Command, Stdio};
3use std::sync::mpsc;
4use std::time::Duration;
5
6use serde_json::Value;
7
8pub const TIMEOUT: Duration = Duration::from_millis(1500);
9pub const EVERY: Duration = Duration::from_secs(1);
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum Pgpu {
13    #[default]
14    Free,
15    Play,
16    Queued,
17}
18
19impl Pgpu {
20    pub fn word(self) -> &'static str {
21        match self {
22            Pgpu::Free => "free",
23            Pgpu::Play => "play",
24            Pgpu::Queued => "queued",
25        }
26    }
27
28    pub fn paused(self) -> bool {
29        self == Pgpu::Play
30    }
31}
32
33pub fn classify(status: &Value, name: &str) -> Pgpu {
34    let play = status.get("play").filter(|play| !play.is_null());
35    if play.is_some_and(|play| play.get("in_front").and_then(Value::as_bool) != Some(false)) {
36        return Pgpu::Play;
37    }
38    let ours = status
39        .get("jobs")
40        .and_then(Value::as_array)
41        .is_some_and(|jobs| {
42            jobs.iter()
43                .any(|job| job.get("as").and_then(Value::as_str) == Some(name))
44        });
45    if ours { Pgpu::Queued } else { Pgpu::Free }
46}
47
48#[cfg(test)]
49fn read(text: &str) -> Pgpu {
50    serde_json::from_str::<Value>(text).map_or(Pgpu::Free, |status| classify(&status, "editor"))
51}
52
53pub fn fetch() -> Option<Value> {
54    let mut child = Command::new("pgpu")
55        .args(["status", "--json"])
56        .stdin(Stdio::null())
57        .stdout(Stdio::piped())
58        .stderr(Stdio::null())
59        .spawn()
60        .ok()?;
61    let Some(mut out) = child.stdout.take() else {
62        let _ = child.kill();
63        let _ = child.wait();
64        return None;
65    };
66    let (send, receive) = mpsc::channel();
67    std::thread::spawn(move || {
68        let mut text = String::new();
69        let read = out.read_to_string(&mut text).map(|_| text);
70        let _ = send.send(read);
71    });
72    match receive.recv_timeout(TIMEOUT) {
73        Ok(Ok(text)) => {
74            let ok = child.wait().is_ok_and(|exit| exit.success());
75            if ok {
76                serde_json::from_str(&text).ok()
77            } else {
78                None
79            }
80        }
81        _ => {
82            let _ = child.kill();
83            let _ = child.wait();
84            None
85        }
86    }
87}
88
89pub fn status(name: &str) -> Pgpu {
90    fetch().map_or(Pgpu::Free, |status| classify(&status, name))
91}
92
93pub fn watch(name: String, mut tell: impl FnMut(Pgpu) -> bool + Send + 'static) {
94    std::thread::spawn(move || {
95        loop {
96            if !tell(status(&name)) {
97                return;
98            }
99            std::thread::sleep(EVERY);
100        }
101    });
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn no_game_is_free() {
110        let text = r#"{"v":1,"paused":false,"gpu_busy":5,"play":null,"away":false,"jobs":[{"pid":7,"state":"running","as":"other"}]}"#;
111        assert_eq!(read(text), Pgpu::Free);
112    }
113
114    #[test]
115    fn a_game_in_front_pauses() {
116        let text = r#"{"v":1,"play":{"game":"a game","in_front":true},"jobs":[]}"#;
117        assert_eq!(read(text), Pgpu::Play);
118        assert!(read(text).paused());
119    }
120
121    #[test]
122    fn a_game_without_in_front_pauses() {
123        assert_eq!(read(r#"{"play":{"game":"a game"}}"#), Pgpu::Play);
124        assert_eq!(read(r#"{"play":{"in_front":null}}"#), Pgpu::Play);
125    }
126
127    #[test]
128    fn a_game_behind_does_not_pause() {
129        let text = r#"{"play":{"game":"a game","in_front":false},"jobs":[]}"#;
130        assert_eq!(read(text), Pgpu::Free);
131    }
132
133    #[test]
134    fn our_own_job_reads_queued() {
135        let text = r#"{"play":null,"jobs":[{"as":"other"},{"as":"editor","state":"waiting","waits_for":"desk"}]}"#;
136        assert_eq!(read(text), Pgpu::Queued);
137        let behind = r#"{"play":{"in_front":false},"jobs":[{"as":"editor"}]}"#;
138        assert_eq!(read(behind), Pgpu::Queued);
139        let front = r#"{"play":{"in_front":true},"jobs":[{"as":"editor"}]}"#;
140        assert_eq!(read(front), Pgpu::Play);
141    }
142
143    #[test]
144    fn a_failure_is_no_game() {
145        for text in ["", "not json", "[]", "{}", r#"{"play":"#] {
146            assert_eq!(read(text), Pgpu::Free, "{text:?}");
147        }
148    }
149}