Commit graph cad-editor/crates/ocs_plugin_api/src
Author SHA1 Message Date
Hakan Seven
1e1eeea527 feat(plugin): gate dependency mismatches 2026-08-25 08:12:13 +03:00
Sebastian
3d35919656 feat(plugin): gate marketplace and load on acadrust source
- Add host_acadrust_source and source-hash comparison helpers to ocs_plugin_api.

- Track acadrust_source and acadrust_declared in ExternalPlugin/ReleaseInfo.

- Apply repo-level policy: if any release in a repo declares acadrust_source, undeclared v4+ releases from that repo are incompatible.

- Preserve API v2/v3 backwards compatibility: legacy undeclared releases stay installable.

- Update Plugin Manager UI to only offer compatible releases and show 'acadrust mismatch' status.

- Drop acadrust_version from gate logic; only acadrust_source is required.
2026-08-24 19:01:06 +02:00
Hakan Seven
94f5971fdb docs(ipc): clarify polled response reads 2026-08-23 13:23:26 +03:00
Sebastian
a67affcb93 fix(ipc): preserve stream shutdown on interrupt during polled reads 2026-08-23 10:46:27 +02:00
Sebastian
4fd94d324a fix(ipc): read response byte-by-byte in request_with_poll to avoid TCP desync 2026-08-23 10:41:03 +02:00
Hakan Seven
9202297d1b fix(plugin): trace MText registry enums 2026-08-20 00:26:32 +03:00
Hakan Seven
c62bbe59a9 fix(plugin): skip non-V4 notifications 2026-08-18 18:46:19 +03:00
Sebastian
fcd4a278e4 fix(plugin): skip non-V4 plugins in broadcast_notification
PluginManager::broadcast_notification was fanning out host notifications to every loaded plugin. Only V4 plugin processes can receive host->plugin notifications; V2/V3 plugins correctly rejected them with notify_plugin requires V4 protocol.

Add PluginProcess::is_v4() and skip non-V4 plugins in broadcast_notification so V4 document snapshot notifications only go to V4-capable plugins. Fixes repeated console errors for Example, Land Survey and V2 Compat.
2026-08-18 12:12:42 +02:00
Hakan Seven
6c3455fb09 fix(plugin): bound shared-memory mappings
Reject malformed or oversized snapshots before mmap and apply checked size arithmetic to both host and reader paths.
2026-08-17 23:22:58 +03:00
Sebastian
73bb7bd673
1GiB guard for mmap
Adding a MAX_GUARD for mmap to file
2026-08-17 18:25:15 +02:00
Hakan Seven
f247d15f6e feat(plugin): harden V4 host integration
Preserve legacy wire indices and route concurrent V4 responses and background requests by correlation and stable tab ID.
2026-08-15 20:17:43 +03:00
Sebastian
83d7a4d7db plugin: parallel release fetches, faster shutdown, uninstall runner stop
- Run Plugin Manager release fetches on OS threads so they execute in

  parallel instead of serialising on the async executor.

- Reduce V4Connection shutdown poll from 3s to 100ms; the runner is killed

  and reaped without blocking host shutdown.

- Add PluginManager::remove and PluginProcess::shutdown_and_wait so

  uninstall can stop the runner before deleting files, fixing Windows

  access-denied errors on plugin removal.

- Rebuild ribbon immediately after uninstall so the tab disappears.
2026-08-14 20:18:59 +02:00
Sebastian
285d35182c Fix for V3 plugin 2026-08-13 12:06:36 +02:00
Sebastian
883e703b02 Fix V2/V3 messaging ABI-break 2026-08-13 09:49:57 +02:00
Sebastian
29c41a4041 Snapshot: V4 plugin API and host integration 2026-08-12 21:01:39 +02:00
Hakan Seven
f7590b1aa8 feat(plugin-api): let add-ons ask for a visual style by name
Removing the binary wireframe event left add-ons with no way to set a
visual style except by spelling out a command string. Give them the
intent back, in the vocabulary everything else now speaks.

The name is carried as text rather than the render-mode type: this crate
is dependency-free by default and only pulls acadrust in behind the host
feature, which is not a weight to add to the manifest contract for one
enum. The host resolves it against the same list its ribbon, its picker
and its commands read, so an add-on can offer exactly the styles the
application does and no others. An unknown name says so and lists the
ones that exist, from that same list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:29:01 +03:00
Hakan Seven
b5a1146966 feat(view): one list of visual styles behind every way of choosing one
The ribbon offered four styles and dispatched each one's id as a command.
Two of those ids were not visual-style commands: "Shaded" carried SOLID,
which draws a 2D filled polygon, so choosing it started a draw command;
"Hidden" carried HIDDEN, which matched nothing at all, so it did nothing.
The intent had been for the button to fire its tool's event, but the
dropdown never reads one -- it dispatches the id.

