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.
This commit is contained in:
Pi Lanningham 2023-11-10 21:35:55 -05:00 committed by KtorZ
parent 771f6d1601
commit 5068da3a17
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
7 changed files with 122 additions and 83 deletions

View File

@ -26,6 +26,7 @@ indexmap = "1.9.2"
itertools = "0.10.5" itertools = "0.10.5"
miette = { version = "5.9.0", features = ["fancy"] } miette = { version = "5.9.0", features = ["fancy"] }
minicbor = "0.19.1" minicbor = "0.19.1"
notify = "6.1.1"
owo-colors = { version = "3.5.0", features = ["supports-colors"] } owo-colors = { version = "3.5.0", features = ["supports-colors"] }
pallas = "0.18.0" pallas = "0.18.0"
pallas-traverse = "0.18.0" pallas-traverse = "0.18.0"

View File

@ -14,6 +14,7 @@ pub mod script;
pub mod telemetry; pub mod telemetry;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub mod watch;
use crate::blueprint::{ use crate::blueprint::{
definitions::Definitions, definitions::Definitions,

View File

@ -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<T, F, A>(
directory: Option<PathBuf>,
events: T,
filter: F,
debounce: u32,
mut action: A,
) -> miette::Result<()>
where
T: Copy + telemetry::EventListener,
F: Fn(&Event) -> bool,
A: FnMut(&mut Project<T>) -> Result<(), Vec<crate::error::Error>>,
{
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<Event>| {
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("??")))?;
}
}
}

View File

@ -25,7 +25,6 @@ hex = "0.4.3"
ignore = "0.4.20" ignore = "0.4.20"
indoc = "2.0" indoc = "2.0"
miette = { version = "5.5.0", features = ["fancy"] } miette = { version = "5.5.0", features = ["fancy"] }
notify = "6.1.1"
owo-colors = { version = "3.5.0", features = ["supports-colors"] } owo-colors = { version = "3.5.0", features = ["supports-colors"] }
pallas-addresses = "0.18.0" pallas-addresses = "0.18.0"
pallas-codec = "0.18.0" pallas-codec = "0.18.0"

View File

@ -12,7 +12,6 @@ pub mod new;
pub mod packages; pub mod packages;
pub mod tx; pub mod tx;
pub mod uplc; pub mod uplc;
pub mod watch;
/// Aiken: a smart-contract language and toolchain for Cardano /// Aiken: a smart-contract language and toolchain for Cardano
#[derive(Parser)] #[derive(Parser)]
@ -24,7 +23,6 @@ pub enum Cmd {
Build(build::Args), Build(build::Args),
Address(blueprint::address::Args), Address(blueprint::address::Args),
Check(check::Args), Check(check::Args),
Watch(watch::Args),
Docs(docs::Args), Docs(docs::Args),
Add(packages::add::Args), Add(packages::add::Args),

View File

@ -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<Event>| {
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())
});
}
}
}

View File

@ -2,7 +2,7 @@ use aiken::cmd::{
blueprint::{self, address}, blueprint::{self, address},
build, check, completion, docs, fmt, lsp, new, build, check, completion, docs, fmt, lsp, new,
packages::{self, add}, packages::{self, add},
tx, uplc, watch, Cmd, tx, uplc, Cmd,
}; };
use aiken_project::{config, pretty}; use aiken_project::{config, pretty};
@ -17,7 +17,6 @@ fn main() -> miette::Result<()> {
Cmd::Build(args) => build::exec(args), Cmd::Build(args) => build::exec(args),
Cmd::Address(args) => address::exec(args), Cmd::Address(args) => address::exec(args),
Cmd::Check(args) => check::exec(args), Cmd::Check(args) => check::exec(args),
Cmd::Watch(args) => watch::exec(args),
Cmd::Docs(args) => docs::exec(args), Cmd::Docs(args) => docs::exec(args),
Cmd::Add(args) => add::exec(args), Cmd::Add(args) => add::exec(args),
Cmd::Blueprint(args) => blueprint::exec(args), Cmd::Blueprint(args) => blueprint::exec(args),