use super::{
host::{Host, on_host},
saved::{panic_message, show_error},
};
use crate::RoughPlanModel;
use slint::ComponentHandle;
use std::{
panic::{AssertUnwindSafe, catch_unwind},
rc::Rc,
sync::atomic::{AtomicU64, Ordering},
};
use tracing::warn;
static NEXT_TICKET: AtomicU64 = AtomicU64::new(1);
pub(super) const MESH_BUSY_MESSAGE: &str =
"The mesh is still being read or scaled. Wait for it to finish first.";
const PLAN_RUNNING_MESSAGE: &str = "A plan started before the mesh was ready, so the mesh was not used. Do it again when the plan has finished.";
#[derive(Debug, Default)]
pub(super) struct MeshJobs {
current: Option<u64>,
}
impl MeshJobs {
pub(super) fn begin(&mut self) -> u64 {
let ticket = NEXT_TICKET.fetch_add(1, Ordering::Relaxed);
self.current = Some(ticket);
ticket
}
pub(super) fn finish(&mut self, ticket: u64) -> bool {
if self.current == Some(ticket) {
self.current = None;
true
} else {
false
}
}
pub(super) const fn cancel(&mut self) -> bool {
self.current.take().is_some()
}
#[must_use]
pub(super) const fn is_busy(&self) -> bool {
self.current.is_some()
}
}
pub(super) fn guarded<T>(work: impl FnOnce() -> Result<T, String>) -> Result<T, String> {
catch_unwind(AssertUnwindSafe(work)).unwrap_or_else(|payload| {
let message = panic_message(&*payload);
warn!("Rough planner: a mesh task panicked: {message}");
Err(format!(
"it stopped unexpectedly ({message}); nothing was changed"
))
})
}
fn show_busy(host: &Host, text: Option<&str>) {
let model = host.window.global::<RoughPlanModel>();
model.set_mesh_busy_text(text.unwrap_or_default().into());
model.set_mesh_busy(text.is_some());
}
pub(super) fn cancel(host: &Host) {
let was_running = host.session.borrow_mut().mesh_jobs.cancel();
if was_running {
show_busy(host, None);
}
}
pub(super) fn is_busy(host: &Host) -> bool {
host.session.borrow().mesh_jobs.is_busy()
}
pub(super) fn spawn<T: Send + 'static>(
host: &Rc<Host>,
busy_text: &str,
work: impl FnOnce() -> Result<T, String> + Send + 'static,
done: impl FnOnce(&Rc<Host>, Result<T, String>) + Send + 'static,
) {
let ticket = host.session.borrow_mut().mesh_jobs.begin();
show_busy(host, Some(busy_text));
let weak = host.window.as_weak();
let spawned = std::thread::Builder::new()
.name("rough-mesh".to_string())
.spawn(move || {
let value = guarded(work);
let _ = weak.upgrade_in_event_loop(move |_window| {
on_host(|host| deliver(host, ticket, value, done));
});
});
if let Err(error) = spawned {
warn!("Rough planner: could not start the mesh thread: {error}");
cancel(host);
show_error(host, &format!("Could not start a background task: {error}"));
}
}
fn deliver<T>(
host: &Rc<Host>,
ticket: u64,
value: Result<T, String>,
done: impl FnOnce(&Rc<Host>, Result<T, String>),
) {
let wanted = host.session.borrow_mut().mesh_jobs.finish(ticket);
if !wanted {
return;
}
show_busy(host, None);
if host.window.global::<RoughPlanModel>().get_running() {
show_error(host, PLAN_RUNNING_MESSAGE);
return;
}
done(host, value);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_job_is_wanted_until_it_finishes_and_only_once() {
let mut jobs = MeshJobs::default();
assert!(!jobs.is_busy());
let ticket = jobs.begin();
assert!(jobs.is_busy());
assert!(jobs.finish(ticket), "the job that was waited for");
assert!(!jobs.is_busy());
assert!(!jobs.finish(ticket), "its result is not applied twice");
}
#[test]
fn a_newer_job_makes_the_older_result_stale() {
let mut jobs = MeshJobs::default();
let first = jobs.begin();
let second = jobs.begin();
assert_ne!(first, second);
assert!(!jobs.finish(first), "a second import supersedes the first");
assert!(jobs.is_busy(), "the second job is still wanted");
assert!(jobs.finish(second));
}
#[test]
fn a_cancelled_job_is_dropped_and_the_window_is_not_busy() {
let mut jobs = MeshJobs::default();
assert!(!jobs.cancel(), "nothing to cancel");
let ticket = jobs.begin();
assert!(jobs.cancel());
assert!(!jobs.is_busy());
assert!(!jobs.finish(ticket), "a reset or a load dropped the result");
}
#[test]
fn a_ticket_from_another_window_never_matches() {
let mut old_window = MeshJobs::default();
let old = old_window.begin();
let mut new_window = MeshJobs::default();
let fresh = new_window.begin();
assert!(!new_window.finish(old));
assert!(new_window.finish(fresh));
}
#[test]
fn a_panic_in_the_work_becomes_a_message() {
let result: Result<u32, String> = guarded(|| panic!("the parse hit a bug"));
let message = result.expect_err("a panic is an error, not a crash");
assert!(message.contains("the parse hit a bug"), "{message}");
assert!(message.contains("nothing was changed"), "{message}");
}
#[test]
fn work_that_does_not_panic_keeps_its_own_result() {
assert_eq!(guarded(|| Ok::<u32, String>(7)), Ok(7));
assert_eq!(
guarded(|| Err::<u32, String>("a plain failure".to_string())),
Err("a plain failure".to_string())
);
}
}