Behind that sat two generations of the same feature. The render-mode
picker offered the seven styles a viewport can actually be drawn in;
everything else -- the ribbon, VSCURRENT, SHADEMODE, VISUALSTYLES and a
handful of bare verbs -- went through a binary wireframe-or-shaded flag
that could only approximate them, reporting "Hidden (shown shaded)" and
"X-Ray (shown as wireframe)" when asked for something it had no way to
draw. Four descriptions of one choice, each drifting on its own.

There is one list now. Each style names itself once -- mode, label, icon,
and the command that applies it -- and the ribbon, the picker, the
VISUALSTYLES verb and the interactive prompt all read from it, down to
the line that lists the choices, so what is offered cannot disagree with
what works. Keywords are the render modes' own names, since there is one
set of styles left to name.

The binary path is gone rather than kept alongside: its message, its
module event, the tool definitions that produced it, the ribbon's
special-cased highlight arms, the bare style verbs and the older keyword
spellings. X-Ray goes with it -- no render mode draws one, and it was
already coming out as a plain wireframe. Three files that had been left
holding a single icon constant each fold into the list.

Note: ModuleEvent::SetWireframe leaves the plugin API with it.

Closes #621

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:15:59 +03:00
Sebastian
b7dc62ffa5 Add handler for document_cache for REPL 2026-07-24 00:30:22 +03:00
Hakan Seven
77c0c5a9d0 feat(plugin): surface loaded plugin commands in command-line autocomplete
Autocomplete suggestions came only from all_registered_command_names() —
the compile-time inventory registry returning &'static str — so
runtime-loaded plugin commands could never appear. Typing a plugin's
command prefix (e.g. `LS_`) showed nothing even with the plugin loaded
and its ribbon tab active; dispatch worked, only discovery was blind.

Collect each enabled plugin's ribbon ToolDef command ids plus its
manifest command_prefixes into a dynamic candidate pool
(OwnedRibbonGroup::command_ids → PluginManager::command_names →
plugin_command_names), refreshed in rebuild_ribbon_modules on startup
load, settings reload, and every enable/disable toggle. ranked_matches
now merges this pool with the static registry and returns owned strings.

A plugin.toml `commands = [...]` list for sub-verbs with no ribbon button
(e.g. LS_AUTOLABEL) stays a follow-up — it's a plugin-API surface change.

