Files
OneCAD-Draft/crates/ocs_plugin_api/src/ipc/protocol.rs
T
Sebastian 1f552038a8 feat(plugin): reference-only entity designation on the HostApi (API v9)
The HostApi gains set_reference_only(handle, reference_only): a plugin can
designate display geometry it owns and refreshes. The entity renders like any
other, but the built-in edit paths treat it like an entity on a locked layer,
while the owning plugin keeps full write access (update, erase, XDATA).

- Appended at the trait tail with a refusing default so plugins keep compiling
  against older hosts; the loader gate range is untouched.
- IPC: SetReferenceOnly / SetReferenceOnlyResult round-trip through the
  shared dispatch, the V3 client and the V4 client, with the shared document
  cache invalidated on success.
- build.rs version_info and manifest API_VERSION bumped to 9 in lockstep.
- Evidence: reference_only_request_and_response_roundtrip (bincode round-trip
  of the request and both response payloads).
2026-10-13 12:38:53 +02:00

321 lines
11 KiB
Rust

//! Request/response envelopes exchanged between the host and a plugin process.
//!
//! A single bidirectional socket is used. Each side sends either a request
//! (expecting a response) or a response (to a previous request). This lets the
//! host handle plugin RPCs inline while it waits for the result of a host→plugin
//! request such as `Dispatch`, avoiding the need for two sockets or threads.
//!
//! Compatibility rule: new variants are appended at the end of every public enum
//! (`HostRequest`, `HostResponse`, `PluginRequest`, `PluginResponse`,
//! `RunnerHandshake`) so older plugins keep their bincode discriminant indices.
//! The V4 additions are a separate frame layer in [`crate::ipc::v4`] and do not
//! alter these enums.
//!
//! The pre-shared runner token is delivered through [`PLUGIN_TOKEN_ENV`]
//! (`OCS_PLUGIN_TOKEN`). The runner must present the same token immediately
//! after connecting or the host closes the connection.
use serde::{Deserialize, Serialize};
use crate::host::{CommandSource, CommandStep, HostSettingValue};
use crate::manifest::ApiVersion;
use crate::ribbon::owned::{OwnedPluginManifest, OwnedRibbonGroup};
pub use crate::host::PreviewWire;
pub use codec::xdata::{ExtendedDataRecord, XDataValue};
pub use codec::{CadDocument, EntityType, Handle};
/// Events the host forwards to an active plugin `InteractiveCommand`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InteractiveEvent {
/// User clicked or specified a point coordinate.
Point([f64; 3]),
/// User pressed Enter or Return to complete input.
Enter,
/// User selected an existing entity in the drawing.
ObjectPick { handle: Handle, pt: [f64; 3] },
/// User cancelled the prompt (e.g. pressed ESC), resetting interactive collection.
Cancel,
}
/// Initial handshake sent by the plugin runner immediately after connecting.
///
/// The runner proves it was spawned by this host by presenting a pre-shared
/// token delivered through the `OCS_PLUGIN_TOKEN` environment variable. A
/// mismatch causes the host to close the connection.
///
/// `TokenV4` is appended as the last variant so the existing `Token(String)`
/// variant keeps its bincode discriminant index (0), preserving V2/V3 wire
/// compatibility.
#[derive(Debug, Serialize, Deserialize)]
pub enum RunnerHandshake {
Token(String),
TokenV4 {
token: String,
protocol_version: u32,
},
}
/// Environment variable through which the host passes the pre-shared
/// authentication token to the plugin runner child process.
pub const PLUGIN_TOKEN_ENV: &str = "OCS_PLUGIN_TOKEN";
/// Requests the host sends to the plugin runner.
#[derive(Debug, Serialize, Deserialize)]
pub enum HostRequest {
GetManifest,
GetRibbon,
Dispatch {
cmd: String,
},
InteractiveEvent {
command_id: u64,
event: InteractiveEvent,
},
GetPrompt {
command_id: u64,
},
NeedsEntityPick {
command_id: u64,
},
Shutdown,
ExecuteCode {
command_id: u64,
source: CommandSource,
code: String,
tab_index: usize,
},
/// V7: release an interactive command after completion or cancellation.
DropInteractive {
command_id: u64,
},
/// V7: cursor move preview update during an interactive command.
CursorMove {
command_id: u64,
pt: [f64; 3],
},
}
/// Responses the plugin runner sends back for `HostRequest`.
#[derive(Debug, Serialize, Deserialize)]
pub enum HostResponse {
Bool(bool),
CommandStep(Box<CommandStep>),
Text(String),
Ribbon(Vec<OwnedRibbonGroup>),
Manifest(OwnedPluginManifest),
Error(String),
CodeExecutionResult(crate::host::ExecutionResult),
PreviewWires(Vec<PreviewWire>),
}
/// Requests the plugin runner sends to the host.
#[derive(Debug, Serialize, Deserialize)]
pub enum PluginRequest {
PushInfo(String),
PushOutput(String),
PushError(String),
AddEntity(EntityType),
/// Replace the existing entity carrying this entity's handle in place.
UpdateEntity(EntityType),
/// Delete the entity with `handle`.
RemoveEntity {
handle: Handle,
},
BumpGeometry,
ReadRecord {
handle: Handle,
app_name: String,
},
WriteRecord {
handle: Handle,
record: ExtendedDataRecord,
},
RemoveRecord {
handle: Handle,
app_name: String,
},
PushUndo {
label: String,
},
SetDirty,
StartInteractive {
command_id: u64,
},
DocumentSnapshot,
/// Ask the host to create/refresh a shared-memory document view and return
/// the file path + current version.
OpenDocumentView,
/// Add multiple entities in a single request.
AddEntities(Vec<EntityType>),
/// V4: ask the host to create/refresh a tab-keyed shared-memory document
/// view and return the file path + current version.
OpenDocumentViewV4 {
tab_id: u64,
},
/// V4: ask the host to close the tab-keyed shared-memory document view.
CloseDocumentViewV4 {
tab_id: u64,
},
/// V4: ask the host for the stable tab identifier of the active tab.
GetTabId,
/// V5: ask the host for the filesystem path of the document in `tab_id`.
DocumentPath {
tab_id: u64,
},
/// Add a layer to the active document with full initial properties.
AddLayer(crate::host::LayerConfig),
/// Modify specified properties of an existing layer in the active document.
ModifyLayer(crate::host::LayerConfig),
/// Run a command on the active document tab's command line (AutoLISP style).
ExecuteCommand(String),
/// Read a host-managed setting without nested command dispatch.
GetSystemVariable {
name: String,
},
/// Change a host-managed setting without nested command dispatch.
SetSystemVariable {
name: String,
value: HostSettingValue,
},
/// V7: validate and replace existing entities in a single undo step.
UpdateEntitiesTransaction {
label: String,
entities: Vec<EntityType>,
},
/// V7: synchronous selection read/write for the dispatch tab.
GetSelection,
SetSelection {
handles: Vec<Handle>,
},
/// V7 (additive): kernel-backed solid create or transform.
SolidOperation {
operation: crate::host::SolidOperation,
},
/// V7 (additive): drawing table record create/modify/rename/delete.
TableOperation {
operation: crate::host::TableOperation,
},
/// V7 (additive): drive an OCS command.
RunCommand {
request: crate::host::CommandRequest,
},
/// V8 (additive): create an XRecord under a named-object-dictionary path.
CreateXRecord {
dictionary_path: Vec<String>,
name: String,
entries: Vec<crate::host::XRecordEntry>,
},
/// V8 (additive): read the entries of an existing XRecord.
ReadXRecord {
dictionary_path: Vec<String>,
name: String,
},
/// V8 (additive): replace the entries of an existing XRecord.
UpdateXRecord {
dictionary_path: Vec<String>,
name: String,
entries: Vec<crate::host::XRecordEntry>,
},
/// V8 (additive): delete an XRecord and its dictionary entry.
DeleteXRecord {
dictionary_path: Vec<String>,
name: String,
},
/// V8 (additive): list the XRecord names under a dictionary path.
ListXRecords {
dictionary_path: Vec<String>,
},
/// V9 (additive): mark an entity reference-only (plugin-owned display data).
SetReferenceOnly {
handle: Handle,
reference_only: bool,
},
}
/// Responses the host sends back for `PluginRequest`.
#[derive(Debug, Serialize, Deserialize)]
pub enum PluginResponse {
Ok,
Bool(bool),
Handle(Handle),
Record(Option<ExtendedDataRecord>),
Document(Box<CadDocument>),
Error(String),
/// Path to the memory-mapped file and the current snapshot version.
DocumentView {
path: String,
version: u64,
},
Handles(Vec<Handle>),
/// V4: path to the tab-keyed memory-mapped file and current version.
DocumentViewV4 {
path: String,
version: u64,
},
/// V4: stable tab identifier of the active tab.
TabId(u64),
/// V5: filesystem path of the document in the requested tab, if any.
DocumentPath(Option<std::ffi::OsString>),
/// Optional entity handle (e.g. from AddLayer).
OptHandle(Option<Handle>),
SystemVariable(Option<HostSettingValue>),
SystemVariableResult(Result<HostSettingValue, String>),
EntityTransactionResult(Result<(), String>),
Selection(Vec<Handle>),
SelectionResult(Result<(), String>),
SolidResult(Result<Handle, String>),
TableResult(Result<Handle, String>),
CommandResult(Result<crate::host::CommandOutcome, String>),
/// V8 (additive): handle of a created XRecord.
XRecordCreateResult(Result<Handle, String>),
/// V8 (additive): entries of a read XRecord.
XRecordEntries(Result<Vec<crate::host::XRecordEntry>, String>),
/// V8 (additive): handle of an updated XRecord.
XRecordUpdateResult(Result<Handle, String>),
/// V8 (additive): the removed XRecord, with its prior content.
XRecordDeleteResult(Result<crate::host::XRecord, String>),
/// V8 (additive): XRecord names under a dictionary path.
XRecordNames(Result<Vec<String>, String>),
/// V9 (additive): outcome of the reference-only designation.
SetReferenceOnlyResult(Result<(), String>),
}
/// Messages sent from the host to the plugin runner.
#[derive(Debug, Serialize, Deserialize)]
pub enum HostToPlugin {
Request(HostRequest),
Response(Box<PluginResponse>),
}
/// Messages sent from the plugin runner to the host.
#[derive(Debug, Serialize, Deserialize)]
pub enum PluginToHost {
Request(Box<PluginRequest>),
Response(HostResponse),
}
/// Convenience helper for manifest serialization.
impl From<&'static crate::manifest::PluginManifest> for OwnedPluginManifest {
fn from(m: &'static crate::manifest::PluginManifest) -> Self {
Self {
id: m.id.to_string(),
name: m.name.to_string(),
version: m.version.to_string(),
description: m.description.to_string(),
api_version: m.api_version.major,
ribbon_order: m.ribbon_order,
xdata_apps: m.xdata_apps.iter().map(|s| s.to_string()).collect(),
command_prefixes: m.command_prefixes.iter().map(|s| s.to_string()).collect(),
}
}
}
impl OwnedPluginManifest {
pub fn api_version(&self) -> ApiVersion {
ApiVersion {
major: self.api_version,
}
}
}