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
@@ -1150,6 +1150,74 @@ fn generate_token() -> Result<String, PluginError> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// In-crate test support: build a `PluginProcess` whose fields are otherwise
|
||||
/// private to this module, so sibling test modules (`process::manager`) can
|
||||
/// put one in a `PluginManager` without spawning a real runner.
|
||||
#[cfg(all(test, feature = "host"))]
|
||||
impl PluginProcess {
|
||||
/// A process backed by a real sleeping child process and a local socket
|
||||
/// pair, carrying the given manifest (no cdylib, no runner). The
|
||||
/// runner-side peer socket is dropped immediately; nothing reads it.
|
||||
pub(crate) fn fake(manifest: OwnedPluginManifest) -> Self {
|
||||
use interprocess::local_socket::traits::Stream as StreamTrait;
|
||||
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let name = format!(
|
||||
"ocs_plugin_fake_process_test_{}_{}",
|
||||
std::process::id(),
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
);
|
||||
let name_ref = name
|
||||
.clone()
|
||||
.to_ns_name::<GenericNamespaced>()
|
||||
.expect("valid name");
|
||||
let listener = ListenerOptions::new()
|
||||
.name(name_ref)
|
||||
.create_sync()
|
||||
.expect("listener");
|
||||
let client_name = name.clone();
|
||||
let client_thread: std::thread::JoinHandle<Stream> = std::thread::spawn(move || {
|
||||
StreamTrait::connect(client_name.to_ns_name::<GenericNamespaced>().unwrap())
|
||||
.expect("connect")
|
||||
});
|
||||
let host_stream = listener.accept().expect("accept");
|
||||
drop(client_thread.join().expect("client thread"));
|
||||
|
||||
Self {
|
||||
stream: Mutex::new(Some(host_stream)),
|
||||
v4: None,
|
||||
child: Mutex::new(Some(sleepy_test_child())),
|
||||
id: manifest.id.clone(),
|
||||
manifest,
|
||||
ribbon: vec![],
|
||||
io_lines: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A child that lives ~30 s — long enough to read as "alive", short enough to
|
||||
/// never hang a leaked test. Shared by `PluginProcess::fake` and this module's
|
||||
/// own tests.
|
||||
#[cfg(all(test, feature = "host"))]
|
||||
fn sleepy_test_child() -> Child {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::process::Command::new("cmd")
|
||||
.arg("/c")
|
||||
.arg("ping -n 30 127.0.0.1")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.expect("spawn sleep")
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
std::process::Command::new("sleep")
|
||||
.arg("30")
|
||||
.spawn()
|
||||
.expect("spawn sleep")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1352,6 +1420,7 @@ mod timeout_tests {
|
||||
ribbon_order: 0,
|
||||
xdata_apps: vec![],
|
||||
command_prefixes: vec![],
|
||||
entity_classes: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user