1use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::Arc;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use parking_lot::RwLock;
9
10use crate::flags::PrinterReason;
11
12pub type JobId = u32;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(u32)]
18#[allow(missing_docs)]
19pub enum JobState {
20 Pending = 3,
21 Held = 4,
22 Processing = 5,
23 ProcessingStopped = 6,
24 Canceled = 7,
25 Aborted = 8,
26 Completed = 9,
27}
28
29impl JobState {
30 pub fn is_terminal(self) -> bool {
33 matches!(self, Self::Canceled | Self::Aborted | Self::Completed)
34 }
35}
36
37#[derive(Debug, Clone)]
39#[allow(missing_docs)]
40pub struct JobRecord {
41 pub id: JobId,
42 pub printer_name: String,
43 pub state: JobState,
44 pub reasons: PrinterReason,
45 pub message: String,
46 pub created_at: SystemTime,
47 pub completed_at: Option<SystemTime>,
48 pub cancel_flag: Arc<AtomicBool>,
50}
51
52impl JobRecord {
53 pub fn created_secs(&self) -> i32 {
55 secs_since_epoch(self.created_at)
56 }
57
58 pub fn completed_secs(&self) -> Option<i32> {
60 self.completed_at.map(secs_since_epoch)
61 }
62
63 pub fn is_canceled(&self) -> bool {
66 self.cancel_flag.load(Ordering::Acquire)
67 }
68}
69
70fn secs_since_epoch(t: SystemTime) -> i32 {
71 t.duration_since(UNIX_EPOCH)
72 .map(|d| d.as_secs() as i32)
73 .unwrap_or(0)
74}
75
76#[derive(Clone)]
78pub struct JobRegistry {
79 inner: Arc<RwLock<Inner>>,
80}
81
82struct Inner {
83 next_id: u32,
84 jobs: Vec<JobRecord>,
85}
86
87impl Default for JobRegistry {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl JobRegistry {
94 pub fn new() -> Self {
96 Self {
97 inner: Arc::new(RwLock::new(Inner {
98 next_id: 1,
99 jobs: Vec::new(),
100 })),
101 }
102 }
103
104 pub fn create(&self, printer_name: String) -> JobRecord {
108 let mut g = self.inner.write();
109 let id = g.next_id;
110 g.next_id = g.next_id.wrapping_add(1).max(1);
111 let rec = JobRecord {
112 id,
113 printer_name,
114 state: JobState::Pending,
115 reasons: PrinterReason::empty(),
116 message: String::new(),
117 created_at: SystemTime::now(),
118 completed_at: None,
119 cancel_flag: Arc::new(AtomicBool::new(false)),
120 };
121 g.jobs.push(rec.clone());
122 rec
123 }
124
125 pub fn get(&self, id: JobId) -> Option<JobRecord> {
127 self.inner.read().jobs.iter().find(|j| j.id == id).cloned()
128 }
129
130 pub fn jobs_for_printer(&self, printer_name: &str) -> Vec<JobRecord> {
132 self.inner
133 .read()
134 .jobs
135 .iter()
136 .filter(|j| j.printer_name == printer_name)
137 .cloned()
138 .collect()
139 }
140
141 pub fn set_state(&self, id: JobId, state: JobState) {
144 let mut g = self.inner.write();
145 if let Some(j) = g.jobs.iter_mut().find(|j| j.id == id) {
146 j.state = state;
147 if state.is_terminal() && j.completed_at.is_none() {
148 j.completed_at = Some(SystemTime::now());
149 }
150 }
151 }
152
153 pub fn set_failure(&self, id: JobId, reasons: PrinterReason, message: String) {
155 let mut g = self.inner.write();
156 if let Some(j) = g.jobs.iter_mut().find(|j| j.id == id) {
157 j.state = JobState::Aborted;
158 j.reasons = reasons;
159 j.message = message;
160 j.completed_at = Some(SystemTime::now());
161 }
162 }
163
164 pub fn cancel(&self, id: JobId) -> Option<JobState> {
167 let mut g = self.inner.write();
168 let j = g.jobs.iter_mut().find(|j| j.id == id)?;
169 if j.state.is_terminal() {
170 return Some(j.state);
171 }
172 j.cancel_flag.store(true, Ordering::Release);
173 j.state = JobState::Canceled;
174 j.completed_at = Some(SystemTime::now());
175 Some(j.state)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn distinct_ids() {
185 let reg = JobRegistry::new();
186 let a = reg.create("p".into());
187 let b = reg.create("p".into());
188 assert_ne!(a.id, b.id);
189 assert_eq!(a.state, JobState::Pending);
190 }
191
192 #[test]
193 fn cancel_flips_flag_and_state() {
194 let reg = JobRegistry::new();
195 let j = reg.create("p".into());
196 let flag = j.cancel_flag.clone();
197 assert!(!flag.load(Ordering::Acquire));
198 assert_eq!(reg.cancel(j.id), Some(JobState::Canceled));
199 assert!(flag.load(Ordering::Acquire));
200 assert_eq!(reg.get(j.id).unwrap().state, JobState::Canceled);
201 }
202
203 #[test]
204 fn cancel_terminal_is_noop() {
205 let reg = JobRegistry::new();
206 let j = reg.create("p".into());
207 reg.set_state(j.id, JobState::Completed);
208 assert_eq!(reg.cancel(j.id), Some(JobState::Completed));
209 assert!(!reg.get(j.id).unwrap().cancel_flag.load(Ordering::Acquire));
210 }
211
212 #[test]
213 fn failure_records_reasons_and_message() {
214 let reg = JobRegistry::new();
215 let j = reg.create("p".into());
216 reg.set_failure(j.id, PrinterReason::MEDIA_EMPTY, "no labels".into());
217 let after = reg.get(j.id).unwrap();
218 assert_eq!(after.state, JobState::Aborted);
219 assert_eq!(after.reasons, PrinterReason::MEDIA_EMPTY);
220 assert_eq!(after.message, "no labels");
221 }
222
223 #[test]
224 fn jobs_for_printer_filters() {
225 let reg = JobRegistry::new();
226 let _ = reg.create("a".into());
227 let _ = reg.create("b".into());
228 let _ = reg.create("a".into());
229 assert_eq!(reg.jobs_for_printer("a").len(), 2);
230 assert_eq!(reg.jobs_for_printer("b").len(), 1);
231 assert_eq!(reg.jobs_for_printer("c").len(), 0);
232 }
233}