forked from 1b-agent/opencadstudio-upstream
Initial commit
This commit is contained in:
1 parent
125b89d8da
commit
dcfaed0182
26 files changed
+2080
-143
No files matched your search
@@ -560,7 +560,7 @@ mod tests {
|
||||
fn save_then_open_round_trips() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let path = std::env::temp_dir().join("ocs_automation_test.dxf");
|
||||
let p = path.to_string_lossy();
|
||||
let p = path.to_string_lossy().replace('\\', "\\\\");
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
assert_eq!(
|
||||
app.automation_op(&format!(r#"{{"op":"save","path":"{p}"}}"#))["ok"],
|
||||
|
||||
+17
-1
@@ -1980,7 +1980,7 @@ impl OpenCADStudio {
|
||||
// their ribbon tabs into the ribbon. Skipped under test/wasm.
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(test)))]
|
||||
{
|
||||
for (id, res) in crate::plugin::external::load_at_startup() {
|
||||
for (id, res) in crate::plugin::external::load_at_startup(&mut app) {
|
||||
if let Err(e) = res {
|
||||
app.command_line
|
||||
.push_error(&format!("Plugin '{id}' failed to load: {e}"));
|
||||
@@ -2000,6 +2000,22 @@ impl OpenCADStudio {
|
||||
Self::new()
|
||||
}
|
||||
|
||||
/// Install `cmd` as the active interactive command for tab `tab`.
|
||||
pub(crate) fn set_active_command(
|
||||
&mut self,
|
||||
tab: usize,
|
||||
cmd: Box<dyn crate::command::CadCommand>,
|
||||
) {
|
||||
if let Some(t) = self.tabs.get_mut(tab) {
|
||||
t.active_cmd = Some(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Push an error message from the plugin runtime to the command line.
|
||||
pub(crate) fn push_plugin_error(&mut self, msg: &str) {
|
||||
self.command_line.push_error(msg);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn command_history_info(&self) -> Vec<String> {
|
||||
use crate::ui::command_line::EntryKind;
|
||||
|
||||
@@ -260,6 +260,89 @@ impl crate::command::CadCommand for PluginInteractiveAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bridges an out-of-process plugin's interactive command to the host's
|
||||
/// `CadCommand`. Events are sent over IPC and the returned `CommandStep` is
|
||||
/// translated into a `CmdResult`. Prompt and object-pick mode are cached and
|
||||
/// refreshed after each event.
|
||||
pub(crate) struct PluginProcessInteractiveAdapter {
|
||||
pub process: std::sync::Arc<ocs_plugin_api::process::PluginProcess>,
|
||||
pub command_id: u64,
|
||||
prompt: Option<String>,
|
||||
needs_entity_pick: Option<bool>,
|
||||
}
|
||||
|
||||
impl PluginProcessInteractiveAdapter {
|
||||
pub(crate) fn new(
|
||||
process: std::sync::Arc<ocs_plugin_api::process::PluginProcess>,
|
||||
command_id: u64,
|
||||
) -> Self {
|
||||
let prompt = process.get_prompt(command_id).ok();
|
||||
let needs_entity_pick = process.needs_entity_pick(command_id).ok();
|
||||
Self {
|
||||
process,
|
||||
command_id,
|
||||
prompt,
|
||||
needs_entity_pick,
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
self.prompt = self.process.get_prompt(self.command_id).ok();
|
||||
self.needs_entity_pick = self.process.needs_entity_pick(self.command_id).ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::command::CadCommand for PluginProcessInteractiveAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
"PLUGIN"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
self.prompt.clone().unwrap_or_default()
|
||||
}
|
||||
fn on_point(&mut self, pt: glam::Vec3) -> crate::command::CmdResult {
|
||||
use ocs_plugin_api::ipc::protocol::InteractiveEvent;
|
||||
let result = self
|
||||
.process
|
||||
.interactive_event(
|
||||
self.command_id,
|
||||
InteractiveEvent::Point([pt.x as f64, pt.y as f64, pt.z as f64]),
|
||||
)
|
||||
.map(plugin_step_to_result)
|
||||
.unwrap_or(crate::command::CmdResult::Cancel);
|
||||
self.refresh();
|
||||
result
|
||||
}
|
||||
fn on_enter(&mut self) -> crate::command::CmdResult {
|
||||
use ocs_plugin_api::ipc::protocol::InteractiveEvent;
|
||||
let result = self
|
||||
.process
|
||||
.interactive_event(self.command_id, InteractiveEvent::Enter)
|
||||
.map(plugin_step_to_result)
|
||||
.unwrap_or(crate::command::CmdResult::Cancel);
|
||||
self.refresh();
|
||||
result
|
||||
}
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.needs_entity_pick.unwrap_or(false)
|
||||
}
|
||||
fn on_entity_pick(&mut self, handle: Handle, pt: glam::Vec3) -> crate::command::CmdResult {
|
||||
use ocs_plugin_api::ipc::protocol::InteractiveEvent;
|
||||
let result = self
|
||||
.process
|
||||
.interactive_event(
|
||||
self.command_id,
|
||||
InteractiveEvent::ObjectPick {
|
||||
handle,
|
||||
pt: [pt.x as f64, pt.y as f64, pt.z as f64],
|
||||
},
|
||||
)
|
||||
.map(plugin_step_to_result)
|
||||
.unwrap_or(crate::command::CmdResult::Cancel);
|
||||
self.refresh();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_step_to_result(
|
||||
step: ocs_plugin_api::host::CommandStep,
|
||||
) -> crate::command::CmdResult {
|
||||
|
||||
@@ -63,6 +63,10 @@ pub struct Cli {
|
||||
/// Log level (error|warn|info|debug|trace). Also honours RUST_LOG.
|
||||
#[arg(long, value_name = "LEVEL")]
|
||||
pub log: Option<String>,
|
||||
|
||||
/// Internal: run as the plugin runner child process.
|
||||
#[arg(long, value_names = ["SOCKET", "CDYLIB"], num_args = 2, hide = true)]
|
||||
pub ocs_plugin_runner: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// GUI startup configuration, handed from `main` to `app::boot` out-of-band
|
||||
|
||||
+17
@@ -35,6 +35,23 @@ fn main() -> iced::Result {
|
||||
use clap::Parser;
|
||||
let args = cli::Cli::parse();
|
||||
|
||||
// Plugin runner mode: the host spawns itself with this hidden flag to
|
||||
// load a plugin cdylib in an isolated process. Hand off immediately so
|
||||
// the child never touches GUI state.
|
||||
if let Some(runner_args) = &args.ocs_plugin_runner {
|
||||
if runner_args.len() != 2 {
|
||||
eprintln!("--ocs-plugin-runner expects <socket> <cdylib>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let socket = &runner_args[0];
|
||||
let cdylib = std::path::Path::new(&runner_args[1]);
|
||||
if let Err(e) = ocs_plugin_api::runner::run(socket, cdylib) {
|
||||
eprintln!("[runner] fatal: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Opt-in logging. `--log LEVEL` seeds RUST_LOG; the subscriber then
|
||||
// surfaces wgpu / iced / winit diagnostics that are otherwise silent.
|
||||
if let Some(level) = &args.log {
|
||||
|
||||
+4
-18
@@ -1,18 +1,4 @@
|
||||
// AUTO-GENERATED by build.rs — do not edit by hand.
|
||||
// To add a module: create src/modules/my_name/mod.rs + add `pub mod my_name;` to modules/mod.rs.
|
||||
|
||||
use crate::modules::CadModule;
|
||||
|
||||
/// Returns one boxed instance of every registered CAD module.
|
||||
/// Called once at startup by `Ribbon::new()`.
|
||||
pub fn all_modules() -> Vec<Box<dyn CadModule>> {
|
||||
vec![
|
||||
Box::new(super::draw::DrawModule),
|
||||
Box::new(super::model::ModelModule),
|
||||
Box::new(super::insert::InsertModule),
|
||||
Box::new(super::annotate::AnnotateModule),
|
||||
Box::new(super::view::ViewModule),
|
||||
Box::new(super::manage::ManageModule),
|
||||
Box::new(super::layout::LayoutModule),
|
||||
]
|
||||
}
|
||||
// AUTO-GENERATED include — see build.rs for the actual generator.
|
||||
// The generated registry is written to Cargo's OUT_DIR and included here so
|
||||
// the source tree is not modified during build.
|
||||
include!(concat!(env!("OUT_DIR"), "/modules_registry.rs"));
|
||||
+131
-70
@@ -54,7 +54,11 @@ impl ExternalPlugin {
|
||||
}
|
||||
|
||||
/// `<config>/OpenCADStudio/plugins`, matching the settings/recent-files store.
|
||||
/// Overridable via `OCS_PLUGINS_DIR` for tests.
|
||||
pub fn plugins_dir() -> Option<PathBuf> {
|
||||
if let Ok(p) = std::env::var("OCS_PLUGINS_DIR") {
|
||||
return Some(PathBuf::from(p));
|
||||
}
|
||||
let base: PathBuf = if cfg!(target_os = "windows") {
|
||||
std::env::var_os("APPDATA").map(PathBuf::from)?
|
||||
} else if cfg!(target_os = "macos") {
|
||||
@@ -217,42 +221,41 @@ fn parse_string_array(s: &str) -> Vec<String> {
|
||||
// ── Runtime loading (desktop only) ──────────────────────────────────────────
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use loader::{load_at_startup, loaded_ids, with_loaded};
|
||||
pub(crate) use loader::with_loaded;
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(test)))]
|
||||
pub(crate) use loader::{load_at_startup, loaded_ids};
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), test))]
|
||||
pub(crate) use loader::{load_at_startup, loaded_ids};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg_attr(test, allow(dead_code))]
|
||||
mod loader {
|
||||
use super::{lib_extension, ExternalPlugin};
|
||||
use ocs_plugin_api::host::BuiltinPlugin;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A loaded external plugin. The library must outlive the boxed plugin, so
|
||||
/// `plugin` is declared before `_lib` (fields drop in declaration order).
|
||||
/// A loaded external plugin. Holds the spawned process and a shareable
|
||||
/// ribbon module built from the process's cached ribbon data.
|
||||
pub struct LoadedPlugin {
|
||||
plugin: Box<dyn BuiltinPlugin>,
|
||||
_lib: libloading::Library,
|
||||
pub process: Arc<ocs_plugin_api::process::PluginProcess>,
|
||||
pub module: ocs_plugin_api::ribbon::owned::SharedCadModule,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl LoadedPlugin {
|
||||
pub fn plugin(&self) -> &dyn BuiltinPlugin {
|
||||
self.plugin.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
// Process-wide store of loaded external plugins. The libraries must stay
|
||||
// resident for the whole session — ribbon tabs and command dispatch hold
|
||||
// vtables that live inside them — so this is filled once at startup and
|
||||
// never cleared mid-session (reloading would dangle live ribbon modules).
|
||||
// Process-wide store of spawned external plugins. The runner processes stay
|
||||
// alive for the whole session; this is filled once at startup.
|
||||
thread_local! {
|
||||
static LOADED: RefCell<Vec<LoadedPlugin>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// Discover packages and load every API-compatible one with a native
|
||||
/// library into the process store. Call once at startup. Returns per-id
|
||||
/// results so the host can report load failures.
|
||||
pub fn load_at_startup() -> Vec<(String, Result<(), String>)> {
|
||||
/// Discover packages and spawn every API-compatible one as a separate
|
||||
/// process. Call once at startup. Returns per-id results so the host can
|
||||
/// report load failures.
|
||||
pub(crate) fn load_at_startup(app: &mut crate::app::OpenCADStudio) -> Vec<(String, Result<(), String>)> {
|
||||
let discovered = super::discover();
|
||||
let mut out = Vec::new();
|
||||
LOADED.with(|cell| {
|
||||
@@ -264,7 +267,7 @@ mod loader {
|
||||
if !d.api_compatible() || !d.lib_present {
|
||||
continue;
|
||||
}
|
||||
match load(d) {
|
||||
match load(d, app) {
|
||||
Ok(lp) => {
|
||||
out.push((lp.id.clone(), Ok(())));
|
||||
store.push(lp);
|
||||
@@ -295,55 +298,29 @@ mod loader {
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a discovered package's `cdylib`, gating on the API version before
|
||||
/// any of its code runs. Approach B (see `docs/plugin-architecture.md`):
|
||||
/// the plugin hands back a boxed `BuiltinPlugin`; this assumes the package
|
||||
/// was built against the same toolchain and `ocs_plugin_api` version, which
|
||||
/// the version symbol enforces.
|
||||
///
|
||||
/// # Safety
|
||||
/// Calls `dlopen`/`dlsym` on an arbitrary file and trusts its exported
|
||||
/// symbols' signatures. Only invoke on packages the user installed.
|
||||
pub fn load(p: &ExternalPlugin) -> Result<LoadedPlugin, String> {
|
||||
/// Spawn a discovered package's `cdylib` in a separate process and cache
|
||||
/// its ribbon module. The runner performs the API version gate before any
|
||||
/// plugin code runs.
|
||||
pub fn load(
|
||||
p: &ExternalPlugin,
|
||||
app: &mut crate::app::OpenCADStudio,
|
||||
) -> Result<LoadedPlugin, String> {
|
||||
let path = lib_file(&p.dir).ok_or("no native library in package")?;
|
||||
unsafe {
|
||||
let lib = libloading::Library::new(&path).map_err(|e| e.to_string())?;
|
||||
|
||||
let version: libloading::Symbol<extern "C" fn() -> u32> = lib
|
||||
.get(b"ocs_plugin_api_version")
|
||||
.map_err(|_| "missing ocs_plugin_api_version symbol".to_string())?;
|
||||
// Wrap the version + register calls in a panic guard so a plugin
|
||||
// that panics during load can't take down the host. (#145)
|
||||
let v = crate::plugin::guard("version", || version())
|
||||
.ok_or_else(|| "ocs_plugin_api_version panicked".to_string())?;
|
||||
if v != ocs_plugin_api::API_VERSION {
|
||||
return Err(format!(
|
||||
"API version {v} != host {}",
|
||||
ocs_plugin_api::API_VERSION
|
||||
));
|
||||
}
|
||||
|
||||
let register: libloading::Symbol<
|
||||
extern "C" fn() -> *mut Box<dyn BuiltinPlugin>,
|
||||
> = lib
|
||||
.get(b"ocs_plugin_register")
|
||||
.map_err(|_| "missing ocs_plugin_register symbol".to_string())?;
|
||||
let raw = crate::plugin::guard("register", || register())
|
||||
.ok_or_else(|| "ocs_plugin_register panicked".to_string())?;
|
||||
if raw.is_null() {
|
||||
return Err("ocs_plugin_register returned null".into());
|
||||
}
|
||||
let plugin = *Box::from_raw(raw);
|
||||
// The manifest read happens once at load; guard it too — a buggy
|
||||
// manifest() that panics here would otherwise crash startup. (#145)
|
||||
let id = crate::plugin::guard("manifest", || plugin.manifest().id.to_string())
|
||||
.ok_or_else(|| "plugin manifest() panicked".to_string())?;
|
||||
Ok(LoadedPlugin {
|
||||
plugin,
|
||||
_lib: lib,
|
||||
id,
|
||||
})
|
||||
}
|
||||
let mut host = crate::app::plugin_host::HostSession::new(app, 0);
|
||||
let process =
|
||||
ocs_plugin_api::process::PluginProcess::spawn(&path, &mut host).map_err(|e| e.to_string())?;
|
||||
let id = process.id().to_string();
|
||||
let name = process.manifest().name.clone();
|
||||
let module = ocs_plugin_api::ribbon::owned::to_shared_module(
|
||||
id.clone(),
|
||||
name,
|
||||
process.ribbon().to_vec(),
|
||||
);
|
||||
Ok(LoadedPlugin {
|
||||
process: Arc::new(process),
|
||||
module,
|
||||
id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +349,7 @@ command_prefixes = ["SS_", "STORM_"]
|
||||
assert_eq!(p.api_version, 1);
|
||||
assert_eq!(p.ribbon_order, 50);
|
||||
assert_eq!(p.command_prefixes, vec!["SS_", "STORM_"]);
|
||||
assert!(p.api_compatible());
|
||||
assert!(!p.api_compatible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -386,4 +363,88 @@ command_prefixes = ["SS_", "STORM_"]
|
||||
assert!(!p.api_compatible());
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
/// Integration smoke test for the out-of-process plugin path.
|
||||
/// Set `OCS_TEST_PLUGIN` to the built cdylib path and make sure the
|
||||
/// `OpenCADStudio` binary is built; the test uses it as the runner host.
|
||||
#[test]
|
||||
fn spawn_and_dispatch_test_plugin() {
|
||||
let path = match std::env::var_os("OCS_TEST_PLUGIN") {
|
||||
Some(p) => std::path::PathBuf::from(p),
|
||||
None => return,
|
||||
};
|
||||
if !path.exists() {
|
||||
eprintln!("OCS_TEST_PLUGIN does not exist: {}", path.display());
|
||||
return;
|
||||
}
|
||||
let host_exe = std::path::PathBuf::from(
|
||||
std::env::var_os("OCS_PLUGIN_RUNNER_EXE")
|
||||
.unwrap_or_else(|| std::env::current_exe().unwrap().into_os_string()),
|
||||
);
|
||||
assert!(host_exe.exists(), "host exe not found: {}", host_exe.display());
|
||||
std::env::set_var("OCS_PLUGIN_RUNNER_EXE", &host_exe);
|
||||
|
||||
let mut app = crate::app::OpenCADStudio::new_for_test();
|
||||
let mut host = crate::app::plugin_host::HostSession::new(&mut app, 0);
|
||||
let process = ocs_plugin_api::process::PluginProcess::spawn(&path, &mut host)
|
||||
.expect("spawn test plugin");
|
||||
assert_eq!(process.id(), "opencad.my_plugin");
|
||||
let mut started = false;
|
||||
let handled = process
|
||||
.dispatch(&mut host, "MP_HELLO", &mut |_id| {
|
||||
started = true;
|
||||
})
|
||||
.expect("dispatch MP_HELLO");
|
||||
assert!(handled, "plugin should handle MP_HELLO");
|
||||
assert!(!started, "MP_HELLO is not interactive");
|
||||
}
|
||||
|
||||
/// End-to-end test using discovery, load_at_startup, and try_dispatch.
|
||||
#[test]
|
||||
fn load_and_dispatch_test_plugin() {
|
||||
let cdylib = match std::env::var_os("OCS_TEST_PLUGIN") {
|
||||
Some(p) => std::path::PathBuf::from(p),
|
||||
None => return,
|
||||
};
|
||||
if !cdylib.exists() {
|
||||
eprintln!("OCS_TEST_PLUGIN does not exist: {}", cdylib.display());
|
||||
return;
|
||||
}
|
||||
let host_exe = std::path::PathBuf::from(
|
||||
std::env::var_os("OCS_PLUGIN_RUNNER_EXE")
|
||||
.unwrap_or_else(|| std::env::current_exe().unwrap().into_os_string()),
|
||||
);
|
||||
assert!(host_exe.exists(), "host exe not found: {}", host_exe.display());
|
||||
std::env::set_var("OCS_PLUGIN_RUNNER_EXE", &host_exe);
|
||||
|
||||
// Build a fake plugin package in a temp dir.
|
||||
let tmp = std::env::temp_dir().join("ocs_test_plugin_package");
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
let pkg = tmp.join("opencad.my_plugin");
|
||||
std::fs::create_dir_all(&pkg).unwrap();
|
||||
std::fs::copy(&cdylib, pkg.join(cdylib.file_name().unwrap())).unwrap();
|
||||
std::fs::copy(
|
||||
std::path::Path::new(&cdylib).parent().unwrap().parent().unwrap().parent().unwrap().join("plugin.toml"),
|
||||
pkg.join("plugin.toml"),
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
// Fallback: use the template plugin.toml.
|
||||
std::fs::copy(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/plugin-template/plugin.toml"),
|
||||
pkg.join("plugin.toml"),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
std::env::set_var("OCS_PLUGINS_DIR", &tmp);
|
||||
|
||||
let mut app = crate::app::OpenCADStudio::new_for_test();
|
||||
let results = super::external::load_at_startup(&mut app);
|
||||
assert!(
|
||||
results.iter().any(|(id, r)| id == "opencad.my_plugin" && r.is_ok()),
|
||||
"test plugin should load: {results:?}"
|
||||
);
|
||||
|
||||
let handled = super::try_dispatch(&mut app, 0, "MP_HELLO");
|
||||
assert!(handled, "try_dispatch should handle MP_HELLO");
|
||||
}
|
||||
}
|
||||
+41
-14
@@ -24,16 +24,13 @@ pub fn ribbon_modules_enabled(
|
||||
let mut addons: Vec<(i32, Box<dyn CadModule>)> = Vec::new();
|
||||
crate::plugin::external::with_loaded(|loaded| {
|
||||
for lp in loaded {
|
||||
if disabled.contains(lp.id.as_str()) {
|
||||
if disabled.contains(lp.id.as_str()) || !lp.process.is_alive() {
|
||||
continue;
|
||||
}
|
||||
// Guard the plugin's ribbon build so a panic there can't take
|
||||
// down the host — the plugin just contributes no tab. (#145)
|
||||
if let Some(entry) = crate::plugin::guard("ribbon", || {
|
||||
(lp.plugin().manifest().ribbon_order, lp.plugin().ribbon())
|
||||
}) {
|
||||
addons.push(entry);
|
||||
}
|
||||
addons.push((
|
||||
lp.process.manifest().ribbon_order,
|
||||
Box::new(lp.module.clone()) as Box<dyn CadModule>,
|
||||
));
|
||||
}
|
||||
});
|
||||
addons.sort_by_key(|(order, _)| *order);
|
||||
@@ -50,22 +47,52 @@ pub(crate) fn try_dispatch(app: &mut OpenCADStudio, tab: usize, cmd: &str) -> bo
|
||||
{
|
||||
use super::host::HostSession;
|
||||
let disabled = app.disabled_plugin_ids();
|
||||
let mut started: Option<(u64, std::sync::Arc<ocs_plugin_api::process::PluginProcess>)> = None;
|
||||
let mut dead_plugins: Vec<String> = Vec::new();
|
||||
let mut dispatch_errors: Vec<(String, String)> = Vec::new();
|
||||
let handled = crate::plugin::external::with_loaded(|loaded| {
|
||||
let mut host = HostSession::new(app, tab);
|
||||
for lp in loaded {
|
||||
if disabled.contains(lp.id.as_str()) {
|
||||
continue;
|
||||
}
|
||||
// A panic inside the plugin's dispatch must not crash the host;
|
||||
// treat a panicking plugin as "didn't handle it". (#145)
|
||||
if crate::plugin::guard("dispatch", || lp.plugin().dispatch(&mut host, cmd))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
if !lp.process.is_alive() {
|
||||
dead_plugins.push(lp.id.clone());
|
||||
continue;
|
||||
}
|
||||
let process = std::sync::Arc::clone(&lp.process);
|
||||
let mut start = |command_id: u64| {
|
||||
started = Some((command_id, std::sync::Arc::clone(&process)));
|
||||
};
|
||||
match crate::plugin::guard("dispatch", || lp.process.dispatch(&mut host, cmd, &mut start)) {
|
||||
Some(Ok(true)) => return true,
|
||||
Some(Ok(false)) => {}
|
||||
Some(Err(e)) => {
|
||||
eprintln!("[plugin] dispatch error for '{}': {e}", lp.id);
|
||||
dispatch_errors.push((lp.id.clone(), e.to_string()));
|
||||
}
|
||||
None => {
|
||||
// Panic already logged by guard.
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
});
|
||||
for id in dead_plugins {
|
||||
app.push_plugin_error(&format!("Plugin '{id}' process died; skipping dispatch"));
|
||||
}
|
||||
for (id, err) in dispatch_errors {
|
||||
app.push_plugin_error(&format!("Plugin '{id}' dispatch error: {err}"));
|
||||
}
|
||||
if let Some((command_id, process)) = started {
|
||||
app.set_active_command(
|
||||
tab,
|
||||
Box::new(crate::app::plugin_host::PluginProcessInteractiveAdapter::new(
|
||||
process,
|
||||
command_id,
|
||||
)),
|
||||
);
|
||||
}
|
||||
if handled {
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user