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
This commit is contained in:
yunuservices
2026-08-25 12:28:43 +03:00
committed by GitHub
parent 85d635a42b
commit f3e4788462

View File

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