forked from 1b-agent/opencadstudio-upstream
Merge pull request #932 from schoeller/plugin_on_load
feat(plugins): add BuiltinPlugin::on_load lifecycle callback
This commit is contained in:
9 files changed
+52
-17
No files matched your search
@@ -44,11 +44,12 @@ The host and each plugin run in separate OS processes. The host re-executes itse
|
|||||||
|
|
||||||
## Versioning and ABI stability
|
## Versioning and ABI stability
|
||||||
|
|
||||||
- `API_VERSION` (currently `4`) is the host's advertised major.
|
- `API_VERSION` (currently `5`) is the host's advertised major.
|
||||||
- `API_VERSION_MIN_SUPPORTED` (currently `2`) is the oldest plugin major the host loads.
|
- `API_VERSION_MIN_SUPPORTED` (currently `2`) is the oldest plugin major the host loads.
|
||||||
- `OCS_PLUGIN_MAX_API_VERSION` can cap the accepted major at runtime (e.g. `3` to disable V4).
|
- `OCS_PLUGIN_MAX_API_VERSION` can cap the accepted major at runtime (e.g. `4` to disable V5).
|
||||||
- A plugin built against major `N` runs on a host whose major is `>= N` because new vtable entries and enum variants are appended at the end.
|
- A plugin built against major `N` runs on a host whose major is `>= N` because new vtable entries and enum variants are appended at the end.
|
||||||
- V4 introduces the **acadrust gate**: plugins targeting API v4 or later must resolve the same `acadrust` source as the host (see [`src/version_info.rs`](src/version_info.rs)).
|
- V4 introduces the **acadrust gate**: plugins targeting API v4 or later must resolve the same `acadrust` source as the host (see [`src/version_info.rs`](src/version_info.rs)).
|
||||||
|
- V5 introduces `BuiltinPlugin::on_load`, invoked once after the runner connects.
|
||||||
|
|
||||||
The runtime enforces three gates:
|
The runtime enforces three gates:
|
||||||
|
|
||||||
@@ -72,18 +73,21 @@ sequenceDiagram
|
|||||||
PP->>R: spawn --ocs-plugin-runner <socket> <cdylib>
|
PP->>R: spawn --ocs-plugin-runner <socket> <cdylib>
|
||||||
R->>L: unsafe { load(cdylib_path) }
|
R->>L: unsafe { load(cdylib_path) }
|
||||||
L-->>R: Box<dyn BuiltinPlugin>
|
L-->>R: Box<dyn BuiltinPlugin>
|
||||||
alt API v4
|
alt API v4+
|
||||||
R->>PP: connect + RunnerHandshake::TokenV4
|
R->>PP: connect + RunnerHandshake::TokenV4
|
||||||
PP->>PP: verify token + V4 protocol/API gate
|
PP->>PP: verify token + V4 protocol/API gate
|
||||||
else API v2/v3
|
else API v2/v3
|
||||||
R->>PP: connect + RunnerHandshake::Token
|
R->>PP: connect + RunnerHandshake::Token
|
||||||
PP->>PP: verify token
|
PP->>PP: verify token
|
||||||
end
|
end
|
||||||
|
opt API v5+
|
||||||
|
R->>L: on_load(HostApi)
|
||||||
|
end
|
||||||
PP->>R: HostToPlugin::Request(GetManifest)
|
PP->>R: HostToPlugin::Request(GetManifest)
|
||||||
R->>L: manifest()
|
R->>L: manifest()
|
||||||
L-->>R: PluginManifest { api_version }
|
L-->>R: PluginManifest { api_version }
|
||||||
R-->>PP: HostResponse::Manifest
|
R-->>PP: HostResponse::Manifest
|
||||||
PP->>PP: verify api_version + acadrust gate
|
PP->>PP: verify api_version + ABI metadata gates
|
||||||
alt accepted
|
alt accepted
|
||||||
PP-->>PM: success, keep alive
|
PP-->>PM: success, keep alive
|
||||||
else rejected
|
else rejected
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ cheaply. The runtime host surface is enabled by the `host` feature.
|
|||||||
## Plugin Architecture
|
## Plugin Architecture
|
||||||
|
|
||||||
OCS is scriptable and extensible through a versioned plugin API. The API
|
OCS is scriptable and extensible through a versioned plugin API. The API
|
||||||
supports three major protocol generations: **V2**, **V3**, and **V4**. Each
|
supports four major generations: **V2**, **V3**, **V4**, and **V5**. Each
|
||||||
generation keeps the previous ABI stable by appending new vtable entries and new
|
generation keeps the previous ABI stable by appending new vtable entries and new
|
||||||
enum variants at the end, so older plugins continue to load on newer hosts.
|
enum variants at the end, so older plugins continue to load on newer hosts.
|
||||||
|
|
||||||
@@ -18,12 +18,17 @@ enum variants at the end, so older plugins continue to load on newer hosts.
|
|||||||
| **V2** | Out-of-process runner | Synchronous commands + interactive point collection (`HostApi::start_interactive`) | Baseline |
|
| **V2** | Out-of-process runner | Synchronous commands + interactive point collection (`HostApi::start_interactive`) | Baseline |
|
||||||
| **V3** | Out-of-process runner | Local cached `document()`/`document_mut()` + shared-memory `DocumentReader`/`document_view` for large reads | New vtable entries appended; new `PluginRequest`/`PluginResponse` variants appended |
|
| **V3** | Out-of-process runner | Local cached `document()`/`document_mut()` + shared-memory `DocumentReader`/`document_view` for large reads | New vtable entries appended; new `PluginRequest`/`PluginResponse` variants appended |
|
||||||
| **V4** | Out-of-process runner over a multiplexed local socket | Full-duplex notifications + asynchronous REPL `ExecuteCode` | New `HostToPluginV4`/`PluginToHostV4` frame layer; `BuiltinPlugin::on_notification` and `start_execute_code` appended to trait |
|
| **V4** | Out-of-process runner over a multiplexed local socket | Full-duplex notifications + asynchronous REPL `ExecuteCode` | New `HostToPluginV4`/`PluginToHostV4` frame layer; `BuiltinPlugin::on_notification` and `start_execute_code` appended to trait |
|
||||||
|
| **V5** | V4 multiplexed runner | One-shot `BuiltinPlugin::on_load` initialization | Lifecycle callback appended to `BuiltinPlugin` |
|
||||||
|
|
||||||
The host controls which generations are accepted at runtime through
|
The host controls which generations are accepted at runtime through
|
||||||
`OCS_PLUGIN_MAX_API_VERSION` (e.g. `2` for V2-only mode, `3` to disable V4). The
|
`OCS_PLUGIN_MAX_API_VERSION` (e.g. `2` for V2-only mode, `4` to disable V5). The
|
||||||
current host advertises [`API_VERSION = 4`](src/manifest.rs) and supports plugins
|
current host advertises [`API_VERSION = 5`](src/manifest.rs) and supports plugins
|
||||||
back to `API_VERSION_MIN_SUPPORTED = 2`.
|
back to `API_VERSION_MIN_SUPPORTED = 2`.
|
||||||
|
|
||||||
|
V5 calls `BuiltinPlugin::on_load` once after the runner connects and before the
|
||||||
|
first user command. Plugins can cache `HostApi::plugin_request_sender()` there
|
||||||
|
for worker-thread access. A panic in the callback fails plugin loading.
|
||||||
|
|
||||||
### High-level layout
|
### High-level layout
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -263,7 +268,7 @@ static MANIFEST: PluginManifest = PluginManifest {
|
|||||||
name: "Hello Plugin",
|
name: "Hello Plugin",
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
description: "A minimal example plugin.",
|
description: "A minimal example plugin.",
|
||||||
api_version: ApiVersion { major: 4 },
|
api_version: ApiVersion::CURRENT,
|
||||||
ribbon_order: 100,
|
ribbon_order: 100,
|
||||||
xdata_apps: &[],
|
xdata_apps: &[],
|
||||||
command_prefixes: &["HELLO"],
|
command_prefixes: &["HELLO"],
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ fn generate_version_info(out_dir: &Path) {
|
|||||||
"acadrust_version": acadrust.version.to_string(),
|
"acadrust_version": acadrust.version.to_string(),
|
||||||
"acadrust_source": acadrust.source.as_ref().map(|s| s.to_string()),
|
"acadrust_source": acadrust.source.as_ref().map(|s| s.to_string()),
|
||||||
"rustc_version": rustc_version,
|
"rustc_version": rustc_version,
|
||||||
"api_version": 4,
|
"api_version": 5,
|
||||||
"api_version_min_supported": 2,
|
"api_version_min_supported": 2,
|
||||||
"build_timestamp": build_timestamp,
|
"build_timestamp": build_timestamp,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -298,6 +298,14 @@ pub trait BuiltinPlugin: Send + Sync {
|
|||||||
/// the V4 runner guarantees it is only called for plugins that report
|
/// the V4 runner guarantees it is only called for plugins that report
|
||||||
/// API major 4 or newer.
|
/// API major 4 or newer.
|
||||||
fn on_notification(&mut self, _command_id: Option<u64>, _notification: HostNotification) {}
|
fn on_notification(&mut self, _command_id: Option<u64>, _notification: HostNotification) {}
|
||||||
|
|
||||||
|
/// Lifecycle callback invoked once after the runner connects. The plugin
|
||||||
|
/// receives the active document tab's `HostApi` so it can set up worker
|
||||||
|
/// threads or cache `plugin_request_sender()` before any user command runs.
|
||||||
|
///
|
||||||
|
/// Added in API v5. The runner only calls this for plugins that report API
|
||||||
|
/// major 5 or newer.
|
||||||
|
fn on_load(&mut self, _host: &mut dyn HostApi) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A point-driven interactive command a plugin starts via
|
/// A point-driven interactive command a plugin starts via
|
||||||
|
|||||||
@@ -17,8 +17,9 @@
|
|||||||
/// `document()` / `document_mut()` to local cached copies for out-of-process
|
/// `document()` / `document_mut()` to local cached copies for out-of-process
|
||||||
/// plugins and appends `document_reader` / `document_view` at the end of the
|
/// plugins and appends `document_reader` / `document_view` at the end of the
|
||||||
/// vtable so API v2 plugins keep working. v4 adds full-duplex notifications on
|
/// vtable so API v2 plugins keep working. v4 adds full-duplex notifications on
|
||||||
/// a multiplexed socket while leaving the V2/V3 ABI and protocol untouched.
|
/// a multiplexed socket while leaving the V2/V3 ABI and protocol untouched. v5
|
||||||
pub const API_VERSION: u32 = 4;
|
/// adds the `BuiltinPlugin::on_load` lifecycle callback.
|
||||||
|
pub const API_VERSION: u32 = 5;
|
||||||
|
|
||||||
/// Oldest plugin API major the current host still loads. This keeps previously
|
/// Oldest plugin API major the current host still loads. This keeps previously
|
||||||
/// compiled cdylibs usable as long as their vtable layout is a prefix of the
|
/// compiled cdylibs usable as long as their vtable layout is a prefix of the
|
||||||
@@ -26,9 +27,8 @@ pub const API_VERSION: u32 = 4;
|
|||||||
pub const API_VERSION_MIN_SUPPORTED: u32 = 2;
|
pub const API_VERSION_MIN_SUPPORTED: u32 = 2;
|
||||||
|
|
||||||
/// Environment variable that caps the API major accepted by the host at
|
/// Environment variable that caps the API major accepted by the host at
|
||||||
/// runtime. Set to `3` to disable V4 plugins while keeping V2/V3 plugins
|
/// runtime. Set to `4` to disable V5 plugins, `3` to disable V4, or `2` for
|
||||||
/// working; set to `2` for V2-only mode. The same mechanism will apply to
|
/// V2-only mode.
|
||||||
/// future API majors.
|
|
||||||
pub const MAX_API_VERSION_ENV: &str = "OCS_PLUGIN_MAX_API_VERSION";
|
pub const MAX_API_VERSION_ENV: &str = "OCS_PLUGIN_MAX_API_VERSION";
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -56,15 +56,33 @@ pub fn run(socket_name: &str, cdylib_path: &Path) -> Result<(), Box<dyn std::err
|
|||||||
|
|
||||||
if version >= 4 {
|
if version >= 4 {
|
||||||
let client = V4Client::connect_handshake(socket_name, &token)?;
|
let client = V4Client::connect_handshake(socket_name, &token)?;
|
||||||
|
if version >= 5 {
|
||||||
|
invoke_on_load(&mut *plugin, &client, &interactive)?;
|
||||||
|
}
|
||||||
run_v4(&mut *plugin, &interactive, client)
|
run_v4(&mut *plugin, &interactive, client)
|
||||||
} else {
|
} else {
|
||||||
let client = IpcClient::connect(socket_name)?;
|
let client = IpcClient::connect(socket_name)?;
|
||||||
eprintln!("[runner] connected to host");
|
eprintln!("[runner] connected to host");
|
||||||
client.send_handshake(&token)?;
|
client.send_handshake(&token)?;
|
||||||
|
// V2/V3 plugins receive HostApi only during dispatch, so on_load is not
|
||||||
|
// meaningful there; skip it to keep the old protocol unchanged.
|
||||||
run_v3(&*plugin, &interactive, client)
|
run_v3(&*plugin, &interactive, client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn invoke_on_load(
|
||||||
|
plugin: &mut dyn BuiltinPlugin,
|
||||||
|
client: &V4Client,
|
||||||
|
interactive: &InteractiveRegistry,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut proxy = client.plugin_host_api(0, interactive.clone());
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
plugin.on_load(&mut proxy)
|
||||||
|
}))
|
||||||
|
.map_err(|_| std::io::Error::other("plugin on_load() panicked"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn run_v3(
|
fn run_v3(
|
||||||
plugin: &dyn BuiltinPlugin,
|
plugin: &dyn BuiltinPlugin,
|
||||||
interactive: &InteractiveRegistry,
|
interactive: &InteractiveRegistry,
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ mod tests {
|
|||||||
value["ocs_plugin_api_version"].as_str(),
|
value["ocs_plugin_api_version"].as_str(),
|
||||||
Some(env!("CARGO_PKG_VERSION"))
|
Some(env!("CARGO_PKG_VERSION"))
|
||||||
);
|
);
|
||||||
assert_eq!(value["api_version"].as_i64(), Some(4));
|
assert_eq!(value["api_version"].as_i64(), Some(5));
|
||||||
assert_eq!(value["api_version_min_supported"].as_i64(), Some(2));
|
assert_eq!(value["api_version_min_supported"].as_i64(), Some(2));
|
||||||
|
|
||||||
// acadrust_version should have a patch component (e.g. "0.4.0").
|
// acadrust_version should have a patch component (e.g. "0.4.0").
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ version = "0.1.0"
|
|||||||
description = "…"
|
description = "…"
|
||||||
|
|
||||||
[opencad]
|
[opencad]
|
||||||
api_version = 4
|
api_version = 5
|
||||||
ribbon_order = 50
|
ribbon_order = 50
|
||||||
command_prefixes = ["EX_"]
|
command_prefixes = ["EX_"]
|
||||||
xdata_apps = []
|
xdata_apps = []
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ license = "GPL-3.0-only"
|
|||||||
repository = "your-account/your-plugin-repo"
|
repository = "your-account/your-plugin-repo"
|
||||||
|
|
||||||
[opencad]
|
[opencad]
|
||||||
api_version = 4
|
api_version = 5
|
||||||
rustc_version = "__RUSTC_VERSION__"
|
rustc_version = "__RUSTC_VERSION__"
|
||||||
ribbon_order = 60
|
ribbon_order = 60
|
||||||
command_prefixes = ["MP_"]
|
command_prefixes = ["MP_"]
|
||||||
|
|||||||
Reference in new issue
Block a user