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 owner: String,
46 pub state: JobState,
47 pub reasons: PrinterReason,
48 pub message: String,
49 pub created_at: SystemTime,
50 pub completed_at: Option<SystemTime>,
51 pub cancel_flag: Arc<AtomicBool>,
53}
54
55impl JobRecord {
56 pub fn created_secs(&self) -> i32 {
58 secs_since_epoch(self.created_at)
59 }
60
61 pub fn completed_secs(&self) -> Option<i32> {
63 self.completed_at.map(secs_since_epoch)
64 }
65
66 pub fn is_canceled(&self) -> bool {
69 self.cancel_flag.load(Ordering::Acquire)
70 }
71}
72
73fn secs_since_epoch(t: SystemTime) -> i32 {
74 t.duration_since(UNIX_EPOCH)
75 .map(|d| d.as_secs() as i32)
76 .unwrap_or(0)
77}
78
79#[derive(Clone)]
81pub struct JobRegistry {
82 inner: Arc<RwLock<Inner>>,
83}
84
85struct Inner {
86 next_id: u32,
87 jobs: Vec<JobRecord>,
88}
89
90impl Default for JobRegistry {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96impl JobRegistry {
97 pub fn new() -> Self {
99 Self {
100 inner: Arc::new(RwLock::new(Inner {
101 next_id: 1,
102 jobs: Vec::new(),
103 })),
104 }
105 }
106
107 pub fn create(&self, printer_name: String, owner: String) -> JobRecord {
112 let mut g = self.inner.write();
113 let id = g.next_id;
114 g.next_id = g.next_id.wrapping_add(1).max(1);
115 let rec = JobRecord {
116 id,
117 printer_name,
118 owner,
119 state: JobState::Pending,
120 reasons: PrinterReason::empty(),
121 message: String::new(),
122 created_at: SystemTime::now(),
123 completed_at: None,
124 cancel_flag: Arc::new(AtomicBool::new(false)),
125 };
126 g.jobs.push(rec.clone());
127 rec
128 }
129
130 pub fn get(&self, id: JobId) -> Option<JobRecord> {
132 self.inner.read().jobs.iter().find(|j| j.id == id).cloned()
133 }
134
135 pub fn jobs_for_printer(&self, printer_name: &str) -> Vec<JobRecord> {
137 self.inner
138 .read()
139 .jobs
140 .iter()
141 .filter(|j| j.printer_name == printer_name)
142 .cloned()
143 .collect()
144 }
145
146 pub fn set_state(&self, id: JobId, state: JobState) {
149 let mut g = self.inner.write();
150 if let Some(j) = g.jobs.iter_mut().find(|j| j.id == id) {
151 j.state = state;
152 if state.is_terminal() && j.completed_at.is_none() {
153 j.completed_at = Some(SystemTime::now());
154 }
155 }
156 }
157
158 pub fn set_failure(&self, id: JobId, reasons: PrinterReason, message: String) {
160 let mut g = self.inner.write();
161 if let Some(j) = g.jobs.iter_mut().find(|j| j.id == id) {
162 j.state = JobState::Aborted;
163 j.reasons = reasons;
164 j.message = message;
165 j.completed_at = Some(SystemTime::now());
166 }
167 }
168
169 pub fn cancel(&self, id: JobId) -> Option<JobState> {
172 let mut g = self.inner.write();
173 let j = g.jobs.iter_mut().find(|j| j.id == id)?;
174 if j.state.is_terminal() {
175 return Some(j.state);
176 }
177 j.cancel_flag.store(true, Ordering::Release);
178 j.state = JobState::Canceled;
179 j.completed_at = Some(SystemTime::now());
180 Some(j.state)
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn distinct_ids() {
190 let reg = JobRegistry::new();
191 let a = reg.create("p".into(), "u".into());
192 let b = reg.create("p".into(), "u".into());
193 assert_ne!(a.id, b.id);
194 assert_eq!(a.state, JobState::Pending);
195 }
196
197 #[test]
198 fn cancel_flips_flag_and_state() {
199 let reg = JobRegistry::new();
200 let j = reg.create("p".into(), "u".into());
201 let flag = j.cancel_flag.clone();
202 assert!(!flag.load(Ordering::Acquire));
203 assert_eq!(reg.cancel(j.id), Some(JobState::Canceled));
204 assert!(flag.load(Ordering::Acquire));
205 assert_eq!(reg.get(j.id).unwrap().state, JobState::Canceled);
206 }
207
208 #[test]
209 fn cancel_terminal_is_noop() {
210 let reg = JobRegistry::new();
211 let j = reg.create("p".into(), "u".into());
212 reg.set_state(j.id, JobState::Completed);
213 assert_eq!(reg.cancel(j.id), Some(JobState::Completed));
214 assert!(!reg.get(j.id).unwrap().cancel_flag.load(Ordering::Acquire));
215 }
216
217 #[test]
218 fn failure_records_reasons_and_message() {
219 let reg = JobRegistry::new();
220 let j = reg.create("p".into(), "u".into());
221 reg.set_failure(j.id, PrinterReason::MEDIA_EMPTY, "no labels".into());
222 let after = reg.get(j.id).unwrap();
223 assert_eq!(after.state, JobState::Aborted);
224 assert_eq!(after.reasons, PrinterReason::MEDIA_EMPTY);
225 assert_eq!(after.message, "no labels");
226 }
227
228 #[test]
229 fn jobs_for_printer_filters() {
230 let reg = JobRegistry::new();
231 let _ = reg.create("a".into(), "u".into());
232 let _ = reg.create("b".into(), "u".into());
233 let _ = reg.create("a".into(), "u".into());
234 assert_eq!(reg.jobs_for_printer("a").len(), 2);
235 assert_eq!(reg.jobs_for_printer("b").len(), 1);
236 assert_eq!(reg.jobs_for_printer("c").len(), 0);
237 }
238}