Skip to content
Merged
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 67 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ use std::{
env, io,
path::{Path, PathBuf},
process::{Child, Command, ExitStatus},
sync::{Arc, Mutex, mpsc},
sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc},
thread,
time::{Duration, Instant},
};
Expand Down Expand Up @@ -235,6 +235,8 @@ pub struct Watch {
exclude_globs: Vec<Pattern>,
#[clap(skip)]
workspace_exclude_globs: Vec<Pattern>,
#[clap(skip)]
watch_lock: WatchLock,
}

impl Watch {
Expand Down Expand Up @@ -287,6 +289,15 @@ impl Watch {
self
}

/// Return the shared lock used by this watcher.
///
/// Clone and share this lock with external code (e.g. HTTP handlers) to coordinate with
/// watch-driven command execution.
#[must_use = "store and share the lock with readers that must coordinate with rebuilds"]
pub fn lock(&self) -> WatchLock {
Comment thread
cecton marked this conversation as resolved.
self.watch_lock.clone()
}

/// Set the debounce duration after relaunching the command.
pub fn debounce(mut self, duration: Duration) -> Self {
self.debounce = duration;
Expand Down Expand Up @@ -363,20 +374,28 @@ impl Watch {
log::info!("Re-running command");
let mut current_child = current_child.clone();
let mut list = list.clone();
let lock = self.watch_lock.clone();
thread::spawn(move || {
let mut status = ExitStatus::default();
list.spawn(|res| match res {
Err(err) => {
log::error!("Could not execute command: {err}");
false
}
Ok(child) => {
log::trace!("new child: {}", child.id());
current_child.replace(child);
status = current_child.wait();
status.success()
}
});

let mut run_batch = || {
list.spawn(|res| match res {
Err(err) => {
log::error!("Could not execute command: {err}");
false
}
Ok(child) => {
log::trace!("new child: {}", child.id());
current_child.replace(child);
status = current_child.wait();
status.success()
}
});
};

let _guard = lock.write();
run_batch();
Comment thread
cecton marked this conversation as resolved.
Outdated

if status.success() {
log::info!("Command succeeded.");
} else if let Some(code) = status.code() {
Expand Down Expand Up @@ -675,6 +694,41 @@ impl CommandList {
}
}

/// Guard returned by [`WatchLock::acquire`].
///
/// Keep this value alive for the duration of the protected read section.
/// The lock is released automatically when the guard is dropped.
pub struct WatchLockGuard<'a> {
_guard: RwLockReadGuard<'a, ()>,
}

/// A lock handle used to coordinate file reads with watch-driven rebuilds.
///
/// Obtain it from [`Watch::lock`], clone it, and call [`WatchLock::acquire`] while
/// reading files that must not race with rebuild writes.
#[derive(Clone, Debug, Default)]
pub struct WatchLock(Arc<RwLock<()>>);

impl WatchLock {
/// Acquire shared access to the protected section.
///
/// Multiple readers may hold this guard concurrently.
pub fn acquire(&self) -> WatchLockGuard<'_> {
WatchLockGuard {
_guard: self
.0
.read()
.expect("watch lock poisoned while acquiring read access"),
}
}

fn write(&self) -> RwLockWriteGuard<'_, ()> {
self.0
.write()
.expect("watch lock poisoned while acquiring write access")
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
Loading