From ea27cc0a2ecb8aba259752435e9d1d486cbdc47d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 17:43:02 -0800 Subject: [PATCH] Plug-ins: a command list read during an install is not cached under the new revision registry::install now changes the revision while it holds the plug-in set's write lock, so a reader that sees the new revision and lists the plug-ins always finds the new one. command_specs also skips caching a list when the revision changed while it was listing. Before, a menu or shortcut lookup that ran during an install could keep the old command list until the next install or removal. --- crates/engine/src/cmd/plugin.rs | 5 +++++ crates/plugins/src/registry.rs | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/plugin.rs b/crates/engine/src/cmd/plugin.rs index 53fdd5ad2..427a03f98 100644 --- a/crates/engine/src/cmd/plugin.rs +++ b/crates/engine/src/cmd/plugin.rs @@ -324,6 +324,11 @@ pub fn command_specs() -> Vec<&'static CommandSpec> { } let specs: Vec<&'static CommandSpec> = registry::list().iter().flat_map(|p| p.manifest().commands.iter().map(|c| spec_of(p, c)).collect::>()).collect(); + // When a plug-in was installed or removed during the listing, the list may not match + // `revision`. It is returned without being cached, and the next call lists again. + if registry::revision() != revision { + return specs; + } let mut l = LISTED.write().unwrap_or_else(|e| e.into_inner()); if l.specs.len() != specs.len() || l.specs.iter().zip(&specs).any(|(a, b)| !std::ptr::eq(*a, *b)) { l.generation += 1; diff --git a/crates/plugins/src/registry.rs b/crates/plugins/src/registry.rs index 0302ab6be..5524166fa 100644 --- a/crates/plugins/src/registry.rs +++ b/crates/plugins/src/registry.rs @@ -45,11 +45,13 @@ pub fn approved(plugin: &Plugin) -> bool { !m.asks_permissions() || granted(&m.id).is_some_and(|g| g.covers(&m.permissions())) } -/// Installs `plugin`, replacing one with the same id. +/// Installs `plugin`, replacing one with the same id. The revision changes while the set is locked, +/// so a caller that reads the new [`revision`] and then [`list`]s always finds the plug-in. pub fn install(mut plugin: Plugin) -> Arc { + let mut plugins = PLUGINS.write().unwrap_or_else(|e| e.into_inner()); plugin.set_serial(REVISION.fetch_add(1, Ordering::Relaxed) + 1); let p = Arc::new(plugin); - PLUGINS.write().unwrap_or_else(|e| e.into_inner()).insert(p.id().to_string(), p.clone()); + plugins.insert(p.id().to_string(), p.clone()); p }