use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{sync_channel, SyncSender};
use std::sync::Arc;
use std::thread;
use anyhow::{anyhow, Result};
use crate::livereload::LiveReload;
use crate::reporter::RebuildReporter;
const TICK_CHANNEL_CAPACITY: usize = 8;
pub type TickSender = SyncSender<Vec<PathBuf>>;
#[must_use]
pub struct RebuildWorker {
sender: TickSender,
stopping: Arc<AtomicBool>,
thread: Option<thread::JoinHandle<()>>,
}
impl RebuildWorker {
#[must_use]
pub fn sender(&self) -> TickSender {
self.sender.clone()
}
pub fn shutdown(mut self) -> Result<()> {
self.stop_and_join()
}
fn stop_and_join(&mut self) -> Result<()> {
self.stopping.store(true, Ordering::Release);
let _ = self.sender.try_send(Vec::new());
if let Some(thread) = self.thread.take() {
thread.join().map_err(|_| {
anyhow!("Rebuild worker panicked; check the preceding build output")
})?;
}
Ok(())
}
}
impl Drop for RebuildWorker {
fn drop(&mut self) {
if let Err(error) = self.stop_and_join() {
eprintln!("{error}");
}
}
}
pub struct RebuildError {
display: String,
message: String,
}
impl RebuildError {
#[must_use]
pub fn new(display: String, message: String) -> Self {
Self { display, message }
}
#[must_use]
pub fn plain(text: String) -> Self {
Self {
display: text.clone(),
message: text,
}
}
}
impl From<String> for RebuildError {
fn from(text: String) -> Self {
Self::plain(text)
}
}
impl From<&str> for RebuildError {
fn from(text: &str) -> Self {
Self::plain(text.to_owned())
}
}
pub fn spawn_rebuild_worker<F>(livereload: LiveReload, mut rebuild: F) -> RebuildWorker
where
F: FnMut() -> Result<Vec<String>, RebuildError> + Send + 'static,
{
let (tx, rx) = sync_channel::<Vec<PathBuf>>(TICK_CHANNEL_CAPACITY);
let stopping = Arc::new(AtomicBool::new(false));
let worker_stopping = Arc::clone(&stopping);
let handle = thread::spawn(move || {
let mut reporter = RebuildReporter::new();
let mut dirty = false;
let mut carryover: Vec<PathBuf> = Vec::new();
while !worker_stopping.load(Ordering::Acquire) {
let mut triggers = std::mem::take(&mut carryover);
if dirty {
while let Ok(paths) = rx.try_recv() {
triggers.extend(paths);
}
} else {
match rx.recv() {
Ok(paths) => triggers.extend(paths),
Err(_) => break,
}
while let Ok(paths) = rx.try_recv() {
triggers.extend(paths);
}
}
if worker_stopping.load(Ordering::Acquire) {
break;
}
let start = std::time::Instant::now();
let result = rebuild();
dirty = false;
while let Ok(paths) = rx.try_recv() {
dirty = true;
carryover.extend(paths);
}
match result {
Ok(warnings) => {
reporter.success(start.elapsed(), &triggers);
reporter.warnings(&warnings);
livereload.broadcast_reload();
}
Err(e) => {
reporter.error(&e.display);
livereload.broadcast_error(e.message);
}
}
}
});
RebuildWorker {
sender: tx,
stopping,
thread: Some(handle),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
use std::sync::mpsc::{channel, TryRecvError};
use std::time::{Duration, Instant};
#[test]
fn shutdown_drains_active_work_but_discards_queued_ticks_with_live_senders() -> Result<()> {
let (started_tx, started_rx) = channel();
let (release_tx, release_rx) = channel();
let completed = Arc::new(AtomicUsize::new(0));
let worker_completed = Arc::clone(&completed);
let worker = spawn_rebuild_worker(LiveReload::new("/reload"), move || {
started_tx.send(()).map_err(|e| e.to_string())?;
release_rx.recv().map_err(|e| e.to_string())?;
worker_completed.fetch_add(1, Ordering::Release);
Ok(Vec::new())
});
let sender = worker.sender();
sender.try_send(Vec::new())?;
started_rx.recv_timeout(Duration::from_secs(5))?;
for _ in 0..TICK_CHANNEL_CAPACITY {
sender.try_send(Vec::new())?;
}
let stopping = Arc::clone(&worker.stopping);
let (joined_tx, joined_rx) = channel();
let shutdown = thread::spawn(move || {
let result = worker.shutdown();
let _ = joined_tx.send(());
result
});
let deadline = Instant::now() + Duration::from_secs(5);
while !stopping.load(Ordering::Acquire) {
assert!(Instant::now() < deadline, "shutdown did not start");
thread::yield_now();
}
assert_eq!(joined_rx.try_recv(), Err(TryRecvError::Empty));
assert_eq!(completed.load(Ordering::Acquire), 0);
release_tx.send(())?;
joined_rx.recv_timeout(Duration::from_secs(5))?;
shutdown
.join()
.map_err(|_| anyhow!("shutdown panicked"))??;
assert_eq!(completed.load(Ordering::Acquire), 1);
assert!(sender.try_send(Vec::new()).is_err());
Ok(())
}
}