1#![deny(missing_docs)]
160
161use anyhow::{Context, Result};
162use clap::Parser;
163use lazy_static::lazy_static;
164use notify::{Event, EventHandler, RecursiveMode, Watcher};
165use std::{
166 env, io,
167 path::{Path, PathBuf},
168 process::{Child, Command, ExitStatus},
169 sync::{mpsc, Arc, Mutex},
170 thread,
171 time::{Duration, Instant},
172};
173
174pub use anyhow;
175pub use cargo_metadata;
176pub use cargo_metadata::camino;
177pub use clap;
178
179pub fn metadata() -> &'static cargo_metadata::Metadata {
181 lazy_static! {
182 static ref METADATA: cargo_metadata::Metadata = cargo_metadata::MetadataCommand::new()
183 .exec()
184 .expect("cannot get crate's metadata");
185 }
186
187 &METADATA
188}
189
190pub fn package(name: &str) -> Option<&cargo_metadata::Package> {
192 metadata().packages.iter().find(|x| x.name == name)
193}
194
195pub fn xtask_command() -> Command {
197 Command::new(env::args_os().next().unwrap())
198}
199
200#[non_exhaustive]
203#[derive(Clone, Debug, Default, Parser)]
204#[clap(about = "Watches over your project's source code.")]
205pub struct Watch {
206 #[clap(long = "watch", short = 'w')]
210 pub watch_paths: Vec<PathBuf>,
211 #[clap(long = "ignore", short = 'i')]
213 pub exclude_paths: Vec<PathBuf>,
214 #[clap(skip)]
216 pub workspace_exclude_paths: Vec<PathBuf>,
217 #[clap(skip = Duration::from_secs(2))]
222 pub debounce: Duration,
223}
224
225impl Watch {
226 pub fn watch_path(mut self, path: impl AsRef<Path>) -> Self {
228 self.watch_paths.push(path.as_ref().to_path_buf());
229 self
230 }
231
232 pub fn watch_paths(mut self, paths: impl IntoIterator<Item = impl AsRef<Path>>) -> Self {
234 for path in paths {
235 self.watch_paths.push(path.as_ref().to_path_buf())
236 }
237 self
238 }
239
240 pub fn exclude_path(mut self, path: impl AsRef<Path>) -> Self {
242 self.exclude_paths.push(path.as_ref().to_path_buf());
243 self
244 }
245
246 pub fn exclude_paths(mut self, paths: impl IntoIterator<Item = impl AsRef<Path>>) -> Self {
248 for path in paths {
249 self.exclude_paths.push(path.as_ref().to_path_buf());
250 }
251 self
252 }
253
254 pub fn exclude_workspace_path(mut self, path: impl AsRef<Path>) -> Self {
257 self.workspace_exclude_paths
258 .push(path.as_ref().to_path_buf());
259 self
260 }
261
262 pub fn exclude_workspace_paths(
265 mut self,
266 paths: impl IntoIterator<Item = impl AsRef<Path>>,
267 ) -> Self {
268 for path in paths {
269 self.workspace_exclude_paths
270 .push(path.as_ref().to_path_buf());
271 }
272 self
273 }
274
275 pub fn debounce(mut self, duration: Duration) -> Self {
277 self.debounce = duration;
278 self
279 }
280
281 pub fn run(mut self, commands: impl Into<CommandList>) -> Result<()> {
286 let commands = commands.into();
287 let metadata = metadata();
288
289 self.exclude_paths
290 .push(metadata.target_directory.clone().into_std_path_buf());
291
292 self.exclude_paths = self
293 .exclude_paths
294 .into_iter()
295 .map(|x| {
296 x.canonicalize()
297 .with_context(|| format!("can't find {}", x.display()))
298 })
299 .collect::<Result<Vec<_>, _>>()?;
300
301 if self.watch_paths.is_empty() {
302 self.watch_paths
303 .push(metadata.workspace_root.clone().into_std_path_buf());
304 }
305
306 self.watch_paths = self
307 .watch_paths
308 .into_iter()
309 .map(|x| {
310 x.canonicalize()
311 .with_context(|| format!("can't find {}", x.display()))
312 })
313 .collect::<Result<Vec<_>, _>>()?;
314
315 let (tx, rx) = mpsc::channel();
316
317 let handler = WatchEventHandler {
318 watch: self.clone(),
319 tx,
320 command_start: Instant::now(),
321 };
322
323 let mut watcher =
324 notify::recommended_watcher(handler).context("could not initialize watcher")?;
325
326 for path in &self.watch_paths {
327 match watcher.watch(path, RecursiveMode::Recursive) {
328 Ok(()) => log::trace!("Watching {}", path.display()),
329 Err(err) => log::error!("cannot watch {}: {err}", path.display()),
330 }
331 }
332
333 let mut current_child = SharedChild::new();
334 loop {
335 {
336 log::info!("Re-running command");
337 let mut current_child = current_child.clone();
338 let mut commands = commands.clone();
339 thread::spawn(move || {
340 let mut status = ExitStatus::default();
341 commands.spawn(|res| match res {
342 Err(err) => {
343 log::error!("Could not execute command: {err}");
344 false
345 }
346 Ok(child) => {
347 log::trace!("new child: {}", child.id());
348 current_child.replace(child);
349 status = current_child.wait();
350 status.success()
351 }
352 });
353 if status.success() {
354 log::info!("Command succeeded.");
355 } else if let Some(code) = status.code() {
356 log::error!("Command failed (exit code: {code})");
357 } else {
358 log::error!("Command failed.");
359 }
360 });
361 }
362
363 let res = rx.recv();
364 if res.is_ok() {
365 log::trace!("changes detected");
366 }
367 current_child.terminate();
368 if res.is_err() {
369 break;
370 }
371 }
372
373 Ok(())
374 }
375
376 fn is_excluded_path(&self, path: &Path) -> bool {
377 if self.exclude_paths.iter().any(|x| path.starts_with(x)) {
378 return true;
379 }
380
381 if let Ok(stripped_path) = path.strip_prefix(metadata().workspace_root.as_std_path()) {
382 if self
383 .workspace_exclude_paths
384 .iter()
385 .any(|x| stripped_path.starts_with(x))
386 {
387 return true;
388 }
389 }
390
391 false
392 }
393
394 fn is_hidden_path(&self, path: &Path) -> bool {
395 self.watch_paths.iter().any(|x| {
396 path.strip_prefix(x)
397 .iter()
398 .any(|x| x.to_string_lossy().starts_with('.'))
399 })
400 }
401
402 fn is_backup_file(&self, path: &Path) -> bool {
403 self.watch_paths.iter().any(|x| {
404 path.strip_prefix(x)
405 .iter()
406 .any(|x| x.to_string_lossy().ends_with('~'))
407 })
408 }
409}
410
411struct WatchEventHandler {
412 watch: Watch,
413 tx: mpsc::Sender<()>,
414 command_start: Instant,
415}
416
417impl EventHandler for WatchEventHandler {
418 fn handle_event(&mut self, event: Result<Event, notify::Error>) {
419 match event {
420 Ok(event) => {
421 if event.paths.iter().any(|x| {
422 !self.watch.is_excluded_path(x)
423 && x.exists()
424 && !self.watch.is_hidden_path(x)
425 && !self.watch.is_backup_file(x)
426 && event.kind != notify::EventKind::Create(notify::event::CreateKind::Any)
427 && event.kind
428 != notify::EventKind::Modify(notify::event::ModifyKind::Name(
429 notify::event::RenameMode::Any,
430 ))
431 && self.command_start.elapsed() >= self.watch.debounce
432 }) {
433 log::trace!("Changes detected in {event:?}");
434 self.command_start = Instant::now();
435
436 self.tx.send(()).expect("can send");
437 } else {
438 log::trace!("Ignoring changes in {event:?}");
439 }
440 }
441 Err(err) => log::error!("watch error: {err}"),
442 }
443 }
444}
445
446#[derive(Debug, Clone)]
447struct SharedChild {
448 child: Arc<Mutex<Option<Child>>>,
449}
450
451impl SharedChild {
452 fn new() -> Self {
453 Self {
454 child: Default::default(),
455 }
456 }
457
458 fn replace(&mut self, child: impl Into<Option<Child>>) {
459 *self.child.lock().expect("not poisoned") = child.into();
460 }
461
462 fn wait(&mut self) -> ExitStatus {
463 loop {
464 let mut child = self.child.lock().expect("not poisoned");
465 match child.as_mut().map(|child| child.try_wait()) {
466 Some(Ok(Some(status))) => {
467 break status;
468 }
469 Some(Ok(None)) => {
470 drop(child);
471 thread::sleep(Duration::from_millis(10));
472 }
473 Some(Err(err)) => {
474 log::error!("could not wait for child process: {err}");
475 break Default::default();
476 }
477 None => {
478 break Default::default();
479 }
480 }
481 }
482 }
483
484 fn terminate(&mut self) {
485 if let Some(child) = self.child.lock().expect("not poisoned").as_mut() {
486 #[cfg(unix)]
487 {
488 let killing_start = Instant::now();
489
490 unsafe {
491 log::trace!("sending SIGTERM to {}", child.id());
492 libc::kill(child.id() as _, libc::SIGTERM);
493 }
494
495 while killing_start.elapsed().as_secs() < 2 {
496 std::thread::sleep(Duration::from_millis(200));
497 if let Ok(Some(_)) = child.try_wait() {
498 break;
499 }
500 }
501 }
502
503 match child.try_wait() {
504 Ok(Some(_)) => {}
505 _ => {
506 log::trace!("killing {}", child.id());
507 let _ = child.kill();
508 let _ = child.wait();
509 }
510 }
511 } else {
512 log::trace!("nothing to terminate");
513 }
514 }
515}
516
517#[derive(Debug, Clone)]
519pub struct CommandList {
520 commands: Arc<Mutex<Vec<Command>>>,
521}
522
523impl From<Command> for CommandList {
524 fn from(command: Command) -> Self {
525 Self {
526 commands: Arc::new(Mutex::new(vec![command])),
527 }
528 }
529}
530
531impl From<Vec<Command>> for CommandList {
532 fn from(commands: Vec<Command>) -> Self {
533 Self {
534 commands: Arc::new(Mutex::new(commands)),
535 }
536 }
537}
538
539impl<const SIZE: usize> From<[Command; SIZE]> for CommandList {
540 fn from(commands: [Command; SIZE]) -> Self {
541 Self {
542 commands: Arc::new(Mutex::new(Vec::from(commands))),
543 }
544 }
545}
546
547impl CommandList {
548 pub fn is_empty(&self) -> bool {
550 self.commands.lock().expect("not poisoned").is_empty()
551 }
552
553 pub fn spawn(&mut self, mut callback: impl FnMut(io::Result<Child>) -> bool) {
557 for process in self.commands.lock().expect("not poisoned").iter_mut() {
558 if !callback(process.spawn()) {
559 break;
560 }
561 }
562 }
563
564 pub fn status(&mut self) -> io::Result<ExitStatus> {
567 for process in self.commands.lock().expect("not poisoned").iter_mut() {
568 let exit_status = process.status()?;
569 if !exit_status.success() {
570 return Ok(exit_status);
571 }
572 }
573 Ok(Default::default())
574 }
575}
576
577#[cfg(test)]
578mod test {
579 use super::*;
580
581 #[test]
582 fn exclude_relative_path() {
583 let watch = Watch {
584 debounce: Default::default(),
585 watch_paths: Vec::new(),
586 exclude_paths: Vec::new(),
587 workspace_exclude_paths: vec![PathBuf::from("src/watch.rs")],
588 };
589
590 assert!(watch.is_excluded_path(
591 metadata()
592 .workspace_root
593 .join("src")
594 .join("watch.rs")
595 .as_std_path()
596 ));
597 assert!(!watch.is_excluded_path(metadata().workspace_root.join("src").as_std_path()));
598 }
599
600 #[test]
601 fn command_list_froms() {
602 let _: CommandList = Command::new("foo").into();
603 let _: CommandList = vec![Command::new("foo")].into();
604 let _: CommandList = [Command::new("foo")].into();
605 }
606}