diff --git a/Cargo.lock b/Cargo.lock index c5064bf..b9b83d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,6 +638,16 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", +] + [[package]] name = "crossbeam-deque" version = "0.7.4" @@ -645,7 +655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" dependencies = [ "crossbeam-epoch", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] @@ -657,7 +667,7 @@ checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ "autocfg", "cfg-if 0.1.10", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", "memoffset", @@ -671,7 +681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" dependencies = [ "cfg-if 0.1.10", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] @@ -686,6 +696,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1481,6 +1500,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" +[[package]] +name = "file-id" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6584280525fb2059cba3db2c04abf947a1a29a45ddae89f3870f8281704fafc9" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "filetime" version = "0.2.23" @@ -1568,6 +1596,15 @@ dependencies = [ "syn 2.0.40", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "fslock" version = "0.1.8" @@ -2070,6 +2107,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.3" @@ -2219,6 +2276,26 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2457,6 +2534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -2535,6 +2613,39 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.10", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f5dab59c348b9b50cf7f261960a20e389feb2713636399cd9082cd4b536154" +dependencies = [ + "crossbeam-channel", + "file-id", + "log", + "notify", + "parking_lot 0.12.1", + "walkdir", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4156,6 +4267,8 @@ dependencies = [ "git2", "indicatif", "insta", + "notify", + "notify-debouncer-full", "once_cell", "openssl", "reqwest", @@ -4809,7 +4922,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures 0.1.31", ] @@ -4862,7 +4975,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures 0.1.31", "lazy_static", "log", @@ -4929,7 +5042,7 @@ checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" dependencies = [ "crossbeam-deque", "crossbeam-queue", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures 0.1.31", "lazy_static", "log", @@ -4944,7 +5057,7 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures 0.1.31", "slab", "tokio-executor", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index dc94a62..fe4ce67 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -46,6 +46,9 @@ directories = "5" git2 = { version = "0.18", features = ["vendored-libgit2"] } cfg-if = "1" serde_with = "3" +notify = "6" +notify-debouncer-full = "0.3" + swarmd_slug-rs.workspace = true swarmd_generated.workspace = true diff --git a/cli/src/application/dev/mod.rs b/cli/src/application/dev/mod.rs index d700419..c8a0171 100644 --- a/cli/src/application/dev/mod.rs +++ b/cli/src/application/dev/mod.rs @@ -1,12 +1,15 @@ +use std::{env::current_dir, time::Duration}; + use crate::domain::{ - worker::simple_worker, + worker::start_background_worker, worker_config::{WorkerConfig, SWARMD_CONFIG_FILE}, Env, }; use anyhow::Context; use clap::Args; use console::{style, Emoji}; -use tokio::task::spawn_blocking; +use notify::{RecursiveMode, Watcher}; +use notify_debouncer_full::new_debouncer; use super::SwarmdCommand; use swarmd_instruments::debug; @@ -43,36 +46,83 @@ impl SwarmdCommand for DevArg { WorkerConfig::from_file(SWARMD_CONFIG_FILE).context("Couldn't load swarmd.toml.")?; let path_dist = config.path_main_dist().to_path_buf(); - // TODO: Add auto-reload when dist file change. + let (notify_sender, mut notify_receiver) = tokio::sync::mpsc::channel(1024); - let handle = spawn_blocking(|| { - let handle = std::thread::spawn(|| { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); + let path = current_dir()?; + let mut debouncer = new_debouncer(Duration::from_secs(1), None, move |evt| { + notify_sender.blocking_send(evt).unwrap(); + })?; - let local = tokio::task::LocalSet::new(); + let src_path = path.join("src"); + let node_modules = path.join("node_modules"); + debouncer + .watcher() + .watch(&src_path, RecursiveMode::Recursive)?; + debouncer + .watcher() + .watch(&node_modules, RecursiveMode::Recursive)?; - local.block_on(&runtime, async move { - let mut _worker = simple_worker(path_dist).await?; - Ok::<_, anyhow::Error>(()) - }) - }); - handle.join() - }); + debouncer + .cache() + .add_root(&src_path, RecursiveMode::Recursive); + debouncer + .cache() + .add_root(&node_modules, RecursiveMode::Recursive); - env.println("")?; - env.println("")?; + let env_cloned = env.clone(); + let handle = tokio::spawn(async move { + config.execute_no_log()?; + let mut handle = Some(start_background_worker(path_dist.clone())); - env.println(format!( - "Worker available at: {}", - style("http://127.0.0.1:13337").cyan().bold().dim(), - ))?; + env_cloned.println("")?; + env_cloned.println("")?; + + env_cloned.println(format!( + "Worker available at: {}", + style("http://127.0.0.1:13337").cyan().bold().dim(), + ))?; + + while let Some(elt) = notify_receiver.recv().await { + let mut should_reload = false; + match elt { + Ok(_) => { + should_reload = true; + } + Err(errors) => { + for e in errors { + let kind = e.kind; + env_cloned + .println(format!("{}:", style(format!("{kind:?}")).red().bold()))?; + + for path in e.paths { + env_cloned.println(format!( + " [{}]", + style(path.to_str().unwrap_or("")).italic() + ))?; + } + } + } + } + + if should_reload { + if let Some((_, isolate_handle)) = handle.take() { + let isolate = isolate_handle.await.expect( + "Shouldn't fail as the isolate is send directly after the isolate creation", + ); + let worker_over = isolate.terminate_execution().await?; + let _ = worker_over.await; + } + env_cloned.println(format!("{}", style("Reloading...").cyan().bold()))?; + config.execute_build()?; + env_cloned.println(format!("{}", style("Reloaded").cyan().bold()))?; + handle = Some(start_background_worker(path_dist.clone())); + } + } + Ok::<_, anyhow::Error>(()) + }); let _ = handle .await - .map_err(|err| anyhow::anyhow!("{err}"))? .map_err(|_| anyhow::anyhow!("An issue happened while joining the thread"))? .map_err(|err| anyhow::anyhow!("{err}"))?; Ok(()) diff --git a/cli/src/domain/worker.rs b/cli/src/domain/worker.rs index 554344e..6928f04 100644 --- a/cli/src/domain/worker.rs +++ b/cli/src/domain/worker.rs @@ -1,11 +1,18 @@ use anyhow::Context; +use deno_core::v8::IsolateHandle; use deno_core::FastString; use deno_core::ModuleSpecifier; use deno_core::NoopModuleLoader; +use futures::future::select; use std::path::Path; use std::rc::Rc; use std::str::FromStr; +use std::thread::JoinHandle; use swarmd_local_runtime::worker::{SwarmdWorker, WorkerOptions}; +use tokio::sync::oneshot; +use tokio::sync::oneshot::Receiver; +use tokio::sync::oneshot::Sender; +use tokio::task::spawn_local; const INITIAL_SRC_CODE: FastString = FastString::Static( r#" @@ -31,7 +38,26 @@ const INITIAL_SRC_CODE: FastString = FastString::Static( "#, ); -pub async fn simple_worker>(js_path: P) -> anyhow::Result { +#[derive(Debug)] +pub struct WorkerHandle { + isolate: IsolateHandle, + tx: Sender<()>, + worker_over: Receiver<()>, +} + +impl WorkerHandle { + pub async fn terminate_execution(self) -> anyhow::Result> { + self.isolate.terminate_execution(); + // If there is an error, we silently ignore it. + let _ = self.tx.send(()); + Ok(self.worker_over) + } +} + +pub async fn simple_worker>( + js_path: P, + tx: Sender, +) -> anyhow::Result<()> { let main_module = ModuleSpecifier::from_str("file://fakemodule.js")?; let mut worker = SwarmdWorker::bootstrap_from_options( @@ -46,6 +72,17 @@ pub async fn simple_worker>(js_path: P) -> anyhow::Result>(js_path: P) -> anyhow::Result(()) + }); + let event_loop_fut = Box::pin(worker.js_runtime.run_event_loop2( + deno_core::PollEventLoopOptions { + wait_for_inspector: false, + pump_v8_message_loop: true, + }, + )); + + match select(interrupt, event_loop_fut).await { + // If interrupt is over, it means we closed or reloaded the server so we don't need to + // send back the error. + futures::future::Either::Left((_, _)) => { + Ok(()) + } + futures::future::Either::Right((a, _)) => { + a + } + } + }); + event_loop_handle.await??; + // We only return the Error when there is an error which is not due to a `oneshot cancel` + if let Ok(Err(err)) = evaluate_fut.await { + return Err(err); + } + tx_worker_over + .send(()) + .map_err(|_| anyhow::anyhow!("Couldn't send worker termination."))?; + + Ok(()) +} + +pub fn start_background_worker>( + js_path: P, +) -> (JoinHandle>, Receiver) { + let js_path = js_path.as_ref().to_owned(); + let (tx, rx) = oneshot::channel::(); + let handle = std::thread::spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); - Ok(worker) + let local = tokio::task::LocalSet::new(); + + local.block_on(&runtime, async move { + if let Err(err) = simple_worker(js_path, tx).await { + println!("{}", err); + } + Ok::<_, anyhow::Error>(()) + }) + }); + (handle, rx) } diff --git a/cli/src/domain/worker_config/mod.rs b/cli/src/domain/worker_config/mod.rs index 0570130..13be884 100644 --- a/cli/src/domain/worker_config/mod.rs +++ b/cli/src/domain/worker_config/mod.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, process::Output}; use config::Config; use console::{style, Emoji}; @@ -76,13 +76,8 @@ impl WorkerConfig { } pub fn execute_build(&self) -> anyhow::Result<()> { - let profile = &self.profile; - let config = profile.get_config(); - let exit_status = NpmEnv::default() - .with_node_env(&profile.to_node_env()) - .init_env() - .raw_append(&config.build_command) - .exec()?; + let output = self.execute_no_log()?; + let exit_status = output.status; if exit_status.success() { Ok(()) @@ -93,6 +88,16 @@ impl WorkerConfig { } } + pub fn execute_no_log(&self) -> anyhow::Result { + let profile = &self.profile; + let config = profile.get_config(); + Ok(NpmEnv::default() + .with_node_env(&profile.to_node_env()) + .init_env() + .raw_append(&config.build_command) + .exec_no_log()?) + } + pub fn path_main_dist(&self) -> &PathBuf { let profile = &self.profile; let config = profile.get_config(); diff --git a/cli/src/package/npm_rs/mod.rs b/cli/src/package/npm_rs/mod.rs index 5338a92..1816182 100644 --- a/cli/src/package/npm_rs/mod.rs +++ b/cli/src/package/npm_rs/mod.rs @@ -43,7 +43,7 @@ use std::{ ffi::OsStr, path::Path, - process::{Command, ExitStatus}, + process::{Command, ExitStatus, Output}, }; use cfg_if::cfg_if; @@ -293,6 +293,11 @@ impl Npm { self } + pub fn exec_no_log(mut self) -> Result { + self.cmd.arg(self.args.join(" && ")); + self.cmd.output() + } + /// Executes all the commands in the invokation order used, waiting for its completion status. /// /// # Example diff --git a/lib/swarmd_local_runtime/tests/util.rs b/lib/swarmd_local_runtime/tests/util.rs index 9165aa7..25da8a5 100644 --- a/lib/swarmd_local_runtime/tests/util.rs +++ b/lib/swarmd_local_runtime/tests/util.rs @@ -2,6 +2,7 @@ use deno_core::{FsModuleLoader, ModuleSpecifier}; use std::rc::Rc; use swarmd_local_runtime::worker::{SwarmdWorker, WorkerOptions}; +#[allow(dead_code)] pub fn simple_worker(js_path: &str) -> (SwarmdWorker, ModuleSpecifier) { let main_module = ModuleSpecifier::from_file_path(js_path).unwrap();