Plugins can now be enabled or disabled via management ui

This commit is contained in:
Exidex 2023-10-19 19:48:12 +02:00
parent b6c879c3fe
commit 13d9b554cb
14 changed files with 507 additions and 166 deletions

23
Cargo.lock generated
View file

@ -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",

View file

@ -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"] }

View file

@ -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()
})();

View file

@ -25,7 +25,6 @@ pub enum NativeUiResponseData {
CloneInstance {
widget: NativeUiWidget
},
Unit,
}
#[derive(Debug, Clone)]

View file

@ -16,6 +16,7 @@ pub struct DBusSearchResult {
pub struct DBusPlugin {
pub plugin_id: String,
pub plugin_name: String,
pub enabled: bool,
pub entrypoints: Vec<DBusEntrypoint>,
}
@ -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)]

View file

@ -7,5 +7,7 @@ use crate::common::dbus::DBusPlugin;
)]
trait DbusManagementServerProxy {
async fn plugins(&self) -> zbus::Result<Vec<DBusPlugin>>;
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<()>;
}

View file

@ -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<PluginId, Plugin>),
PluginsReloaded(HashMap<PluginId, Plugin>),
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<EntrypointId, Entrypoint>,
}
@ -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<PluginId, Plugin> {
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()
}

View file

@ -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<DBusPlugin> {
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)
}
}

View file

@ -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 };

View file

@ -79,6 +79,10 @@ pub enum JsUiEvent {
#[serde(rename = "eventName")]
event_name: UiEventName,
},
PluginCommand {
#[serde(rename = "commandType")]
command_type: String,
}
}
#[derive(Debug)]

View file

@ -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<PluginCommand>,
}
#[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::<JsUiRequestData, JsUiResponseData>();
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<RefCell<OpState>>,
) -> anyhow::Result<JsUiEvent> {
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()

View file

@ -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<Plugin>,
plugins: HashMap<PluginId, Plugin>,
search_index: SearchIndex,
command_broadcaster: tokio::sync::broadcast::Sender<PluginCommand>,
}
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::<PluginCommand>(100);
Self {
inner: Arc::new(RwLock::new(PluginManagerInner {
plugins,
}))
search_index,
command_broadcaster: tx
})),
}
}
pub fn plugins(&self) -> Vec<Plugin> {
self.inner.read().unwrap().plugins.clone()
pub fn plugins(&self) -> Vec<DBusPlugin> {
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<bool>,
readonly_ui: Option<bool>, // TODO 3 modes: no changes, changes saved to config, changes saved to data
plugins: Option<Vec<PluginConfig>>,
}
@ -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<PluginInner>,
}
pub struct PluginInner {
id: String,
id: PluginId,
name: String,
enabled: bool,
code: PluginCode,
entrypoints: Vec<PluginEntrypoint>,
}
impl Plugin {
fn new(id: &str, name: &str, code: PluginCode, entrypoints: Vec<PluginEntrypoint>) -> Self {
fn new(id: PluginId, name: &str, enabled: bool, code: PluginCode, entrypoints: Vec<PluginEntrypoint>) -> 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<PluginEntrypoint> {
&self.inner.entrypoints
&self.entrypoints
}
pub fn entrypoints_mut(&mut self) -> &mut Vec<PluginEntrypoint> {
&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<String, String>,
js: HashMap<String, JsCode>,
}
impl PluginCode {
fn new(js: HashMap<String, String>) -> Self {
fn new(js: HashMap<String, JsCode>) -> Self {
Self {
js,
}
}
pub fn js(&self) -> &HashMap<String, String> {
pub fn js(&self) -> &HashMap<String, JsCode> {
&self.js
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct JsCode(Arc<str>);
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()
}
}

View file

@ -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<SearchItem>) -> tantivy::Result<()> {
let index_writer = &mut self.index_writer;
pub fn reload(&mut self, search_items: Vec<SearchItem>) -> 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(())
}

View file

@ -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"
},
{