Skip to content
Merged
Changes from 7 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
79 changes: 67 additions & 12 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, PoisonError, 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: Option<WatchLock>,
Comment thread
yozhgoor marked this conversation as resolved.
Outdated
}

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

/// Return the shared lock for this watcher, creating it if it does not exist yet.
///
/// Clone and share this lock with external code (e.g. HTTP handlers) to coordinate with
/// watch-driven command execution.
pub fn lock(&mut self) -> WatchLock {
Comment thread
yozhgoor marked this conversation as resolved.
Outdated
self.watch_lock.get_or_insert_with(WatchLock::new).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 +373,37 @@ 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()
}
});
};

if let Some(lock) = lock {
match lock.write() {
Ok(_guard) => run_batch(),
Err(err) => {
log::error!("could not acquire write lock: {err}");
return;
}
}
});
} else {
run_batch();
}

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

/// Shared reader/writer synchronization primitive used to coordinate watch-driven
Comment thread
yozhgoor marked this conversation as resolved.
Outdated
/// command execution with external code.
///
/// Clone this type to share the same lock across threads/components.
#[derive(Clone, Debug, Default)]
pub struct WatchLock(Arc<RwLock<()>>);

impl WatchLock {
/// Create a new lock instance.
pub fn new() -> Self {
Comment thread
yozhgoor marked this conversation as resolved.
Outdated
Self::default()
}

/// Acquire a shared read lock, blocking until no writer holds the lock.
///
/// Multiple readers may hold this lock concurrently.
pub fn read(&self) -> Result<RwLockReadGuard<'_, ()>, PoisonError<RwLockReadGuard<'_, ()>>> {
Comment thread
yozhgoor marked this conversation as resolved.
Outdated
self.0.read()
}

/// Acquire an exclusive write lock, blocking until all readers/writers release it.
///
/// Use this for operations that mutate shared state (e.g. rebuild output files).
pub fn write(&self) -> Result<RwLockWriteGuard<'_, ()>, PoisonError<RwLockWriteGuard<'_, ()>>> {
Comment thread
yozhgoor marked this conversation as resolved.
Outdated
self.0.write()
}
}
Comment thread
cecton marked this conversation as resolved.

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