diff --git a/Cargo.lock b/Cargo.lock index 508bd3b4e..627198f18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,12 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" @@ -1297,6 +1303,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.32" @@ -1391,7 +1406,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" dependencies = [ - "bitflags", + "bitflags 2.11.1", "debugid", "rustc-hash", "serde", @@ -1890,6 +1905,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +dependencies = [ + "bitflags 2.11.1", + "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.2.2" @@ -1985,6 +2020,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +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.5.0" @@ -2190,6 +2245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -2209,7 +2265,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -2225,6 +2281,33 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.1", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.1", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -2313,7 +2396,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -2755,7 +2838,7 @@ version = "0.1.0-dev+26.1" dependencies = [ "arc-swap", "base64 0.22.1", - "bitflags", + "bitflags 2.11.1", "bytes", "chrono", "console-subscriber", @@ -2764,6 +2847,7 @@ dependencies = [ "futures", "hmac 0.13.0", "libloading", + "notify", "num-bigint", "num_cpus", "ordered-float", @@ -2901,7 +2985,7 @@ version = "0.1.0-dev+26.1" dependencies = [ "aes", "async-compression", - "bitflags", + "bitflags 2.11.1", "bytes", "cfb8", "flate2", @@ -2946,7 +3030,7 @@ dependencies = [ name = "pumpkin-world" version = "0.1.0-dev+26.1" dependencies = [ - "bitflags", + "bitflags 2.11.1", "bytes", "criterion", "crossbeam", @@ -3091,7 +3175,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -3107,13 +3191,13 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952ddbfc6f9f64d006c3efd8c9851a6ba2f2b944ba94730db255d55006e0ffda" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.5", + "hashbrown 0.17.0", "log", "rustc-hash", "smallvec", @@ -3227,7 +3311,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3240,7 +3324,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -3304,7 +3388,7 @@ version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cfg-if", "clipboard-win", "home", @@ -3694,7 +3778,7 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cap-fs-ext", "cap-std", "fd-lock", @@ -4434,7 +4518,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -4446,7 +4530,7 @@ version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.16.1", "indexmap", "semver", @@ -4459,7 +4543,7 @@ version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.16.1", "indexmap", "semver", @@ -4485,7 +4569,7 @@ checksum = "ce205cd643d661b5ba5ba4717e13730262e8cdbc8f2eacbc7b906d45c1a74026" dependencies = [ "addr2line", "async-trait", - "bitflags", + "bitflags 2.11.1", "bumpalo", "cc", "cfg-if", @@ -4728,7 +4812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7900c3e3c1d6e475bc225d73b02d6d5484815f260022e6964dca9558e50dd01a" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "heck", "indexmap", "wit-parser 0.245.1", @@ -4741,7 +4825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3e3ddcfad69e9eb025bd19bff70dad45bafe1d6eacd134c0ffdfc4c161d045" dependencies = [ "async-trait", - "bitflags", + "bitflags 2.11.1", "bytes", "cap-fs-ext", "cap-net-ext", @@ -4823,7 +4907,7 @@ version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc1b1135efc8e5a008971897bea8d41ca56d8d501d4efb807842ae0a1c78f639" dependencies = [ - "bitflags", + "bitflags 2.11.1", "thiserror 2.0.18", "tracing", "wasmtime", @@ -5014,7 +5098,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -5023,7 +5107,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -5041,14 +5134,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -5066,48 +5176,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.15" @@ -5126,7 +5284,7 @@ version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ - "bitflags", + "bitflags 2.11.1", "windows-sys 0.59.0", ] @@ -5145,7 +5303,7 @@ version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7607d30e7e5e8fd5a0695f7cb8b2128829e0bf9dca7a1fe8c4d6ed3ca1058fce" dependencies = [ - "bitflags", + "bitflags 2.11.1", "wit-bindgen-rust-macro 0.56.0", ] @@ -5241,7 +5399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -5260,7 +5418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1936c26cb24b93dc36bf78fb5dc35c55cd37f66ecdc2d2663a717d9fb3ee951e" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index cd23b0830..f74c54fd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,6 +177,7 @@ arc-swap = "1.9" tokio-util = "0.7.18" toml = "1.1" ureq = "3.3.0" +notify = "8.2.0" wasmtime = "43.0" wasmtime-wasi = "43.0" diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 071e35c97..b9ee47026 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -45,6 +45,7 @@ tokio = { workspace = true, features = [ ] } thiserror.workspace = true futures.workspace = true +notify.workspace = true # config serde.workspace = true diff --git a/pumpkin/src/command/commands/plugin.rs b/pumpkin/src/command/commands/plugin.rs index 0061c2378..54f26e1d4 100644 --- a/pumpkin/src/command/commands/plugin.rs +++ b/pumpkin/src/command/commands/plugin.rs @@ -158,6 +158,53 @@ impl CommandExecutor for UnloadExecutor { } } +struct HotReloadExecutor(bool); + +impl CommandExecutor for HotReloadExecutor { + fn execute<'a>( + &'a self, + sender: &'a CommandSender, + server: &'a crate::server::Server, + _args: &'a ConsumedArgs<'a>, + ) -> CommandResult<'a> { + Box::pin(async move { + let enabled = self.0; + + if enabled { + if let Err(e) = server.plugin_manager.start_watcher().await { + return Err(CommandError::CommandFailed(TextComponent::text(format!( + "Failed to start plugin watcher: {e}" + )))); + } + + sender + .send_message( + TextComponent::text("Hot reloading has been enabled.") + .color_named(NamedColor::Green), + ) + .await; + sender + .send_message( + TextComponent::text("WARNING: Hot reloading can impact performance and should only be enabled during plugin development.") + .color_named(NamedColor::Red), + ) + .await; + } else { + server.plugin_manager.stop_watcher().await; + + sender + .send_message( + TextComponent::text("Hot reloading has been disabled.") + .color_named(NamedColor::Green), + ) + .await; + } + + Ok(1) + }) + } +} + pub fn init_command_tree() -> CommandTree { CommandTree::new(NAMES, DESCRIPTION).then( require(|sender| sender.has_permission_lvl(PermissionLvl::Three)) @@ -169,6 +216,11 @@ pub fn init_command_tree() -> CommandTree { literal("unload") .then(argument(PLUGIN_NAME, SimpleArgConsumer).execute(UnloadExecutor)), ) + .then( + literal("hotreload") + .then(literal("enable").execute(HotReloadExecutor(true))) + .then(literal("disable").execute(HotReloadExecutor(false))), + ) .then(literal("list").execute(ListExecutor)), ) } diff --git a/pumpkin/src/plugin/mod.rs b/pumpkin/src/plugin/mod.rs index 25d8b0017..37d4477a9 100644 --- a/pumpkin/src/plugin/mod.rs +++ b/pumpkin/src/plugin/mod.rs @@ -1,15 +1,20 @@ use futures::future::join_all; use loader::{LoaderError, PluginLoader, native::NativePluginLoader}; +use notify::{EventKind, RecursiveMode, Watcher, event::ModifyKind}; use std::{ any::Any, collections::{HashMap, HashSet}, path::{Path, PathBuf}, pin::Pin, - sync::Arc, + sync::{Arc, atomic::AtomicBool}, + time::Duration, }; use thiserror::Error; -use tokio::sync::{Notify, RwLock}; -use tracing::{error, info}; +use tokio::{ + sync::{Notify, RwLock}, + task::JoinHandle, +}; +use tracing::{debug, error, info}; pub mod api; pub mod loader; @@ -23,6 +28,8 @@ pub type BoxFuture<'a, T> = Pin + Send + 'a>>; /// that makes old binary plugins incompatible. pub const PLUGIN_API_VERSION: u32 = 2; +const PLUGIN_DIR: &str = "./plugins"; + /// A trait for handling events dynamically. /// /// This trait allows for handling events of any type that implements the `Event` trait. @@ -169,6 +176,9 @@ pub struct PluginManager { plugin_states: RwLock>, // Notification for plugin state changes state_notify: Arc, + // Background task for hot reloading + hot_reload_task: RwLock>>, + hot_reload_enabled: AtomicBool, } /// Represents a successfully loaded plugin @@ -182,6 +192,7 @@ struct LoadedPlugin { loader_data: Option>, is_active: bool, context: Arc, + path: PathBuf, } /// Error types for plugin management @@ -221,6 +232,8 @@ impl Default for PluginManager { services: Arc::new(RwLock::new(HashMap::new())), plugin_states: RwLock::new(HashMap::new()), state_notify: Arc::new(Notify::new()), + hot_reload_task: RwLock::new(None), + hot_reload_enabled: AtomicBool::new(false), } } } @@ -260,6 +273,108 @@ impl PluginManager { self.retry_unloaded_files().await; } + /// Start watching the plugins directory for changes + pub async fn start_watcher(&self) -> Result<(), ManagerError> { + if self.hot_reload_task.read().await.is_some() { + return Ok(()); + } + + let (tx, mut rx) = tokio::sync::mpsc::channel(100); + let mut watcher = notify::recommended_watcher(move |res| { + if let Ok(event) = res { + let _ = tx.blocking_send(event); + } + }) + .map_err(|e| ManagerError::IoError(std::io::Error::other(e)))?; + + let plugin_dir = Path::new(PLUGIN_DIR); + if !plugin_dir.exists() { + std::fs::create_dir_all(plugin_dir)?; + } + + watcher + .watch(plugin_dir, RecursiveMode::NonRecursive) + .map_err(|e| ManagerError::IoError(std::io::Error::other(e)))?; + + let self_ref = self + .self_ref + .read() + .await + .clone() + .ok_or(ManagerError::ManagerNotInitialized)?; + + let task = tokio::spawn(async move { + // Keep watcher alive by moving it into the task + let _watcher = watcher; + + while let Some(event) = rx.recv().await { + if !self_ref + .hot_reload_enabled + .load(std::sync::atomic::Ordering::Relaxed) + { + continue; + } + + match event.kind { + EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(_) => { + for path in event.paths { + if path.extension().is_some_and(|ext| ext == "wasm") { + debug!("Detected change in plugin: {:?}", path); + // Give it a small delay to ensure file is completely written + tokio::time::sleep(Duration::from_millis(100)).await; + + // We need to find if this plugin is already loaded to unload it first + let plugin_name = { + let plugins = self_ref.plugins.read().await; + plugins + .iter() + .find(|p| p.path == path) + .map(|p| p.metadata.name.clone()) + }; + + if let Some(name) = plugin_name { + info!("Hot-reloading plugin: {}", name); + let _ = self_ref.unload_plugin(&name).await; + } + + // For now, we just try to load it. If it's already loaded, + // the loader might handle it or we might get a duplicate. + // Most WASM loaders will just create a new instance. + if let Err(e) = self_ref.start_loading_plugin(&path).await { + error!("Failed to hot-reload plugin {:?}: {}", path, e); + } + } + } + } + _ => {} + } + } + }); + + *self.hot_reload_task.write().await = Some(task); + self.set_hot_reload_enabled(true); + Ok(()) + } + + /// Stop watching the plugins directory for changes + pub async fn stop_watcher(&self) { + let mut task_lock = self.hot_reload_task.write().await; + if let Some(handle) = task_lock.take() { + handle.abort(); + } + self.set_hot_reload_enabled(false); + } + + pub fn set_hot_reload_enabled(&self, enabled: bool) { + self.hot_reload_enabled + .store(enabled, std::sync::atomic::Ordering::Relaxed); + } + + pub fn is_hot_reload_enabled(&self) -> bool { + self.hot_reload_enabled + .load(std::sync::atomic::Ordering::Relaxed) + } + /// Retry loading files that couldn't be loaded previously async fn retry_unloaded_files(&self) { let files_to_retry: Vec = @@ -353,6 +468,7 @@ impl PluginManager { metadata: PluginMetadata, loader_data: Box, loader: Arc, + path: PathBuf, ) -> Result, ManagerError> { // Mark plugin as loading self.plugin_states @@ -390,6 +506,7 @@ impl PluginManager { loader_data: Some(loader_data), is_active: false, // Will be set to true after successful initialization context: context.clone(), + path, }; let plugin_index = { @@ -470,7 +587,6 @@ impl PluginManager { /// Load all plugins from the plugin directory pub async fn load_plugins(&self) -> Result<(), ManagerError> { - const PLUGIN_DIR: &str = "./plugins"; let path = Path::new(PLUGIN_DIR); if !path.exists() { @@ -508,6 +624,7 @@ impl PluginManager { metadata, loader_data, loader.clone(), + path.clone(), )); loader_found = true; } @@ -525,7 +642,7 @@ impl PluginManager { // Resolve dependencies let metadata_list: Vec<(String, Vec)> = prepared_plugins .iter() - .map(|(_, m, _, _)| (m.name.clone(), m.dependencies.clone())) + .map(|(_, m, _, _, _)| (m.name.clone(), m.dependencies.clone())) .collect(); let sorted_names = @@ -540,16 +657,18 @@ impl PluginManager { PluginMetadata, Box, Arc, + PathBuf, ), > = prepared_plugins .into_iter() - .map(|(i, m, d, l)| (m.name.clone(), (i, m, d, l))) + .map(|(i, m, d, l, p)| (m.name.clone(), (i, m, d, l, p))) .collect(); for name in sorted_names { - if let Some((instance, metadata, loader_data, loader)) = plugins_map.remove(&name) { + if let Some((instance, metadata, loader_data, loader, path)) = plugins_map.remove(&name) + { match self - .spawn_plugin_initialization(instance, metadata, loader_data, loader) + .spawn_plugin_initialization(instance, metadata, loader_data, loader, path) .await { Ok(task) => { @@ -575,7 +694,13 @@ impl PluginManager { if loader.can_load(path) { let (instance, metadata, loader_data) = loader.load(path).await?; return self - .spawn_plugin_initialization(instance, metadata, loader_data, loader.clone()) + .spawn_plugin_initialization( + instance, + metadata, + loader_data, + loader.clone(), + path.to_path_buf(), + ) .await; } }