VS-34: refresh plugin catalog at runtime on install/remove #53

Merged
nrupard merged 1 commit from feat/plugin-catalog-runtime-refresh-vs-34 into main 2026-06-08 02:20:49 +02:00
Owner

Implements VS-34. Follow-up to VS-33 (#52).

Problem

The plugin catalog was a startup-only snapshot. PluginCatalog wrapped an immutable Arc<Vec<DiscoveredPlugin>> built once at boot (boot.rs:load_plugin_catalog), stored in AppState.plugins, and never rebuilt. The lifecycle handlers only mutated the DB. Two stale-snapshot defects followed (both flagged in the VS-33 review):

  1. Removed plugin resurrected. removeplugin deleted the plugin/<name> doc but left the on-disk files, so list_for_ui re-emitted the plugin from the catalog stamped status: "installed" until restart.
  2. Runtime-installed plugin lost permissions. A plugin installed after boot had no catalog entry, so list_for_ui (and GetPluginPermissionList) reported permissions: [] for it until restart.

What changed

  • PluginCatalog now holds Arc<ArcSwap<CatalogState>>, where CatalogState carries the scanned plugins plus the directory they were scanned from. All clones share one ArcSwap, so a refresh on any handle is visible to every session. Reads (manifests / find / len / is_empty / wasm_path) load the current snapshot lock-free. Public method signatures are unchanged, so boot.rs / main.rs consumers are untouched.
  • New refresh() re-scans the remembered directory and atomically swaps the view (no-op for empty / test catalogs with no directory).
  • New remove_on_disk() deletes a plugin's on-disk directory so it cannot reappear from a re-scan. The name is rejected if empty or containing path separators / . / .., and the resolved target must stay under the plugins directory.
  • The installplugin handler calls refresh() on success. The removeplugin handler deletes the on-disk directory and calls refresh() after the DB doc is removed.
  • list_for_ui's disk-only installed fallback now only fires for genuinely doc-less plugins present on disk at boot, instead of papering over staleness.

Acceptance criteria

  • After removeplugin, the next updatePluginList no longer lists the plugin (doc deleted and on-disk directory removed)
  • A plugin installed at runtime appears in updatePluginList with its real permissions without a restart (catalog re-scanned on install)
  • GetPluginPermissionList includes a runtime-installed plugin's permissions without a restart (reads the refreshed catalog)
  • The plugin catalog is refreshed after install and remove succeed
  • Existing plugin lifecycle and catalog tests still pass

Testing

cargo test -p meshcentral-web --lib (433 passed), including 7 plugin_catalog tests (4 new: refresh_picks_up_new_plugin asserting a clone sees the refresh, remove_on_disk_then_refresh_drops_plugin, remove_on_disk_rejects_path_escape, refresh_without_dir_is_noop). cargo check -p meshcentral, cargo fmt --check, and cargo clippy -p meshcentral-web --all-targets -- -D warnings all clean.

Implements VS-34. Follow-up to VS-33 (#52). ## Problem The plugin catalog was a startup-only snapshot. `PluginCatalog` wrapped an immutable `Arc<Vec<DiscoveredPlugin>>` built once at boot (`boot.rs:load_plugin_catalog`), stored in `AppState.plugins`, and never rebuilt. The lifecycle handlers only mutated the DB. Two stale-snapshot defects followed (both flagged in the VS-33 review): 1. Removed plugin resurrected. `removeplugin` deleted the `plugin/<name>` doc but left the on-disk files, so `list_for_ui` re-emitted the plugin from the catalog stamped `status: "installed"` until restart. 2. Runtime-installed plugin lost permissions. A plugin installed after boot had no catalog entry, so `list_for_ui` (and `GetPluginPermissionList`) reported `permissions: []` for it until restart. ## What changed - `PluginCatalog` now holds `Arc<ArcSwap<CatalogState>>`, where `CatalogState` carries the scanned plugins plus the directory they were scanned from. All clones share one `ArcSwap`, so a refresh on any handle is visible to every session. Reads (`manifests` / `find` / `len` / `is_empty` / `wasm_path`) load the current snapshot lock-free. Public method signatures are unchanged, so `boot.rs` / `main.rs` consumers are untouched. - New `refresh()` re-scans the remembered directory and atomically swaps the view (no-op for empty / test catalogs with no directory). - New `remove_on_disk()` deletes a plugin's on-disk directory so it cannot reappear from a re-scan. The name is rejected if empty or containing path separators / `.` / `..`, and the resolved target must stay under the plugins directory. - The `installplugin` handler calls `refresh()` on success. The `removeplugin` handler deletes the on-disk directory and calls `refresh()` after the DB doc is removed. - `list_for_ui`'s disk-only `installed` fallback now only fires for genuinely doc-less plugins present on disk at boot, instead of papering over staleness. ## Acceptance criteria - [x] After `removeplugin`, the next `updatePluginList` no longer lists the plugin (doc deleted and on-disk directory removed) - [x] A plugin installed at runtime appears in `updatePluginList` with its real `permissions` without a restart (catalog re-scanned on install) - [x] `GetPluginPermissionList` includes a runtime-installed plugin's permissions without a restart (reads the refreshed catalog) - [x] The plugin catalog is refreshed after `install` and `remove` succeed - [x] Existing plugin lifecycle and catalog tests still pass ## Testing `cargo test -p meshcentral-web --lib` (433 passed), including 7 `plugin_catalog` tests (4 new: `refresh_picks_up_new_plugin` asserting a clone sees the refresh, `remove_on_disk_then_refresh_drops_plugin`, `remove_on_disk_rejects_path_escape`, `refresh_without_dir_is_noop`). `cargo check -p meshcentral`, `cargo fmt --check`, and `cargo clippy -p meshcentral-web --all-targets -- -D warnings` all clean.
feat(web): refresh plugin catalog at runtime on install/remove
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m35s
Create release / Create release from merged PR (pull_request) Has been skipped
eeee68ebf3
The plugin catalog was a startup-only snapshot: PluginCatalog wrapped an immutable Arc<Vec<DiscoveredPlugin>> built once at boot, and the lifecycle handlers only touched the DB. Two stale-snapshot defects followed. A plugin removed via removeplugin kept its on-disk files, so list_for_ui re-emitted it from the catalog stamped status "installed" until restart. A plugin installed at runtime had no catalog entry, so its permissions surfaced as [] in updatePluginList and in GetPluginPermissionList until restart.

PluginCatalog now holds Arc<ArcSwap<CatalogState>> carrying the scanned plugins plus the directory they came from. All clones share one ArcSwap, so a refresh on any handle is visible to every session. refresh() re-scans the remembered directory and atomically swaps the view; remove_on_disk() deletes a plugin's directory (name validated, target confined under the plugins dir) so it cannot reappear from a re-scan. Reads (manifests / find / len / wasm_path) load the current snapshot lock-free.

The installplugin handler now calls refresh() on success; the removeplugin handler deletes the on-disk directory and refreshes after the DB doc is removed. list_for_ui's disk-only "installed" fallback now only fires for genuinely doc-less plugins present at boot.

#VS-34
nrupard deleted branch feat/plugin-catalog-runtime-refresh-vs-34 2026-06-08 02:20:49 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-server!53
No description provided.