Closes #272

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:01:53 +03:00
Hakan Seven
35468004e6 test(core): ignore ribbon_groups microbenchmark by default
The once_lock_eliminates_allocation_after_first_call test from PR #257
is a timing microbenchmark that Box::leaks ~400k strings by design to
defeat allocator reuse. Mark it #[ignore] so it stays out of the default
suite; cache correctness is already covered by the pointer-identity test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:48:31 +03:00
Karim Jerbi
73b05e1dec perf(core): cache ribbon_groups() with OnceLock, avoid per-frame tree allocations
Change CadModule::ribbon_groups() from -> Vec<RibbonGroup> to -> &[RibbonGroup],
backed by a per-module OnceLock cache. The ribbon group tree is static data
(&'static str, Copy enums) that was being fully reconstructed — Vecs, String
clones, enum discriminants — on every call, including all three per-frame
call sites (view(), dropdown_overlay(), style_combo_overlay()).

- 7 built-in modules + 2 plugin templates cache via function-local OnceLock.
  Safe because each is a unit struct with exactly one instance per process
  (plugins run in isolated child processes; see PluginProcess::spawn()).
- SharedCadModule avoids the static pattern entirely, storing groups in an
  instance field (owned.rs).
- IPC runner converts &RibbonGroup -> OwnedRibbonGroup directly via new
  From<&T> impls, avoiding an intermediate clone.
- render_small/render_large and the two make_tool_row closures now borrow
  (&RibbonItem, &[ToolDef]) instead of taking ownership, so the view loop
  doesn't need to clone items to satisfy the old by-value signatures.
- Various match-ergonomics deref fixes (*id, *default, *icon, etc.) from
  the &RibbonItem pattern change.
2026-07-02 21:00:57 +01:00
Hakan Seven
f9f7138370 feat(plugin): persist XDATA to DWG and let plugins modify/delete entities (#249, #250)
#249 — plugin XDATA written via write_record survived only in memory: the
acadrust DWG writer dropped ExtendedData::records on save. Bump acadrust to
e88a9a6 (records now encode to EED and decode back on read) and fix the host
side that fed it:
  - ensure_app_id allocates a real APPID handle; a null handle serializes as
    0 and the EED reference can't resolve, so the XDATA vanished on reopen.
  - write_record / remove_record drop stale raw_dwg_eed for the target app so
    an edit made after a save/reopen wins over the pre-edit bytes.

#250 — out-of-process plugins got a throwaway document_mut() snapshot, so
edits to existing entities were silently discarded and deletion wasn't
expressible at all. Add the missing mutation surface:
  - UpdateEntity / RemoveEntity IPC requests + HostApi::update_entity /
    remove_entity (default in-process impls, RPC overrides on the client that
    invalidate the stale document cache).
  - Scene::update_entity replaces the entity in place, preserving its handle
    and owning block, and reseeds only its derived caches; remove reuses the
    cache-coherent erase_entities (which also honours layer locks).
  - document_mut() is documented as a local-only snapshot out-of-process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:39:26 +03:00
Hakan Seven
e3d1780a69 feat(plugin): quiet plugin IPC logs; print one "Loaded plugin" line
A normal run dumped the whole host↔runner trace (spawn, handshake, every
per-command dispatch and request/response) to the terminal. Gate that
behind OCS_PLUGIN_VERBOSE and, in normal runs, print just one line per
plugin: `Loaded plugin: <name> (<id> <version>)`.

The chatty host-side `[plugin]` lines now go through a `vlog!` macro that
only fires when OCS_PLUGIN_VERBOSE is set. Runner-side logs were already
suppressed (the runner is spawned with stderr = null); the remaining
`[plugin]` lines in the IPC client are genuine error messages, left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:28:10 +03:00
Hakan Seven
ccd249d40f fix(plugin): bound the runner handshake read with a deadline
The handshake verification used a raw blocking `recv`, the one unbounded
read in the spawn path: `accept` is guarded by `spawn_timeout` and every
host->runner `call` by `call_timeout`, but a process that won the accept
race and then sent nothing — or a runner that died mid-handshake — would
hang the host forever.

Route the handshake through the existing `recv_with_deadline` helper so
the first frame is bounded too (marking the process dead on timeout), and
reduce `verify_runner_handshake` to a pure token check on the received
message. Tests updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 10:41:01 +03:00
Sebastian
04240862e1 Adding runner handshake 2026-06-28 10:41:01 +03:00
Hakan Seven
5e59421e92 test(plugin): drop Dispatch floor in timeout test; doc stream-close path
The call-timeout test asserted the real 10 s Dispatch floor, adding 10 s+
to every suite run. Add a cfg(test)-only OCS_PLUGIN_TEST_FLOOR_SECS seam
so the test fires at its 1 s base instead; production still enforces the
hard safety floors. Test now finishes in ~1 s.

Also document in mark_dead that the host-side socket is closed indirectly
by killing the child (which unblocks and drops the detached reader
thread's Stream), not by the take() in mark_dead itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:56:24 +03:00
Sebastian
b18be4dacc Adding timeout protection to host from stuck runner 2026-06-26 10:10:56 +02:00
Hakan Seven
b4093724f2 fix(plugin): f64 pick coords + harden out-of-process lifecycle
Follow-ups after merging the out-of-process plugin isolation work:

- Convert the out-of-process interactive adapter's `on_point` /
  `on_entity_pick` to `glam::DVec3`, matching the `CadCommand` trait,
  which moved to f64 pick coordinates. The branch predated that change
  and no longer compiled against the trait.
- Contain plugin constructor panics in `export_plugin!`: the constructor
  runs across the C ABI boundary in `ocs_plugin_register`, where an
  unwinding panic is undefined behavior. Catch it and return null, which
  the loader already treats as a failed registration.
- Reap killed runner processes. The spawn-timeout and disconnect paths
  called `child.kill()` without `wait()`, leaving a zombie on Unix until
  the host exited. Route both — and `shutdown()` — through a shared
  `reap()` that kills and waits on a detached thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 00:16:08 +03:00
Sebastian
d0003534a0 Fix 5: Allow backward-compatibility and add plugin-template-v2 2026-06-25 00:02:17 +03:00
Sebastian
2b898d6f22 Fix 4: shared-memory 2026-06-25 00:01:36 +03:00
Sebastian
a93d425002 Fix 3: optimize shutdown times 2026-06-25 00:01:36 +03:00
Sebastian
8eb199bfc7 Fix 2: optimize src 2026-06-25 00:01:36 +03:00
Sebastian
dcfaed0182 Initial commit 2026-06-25 00:01:36 +03:00
Hakan Seven
707c143212 feat(plugin): object-pick acquisition for interactive commands
Extend InteractiveCommand with needs_object_pick / on_object_pick so a
plugin tool can prompt the user to pick an existing entity (handle + point)
rather than a free point — the structure-pick path Storm Sewer's SS_PIPE
needs to connect existing structures. The host adapter delegates to the
internal entity-pick flow; over --serve the pick is fed as a hex handle.
Stays API v2 (no released plugin implements InteractiveCommand yet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 01:43:53 +03:00
Hakan Seven
931eb908a9 feat(plugin): InteractiveCommand hook for click-to-place (API v2)
Plugins could dispatch commands but not register interactive (click-to-
place) tools — the gap mf4633 flagged on #100 for Storm Sewer's SS_INLET
/ SS_PIPE. Add an InteractiveCommand trait + CommandStep to ocs_plugin_api
and HostApi::start_interactive; a host adapter bridges it to the internal
CadCommand, so a plugin tool drives the host's point-collection flow.

Hybrid by construction: the same command works by clicking in the viewport
AND by feeding coordinates over --serve (run "CMD x,y x,y"). Adding a
HostApi method changes the contract vtable, so API_VERSION bumps to 2 —
v1 plugin binaries are now refused at load.

Part of the #100 extensibility epic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:43:35 +03:00
Hakan Seven
29c71be101 feat(plugin): load external cdylib plugins at runtime (phase 2)
Move BuiltinPlugin into ocs_plugin_api (host feature) so out-of-tree
crates can implement it, and add export_plugin! to emit the two C
symbols a cdylib exposes: ocs_plugin_api_version (checked first, so an
ABI-incompatible build never runs) and ocs_plugin_register -> boxed
BuiltinPlugin. The host loads every compatible package from the plugins
folder at startup via libloading (desktop only), keeps the library
resident for the session, and merges its ribbon tab + command dispatch
into the same paths as built-ins (honouring enable/disable). The Plugin
Manager shows external packages with a Loaded / incompatible status.

Approach B: the plugin hands back a boxed trait object, assuming a
matching toolchain + ocs_plugin_api version (the version symbol enforces
the latter). crates/ocs_example_plugin is the reference cdylib.

Part of the #100 extensibility epic (phase 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:48:56 +03:00
Hakan Seven
5ccf5cfe36 feat(plugin): lift host surface behind a HostApi trait (phase 1b)
Plugins targeted the host's concrete HostSession type. Add a HostApi
trait in ocs_plugin_api behind an optional `host` feature (the only thing
that pulls acadrust, so the core crate stays dependency-free). HostSession
implements it and BuiltinPlugin::dispatch now takes `&mut dyn HostApi`, so
an out-of-tree add-on compiles against the contract crate alone. Per-tab
plugin state is reached through object-safe plugin_state* helpers.

Completes the phase-1 host-surface extraction in the #100 epic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:31:05 +03:00
Hakan Seven
50825b6346 feat(plugin): ModuleEvent::PluginFileDialog for native file import
Add-ons had no way to request a file picker — only the host's own Open
dialog. PluginFileDialog lets a plugin tool ask the host to open a native
picker; on selection the host dispatches "<command> <path>" back to the
plugin with original case preserved (bypassing the command-line
upper-casing that mangles case-sensitive paths on Linux/macOS). The demo
plugin gains an Import tool exercising it.

Part of the #100 extensibility epic (surfaced in #106).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 10:22:08 +03:00
Hakan Seven
72ec1146df refactor(plugin): extract ocs_plugin_api crate for the stable add-on contract
Move the dependency-free, semver-versioned half of the plugin contract into a
standalone workspace crate `crates/ocs_plugin_api`:

- manifest types: PluginManifest, ApiVersion, API_VERSION
- ribbon vocabulary: CadModule trait + ToolDef/RibbonGroup/RibbonItem/IconKind/
  ModuleEvent/StyleKey

The host re-exports them from `crate::plugin::manifest` and `crate::modules`, so
every existing call site is unchanged. The acadrust-typed runtime surface
(HostSession) stays in the host binary; lifting it behind a HostApi trait in the
same crate is the remaining phase-1b step.

Part of #100. Docs updated in docs/plugin-architecture.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 01:36:04 +03:00