forked from 1b-agent/opencadstudio-upstream
feat(plugin-api): entity-class display declarations on the manifest (API v10)
Plugins can now declare the entity classes they identify via XDATA, and the
host resolves them to display-only type labels while the declaring plugin is
loaded.
- Append `BuiltinPlugin::entity_classes()` (a guarded trailing vtable slot,
the same additive pattern as `on_load` in v5) returning a &'static
`EntityClasses { app_id, class_prefix, labels }`. The static
`PluginManifest` struct is deliberately untouched: older cdylibs share it
by pointer and its layout is frozen ABI since v1, so adding a field would
read past their allocation.
- `OwnedEntityClasses` travels on the registered manifest as
`OwnedPluginManifest.entity_classes` with `#[serde(default)]`, so manifests
serialized before the field keep decoding (older runners tolerate it
both ways).
- The runner folds the declaration into the GetManifest registration only
when the loaded cdylib's exported `ocs_plugin_api_version` symbol reports
>= 10 — an older binary's vtable predates the slot, so it is never called;
its entities simply keep their native type names.
- `PluginManager::entity_class_declarations` exposes the live set, filtered
to alive, non-disabled plugins in load order. The registered manifests are
the only source and nothing is cached: not installed, disabled,
cap-excluded (`OCS_PLUGIN_MAX_API_VERSION`), failed at load, removed, or a
dead process all contribute nothing.
- Embedded version info `api_version` follows the bump to 10;
`API_VERSION_MIN_SUPPORTED` stays 3.
Tests: runner fold (v10 folds, pre-v10 skipped, `entity_classes()` panic is
contained and reported), manager filtering (disabled / dead / undeclaring
skipped; declarations disappear once the process dies — verified with a real
killed child), bincode registration round-trip, and older-manifest serde
default.
This commit is contained in:
1 parent
859070403c
commit
60eb7b086e
9 files changed
+502
-13
No files matched your search
@@ -21,7 +21,7 @@ use std::time::Duration;
|
||||
|
||||
use crate::host::{HostApi, HostNotification, PluginNotification};
|
||||
use crate::process::{PluginError, PluginProcess};
|
||||
use crate::ribbon::owned::{to_shared_module, SharedCadModule};
|
||||
use crate::ribbon::owned::{to_shared_module, OwnedEntityClasses, SharedCadModule};
|
||||
|
||||
/// Callback the host installs to receive plugin-to-host notifications.
|
||||
pub type NotificationHandler = Arc<dyn Fn(&str, Option<u64>, PluginNotification) + Send + Sync>;
|
||||
@@ -197,6 +197,29 @@ impl PluginManager {
|
||||
out
|
||||
}
|
||||
|
||||
/// Entity-class declarations of every alive, non-disabled plugin that
|
||||
/// made one (API v10), in load order, as `(plugin id, declaration)`.
|
||||
///
|
||||
/// The host consults this at display time only — never cached — so the
|
||||
/// result always mirrors the live loaded set: a plugin that failed to
|
||||
/// load, is disabled, was removed, or whose process has died contributes
|
||||
/// nothing, and entities it stamped fall back to their native type names.
|
||||
pub fn entity_class_declarations<F: Fn(&str) -> bool>(
|
||||
&self,
|
||||
is_disabled: F,
|
||||
) -> Vec<(String, OwnedEntityClasses)> {
|
||||
let mut out = Vec::new();
|
||||
for p in &self.plugins {
|
||||
if is_disabled(p.process.id()) || !p.process.is_alive() {
|
||||
continue;
|
||||
}
|
||||
if let Some(classes) = p.process.manifest().entity_classes.clone() {
|
||||
out.push((p.process.id().to_string(), classes));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Drain any plugin→host requests that have arrived asynchronously across
|
||||
/// all loaded plugins. Errors are logged; this is best-effort.
|
||||
pub fn drain_requests(
|
||||
@@ -358,4 +381,103 @@ mod tests {
|
||||
assert!(!result.handled);
|
||||
assert!(result.started.is_none());
|
||||
}
|
||||
|
||||
fn manifest_with_entity_classes(id: &str) -> crate::ribbon::owned::OwnedPluginManifest {
|
||||
crate::ribbon::owned::OwnedPluginManifest {
|
||||
id: id.to_string(),
|
||||
name: format!("{id} name"),
|
||||
version: "0.1.0".to_string(),
|
||||
description: "test".to_string(),
|
||||
api_version: 10,
|
||||
ribbon_order: 0,
|
||||
xdata_apps: vec![],
|
||||
command_prefixes: vec![],
|
||||
entity_classes: Some(OwnedEntityClasses {
|
||||
app_id: "CVIFC".to_string(),
|
||||
class_prefix: "CVIFC-CLASS:".to_string(),
|
||||
labels: [("LINE".to_string(), "Linear Element".to_string())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn plain_manifest(id: &str) -> crate::ribbon::owned::OwnedPluginManifest {
|
||||
crate::ribbon::owned::OwnedPluginManifest {
|
||||
id: id.to_string(),
|
||||
name: format!("{id} name"),
|
||||
version: "0.1.0".to_string(),
|
||||
description: "test".to_string(),
|
||||
api_version: 10,
|
||||
ribbon_order: 0,
|
||||
xdata_apps: vec![],
|
||||
command_prefixes: vec![],
|
||||
entity_classes: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_class_declarations_skip_disabled_and_undeclaring_plugins() {
|
||||
let mut manager = PluginManager::new();
|
||||
manager.plugins.push(LoadedPlugin {
|
||||
process: Arc::new(PluginProcess::fake(manifest_with_entity_classes(
|
||||
"test.live",
|
||||
))),
|
||||
module: to_shared_module("test.live".into(), "Live".into(), vec![]),
|
||||
});
|
||||
manager.plugins.push(LoadedPlugin {
|
||||
process: Arc::new(PluginProcess::fake(plain_manifest("test.plain"))),
|
||||
module: to_shared_module("test.plain".into(), "Plain".into(), vec![]),
|
||||
});
|
||||
manager.plugins.push(LoadedPlugin {
|
||||
process: Arc::new(PluginProcess::fake(manifest_with_entity_classes(
|
||||
"test.off",
|
||||
))),
|
||||
module: to_shared_module("test.off".into(), "Off".into(), vec![]),
|
||||
});
|
||||
|
||||
// Alive + enabled + declaring → reported, with the declaration intact.
|
||||
let declarations = manager.entity_class_declarations(|id| id == "test.off");
|
||||
assert_eq!(
|
||||
declarations.len(),
|
||||
1,
|
||||
"only the live, enabled plugin reports"
|
||||
);
|
||||
assert_eq!(declarations[0].0, "test.live");
|
||||
assert_eq!(declarations[0].1.app_id, "CVIFC");
|
||||
assert_eq!(declarations[0].1.class_prefix, "CVIFC-CLASS:");
|
||||
assert_eq!(
|
||||
declarations[0].1.labels.get("LINE").map(String::as_str),
|
||||
Some("Linear Element")
|
||||
);
|
||||
|
||||
// Everything disabled → nothing contributes.
|
||||
assert!(manager.entity_class_declarations(|_| true).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_class_declarations_revert_when_the_plugin_process_dies() {
|
||||
let mut manager = PluginManager::new();
|
||||
manager.plugins.push(LoadedPlugin {
|
||||
process: Arc::new(PluginProcess::fake(manifest_with_entity_classes(
|
||||
"test.live",
|
||||
))),
|
||||
module: to_shared_module("test.live".into(), "Live".into(), vec![]),
|
||||
});
|
||||
assert_eq!(
|
||||
manager.entity_class_declarations(|_| false).len(),
|
||||
1,
|
||||
"declaration visible while the process is alive"
|
||||
);
|
||||
|
||||
manager.plugins[0]
|
||||
.process
|
||||
.shutdown_and_wait(Duration::from_secs(5))
|
||||
.expect("child shut down");
|
||||
|
||||
assert!(
|
||||
manager.entity_class_declarations(|_| false).is_empty(),
|
||||
"a dead plugin process must contribute no declarations"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user