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}