fix(tolerance): complete workflow integration
Sync current main and companion codec changes before preserving unsupported frame rows, rendering in the entity plane, and sharing final layout metrics with placement preview.
This commit is contained in:
parent
66bf4b1d5f
commit
81e7add1dd
65 changed files with 3703 additions and 929 deletions
264
crates/ocs_plugin_api/ARCHITECTURE.md
Normal file
264
crates/ocs_plugin_api/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
# `ocs_plugin_api` — Internal Architecture
|
||||
|
||||
Internal-facing architecture documentation for `crates/ocs_plugin_api`. For plugin-author tutorials see [`README.md`](./README.md); for host/plugin integration design goals see [`../../docs/plugin-architecture.md`](../../docs/plugin-architecture.md).
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`ocs_plugin_api` is the stable, semver-versioned contract between the Open CAD Studio host and add-on plugins. The crate is intentionally split into two tiers:
|
||||
|
||||
| Tier | Feature | Dependencies | Purpose |
|
||||
|------|---------|--------------|---------|
|
||||
| **Core** | default | none | `PluginManifest`, `ApiVersion`, ribbon vocabulary, type registry, version info. Engine crates and external tooling can depend on this cheaply. |
|
||||
| **Runtime host surface** | `host` | `acadrust`, `interprocess`, `memmap2`, `rkyv`, etc. | `HostApi`, `BuiltinPlugin`, out-of-process spawn, IPC, shared-memory snapshots, runner. |
|
||||
|
||||
The host and each plugin run in separate OS processes. The host re-executes itself in `--ocs-plugin-runner` mode, the runner dynamically loads the plugin `cdylib`, and the two sides talk over a local socket. Process isolation keeps plugin crashes from affecting the host or other plugins.
|
||||
|
||||
---
|
||||
|
||||
## Module map
|
||||
|
||||
| Module | Responsibility |
|
||||
|--------|----------------|
|
||||
| [`src/manifest.rs`](src/manifest.rs) | `API_VERSION`, `API_VERSION_MIN_SUPPORTED`, `ApiVersion` compatibility, runtime `OCS_PLUGIN_MAX_API_VERSION` gate. |
|
||||
| [`src/ribbon`](src/ribbon) | `CadModule` trait and plain-data ribbon types (`RibbonGroup`, `ToolDef`, `IconKind`, `ModuleEvent`). |
|
||||
| [`src/host.rs`](src/host.rs) | `HostApi` plugin-facing runtime trait, `BuiltinPlugin` entry-point trait, `export_plugin!` macro, `HostNotification`, `PluginNotification`. |
|
||||
| [`src/ipc/mod.rs`](src/ipc/mod.rs) | IPC layer public exports. |
|
||||
| [`src/ipc/protocol.rs`](src/ipc/protocol.rs) | V2/V3 request/response enums (`HostRequest`, `HostResponse`, `PluginRequest`, `PluginResponse`), `RunnerHandshake`, `PLUGIN_TOKEN_ENV`. |
|
||||
| [`src/ipc/transport.rs`](src/ipc/transport.rs) | Length-framed bincode send/recv over `interprocess::local_socket::Stream`. |
|
||||
| [`src/ipc/client.rs`](src/ipc/client.rs) | Plugin-side V2/V3 `IpcClient` and `PluginHostApi` proxy. |
|
||||
| [`src/ipc/server.rs`](src/ipc/server.rs) | Host-side handler that applies a `PluginRequest` to a `HostApi` implementation. |
|
||||
| [`src/ipc/v4`](src/ipc/v4) | Multiplexed V4 protocol: frames, notifications, correlation ids. |
|
||||
| [`src/ipc/proxy.rs`](src/ipc/proxy.rs) | Optional TCP request proxy for plugin child processes. |
|
||||
| [`src/process.rs`](src/process.rs) | `PluginProcess` lifecycle: spawn, handshake, call, timeouts, `NullHost`, io draining. |
|
||||
| [`src/process/manager.rs`](src/process/manager.rs) | `PluginManager`: owns loaded plugins, dispatch routing, notification broadcast, V4 gating. |
|
||||
| [`src/process/v4.rs`](src/process/v4.rs) | Host-side V4 connection and reader thread. |
|
||||
| [`src/runner.rs`](src/runner.rs) | Runner entry point and V2/V3 vs V4 request loops; loads cdylib via `libloading`. |
|
||||
| [`src/shm.rs`](src/shm.rs) | Shared-memory document snapshot layout, `DocumentSnapshotStore`, `SharedDocumentReader`, V3/V4 view data. |
|
||||
| [`src/host_v4.rs`](src/host_v4.rs) | Host-side per-tab V4 snapshot manager. |
|
||||
| [`src/type_registry.rs`](src/type_registry.rs) / [`src/type_registry_types.rs`](src/type_registry_types.rs) | Build-time `serde-reflection` registry, embedded JSON, schema types. |
|
||||
| [`src/version_info.rs`](src/version_info.rs) | Embedded version metadata and acadrust source-compatibility helper. |
|
||||
|
||||
---
|
||||
|
||||
## Versioning and ABI stability
|
||||
|
||||
- `API_VERSION` (currently `4`) is the host's advertised major.
|
||||
- `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).
|
||||
- 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)).
|
||||
|
||||
The runtime enforces two gates:
|
||||
|
||||
1. `ocs_plugin_api_version()` exported by the cdylib must be within `[API_VERSION_MIN_SUPPORTED, effective_max_api_version()]`.
|
||||
2. For v4+, `acadrust_sources_compatible(host_acadrust_source(), plugin_acadrust_source)` must be true.
|
||||
|
||||
---
|
||||
|
||||
## Process model
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant PM as PluginManager (host)
|
||||
participant PP as PluginProcess (host)
|
||||
participant R as Runner child
|
||||
participant L as Plugin cdylib
|
||||
|
||||
PM->>PP: spawn(cdylib_path, host)
|
||||
PP->>PP: create local socket listener
|
||||
PP->>R: spawn --ocs-plugin-runner <socket> <cdylib>
|
||||
R->>L: unsafe { load(cdylib_path) }
|
||||
L-->>R: Box<dyn BuiltinPlugin>
|
||||
alt API v4
|
||||
R->>PP: connect + RunnerHandshake::TokenV4
|
||||
PP->>PP: verify token + V4 protocol/API gate
|
||||
else API v2/v3
|
||||
R->>PP: connect + RunnerHandshake::Token
|
||||
PP->>PP: verify token
|
||||
end
|
||||
PP->>R: HostToPlugin::Request(GetManifest)
|
||||
R->>L: manifest()
|
||||
L-->>R: PluginManifest { api_version }
|
||||
R-->>PP: HostResponse::Manifest
|
||||
PP->>PP: verify api_version + acadrust gate
|
||||
alt accepted
|
||||
PP-->>PM: success, keep alive
|
||||
else rejected
|
||||
PP->>R: Shutdown
|
||||
PP-->>PM: error
|
||||
end
|
||||
```
|
||||
|
||||
Key invariants:
|
||||
|
||||
- The runner process is the host binary re-executed with special CLI args, so runner and host share the same `ocs_plugin_api` build.
|
||||
- The plugin cdylib is `dlopen`-ed inside the runner, not the host.
|
||||
- A pre-shared token (`OCS_PLUGIN_TOKEN`) authenticates the runner to the host.
|
||||
- `PluginProcess` owns the `Child` handle and a reader thread; `PluginManager` owns the loaded plugins.
|
||||
|
||||
---
|
||||
|
||||
## Wire protocols
|
||||
|
||||
### V2/V3: request/response over a single socket
|
||||
|
||||
A single bidirectional socket carries both directions. While the host waits for a response to a `HostRequest`, it may receive nested `PluginRequest`s from the runner and handles them inline.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H as Host
|
||||
participant R as Runner
|
||||
participant P as BuiltinPlugin
|
||||
|
||||
H->>R: HostToPlugin::Request(Dispatch { cmd: "LINE" })
|
||||
R->>P: dispatch(host, "LINE")
|
||||
P->>R: PluginToHost::Request(AddEntity(...))
|
||||
R->>H: HostToPlugin::Request(AddEntity(...))
|
||||
H-->>R: PluginToHost::Response(Handle)
|
||||
R-->>P: PluginResponse::Handle
|
||||
P-->>R: true
|
||||
R-->>H: PluginToHost::Response(Bool(true))
|
||||
```
|
||||
|
||||
Compatibility rule: new variants are appended at the end of `HostRequest`, `HostResponse`, `PluginRequest`, and `PluginResponse`, preserving bincode discriminant indices for old plugins.
|
||||
|
||||
### V4: multiplexed frames
|
||||
|
||||
V4 keeps the V3 request vocabulary but multiplexes requests, responses, and best-effort notifications over one local socket. Every request/response carries a correlation `id`; notifications carry an optional `command_id`.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H as Host
|
||||
participant R as V4 Runner
|
||||
participant P as BuiltinPlugin
|
||||
|
||||
H->>R: HostToPluginV4::Notification(DocumentChangedV4 { tab_id })
|
||||
H->>R: HostToPluginV4::Request { id: 7, ExecuteCode }
|
||||
R->>P: start_execute_code(host, 7, ...)
|
||||
P-->>R: true
|
||||
P->>R: Notification(Output { text: "..." })
|
||||
R->>H: PluginToHostV4::Notification(Output)
|
||||
P->>R: Response { id: 7, result }
|
||||
R->>H: PluginToHostV4::Response { id: 7, CodeExecutionResult }
|
||||
```
|
||||
|
||||
Key types in [`src/ipc/v4/protocol.rs`](src/ipc/v4/protocol.rs):
|
||||
|
||||
- `HostToPluginV4`: `Request { id, HostRequest }`, `Response { id, PluginResponse }`, `Notification<HostNotification>`.
|
||||
- `PluginToHostV4`: `Request { id, tab_id, PluginRequest }`, `Response { id, HostResponse }`, `Notification<PluginNotification>`.
|
||||
- `NotificationEnvelope<T>`: `{ command_id: Option<u64>, payload: T }`.
|
||||
|
||||
Notifications are best-effort: per-process errors are logged, not propagated. Unknown host-notification discriminants deserialize to `HostNotification::Unknown(raw)` so a newer host does not crash an older plugin.
|
||||
|
||||
---
|
||||
|
||||
## Shared-memory document views
|
||||
|
||||
V3 and V4 avoid cloning the full `CadDocument` over IPC for large reads. The host owns a memory-mapped file; the plugin maps it read-only.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant P as Plugin
|
||||
participant R as Runner
|
||||
participant H as Host
|
||||
participant S as DocumentSnapshotStore
|
||||
|
||||
P->>R: request(OpenDocumentView)
|
||||
R->>H: PluginRequest::OpenDocumentView
|
||||
H->>S: DocumentSnapshotStore::new / publish
|
||||
S-->>H: DocumentViewInfo { path, version }
|
||||
H-->>R: PluginResponse::DocumentView { path, version }
|
||||
R-->>P: DocumentViewInfo
|
||||
P->>S: SharedDocumentReader::open(path)
|
||||
S-->>P: read-only mapping
|
||||
loop host publishes new version
|
||||
H->>S: publish(data)
|
||||
S->>S: swap active segment, increment version
|
||||
P->>S: read() + check version
|
||||
end
|
||||
P->>P: close reader
|
||||
H->>S: close(tab_id)
|
||||
```
|
||||
|
||||
- [`src/shm.rs`](src/shm.rs) defines the generic double-buffered control page and the `SnapshotData` trait. Implementations live in the same file for V3 (`DocumentViewData`) and V4 (`DocumentViewDataV4`).
|
||||
- [`src/host_v4.rs`](src/host_v4.rs) keeps a global `HostV4SnapshotManager` keyed by `tab_id`, so each document tab has its own snapshot.
|
||||
- Segment size defaults to 16 MiB and is configurable via `OCS_V4_SNAPSHOT_SEGMENT_SIZE` (minimum 1 MiB).
|
||||
|
||||
---
|
||||
|
||||
## Notifications
|
||||
|
||||
- **Host → plugin:** `HostNotification` in [`src/host.rs`](src/host.rs). V4-only delivery via `PluginManager::broadcast_notification`. Currently emitted examples: `DocumentChangedV4`, `SelectionChangedV4`, `DocumentTabClosed`.
|
||||
- **Plugin → host:** `PluginNotification` in [`src/host.rs`](src/host.rs). Examples: `Output`, `Progress`, `Log`. Handler installed via `PluginManager::set_notification_handler`.
|
||||
|
||||
Broadcasting is V4-gated inside `PluginManager::broadcast_notification`: V2/V3 processes are skipped. The handler runs on the V4 reader thread and must not block.
|
||||
|
||||
---
|
||||
|
||||
## Error handling and timeouts
|
||||
|
||||
| Kind | Default | Env var | Behavior |
|
||||
|------|---------|---------|----------|
|
||||
| Spawn / connect timeout | 30 s | `OCS_PLUGIN_SPAWN_TIMEOUT_SECS` | Host aborts spawn, plugin is not loaded. |
|
||||
| Per-call timeout | 30 s | `OCS_PLUGIN_CALL_TIMEOUT_SECS` | Host kills runner, plugin marked dead. Floor timeouts apply to `GetManifest`/`GetRibbon`, `Dispatch`, and interactive events. |
|
||||
| Oversized message | — | — | Transport rejects messages > 64 MiB with `TransportError::TooLarge`. |
|
||||
| Malformed message | — | — | `bincode` deserialize error; V4 reader logs and continues if possible. |
|
||||
| Panic in plugin code | — | — | Caught by `catch_unwind` in the runner; converted to `HostResponse::Error`. |
|
||||
| Dead runner | — | — | Detected via `try_wait` on next dispatch or ribbon rebuild; plugin dropped. |
|
||||
|
||||
---
|
||||
|
||||
## Embedded metadata
|
||||
|
||||
The crate embeds two JSON blobs at build time:
|
||||
|
||||
- **Type registry** (`OUT_DIR/type_registry.json`) — a language-binding-friendly schema for a curated allow-list of `acadrust` types, generated by `serde-reflection` in `build.rs`. See [`src/type_registry.rs`](src/type_registry.rs).
|
||||
- **Version info** (`OUT_DIR/version_info.json`) — host version, `ocs_plugin_api` version, `acadrust` version and source, API versions, build timestamp. See [`src/version_info.rs`](src/version_info.rs).
|
||||
|
||||
Both are accessible without enabling the `host` feature.
|
||||
|
||||
---
|
||||
|
||||
## Security / isolation boundaries
|
||||
|
||||
- **Process isolation:** plugin code runs in a child process.
|
||||
- **Token handshake:** `OCS_PLUGIN_TOKEN` is generated per spawn and checked before accepting the runner connection.
|
||||
- **C-ABI export:** plugins expose only two symbols, `ocs_plugin_api_version` and `ocs_plugin_register`, defined by `export_plugin!`.
|
||||
- **Message size cap:** 64 MiB per length-framed message.
|
||||
- **Shared memory:** plugin maps snapshots read-only; the host controls publish/close.
|
||||
|
||||
This is defense-in-depth, not a sandbox: plugins execute native code.
|
||||
|
||||
---
|
||||
|
||||
## Failure modes
|
||||
|
||||
| Scenario | Result |
|
||||
|----------|--------|
|
||||
| Plugin panics during dispatch | Runner catches, returns error response, process stays alive. |
|
||||
| Call timeout | Host kills runner, plugin marked dead. |
|
||||
| Spawn timeout or crash | Spawn fails; plugin not loaded; error surfaced in UI/logs. |
|
||||
| Version mismatch | Host refuses plugin before running plugin code. |
|
||||
| Acadrust source mismatch (v4+) | Host refuses plugin to avoid binary incompatibilities. |
|
||||
| Unknown notification discriminant | Deserializes to `HostNotification::Unknown`; plugin can ignore. |
|
||||
|
||||
---
|
||||
|
||||
## Open tasks
|
||||
|
||||
- **Reduce broadcast notification clone/serialize overhead.** `PluginManager::broadcast_notification` clones the `HostNotification` (including its `Vec<Handle>`) once per loaded V4 plugin, and `transport::send` re-serializes the payload into a new `Vec<u8>` per plugin. Fixing this properly requires either a public API change (`Arc<Vec<Handle>>` in `HostNotification`) or a new bytes-oriented broadcast path in the IPC layer. It only matters with many plugins plus very large selections.
|
||||
|
||||
---
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Plugin-author quick start: [`README.md`](./README.md)
|
||||
- Host/plugin integration spec: [`../../docs/plugin-architecture.md`](../../docs/plugin-architecture.md)
|
||||
- Plugin template: [`../../docs/plugin-template/`](../../docs/plugin-template/)
|
||||
- Process spawn: [`src/process.rs`](src/process.rs)
|
||||
- Process manager: [`src/process/manager.rs`](src/process/manager.rs)
|
||||
- Runner loop: [`src/runner.rs`](src/runner.rs)
|
||||
- V4 protocol: [`src/ipc/v4/protocol.rs`](src/ipc/v4/protocol.rs)
|
||||
- Shared memory: [`src/shm.rs`](src/shm.rs)
|
||||
Loading…
Reference in a new issue