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

  1. Check worker thread logs/panic hooks to find why the plugin thread exited mid-request (a panicking plugin is the usual culprit).
  2. 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.
  3. Avoid calling shutdown() while loads are pending; use a completion signal/join handle before teardown.
  4. 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

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)