From 5068da3a17bca72a87bbbda4ae7c682b34798230 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Fri, 10 Nov 2023 21:35:55 -0500 Subject: [PATCH] Refactor into cargo-project Rather than have this logic in the aiken binary, this provides a generic mechanism to do "something" on file change events. KtorZ is going to handle wiring it up to the CLI in the best way for the project. I tried to write some tests for this, but it's hard to isolate the watcher logic without wrestling with the borrow checker, or overly neutering this utility. --- crates/aiken-project/Cargo.toml | 1 + crates/aiken-project/src/lib.rs | 1 + crates/aiken-project/src/watch.rs | 119 ++++++++++++++++++++++++++++++ crates/aiken/Cargo.toml | 1 - crates/aiken/src/cmd/mod.rs | 2 - crates/aiken/src/cmd/watch.rs | 78 -------------------- crates/aiken/src/main.rs | 3 +- 7 files changed, 122 insertions(+), 83 deletions(-) create mode 100644 crates/aiken-project/src/watch.rs delete mode 100644 crates/aiken/src/cmd/watch.rs diff --git a/crates/aiken-project/Cargo.toml b/crates/aiken-project/Cargo.toml index 1e425cf3..fa7e8bc5 100644 --- a/crates/aiken-project/Cargo.toml +++ b/crates/aiken-project/Cargo.toml @@ -26,6 +26,7 @@ indexmap = "1.9.2" itertools = "0.10.5" miette = { version = "5.9.0", features = ["fancy"] } minicbor = "0.19.1" +notify = "6.1.1" owo-colors = { version = "3.5.0", features = ["supports-colors"] } pallas = "0.18.0" pallas-traverse = "0.18.0" diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 9b0f14c2..b3f0d3ba 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -14,6 +14,7 @@ pub mod script; pub mod telemetry; #[cfg(test)] mod tests; +pub mod watch; use crate::blueprint::{ definitions::Definitions, diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs new file mode 100644 index 00000000..ac6fea08 --- /dev/null +++ b/crates/aiken-project/src/watch.rs @@ -0,0 +1,119 @@ +use miette::IntoDiagnostic; +use notify::{Event, RecursiveMode, Watcher}; +use std::{ + collections::VecDeque, + env, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use crate::{telemetry, Project}; + +/// A default filter for file events that catches the most relevant "source" changes +pub fn default_filter(evt: &Event) -> bool { + // Only watch for changes to .ak and aiken.toml files, and ignore the build directory + let source_file = evt + .paths + .iter() + .any(|p| p.ends_with(".ak") || p.ends_with("aiken.toml")); + let build_dir = evt + .paths + .iter() + .all(|p| p.ancestors().any(|a| a.ends_with("build"))); + match evt.kind { + notify::EventKind::Create(_) + | notify::EventKind::Modify(_) + | notify::EventKind::Remove(_) => source_file && !build_dir, + _ => false, + } +} + +/// Run a function each time a file in the project changes +/// +/// ``` +/// use aiken_project::watch::{watch_project, default_filter}; +/// use aiken_project::Project; +/// watch_project(None, Terminal, default_filter, 500, |project| { +/// println!("Project changed!"); +/// Ok(()) +/// }) +/// ``` +pub fn watch_project( + directory: Option, + events: T, + filter: F, + debounce: u32, + mut action: A, +) -> miette::Result<()> +where + T: Copy + telemetry::EventListener, + F: Fn(&Event) -> bool, + A: FnMut(&mut Project) -> Result<(), Vec>, +{ + let project_path = directory.unwrap_or(env::current_dir().into_diagnostic()?); + + // Set up a queue for events, primarily so we can debounce on related events + let queue = Arc::new(Mutex::new(VecDeque::new())); + + // pre-seed that queue with a single event, so it builds once at the start + queue.lock().unwrap().push_back(Event::default()); + + // Spawn a file-watcher that will put each change event on the queue + let queue_write = queue.clone(); + let mut watcher = notify::recommended_watcher(move |res: notify::Result| { + match res { + Ok(event) => queue_write + .lock() + .expect("lock queue") + .push_back(event.clone()), + Err(e) => { + // TODO: miette diagnostic? + println!( + "Encountered an error while monitoring for file changes: {:?}", + e + ) + } + }; + }) + .into_diagnostic()?; + + // Start watching for any changes in the project directory + let _ = watcher.watch(project_path.as_path(), RecursiveMode::Recursive); + + // And then start reading from the queue + let queue_read = queue.clone(); + loop { + // We sleep for the debounce interval, because notify will dump 12 related events into the queue all at once + std::thread::sleep(std::time::Duration::from_millis(debounce.into())); + + // Grab the lock, and pop all events except the last one off the queue + let mut queue = queue_read.lock().expect("lock queue"); + let mut latest = None; + // debounce the events, and ignore build/lock changes, because they come in in large batches + while let Some(evt) = queue.pop_back() { + // check if this event is meaningful to the caller + if !filter(&evt) { + continue; + } + latest = Some(evt); + } + // release the lock here, in case other events come in + drop(queue); + + // If we have an event that survived the filter, then we can construct the project and invoke the action + if latest.is_some() { + let mut project = match Project::new(project_path.clone(), events) { + Ok(p) => p, + Err(e) => { + // TODO: what should we actually do here? + e.report(); + return Err(miette::Report::msg("??")); + } + }; + + // Invoke the action, and abort on an error + // TODO: what should we actually do with the error here? + action(&mut project).or(Err(miette::Report::msg("??")))?; + } + } +} diff --git a/crates/aiken/Cargo.toml b/crates/aiken/Cargo.toml index f6b24621..6b87830a 100644 --- a/crates/aiken/Cargo.toml +++ b/crates/aiken/Cargo.toml @@ -25,7 +25,6 @@ hex = "0.4.3" ignore = "0.4.20" indoc = "2.0" miette = { version = "5.5.0", features = ["fancy"] } -notify = "6.1.1" owo-colors = { version = "3.5.0", features = ["supports-colors"] } pallas-addresses = "0.18.0" pallas-codec = "0.18.0" diff --git a/crates/aiken/src/cmd/mod.rs b/crates/aiken/src/cmd/mod.rs index b96c0727..83f59ff4 100644 --- a/crates/aiken/src/cmd/mod.rs +++ b/crates/aiken/src/cmd/mod.rs @@ -12,7 +12,6 @@ pub mod new; pub mod packages; pub mod tx; pub mod uplc; -pub mod watch; /// Aiken: a smart-contract language and toolchain for Cardano #[derive(Parser)] @@ -24,7 +23,6 @@ pub enum Cmd { Build(build::Args), Address(blueprint::address::Args), Check(check::Args), - Watch(watch::Args), Docs(docs::Args), Add(packages::add::Args), diff --git a/crates/aiken/src/cmd/watch.rs b/crates/aiken/src/cmd/watch.rs deleted file mode 100644 index efc224d7..00000000 --- a/crates/aiken/src/cmd/watch.rs +++ /dev/null @@ -1,78 +0,0 @@ -use notify::{event::EventAttributes, Event, RecursiveMode, Watcher}; -use std::{ - collections::VecDeque, - path::Path, - sync::{Arc, Mutex}, -}; -#[derive(clap::Args)] -/// Type-check an Aiken project -pub struct Args { - /// Clear the screen between each run - #[clap(long)] - clear: bool, -} - -pub fn exec(Args { clear }: Args) -> miette::Result<()> { - let project = Path::new("../sundae-contracts/aiken") - .to_path_buf() - .canonicalize() - .expect(""); - let build = project.join("build").canonicalize().expect(""); - let lock = project.join("aiken.lock"); - let queue = Arc::new(Mutex::new(VecDeque::new())); - queue.lock().unwrap().push_back(Event { - kind: notify::EventKind::Any, - paths: vec![], - attrs: EventAttributes::new(), - }); - - let queue_write = queue.clone(); - let mut watcher = notify::recommended_watcher(move |res: notify::Result| { - match res { - Ok(event) => match event.kind { - notify::EventKind::Create(_) - | notify::EventKind::Modify(_) - | notify::EventKind::Remove(_) => { - let mut queue = queue_write.lock().expect("lock queue"); - queue.push_back(event.clone()); - drop(queue); - } - _ => {} - }, - Err(e) => { - println!("watch error: {:?}", e) - } - }; - }) - .expect("watcher"); - let _ = watcher.watch( - Path::new("../sundae-contracts/aiken"), - RecursiveMode::Recursive, - ); - let queue_read = queue.clone(); - loop { - std::thread::sleep(std::time::Duration::from_millis(300)); - - let mut queue = queue_read.lock().expect("lock queue"); - let mut latest = None; - // debounce the events, and ignore build/lock changes, because they come in in large batches - while let Some(evt) = queue.pop_back() { - if evt.paths.iter().any(|p| { - let p = p.canonicalize().expect(""); - p.starts_with(&build) || p.starts_with(&lock) - }) { - continue; - } - latest = Some(evt); - } - drop(queue); - if latest.is_some() { - if clear { - println!("{esc}c", esc = 27 as char); - } - let _ = crate::with_project_ok(Some(project.clone()), false, |p| { - p.check(false, None, true, false, false.into()) - }); - } - } -} diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index 16791d25..777766c8 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -2,7 +2,7 @@ use aiken::cmd::{ blueprint::{self, address}, build, check, completion, docs, fmt, lsp, new, packages::{self, add}, - tx, uplc, watch, Cmd, + tx, uplc, Cmd, }; use aiken_project::{config, pretty}; @@ -17,7 +17,6 @@ fn main() -> miette::Result<()> { Cmd::Build(args) => build::exec(args), Cmd::Address(args) => address::exec(args), Cmd::Check(args) => check::exec(args), - Cmd::Watch(args) => watch::exec(args), Cmd::Docs(args) => docs::exec(args), Cmd::Add(args) => add::exec(args), Cmd::Blueprint(args) => blueprint::exec(args),