feat(plugin-api): add hot-reloading

This commit is contained in:
Alexander Medvedev
2026-04-15 18:37:32 +02:00
parent b31f8a0fdd
commit 7cc4854078
5 changed files with 381 additions and 44 deletions

228
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -45,6 +45,7 @@ tokio = { workspace = true, features = [
] }
thiserror.workspace = true
futures.workspace = true
notify.workspace = true
# config
serde.workspace = true

View File

@@ -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)),
)
}

View File

@@ -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<Box<dyn Future<Output = T> + 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<HashMap<String, PluginState>>,
// Notification for plugin state changes
state_notify: Arc<Notify>,
// Background task for hot reloading
hot_reload_task: RwLock<Option<JoinHandle<()>>>,
hot_reload_enabled: AtomicBool,
}
/// Represents a successfully loaded plugin
@@ -182,6 +192,7 @@ struct LoadedPlugin {
loader_data: Option<Box<dyn Any + Send + Sync>>,
is_active: bool,
context: Arc<Context>,
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<PathBuf> =
@@ -353,6 +468,7 @@ impl PluginManager {
metadata: PluginMetadata,
loader_data: Box<dyn Any + Send + Sync>,
loader: Arc<dyn PluginLoader>,
path: PathBuf,
) -> Result<tokio::task::JoinHandle<()>, 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<String>)> = 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<dyn Any + Send + Sync>,
Arc<dyn PluginLoader>,
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;
}
}