From 13d9b554cb6cff13159efaa9682062bf9ee4e666 Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Thu, 19 Oct 2023 19:48:12 +0200 Subject: [PATCH] Plugins can now be enabled or disabled via management ui --- Cargo.lock | 23 +++ Cargo.toml | 1 + js/core/src/init.ts | 27 +++- src/client/model.rs | 1 - src/common/dbus.rs | 2 + src/management_client/dbus.rs | 2 + src/management_client/ui.rs | 174 +++++++++++++++++----- src/server/dbus.rs | 26 ++-- src/server/mod.rs | 26 +--- src/server/model.rs | 4 + src/server/plugins/js.rs | 84 ++++++++--- src/server/plugins/mod.rs | 268 ++++++++++++++++++++++++++++------ src/server/search.rs | 31 ++-- test_data/plugin/package.json | 4 +- 14 files changed, 507 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1d78e9..6d59f69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,6 +426,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", +] + [[package]] name = "async-task" version = "4.4.1" @@ -5672,6 +5694,7 @@ name = "placeholdername" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "clap", "deno_core", "deno_runtime", diff --git a/Cargo.toml b/Cargo.toml index d8b4e06..6b48cc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ clap = { version = "4.3.22", features = ["derive"] } gix = { version = "0.52.0", features = ["blocking-network-client"] } tempfile = "3" futures-concurrency = "7.4.2" +async-stream = "0.3.5" anyhow = "1.0.75" thiserror = "1.0.48" iced = { version = "0.10", features = ["tokio", "lazy", "advanced"] } diff --git a/js/core/src/init.ts b/js/core/src/init.ts index 600db2c..79a071b 100644 --- a/js/core/src/init.ts +++ b/js/core/src/init.ts @@ -16,29 +16,40 @@ declare interface InternalApi { op_react_call_event_listener(instance: InstanceSync, eventName: string): void; } -(async () => { - // noinspection InfiniteLoopJS +const run_loop = async () => { while (true) { - console.log("before op_react_get_next_pending_ui_event") - const uiEvent = await denoCore.opAsync("op_react_get_next_pending_ui_event"); - switch (uiEvent.type) { + console.log("before op_plugin_get_pending_event") + const pluginEvent = await denoCore.opAsync("op_plugin_get_pending_event"); + switch (pluginEvent.type) { case "ViewEvent": { console.log("ViewEvent") - InternalApi.op_react_call_event_listener(uiEvent.widget, uiEvent.eventName) + InternalApi.op_react_call_event_listener(pluginEvent.widget, pluginEvent.eventName) break; } case "ViewCreated": { console.log("ViewCreated") - const view = (await import(`plugin:view?${uiEvent.viewName}`)).default; + const view = (await import(`plugin:view?${pluginEvent.viewName}`)).default; const { render } = await import("plugin:renderer"); - render(uiEvent.reconcilerMode, view) + render(pluginEvent.reconcilerMode, view) break; } case "ViewDestroyed": { console.log("ViewDestroyed") break; } + case "PluginCommand": { + switch (pluginEvent.commandType) { + case "stop": { + console.log("PluginCommand stop") + return; + } + } + } } } +} + +(async () => { + await run_loop() })(); diff --git a/src/client/model.rs b/src/client/model.rs index 64cd05d..d15d22f 100644 --- a/src/client/model.rs +++ b/src/client/model.rs @@ -25,7 +25,6 @@ pub enum NativeUiResponseData { CloneInstance { widget: NativeUiWidget }, - Unit, } #[derive(Debug, Clone)] diff --git a/src/common/dbus.rs b/src/common/dbus.rs index 6e156fd..2671c3e 100644 --- a/src/common/dbus.rs +++ b/src/common/dbus.rs @@ -16,6 +16,7 @@ pub struct DBusSearchResult { pub struct DBusPlugin { pub plugin_id: String, pub plugin_name: String, + pub enabled: bool, pub entrypoints: Vec, } @@ -23,6 +24,7 @@ pub struct DBusPlugin { pub struct DBusEntrypoint { pub entrypoint_id: String, pub entrypoint_name: String, + pub enabled: bool, } #[derive(Debug, Deserialize, Serialize, Type)] diff --git a/src/management_client/dbus.rs b/src/management_client/dbus.rs index 7dd180e..28b0d89 100644 --- a/src/management_client/dbus.rs +++ b/src/management_client/dbus.rs @@ -7,5 +7,7 @@ use crate::common::dbus::DBusPlugin; )] trait DbusManagementServerProxy { async fn plugins(&self) -> zbus::Result>; + async fn set_plugin_state(&self, plugin_id: &str, enabled: bool) -> zbus::Result<()>; + async fn set_entrypoint_state(&self, plugin_id: &str, entrypoint_id: &str, enabled: bool) -> zbus::Result<()>; } diff --git a/src/management_client/ui.rs b/src/management_client/ui.rs index 14ab05b..128c79c 100644 --- a/src/management_client/ui.rs +++ b/src/management_client/ui.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use deno_core::error::AnyError; use iced::{Application, Command, Element, executor, font, futures, Length, Padding, Renderer, Settings, theme, window}; -use iced::widget::{button, column, container, horizontal_space, row, scrollable, text}; +use iced::widget::{button, checkbox, column, container, horizontal_space, row, scrollable, text}; use iced_aw::graphics::icons; use iced_table::table; use zbus::Connection; @@ -37,11 +37,12 @@ struct ManagementAppModel { enum ManagementAppMsg { TableSyncHeader(scrollable::AbsoluteOffset), FontLoaded(Result<(), font::Error>), - PluginsLoaded(HashMap), + PluginsReloaded(HashMap), ToggleShowEntrypoints { plugin_id: PluginId, }, - SelectItem(SelectedItem) + SelectItem(SelectedItem), + EnabledToggleItem(EnabledItem), } #[derive(Debug, Clone)] @@ -56,12 +57,26 @@ enum SelectedItem { } } +#[derive(Debug, Clone)] +enum EnabledItem { + Plugin { + enabled: bool, + plugin_id: PluginId + }, + Entrypoint { + enabled: bool, + plugin_id: PluginId, + entrypoint_id: EntrypointId + } +} + #[derive(Debug, Clone)] struct Plugin { plugin_id: PluginId, plugin_name: String, show_entrypoints: bool, + enabled: bool, entrypoints: HashMap, } @@ -69,6 +84,7 @@ struct Plugin { struct Entrypoint { entrypoint_id: EntrypointId, entrypoint_name: String, + enabled: bool, } impl Application for ManagementAppModel { @@ -95,8 +111,9 @@ impl Application for ManagementAppModel { dbus_connection, dbus_server, columns: vec![ - Column::new(ColumnKind::EntrypointToggle), + Column::new(ColumnKind::ShowEntrypointsToggle), Column::new(ColumnKind::Name), + Column::new(ColumnKind::EnableToggle), ], plugins: HashMap::new(), selected_item: SelectedItem::None, @@ -106,36 +123,8 @@ impl Application for ManagementAppModel { Command::batch([ font::load(icons::ICON_FONT_BYTES).map(ManagementAppMsg::FontLoaded), Command::perform(async move { - let plugins = dbus_server_clone.plugins().await.unwrap(); - - let plugins: HashMap<_, _> = plugins.into_iter() - .map(|plugin| { - let entrypoints: HashMap<_, _> = plugin.entrypoints - .into_iter() - .map(|entrypoint| { - let id = EntrypointId::new(entrypoint.entrypoint_id); - let entrypoint = Entrypoint { - entrypoint_id: id.clone(), - entrypoint_name: entrypoint.entrypoint_name.clone(), - }; - (id, entrypoint) - }) - .collect(); - - let id = PluginId::new(plugin.plugin_id); - let plugin = Plugin { - plugin_id: id.clone(), - plugin_name: plugin.plugin_name, - show_entrypoints: true, - entrypoints, - }; - - (id, plugin) - }) - .collect(); - - plugins - }, ManagementAppMsg::PluginsLoaded) + reload_plugins(dbus_server_clone).await + }, ManagementAppMsg::PluginsReloaded) ]), ) } @@ -158,7 +147,7 @@ impl Application for ManagementAppModel { plugin.show_entrypoints = !plugin.show_entrypoints; Command::none() } - ManagementAppMsg::PluginsLoaded(plugins) => { + ManagementAppMsg::PluginsReloaded(plugins) => { self.plugins = plugins; Command::none() } @@ -166,6 +155,29 @@ impl Application for ManagementAppModel { self.selected_item = selected_item; Command::none() } + ManagementAppMsg::EnabledToggleItem(item) => { + match item { + EnabledItem::Plugin { enabled, plugin_id } => { + let dbus_server = self.dbus_server.clone(); + + Command::perform(async move { + dbus_server.set_plugin_state(&plugin_id.to_string(), enabled).await.unwrap(); + + reload_plugins(dbus_server).await + + }, ManagementAppMsg::PluginsReloaded) + } + EnabledItem::Entrypoint { enabled, plugin_id, entrypoint_id } => { + let dbus_server = self.dbus_server.clone(); + + Command::perform(async move { + dbus_server.set_entrypoint_state(&plugin_id.to_string(), &entrypoint_id.to_string(), enabled).await.unwrap(); + + reload_plugins(dbus_server).await + }, ManagementAppMsg::PluginsReloaded) + } + } + } } } @@ -268,8 +280,9 @@ enum Row<'a> { } enum ColumnKind { - EntrypointToggle, + ShowEntrypointsToggle, Name, + EnableToggle, } struct Column { @@ -289,7 +302,7 @@ impl<'a, 'b> table::Column<'a, 'b, ManagementAppMsg, Renderer> for Column { fn header(&'b self, _col_index: usize) -> Element<'a, ManagementAppMsg> { match self.kind { - ColumnKind::EntrypointToggle => { + ColumnKind::ShowEntrypointsToggle => { horizontal_space(Length::Fill) .into() } @@ -298,6 +311,11 @@ impl<'a, 'b> table::Column<'a, 'b, ManagementAppMsg, Renderer> for Column { .center_y() .into() } + ColumnKind::EnableToggle => { + container(text("Enabled")) + .center_y() + .into() + } } } @@ -308,7 +326,7 @@ impl<'a, 'b> table::Column<'a, 'b, ManagementAppMsg, Renderer> for Column { row_entry: &'b Self::Row, ) -> Element<'a, ManagementAppMsg> { match self.kind { - ColumnKind::EntrypointToggle => { + ColumnKind::ShowEntrypointsToggle => { match row_entry { Row::Plugin { plugin } => { let icon = if plugin.show_entrypoints { icons::Icon::CaretDown } else { icons::Icon::CaretRight }; @@ -351,7 +369,9 @@ impl<'a, 'b> table::Column<'a, 'b, ManagementAppMsg, Renderer> for Column { }; let msg = match &row_entry { - Row::Plugin { plugin } => SelectedItem::Plugin { plugin_id: plugin.plugin_id.clone() }, + Row::Plugin { plugin } => SelectedItem::Plugin { + plugin_id: plugin.plugin_id.clone() + }, Row::Entrypoint { entrypoint, plugin } => SelectedItem::Entrypoint { plugin_id: plugin.plugin_id.clone(), entrypoint_id: entrypoint.entrypoint_id.clone() @@ -364,13 +384,54 @@ impl<'a, 'b> table::Column<'a, 'b, ManagementAppMsg, Renderer> for Column { .width(Length::Fill) .into() } + ColumnKind::EnableToggle => { + let (enabled, plugin_id, entrypoint_id) = match &row_entry { + Row::Plugin { plugin } => { + ( + plugin.enabled, + plugin.plugin_id.clone(), + None + ) + }, + Row::Entrypoint { entrypoint, plugin } => { + ( + entrypoint.enabled, + plugin.plugin_id.clone(), + Some(entrypoint.entrypoint_id.clone()) + ) + } + }; + + + // TODO disable if plugin is disabled but preserve current state https://github.com/iced-rs/iced/pull/2109 + let checkbox: Element<_> = checkbox("", enabled, move |enabled| { + let enabled_item = match &entrypoint_id { + None => EnabledItem::Plugin { + enabled, + plugin_id: plugin_id.clone(), + }, + Some(entrypoint_id) => EnabledItem::Entrypoint { + enabled, + plugin_id: plugin_id.clone(), + entrypoint_id: entrypoint_id.clone() + } + }; + ManagementAppMsg::EnabledToggleItem(enabled_item) + }).into(); + + container(checkbox) + .width(Length::Fill) + .center_x() + .into() + } } } fn width(&self) -> f32 { match self.kind { - ColumnKind::EntrypointToggle => 35.0, + ColumnKind::ShowEntrypointsToggle => 35.0, ColumnKind::Name => 550.0, + ColumnKind::EnableToggle => 75.0 } } @@ -378,3 +439,36 @@ impl<'a, 'b> table::Column<'a, 'b, ManagementAppMsg, Renderer> for Column { None } } + + +async fn reload_plugins(dbus_server: DbusManagementServerProxyProxy<'static>) -> HashMap { + let plugins = dbus_server.plugins().await.unwrap(); + + plugins.into_iter() + .map(|plugin| { + let entrypoints: HashMap<_, _> = plugin.entrypoints + .into_iter() + .map(|entrypoint| { + let id = EntrypointId::new(entrypoint.entrypoint_id); + let entrypoint = Entrypoint { + enabled: entrypoint.enabled, + entrypoint_id: id.clone(), + entrypoint_name: entrypoint.entrypoint_name.clone(), + }; + (id, entrypoint) + }) + .collect(); + + let id = PluginId::new(plugin.plugin_id); + let plugin = Plugin { + plugin_id: id.clone(), + plugin_name: plugin.plugin_name, + show_entrypoints: true, + enabled: plugin.enabled, + entrypoints, + }; + + (id, plugin) + }) + .collect() +} \ No newline at end of file diff --git a/src/server/dbus.rs b/src/server/dbus.rs index 413a9f2..c504a10 100644 --- a/src/server/dbus.rs +++ b/src/server/dbus.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; -use crate::common::dbus::{DBusEntrypoint, DbusEventViewCreated, DbusEventViewEvent, DBusPlugin, DBusSearchResult, DBusUiPropertyContainer, DBusUiWidget}; +use crate::common::dbus::{DbusEventViewCreated, DbusEventViewEvent, DBusPlugin, DBusSearchResult, DBusUiPropertyContainer, DBusUiWidget}; +use crate::common::model::{EntrypointId, PluginId}; use crate::server::plugins::PluginManager; use crate::server::search::SearchIndex; @@ -36,19 +37,16 @@ pub struct DbusManagementServer { impl DbusManagementServer { fn plugins(&mut self) -> Vec { self.plugin_manager.plugins() - .iter() - .map(|plugin| DBusPlugin { - plugin_id: plugin.id().to_owned(), - plugin_name: plugin.name().to_owned(), - entrypoints: plugin.entrypoints() - .into_iter() - .map(|entrypoint| DBusEntrypoint { - entrypoint_id: entrypoint.id().to_owned(), - entrypoint_name: entrypoint.name().to_owned() - }) - .collect() - }) - .collect() + } + + fn set_plugin_state(&mut self, plugin_id: &str, enabled: bool) { + println!("set_plugin_state {:?} {:?}", plugin_id, enabled); + self.plugin_manager.set_plugin_state(PluginId::new(plugin_id), enabled) + } + + fn set_entrypoint_state(&mut self, plugin_id: &str, entrypoint_id: &str, enabled: bool) { + println!("set_entrypoint_state {:?} {:?}", plugin_id, enabled); + self.plugin_manager.set_entrypoint_state(PluginId::new(plugin_id), EntrypointId::new(entrypoint_id), enabled) } } diff --git a/src/server/mod.rs b/src/server/mod.rs index f3a279c..2a0891e 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,6 +1,6 @@ use crate::server::dbus::{DbusManagementServer, DbusServer}; use crate::server::plugins::PluginManager; -use crate::server::search::{SearchIndex, SearchItem}; +use crate::server::search::SearchIndex; pub mod dbus; pub(in crate::server) mod search; @@ -19,28 +19,10 @@ pub fn start_server() { } async fn run_server() -> anyhow::Result<()> { - let mut plugin_manager = PluginManager::create(); - let mut search_index = SearchIndex::create_index().unwrap(); + let search_index = SearchIndex::create_index().unwrap(); + let mut plugin_manager = PluginManager::create(search_index.clone()); - let search_items: Vec<_> = plugin_manager.plugins() - .iter() - .flat_map(|plugin| { - plugin.entrypoints() - .iter() - .map(|entrypoint| { - SearchItem { - entrypoint_name: entrypoint.name().to_owned(), - entrypoint_id: entrypoint.id().to_owned(), - plugin_name: plugin.name().to_owned(), - plugin_id: plugin.id().to_owned(), - } - }) - }) - .collect(); - - search_index.add_entries(search_items).unwrap(); - - plugin_manager.start_all_contexts(); + plugin_manager.reload_all_plugins(); let interface = DbusServer { search_index }; let management_interface = DbusManagementServer { plugin_manager }; diff --git a/src/server/model.rs b/src/server/model.rs index e89f7f2..b1b93b4 100644 --- a/src/server/model.rs +++ b/src/server/model.rs @@ -79,6 +79,10 @@ pub enum JsUiEvent { #[serde(rename = "eventName")] event_name: UiEventName, }, + PluginCommand { + #[serde(rename = "commandType")] + command_type: String, + } } #[derive(Debug)] diff --git a/src/server/plugins/js.rs b/src/server/plugins/js.rs index 8c05d3d..bacbd6d 100644 --- a/src/server/plugins/js.rs +++ b/src/server/plugins/js.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::future::Future; -use std::pin::Pin; +use std::pin::{Pin}; use std::rc::Rc; use anyhow::anyhow; @@ -14,17 +14,35 @@ use deno_runtime::worker::WorkerOptions; use futures_concurrency::stream::Merge; use once_cell::sync::Lazy; use regex::Regex; +use crate::common::model::PluginId; use crate::server::dbus::{DbusClientProxyProxy, ViewCreatedSignal, ViewEventSignal}; use crate::server::model::{JsUiEvent, JsUiEventName, JsUiPropertyValue, JsUiWidget, JsUiWidgetId, JsUiRequestData, JsUiResponseData}; -use crate::server::plugins::Plugin; +use crate::server::plugins::{PluginCode}; use crate::utils::channel::{channel, RequestSender}; -pub async fn start_js_runtime(plugin: Plugin) -> anyhow::Result<()> { +pub struct PluginContextData { + pub id: PluginId, + pub code: PluginCode, + pub command_receiver: tokio::sync::broadcast::Receiver, +} + +#[derive(Clone, Debug)] +pub struct PluginCommand { + pub id: PluginId, + pub data: PluginCommandData, +} + +#[derive(Clone, Debug)] +pub enum PluginCommandData { + Stop +} + +pub async fn start_js_runtime(data: PluginContextData) -> anyhow::Result<()> { let conn = zbus::Connection::session().await?; let client_proxy = DbusClientProxyProxy::new(&conn).await?; - let plugin_id = plugin.id().to_owned(); + let plugin_id = data.id.clone(); let view_created_signal = client_proxy.receive_view_created_signal() .await? .filter_map(move |signal: ViewCreatedSignal| { @@ -33,7 +51,7 @@ pub async fn start_js_runtime(plugin: Plugin) -> anyhow::Result<()> { let signal = signal.args().unwrap(); // TODO add logging here that we received signal - if signal.plugin_id != plugin_id { + if PluginId::new(signal.plugin_id) != plugin_id { None } else { Some(JsUiEvent::ViewCreated { @@ -44,7 +62,7 @@ pub async fn start_js_runtime(plugin: Plugin) -> anyhow::Result<()> { } }); - let plugin_id = plugin.id().to_owned(); + let plugin_id = data.id.clone(); let view_event_signal = client_proxy.receive_view_event_signal() .await? .filter_map(move |signal: ViewEventSignal| { @@ -53,7 +71,7 @@ pub async fn start_js_runtime(plugin: Plugin) -> anyhow::Result<()> { let signal = signal.args().unwrap(); // TODO add logging here that we received signal - if signal.plugin_id != plugin_id { + if PluginId::new(signal.plugin_id) != plugin_id { None } else { Some(JsUiEvent::ViewEvent { @@ -64,12 +82,42 @@ pub async fn start_js_runtime(plugin: Plugin) -> anyhow::Result<()> { } }); - let event_stream = (view_event_signal, view_created_signal).merge(); + let mut command_receiver = data.command_receiver; + let command_stream = async_stream::stream! { + loop { + yield command_receiver.recv().await.unwrap(); + } + }; + + let plugin_id = data.id.clone(); + let command_stream = command_stream + .filter_map(move |command: PluginCommand| { + let plugin_id = plugin_id.clone(); + async move { + let id = command.id; + + // TODO add logging here that we received signal + if id != plugin_id { + None + } else { + match command.data { + PluginCommandData::Stop => { + Some(JsUiEvent::PluginCommand { + command_type: "stop".to_string(), + }) + } + } + } + } + }); + + let event_stream = (view_event_signal, view_created_signal, command_stream).merge(); let (tx, mut rx) = channel::(); - let plugin_id: String = plugin.id().to_owned(); + let plugin_id = data.id.clone(); tokio::spawn(tokio::task::unconstrained(async move { + let plugin_id = plugin_id.to_string(); println!("starting request handler loop"); while let Ok((request_data, responder)) = rx.recv().await { @@ -154,7 +202,7 @@ pub async fn start_js_runtime(plugin: Plugin) -> anyhow::Result<()> { "plugin:unused".parse().unwrap(), PermissionsContainer::allow_all(), WorkerOptions { - module_loader: Rc::new(CustomModuleLoader::new(plugin)), + module_loader: Rc::new(CustomModuleLoader::new(data.code)), extensions: vec![react_ext::init_ops_and_esm( EventHandlers::new(), EventReceiver::new(Box::pin(event_stream)), @@ -177,17 +225,17 @@ pub async fn start_js_runtime(plugin: Plugin) -> anyhow::Result<()> { } pub struct CustomModuleLoader { - plugin: Plugin, + code: PluginCode, static_loader: StaticModuleLoader, } impl CustomModuleLoader { - fn new(plugin: Plugin) -> Self { + fn new(code: PluginCode) -> Self { let module_map: HashMap<_, _> = MODULES.iter() .map(|(key, value)| (key.parse().unwrap(), FastString::from_static(value))) .collect(); Self { - plugin, + code, static_loader: StaticModuleLoader::new(module_map), } } @@ -246,10 +294,10 @@ impl ModuleLoader for CustomModuleLoader { if &specifier == &"plugin:view".parse().unwrap() || &specifier == &"plugin:module".parse().unwrap() { let view_name = module_specifier.query().unwrap(); - let js = self.plugin.code().js(); + let js = self.code.js(); let js = js.get(view_name).unwrap(); - let module = ModuleSource::new(ModuleType::JavaScript, js.to_owned().into(), module_specifier); + let module = ModuleSource::new(ModuleType::JavaScript, js.to_string().into(), module_specifier); return futures::future::ready(Ok(module)).boxed_local(); } @@ -270,7 +318,7 @@ deno_core::extension!( op_react_remove_child, op_react_set_properties, op_react_set_text, - op_react_get_next_pending_ui_event, + op_plugin_get_pending_event, op_react_call_event_listener, op_react_clone_instance, op_react_replace_container_children, @@ -452,7 +500,7 @@ fn op_react_set_properties<'a>( } #[op] -async fn op_react_get_next_pending_ui_event<'a>( +async fn op_plugin_get_pending_event<'a>( state: Rc>, ) -> anyhow::Result { let event_stream = { @@ -462,7 +510,7 @@ async fn op_react_get_next_pending_ui_event<'a>( .clone() }; - println!("op_react_get_next_pending_ui_event"); + println!("op_plugin_get_pending_event"); let mut event_stream = event_stream.borrow_mut(); let event = event_stream.next() diff --git a/src/server/plugins/mod.rs b/src/server/plugins/mod.rs index 131e79b..8afbc3a 100644 --- a/src/server/plugins/mod.rs +++ b/src/server/plugins/mod.rs @@ -7,8 +7,11 @@ use std::sync::{Arc, RwLock}; use anyhow::Context; use deno_core::serde_json; use serde::Deserialize; +use crate::common::dbus::{DBusEntrypoint, DBusPlugin}; -use crate::server::plugins::js::start_js_runtime; +use crate::common::model::{EntrypointId, PluginId}; +use crate::server::plugins::js::{PluginCommand, PluginCommandData, PluginContextData, start_js_runtime}; +use crate::server::search::{SearchIndex, SearchItem}; pub mod js; @@ -18,31 +21,175 @@ pub struct PluginManager { } pub struct PluginManagerInner { - plugins: Vec, + plugins: HashMap, + search_index: SearchIndex, + command_broadcaster: tokio::sync::broadcast::Sender, } impl PluginManager { - pub fn create() -> Self { - let plugins = PluginLoader.load_plugins(); + pub fn create(search_index: SearchIndex) -> Self { + let plugins = PluginLoader.load_plugins() + .into_iter() + .map(|plugin| (plugin.id.clone(), plugin)) + .collect(); + + let (tx, _) = tokio::sync::broadcast::channel::(100); Self { inner: Arc::new(RwLock::new(PluginManagerInner { plugins, - })) + search_index, + command_broadcaster: tx + })), } } - pub fn plugins(&self) -> Vec { - self.inner.read().unwrap().plugins.clone() + pub fn plugins(&self) -> Vec { + let plugins = &self.inner.read().unwrap().plugins; + + plugins.iter() + .map(|(_, plugin)| DBusPlugin { + plugin_id: plugin.id().to_string(), + plugin_name: plugin.name().to_owned(), + enabled: plugin.enabled(), + entrypoints: plugin.entrypoints() + .into_iter() + .map(|entrypoint| DBusEntrypoint { + enabled: entrypoint.enabled(), + entrypoint_id: entrypoint.id().to_string(), + entrypoint_name: entrypoint.name().to_owned() + }) + .collect() + }) + .collect() } - pub fn start_all_contexts(&mut self) { - self.plugins() + pub fn set_plugin_state(&mut self, plugin_id: PluginId, enabled: bool) { + let mut inner = self.inner.write().unwrap(); + inner.set_plugin_state(plugin_id, enabled); + } + + pub fn set_entrypoint_state(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, enabled: bool) { + let mut inner = self.inner.write().unwrap(); + inner.set_entrypoint_state(plugin_id, entrypoint_id, enabled); + } + + pub fn reload_all_plugins(&mut self) { + let mut inner = self.inner.write().unwrap(); + inner.reload_all_plugins(); + } +} + +impl PluginManagerInner { + fn set_plugin_state(&mut self, plugin_id: PluginId, enabled: bool) { + let x = self.is_plugin_enabled(&plugin_id); + println!("set_plugin_state {:?} {:?}", x, enabled ); + match (x, enabled) { + (false, true) => { + self.start_plugin(plugin_id); + }, + (true, false) => { + self.stop_plugin(plugin_id); + } + _ => {} + } + } + + fn set_entrypoint_state(&mut self, plugin_id: PluginId, entrypoint_id: EntrypointId, enabled: bool) { + let entrypoint = self.plugins.get_mut(&plugin_id) + .unwrap() + .entrypoints_mut() + .iter_mut() + .find(|entrypoint| entrypoint.id() == entrypoint_id) + .unwrap(); + + entrypoint.enabled = enabled; + + self.reload_search_index(); + } + + fn reload_all_plugins(&mut self) { + self.reload_search_index(); + + self.plugins .iter() - .for_each(|plugin| self.start_context_for_plugin(plugin.clone())); + .filter(|(_, plugin)| plugin.enabled) + .for_each(|(_, plugin)| { + let receiver = self.command_broadcaster.subscribe(); + + let data = PluginContextData { + id: plugin.id(), + code: plugin.code().clone(), + command_receiver: receiver, + }; + + self.start_plugin_context(data) + }); } - fn start_context_for_plugin(&self, plugin: Plugin) { + fn is_plugin_enabled(&self, plugin_id: &PluginId) -> bool { + let plugin = self.plugins.get(plugin_id).unwrap(); + + plugin.enabled + } + + fn start_plugin(&mut self, plugin_id: PluginId) { + println!("plugin_id {:?}", plugin_id); + let plugin = self.plugins.get_mut(&plugin_id).unwrap(); + + plugin.enabled = true; + + let receiver = self.command_broadcaster.subscribe(); + let data = PluginContextData { + id: plugin_id.clone(), + code: plugin.code().clone(), + command_receiver: receiver, + }; + + self.reload_search_index(); + self.start_plugin_context(data) + } + + fn stop_plugin(&mut self, plugin_id: PluginId) { + println!("stop_plugin {:?}", plugin_id); + let plugin = self.plugins.get_mut(&plugin_id).unwrap(); + + plugin.enabled = false; + + let data = PluginCommand { + id: plugin.id(), + data: PluginCommandData::Stop, + }; + + self.reload_search_index(); + self.send_command(data) + } + + fn reload_search_index(&mut self) { + println!("reload_search_index"); + + let search_items: Vec<_> = self.plugins + .iter() + .filter(|(_, plugin)| plugin.enabled) + .flat_map(|(_, plugin)| { + plugin.entrypoints() + .iter() + .filter(|entrypoint| entrypoint.enabled) + .map(|entrypoint| { + SearchItem { + entrypoint_name: entrypoint.name().to_owned(), + entrypoint_id: entrypoint.id().to_string(), + plugin_name: plugin.name().to_owned(), + plugin_id: plugin.id().to_string(), + } + }) + }) + .collect(); + + self.search_index.reload(search_items).unwrap(); + } + + fn start_plugin_context(&self, data: PluginContextData) { let handle = move || { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() @@ -51,20 +198,24 @@ impl PluginManager { let local_set = tokio::task::LocalSet::new(); local_set.block_on(&runtime, tokio::task::unconstrained(async move { - start_js_runtime(plugin).await + start_js_runtime(data).await })) }; std::thread::Builder::new() - .name("react-thread".into()) + .name("plugin-js-thread".into()) .spawn(handle) - .expect("failed to spawn react thread"); + .expect("failed to spawn plugin js thread"); + } + + fn send_command(&self, command: PluginCommand) { + self.command_broadcaster.send(command).unwrap(); } } #[derive(Debug, Deserialize)] struct Config { - readonly_ui: Option, + readonly_ui: Option, // TODO 3 modes: no changes, changes saved to config, changes saved to data plugins: Option>, } @@ -135,7 +286,7 @@ impl PluginLoader { let js_content = std::fs::read_to_string(&dist_path).unwrap(); let id = dist_path.file_stem().unwrap().to_str().unwrap().to_owned(); - (id, js_content) + (id, JsCode::new(js_content)) }) .collect(); @@ -146,78 +297,93 @@ impl PluginLoader { let entrypoints: Vec<_> = package_json.plugin .entrypoints .into_iter() - .map(|entrypoint| PluginEntrypoint::new(entrypoint.id, entrypoint.name, entrypoint.path)) + .map(|entrypoint| PluginEntrypoint::new(EntrypointId::new(entrypoint.id), entrypoint.name, entrypoint.path)) .collect(); - Plugin::new(&plugin.id, &package_json.plugin.metadata.name, PluginCode::new(js), entrypoints) + Plugin::new( + PluginId::new(plugin.id), + &package_json.plugin.metadata.name, + true, + PluginCode::new(js), + entrypoints + ) } } -#[derive(Clone)] pub struct Plugin { - inner: Arc, -} - -pub struct PluginInner { - id: String, + id: PluginId, name: String, + enabled: bool, code: PluginCode, entrypoints: Vec, } impl Plugin { - fn new(id: &str, name: &str, code: PluginCode, entrypoints: Vec) -> Self { + fn new(id: PluginId, name: &str, enabled: bool, code: PluginCode, entrypoints: Vec) -> Self { Self { - inner: Arc::new(PluginInner { - id: id.into(), - name: name.into(), - code, - entrypoints, - }) + id, + name: name.into(), + enabled, + code, + entrypoints, } } - pub fn id(&self) -> &str { - &self.inner.id + pub fn id(&self) -> PluginId { + self.id.clone() } pub fn name(&self) -> &str { - &self.inner.name + &self.name + } + + pub fn enabled(&self) -> bool { + self.enabled } pub fn code(&self) -> &PluginCode { - &self.inner.code + &self.code } pub fn entrypoints(&self) -> &Vec { - &self.inner.entrypoints + &self.entrypoints + } + + pub fn entrypoints_mut(&mut self) -> &mut Vec { + &mut self.entrypoints } } #[derive(Clone)] pub struct PluginEntrypoint { - id: String, + id: EntrypointId, name: String, + enabled: bool, path: String, } impl PluginEntrypoint { - fn new(id: String, name: String, path: String) -> Self { + fn new(id: EntrypointId, name: String, path: String) -> Self { Self { id, name, + enabled: true, // TODO load from config path, } } - pub fn id(&self) -> &str { - &self.id + pub fn id(&self) -> EntrypointId { + self.id.clone() } pub fn name(&self) -> &str { &self.name } + pub fn enabled(&self) -> bool { + self.enabled + } + pub fn path(&self) -> &str { &self.path } @@ -225,17 +391,33 @@ impl PluginEntrypoint { #[derive(Clone)] pub struct PluginCode { - js: HashMap, + js: HashMap, } impl PluginCode { - fn new(js: HashMap) -> Self { + fn new(js: HashMap) -> Self { Self { js, } } - pub fn js(&self) -> &HashMap { + pub fn js(&self) -> &HashMap { &self.js } } + + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct JsCode(Arc); + +impl JsCode { + pub fn new(code: impl ToString) -> Self { + JsCode(code.to_string().into()) + } +} + +impl ToString for JsCode { + fn to_string(&self) -> String { + self.0.to_string() + } +} diff --git a/src/server/search.rs b/src/server/search.rs index 2e6aaeb..28c0b15 100644 --- a/src/server/search.rs +++ b/src/server/search.rs @@ -1,15 +1,13 @@ -use std::thread; - -use tantivy::{doc, Index, IndexReader, IndexWriter, ReloadPolicy, Searcher}; +use tantivy::{doc, Index, IndexReader, ReloadPolicy, Searcher}; use tantivy::collector::TopDocs; use tantivy::query::{AllQuery, BooleanQuery, FuzzyTermQuery, Query}; use tantivy::schema::*; use tantivy::tokenizer::TokenizerManager; +#[derive(Clone)] pub struct SearchIndex { index: Index, index_reader: IndexReader, - index_writer: IndexWriter, entrypoint_name: Field, entrypoint_id: Field, @@ -35,11 +33,8 @@ impl SearchIndex { let plugin_name = schema.get_field("plugin_name").unwrap(); let plugin_id = schema.get_field("plugin_id").unwrap(); - let index = Index::create_in_ram(schema.clone()); - let index_writer = index.writer(50_000_000)?; - let index_reader = index .reader_builder() .reload_policy(ReloadPolicy::OnCommit) @@ -48,7 +43,6 @@ impl SearchIndex { Ok(Self { index, index_reader, - index_writer, entrypoint_name, entrypoint_id, plugin_name, @@ -56,23 +50,24 @@ impl SearchIndex { }) } - pub fn add_entries(&mut self, entries: Vec) -> tantivy::Result<()> { - let index_writer = &mut self.index_writer; + pub fn reload(&mut self, search_items: Vec) -> tantivy::Result<()> { + let mut index_writer = self.index.writer(50_000_000)?; - for entry in entries { + index_writer.delete_all_documents()?; + + println!("{:?}", search_items); + + for search_item in search_items { index_writer.add_document(doc!( - self.entrypoint_name => entry.entrypoint_name, - self.entrypoint_id => entry.entrypoint_id, - self.plugin_name => entry.plugin_name, - self.plugin_id => entry.plugin_id, + self.entrypoint_name => search_item.entrypoint_name, + self.entrypoint_id => search_item.entrypoint_id, + self.plugin_name => search_item.plugin_name, + self.plugin_id => search_item.plugin_id, ))?; } index_writer.commit()?; - thread::sleep(std::time::Duration::from_secs(1)); // FIXME this shouldn't be needed because commit blocks, maybe inmemory index has race condition? - println!("num_docs {:?}", self.index_reader.searcher().num_docs()); // shouldn't return 0 - Ok(()) } diff --git a/test_data/plugin/package.json b/test_data/plugin/package.json index b89e542..db38942 100644 --- a/test_data/plugin/package.json +++ b/test_data/plugin/package.json @@ -25,12 +25,12 @@ }, { "id": "other-view-1", - "name": "Other view", + "name": "Other view 1", "path": "src/other_view.tsx" }, { "id": "other-view-2", - "name": "Other view", + "name": "Other view 2", "path": "src/other_view.tsx" }, {