From f3e478846292eaa241e2cf14060a9d94c4b68eab Mon Sep 17 00:00:00 2001 From: yunuservices Date: Tue, 25 Aug 2026 12:28:43 +0300 Subject: [PATCH] fix(wasm-plugins): fix the panic on hot-reloading (#3036) * fix(wasm-plugins): fix the panic on hot-reloading * fix(wasm-plugins): do not cache permission denials from non-main threads --- crates/pumpkin/src/plugin/mod.rs | 61 ++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/crates/pumpkin/src/plugin/mod.rs b/crates/pumpkin/src/plugin/mod.rs index 4351557ea..175d64a52 100644 --- a/crates/pumpkin/src/plugin/mod.rs +++ b/crates/pumpkin/src/plugin/mod.rs @@ -8,6 +8,7 @@ use std::{ path::{Path, PathBuf}, pin::Pin, sync::{Arc, atomic::AtomicBool}, + thread::ThreadId, time::Duration, }; use thiserror::Error; @@ -183,6 +184,9 @@ pub struct PluginManager { // Background task for hot reloading hot_reload_task: RwLock>>, hot_reload_enabled: AtomicBool, + // Thread ID of the thread that created the plugin manager. + // Permission prompts use rustyline, which is only safe on this thread. + main_thread_id: ThreadId, } /// Represents a successfully loaded plugin @@ -235,6 +239,7 @@ impl PluginManager { state_notify: Arc::new(Notify::new()), hot_reload_task: RwLock::new(None), hot_reload_enabled: AtomicBool::new(false), + main_thread_id: std::thread::current().id(), } } @@ -438,13 +443,32 @@ impl PluginManager { } /// Ask the server owner if they allow the permissions requested by a plugin + /// + /// This must only be called from the main thread, because the underlying + /// `rustyline` console handle is neither `Send` nor `Sync`. Calling it from a + /// different thread (e.g. the hot-reload watcher task) will panic. + /// + /// Returns `(allowed, wait_time, cacheable)`. Non-main-thread denials are not + /// cacheable so the user gets prompted again on the next cold load. #[expect(clippy::print_stdout)] - fn ask_permission_confirmation(metadata: &PluginMetadata) -> (bool, std::time::Duration) { + fn ask_permission_confirmation( + &self, + metadata: &PluginMetadata, + ) -> (bool, std::time::Duration, bool) { use colored::Colorize; - use rustyline::DefaultEditor; if metadata.permissions.is_empty() { - return (true, std::time::Duration::ZERO); + return (true, std::time::Duration::ZERO, true); + } + + if std::thread::current().id() != self.main_thread_id { + warn!( + "Plugin \"{}\" ({}) requests permissions from a non-main thread. \ + Permission prompts are only supported on the main thread. \ + Denying permissions. To load this plugin interactively, restart the server.", + metadata.name, metadata.version + ); + return (false, Duration::ZERO, false); } let start_time = std::time::Instant::now(); @@ -484,12 +508,11 @@ impl PluginManager { let input = line.trim().to_lowercase(); input == "y" || input == "yes" }) - } else if let Ok(mut rl) = DefaultEditor::new() { - rl.readline(&prompt).is_ok_and(|line| { - let input = line.trim().to_lowercase(); - input == "y" || input == "yes" - }) } else { + warn!( + "Console readline is not available; cannot prompt for plugin \"{}\" permissions", + metadata.name + ); false }; @@ -497,7 +520,7 @@ impl PluginManager { wrapper.return_readline(rl); } - (result, start_time.elapsed()) + (result, start_time.elapsed(), true) } /// Spawn initialization for a single plugin @@ -842,15 +865,17 @@ impl PluginManager { return (true, std::time::Duration::ZERO); } - let (allowed, wait_time) = Self::ask_permission_confirmation(metadata); - cache.entries.insert( - hash, - cache::PermissionCacheEntry { - permissions_requested: metadata.permissions.clone(), - approved: allowed, - }, - ); - let _ = cache.save(cache_path).await; + let (allowed, wait_time, cacheable) = self.ask_permission_confirmation(metadata); + if cacheable { + cache.entries.insert( + hash, + cache::PermissionCacheEntry { + permissions_requested: metadata.permissions.clone(), + approved: allowed, + }, + ); + let _ = cache.save(cache_path).await; + } (allowed, wait_time) }