Skip to main content

objdiff_core/jobs/
mod.rs

1use std::{
2    sync::{
3        Arc, RwLock,
4        atomic::{AtomicUsize, Ordering},
5        mpsc::{Receiver, Sender, TryRecvError},
6    },
7    task::Waker,
8    thread::JoinHandle,
9};
10
11use anyhow::Result;
12
13use crate::jobs::{
14    check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
15    update::UpdateResult,
16};
17
18pub mod check_update;
19pub mod create_scratch;
20pub mod objdiff;
21pub mod update;
22
23#[derive(Debug, Eq, PartialEq, Copy, Clone)]
24pub enum Job {
25    ObjDiff,
26    CheckUpdate,
27    Update,
28    CreateScratch,
29}
30pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
31
32#[derive(Default)]
33pub struct JobQueue {
34    pub jobs: Vec<JobState>,
35    pub results: Vec<JobResult>,
36}
37
38impl JobQueue {
39    /// Adds a job to the queue.
40    #[inline]
41    pub fn push(&mut self, state: JobState) { self.jobs.push(state); }
42
43    /// Adds a job to the queue if a job of the given kind is not already running.
44    #[inline]
45    pub fn push_once(&mut self, job: Job, func: impl FnOnce() -> JobState) {
46        if !self.is_running(job) {
47            self.push(func());
48        }
49    }
50
51    /// Returns whether a job of the given kind is running.
52    pub fn is_running(&self, kind: Job) -> bool {
53        self.jobs.iter().any(|j| j.kind == kind && j.handle.is_some())
54    }
55
56    /// Returns whether any job is running.
57    pub fn any_running(&self) -> bool {
58        self.jobs.iter().any(|job| {
59            if let Some(handle) = &job.handle {
60                return !handle.is_finished();
61            }
62            false
63        })
64    }
65
66    /// Iterates over all jobs mutably.
67    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut JobState> + '_ { self.jobs.iter_mut() }
68
69    /// Iterates over all finished jobs, returning the job state and the result.
70    pub fn iter_finished(
71        &mut self,
72    ) -> impl Iterator<Item = (&mut JobState, std::thread::Result<JobResult>)> + '_ {
73        self.jobs.iter_mut().filter_map(|job| {
74            if let Some(handle) = &job.handle {
75                if !handle.is_finished() {
76                    return None;
77                }
78                let result = job.handle.take().unwrap().join();
79                return Some((job, result));
80            }
81            None
82        })
83    }
84
85    /// Clears all finished jobs.
86    pub fn clear_finished(&mut self) {
87        self.jobs.retain(|job| {
88            !(job.handle.is_none() && job.context.status.read().unwrap().error.is_none())
89        });
90    }
91
92    /// Clears all errored jobs.
93    pub fn clear_errored(&mut self) {
94        self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none());
95    }
96
97    /// Removes a job from the queue given its ID.
98    pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
99
100    /// Collects the results of all finished jobs and handles any errors.
101    pub fn collect_results(&mut self) {
102        let mut results = vec![];
103        for (job, result) in self.iter_finished() {
104            match result {
105                Ok(result) => {
106                    match result {
107                        JobResult::None => {
108                            // Job context contains the error
109                        }
110                        _ => results.push(result),
111                    }
112                }
113                Err(err) => {
114                    let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
115                        anyhow::Error::msg(*msg)
116                    } else if let Some(msg) = err.downcast_ref::<String>() {
117                        anyhow::Error::msg(msg.clone())
118                    } else {
119                        anyhow::Error::msg("Thread panicked")
120                    };
121                    let result = job.context.status.write();
122                    if let Ok(mut guard) = result {
123                        guard.error = Some(err);
124                    } else {
125                        drop(result);
126                        job.context.status = Arc::new(RwLock::new(JobStatus {
127                            title: "Error".to_string(),
128                            progress_percent: 0.0,
129                            progress_items: None,
130                            status: String::new(),
131                            error: Some(err),
132                        }));
133                    }
134                }
135            }
136        }
137        self.results.append(&mut results);
138        self.clear_finished();
139    }
140}
141
142#[derive(Clone)]
143pub struct JobContext {
144    pub status: Arc<RwLock<JobStatus>>,
145    pub waker: Waker,
146}
147
148pub struct JobState {
149    pub id: usize,
150    pub kind: Job,
151    pub handle: Option<JoinHandle<JobResult>>,
152    pub context: JobContext,
153    pub cancel: Sender<()>,
154}
155
156#[derive(Default)]
157pub struct JobStatus {
158    pub title: String,
159    pub progress_percent: f32,
160    pub progress_items: Option<[u32; 2]>,
161    pub status: String,
162    pub error: Option<anyhow::Error>,
163}
164
165pub enum JobResult {
166    None,
167    ObjDiff(Option<Box<ObjDiffResult>>),
168    CheckUpdate(Option<Box<CheckUpdateResult>>),
169    Update(Box<UpdateResult>),
170    CreateScratch(Option<Box<CreateScratchResult>>),
171}
172
173fn start_job(
174    waker: Waker,
175    title: &str,
176    kind: Job,
177    run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
178) -> JobState {
179    let status = Arc::new(RwLock::new(JobStatus {
180        title: title.to_string(),
181        progress_percent: 0.0,
182        progress_items: None,
183        status: String::new(),
184        error: None,
185    }));
186    let context = JobContext { status: status.clone(), waker: waker.clone() };
187    let context_inner = JobContext { status: status.clone(), waker };
188    let (tx, rx) = std::sync::mpsc::channel();
189    let handle = std::thread::spawn(move || match run(context_inner, rx) {
190        Ok(state) => state,
191        Err(e) => {
192            if let Ok(mut w) = status.write() {
193                w.error = Some(e);
194            }
195            JobResult::None
196        }
197    });
198    let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
199    JobState { id, kind, handle: Some(handle), context, cancel: tx }
200}
201
202fn update_status(
203    context: &JobContext,
204    str: String,
205    count: u32,
206    total: u32,
207    cancel: &Receiver<()>,
208) -> Result<()> {
209    let mut w =
210        context.status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
211    w.progress_items = Some([count, total]);
212    w.progress_percent = count as f32 / total as f32;
213    if should_cancel(cancel) {
214        w.status = "Cancelled".to_string();
215        return Err(anyhow::Error::msg("Cancelled"));
216    } else {
217        w.status = str;
218    }
219    drop(w);
220    context.waker.wake_by_ref();
221    Ok(())
222}
223
224fn should_cancel(rx: &Receiver<()>) -> bool {
225    match rx.try_recv() {
226        Ok(_) | Err(TryRecvError::Disconnected) => true,
227        Err(_) => false,
228    }
229}