sinelaw/fresh · error
Plugin thread closed
Error message
Plugin thread closed
What it means
After successfully enqueueing the LoadPlugin request, load_plugin() blocks on the oneshot receiver `rx.recv()`. This error means the request was sent but the response half was dropped before a reply arrived — the plugin thread stopped (shutdown, panic, or loop exit) while the load was being processed. The plugin was therefore not loaded and no per-plugin error is available.
Solutions
- Check worker thread logs/panic hooks to find why the plugin thread exited mid-request (a panicking plugin is the usual culprit).
- Re-issue the load after restarting the plugin thread if the exit was due to a panicking plugin; consider isolating plugin execution from the request loop.
- Avoid calling shutdown() while loads are pending; use a completion signal/join handle before teardown.
- Treat this error as terminal for the request and report that the plugin was NOT loaded.
Example fix
// before
let handle = thread::spawn(move || runtime.load_plugin(&path).unwrap()); // ignores shutdown ordering
// after
shutdown_tx.send(());
worker_handle.join().expect("plugin thread panicked"); // join BEFORE assuming loads finished
// then, on a live runtime:
runtime.load_plugin(&path)?; Defensive patterns
Strategy: try-catch
Validate before calling
// Track pending requests and only shut down when zero
fn safe_shutdown(rt: &PluginRuntimeHandle, pending: &AtomicUsize) {
if pending.load(Ordering::SeqCst) == 0 {
rt.shutdown();
}
} Type guard
fn got_reply(r: &Result<PluginResponse, RecvError>) -> bool { r.is_ok() } Try / catch
match rt.load_plugin(&path) {
Err(e) if e.to_string().contains("Plugin thread closed") => {
error!("worker died while loading {}; plugin NOT loaded", path.display());
rt.restart_thread()?;
}
other => other?,
} Prevention
- Isolate plugin evaluation so a panicking plugin cannot kill the request loop (catch_unwind around execution).
- Block shutdown until all pending loads resolve; use a request counter or join handle.
- Enable worker panic logging to diagnose mid-request deaths.
- Keep plugin loads short; avoid heavy transpilation directly inside the worker's critical loop.
When it happens
Trigger: The plugin thread processes (or is mid-processing) a LoadPlugin request and then exits — e.g. `shutdown()` is invoked from another thread, or the worker loop returns/panics — dropping the response sender without replying.
Common situations: User quits the editor while a plugin is still loading; a panic inside the worker's plugin evaluation kills the thread; long plugin loads (heavy JS/TS source) that overlap application teardown.
Related errors
AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13).
Data as JSON: /api/errors/b57c74c3bc43b6af.
Report an issue: GitHub.
Appendix: source
Thrown at crates/fresh-plugin-runtime/src/thread.rs:581
resource,
}));
}
}
}
/// Load a plugin from a file (blocking)
pub fn load_plugin(&self, path: &Path) -> Result<()> {
let (tx, rx) = oneshot::channel();
self.request_sender
.as_ref()
.ok_or_else(|| anyhow!("Plugin thread shut down"))?
.send(PluginRequest::LoadPlugin {
path: path.to_path_buf(),
response: tx,
})
.map_err(|_| anyhow!("Plugin thread not responding"))?;
rx.recv().map_err(|_| anyhow!("Plugin thread closed"))?
}
/// Load all plugins from a directory (blocking)
pub fn load_plugins_from_dir(&self, dir: &Path) -> Vec<String> {
let (tx, rx) = oneshot::channel();
let Some(sender) = self.request_sender.as_ref() else {
return vec!["Plugin thread shut down".to_string()];
};
if sender
.send(PluginRequest::LoadPluginsFromDir {
dir: dir.to_path_buf(),
response: tx,
})
.is_err()
{
return vec!["Plugin thread not responding".to_string()];
}
View on GitHub (pinned to 67894ca546)