use std::sync::mpsc;
use corundum_framework_service::{Environment, ServiceSpec, ServiceState, SystemManager};
#[derive(Clone, Copy)]
pub(crate) struct ServiceOps {
pub(crate) state: fn(&ServiceSpec) -> Result<ServiceState, String>,
pub(crate) install: fn(&ServiceSpec) -> Result<(), String>,
pub(crate) uninstall: fn(&ServiceSpec) -> Result<(), String>,
pub(crate) stop: fn(&ServiceSpec) -> Result<(), String>,
pub(crate) start: fn(&ServiceSpec) -> Result<(), String>,
}
impl ServiceOps {
pub(crate) const SYSTEM: ServiceOps = ServiceOps {
state: system_state,
install: system_install,
uninstall: system_uninstall,
stop: system_stop,
start: system_start,
};
}
fn system_state(spec: &ServiceSpec) -> Result<ServiceState, String> {
corundum_framework_service::state(spec, &Environment::detect(), &SystemManager)
.map_err(|err| err.to_string())
}
fn system_install(spec: &ServiceSpec) -> Result<(), String> {
corundum_framework_service::install(spec, &Environment::detect(), &SystemManager)
.map(drop)
.map_err(|err| err.to_string())
}
fn system_uninstall(spec: &ServiceSpec) -> Result<(), String> {
corundum_framework_service::uninstall(spec, &Environment::detect(), &SystemManager)
.map_err(|err| err.to_string())
}
fn system_stop(spec: &ServiceSpec) -> Result<(), String> {
corundum_framework_service::stop(spec, &Environment::detect(), &SystemManager)
.map_err(|err| err.to_string())
}
fn system_start(spec: &ServiceSpec) -> Result<(), String> {
corundum_framework_service::start(spec, &Environment::detect(), &SystemManager)
.map_err(|err| err.to_string())
}
struct RepaintOnDrop<T> {
ctx: egui::Context,
reply: Option<mpsc::Sender<Result<T, String>>>,
}
impl<T> Drop for RepaintOnDrop<T> {
fn drop(&mut self) {
drop(self.reply.take());
self.ctx.request_repaint();
}
}
pub(crate) struct Job<T> {
answer: mpsc::Receiver<Result<T, String>>,
}
impl<T: Send + 'static> Job<T> {
pub(crate) fn spawn(
ctx: &egui::Context,
name: &str,
work: impl FnOnce() -> T + Send + 'static,
) -> Job<T> {
let (sender, answer) = mpsc::channel();
let ctx = ctx.clone();
let reply = sender.clone();
let spawned = std::thread::Builder::new()
.name(name.to_owned())
.spawn(move || {
let guard = RepaintOnDrop {
ctx,
reply: Some(reply),
};
let answer = work();
if let Some(reply) = &guard.reply {
let _ = reply.send(Ok(answer));
}
});
if let Err(err) = spawned {
tracing::warn!("cannot start the {name} thread: {err}");
let _ = sender.send(Err(err.to_string()));
}
Job { answer }
}
pub(crate) fn poll(&self) -> Option<Result<T, String>> {
match self.answer.try_recv() {
Ok(answer) => Some(answer),
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => Some(Err(
"the background task stopped without an answer".to_owned(),
)),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use super::*;
#[test]
fn a_panicking_job_still_repaints_with_its_answer_ready() {
let ctx = egui::Context::default();
let repainted = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&repainted);
ctx.set_request_repaint_callback(move |_| flag.store(true, Ordering::SeqCst));
let job = Job::spawn(&ctx, "panicking job", || -> () { panic!("on purpose") });
let deadline = Instant::now() + Duration::from_secs(5);
while !repainted.load(Ordering::SeqCst) && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(2));
}
assert!(repainted.load(Ordering::SeqCst), "no repaint was requested");
assert!(matches!(job.poll(), Some(Err(_))));
}
}