sinelaw/fresh · error

Plugin thread shut down

Error message

Plugin thread shut down

What it means

load_plugin() sends a LoadPlugin request over a oneshot channel to a dedicated plugin worker thread. This error is raised when the runtime's `request_sender` is `None`, meaning the plugin thread has already been shut down (e.g. `shutdown()` was called or the handle was created after teardown) and no receiver exists to accept the request. The library refuses to dispatch work to a dead worker rather than hang or silently drop the load.

Solutions

  1. Ensure the plugin thread is started (and not yet shut down) before calling load_plugin; check the runtime's lifecycle ordering.
  2. Guard call sites so plugin loading is skipped once shutdown has begun (e.g. an AtomicBool 'shutting_down' checked before load_plugin).
  3. If the thread exited unexpectedly, recreate the runtime handle by spawning a new plugin thread instead of reusing the old handle.
  4. Match on this error and surface a clear 'plugin runtime unavailable' message instead of retrying.

Example fix

// before
runtime.shutdown();
runtime.load_plugin(&plugin_path)?;

// after
if !runtime.is_shutdown() {
    runtime.load_plugin(&plugin_path)?;
} else {
    eprintln!("plugin runtime already shut down; skipping load of {}", plugin_path.display());
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Rust
fn can_load(rt: &PluginRuntimeHandle, shutting_down: &AtomicBool) -> bool {
    !shutting_down.load(Ordering::SeqCst) && rt.is_running()
}

Type guard

fn is_live(sender: &Option<Sender<PluginRequest>>) -> bool { sender.is_some() }

Try / catch

match rt.load_plugin(&path) {
    Ok(()) => { /* plugin loaded */ }
    Err(e) if e.to_string().contains("Plugin thread shut down") => {
        warn!("plugin runtime stopped; not loading {}", path.display());
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `PluginRuntimeHandle::load_plugin(path)` after the plugin thread has been stopped (`request_sender` set to `None`), or on a handle whose worker thread already exited/panicked and was joined.

Common situations: Shutting the editor/plugin runtime down while a background task still tries to load a plugin; calling load_plugin from a drop path or destructor that runs after shutdown; reusing a stale handle across application restart phases.

Related errors


AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/f0ca5afb8b2262a2. Report an issue: GitHub.

Appendix: source

Thrown at crates/fresh-plugin-runtime/src/thread.rs:574

            .lock()
            .ok()
            .and_then(|mut owners| owners.remove(&request_id));
        if let Some(plugin_name) = plugin_name {
            if let Some(sender) = self.request_sender.as_ref() {
                fire_and_forget(sender.send(PluginRequest::TrackAsyncResource {
                    plugin_name,
                    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(),

View on GitHub (pinned to 67894ca546)