diff --git a/Cargo.lock b/Cargo.lock index 4846692..a3c75c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7305,6 +7305,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tokio-util", "toml 0.8.12", "tonic", "tracing", @@ -8904,16 +8905,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index 1ece6a9..3e4ed73 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -68,6 +68,17 @@ export default function DetailView(): ReactElement { }; }, []); + // promise that takes a long time to resolve + // to test that plugin can be stopped even though there is a pending promise somewhere + new Promise(resolve => { + setTimeout( + () => { + resolve("Promise resolved after 10 minutes!"); + }, + 10 * 60 * 1000 + ); + }) + return ( Command { - tracing::info!("on_focused"); self.focused = true; Command::none() } fn on_unfocused(&mut self) -> Command { - tracing::info!("on_unfocused: {}", self.focused); // for some reason (on both macos and linux x11) duplicate Unfocused fires right before Focus event if self.focused { self.focused = false; diff --git a/rust/server/Cargo.toml b/rust/server/Cargo.toml index 6c4bbd5..830a15e 100644 --- a/rust/server/Cargo.toml +++ b/rust/server/Cargo.toml @@ -7,6 +7,7 @@ serde = { version = "1.0", features = ["derive"] } deno_core = { version = "0.204.0" } deno_runtime = { version = "0.126.0" } tokio = "1.28.1" +tokio-util = "0.7.11" toml = "0.8.10" tantivy = "0.20.2" zstd-sys = "=2.0.9" # TODO REMOVE https://github.com/gyscos/zstd-rs/issues/270 diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index cff77d6..a14ad64 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -73,7 +73,6 @@ pub enum JsUiEvent { #[serde(rename = "modifierMeta")] modifier_meta: bool }, - StopPlugin, OpenInlineView { #[serde(rename = "text")] text: String, @@ -134,7 +133,6 @@ pub enum IntermediateUiEvent { modifier_alt: bool, modifier_meta: bool }, - StopPlugin, OpenInlineView { text: String, }, diff --git a/rust/server/src/plugins/js/mod.rs b/rust/server/src/plugins/js/mod.rs index 7a4abeb..b92fdf9 100644 --- a/rust/server/src/plugins/js/mod.rs +++ b/rust/server/src/plugins/js/mod.rs @@ -1,12 +1,3 @@ -mod ui; -mod plugins; -mod logs; -mod assets; -mod preferences; -mod search; -mod command_generators; -mod clipboard; - use std::cell::RefCell; use std::collections::HashMap; use std::fs::File; @@ -27,25 +18,26 @@ use deno_runtime::permissions::{Permissions, PermissionsContainer, PermissionsOp use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; use indexmap::IndexMap; - use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; use tokio::net::TcpStream; +use tokio_util::sync::CancellationToken; -use common::model::{EntrypointId, PluginId, UiRenderLocation, UiPropertyValue, UiWidget, UiWidgetId, SearchResultEntrypointType, PhysicalKey}; +use common::dirs::Dirs; +use common::model::{EntrypointId, PhysicalKey, PluginId, SearchResultEntrypointType, UiPropertyValue, UiRenderLocation, UiWidget, UiWidgetId}; use common::rpc::frontend_api::FrontendApi; use component_model::{Children, Component, create_component_model, Property, PropertyType, SharedType}; -use common::dirs::Dirs; -use crate::model::{IntermediateUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiEvent, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; + +use crate::model::{IntermediateUiEvent, JsUiEvent, JsUiPropertyValue, JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData}; use crate::plugins::applications::{DesktopEntry, get_apps}; use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; use crate::plugins::icon_cache::IconCache; -use crate::plugins::js::plugins::applications::{list_applications, open_application}; use crate::plugins::js::assets::{asset_data, asset_data_blocking}; use crate::plugins::js::clipboard::{clipboard_clear, clipboard_read, clipboard_read_text, clipboard_write, clipboard_write_text}; use crate::plugins::js::command_generators::get_command_generator_entrypoint_ids; use crate::plugins::js::logs::{op_log_debug, op_log_error, op_log_info, op_log_trace, op_log_warn}; +use crate::plugins::js::plugins::applications::{list_applications, open_application}; use crate::plugins::js::plugins::numbat::run_numbat; use crate::plugins::js::plugins::settings::open_settings; use crate::plugins::js::preferences::{entrypoint_preferences_required, get_entrypoint_preferences, get_plugin_preferences, plugin_preferences_required}; @@ -54,6 +46,15 @@ use crate::plugins::js::ui::{clear_inline_view, fetch_action_id_for_shortcut, op use crate::plugins::run_status::RunStatusGuard; use crate::search::{SearchIndex, SearchIndexItem}; +mod ui; +mod plugins; +mod logs; +mod assets; +mod preferences; +mod search; +mod command_generators; +mod clipboard; + pub struct PluginRuntimeData { pub id: PluginId, pub uuid: String, @@ -96,7 +97,6 @@ pub enum PluginCommand { #[derive(Clone, Debug)] pub enum OnePluginCommandData { - Stop, RenderView { entrypoint_id: EntrypointId, }, @@ -151,9 +151,6 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run None } else { match data { - OnePluginCommandData::Stop => { - Some(IntermediateUiEvent::StopPlugin) - } OnePluginCommandData::RenderView { entrypoint_id } => { Some(IntermediateUiEvent::OpenView { entrypoint_id, @@ -211,42 +208,55 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run let event_stream = Box::pin(event_stream); - let thread_fn = move || { - let _run_status_guard = run_status_guard; + let cache = data.icon_cache.clone(); + let plugin_uuid = data.uuid.clone(); + let plugin_id = data.id.clone(); - let result_plugin_id = data.id.clone(); - let result = tokio::runtime::Builder::new_current_thread() + let thread_fn = move || { + let plugin_id = data.id.clone(); + + tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("unable to start tokio runtime for plugin") - .block_on(tokio::task::unconstrained(async move { - let result_plugin_id = data.id.clone(); - let result = start_js_runtime( - data.id, - data.uuid.clone(), - data.code, - data.permissions, - data.inline_view_entrypoint_id, - event_stream, - data.frontend_api, - component_model, - data.db_repository, - data.search_index, - data.icon_cache.clone(), - data.dirs.clone() - ).await; + .block_on({ + let plugin_id = data.id.clone(); - if let Err(err) = data.icon_cache.clear_plugin_icon_cache_dir(&data.uuid) { - tracing::error!(target = "plugin", "plugin {:?} unable to cleanup icon cache {:?}", result_plugin_id, err) + async move { + tokio::select! { + _ = run_status_guard.stopped() => { + tracing::info!(target = "plugin", "Plugin runtime has been stopped {:?}", plugin_id) + } + result @ _ = { + tokio::task::unconstrained(async move { + start_js_runtime( + data.id.clone(), + data.uuid, + data.code, + data.permissions, + data.inline_view_entrypoint_id, + event_stream, + data.frontend_api, + component_model, + data.db_repository, + data.search_index, + data.icon_cache, + data.dirs + ).await + }) + } => { + if let Err(err) = result { + tracing::error!(target = "plugin", "Plugin runtime has failed {:?} - {:?}", plugin_id, err) + } else { + tracing::error!(target = "plugin", "Plugin runtime has stopped unexpectedly {:?}", plugin_id) + } + } + } } + }); - result - })); - - if let Err(err) = result { - tracing::error!(target = "plugin", "Plugin runtime failed {:?} - {:?}", result_plugin_id, err) - } else { - tracing::info!(target = "plugin", "Plugin runtime stopped {:?}", result_plugin_id) + if let Err(err) = cache.clear_plugin_icon_cache_dir(&plugin_uuid) { + tracing::error!(target = "plugin", "plugin {:?} unable to cleanup icon cache {:?}", plugin_id, err) } }; @@ -647,7 +657,6 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { modifier_meta } } - IntermediateUiEvent::StopPlugin => JsUiEvent::StopPlugin, IntermediateUiEvent::OpenInlineView { text } => JsUiEvent::OpenInlineView { text }, IntermediateUiEvent::ReloadSearchIndex => JsUiEvent::ReloadSearchIndex, } diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 5ba9c26..7420617 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -578,12 +578,7 @@ impl ApplicationManager { async fn stop_plugin(&self, plugin_id: PluginId) { tracing::info!(target = "plugin", "Stopping plugin with id: {:?}", plugin_id); - let data = PluginCommand::One { - id: plugin_id, - data: OnePluginCommandData::Stop, - }; - - self.send_command(data) + self.run_status_holder.stop_plugin(&plugin_id) } fn start_plugin_runtime(&self, data: PluginRuntimeData) { diff --git a/rust/server/src/plugins/run_status.rs b/rust/server/src/plugins/run_status.rs index a0444b2..dc83128 100644 --- a/rust/server/src/plugins/run_status.rs +++ b/rust/server/src/plugins/run_status.rs @@ -1,22 +1,24 @@ -use std::collections::HashSet; +use std::collections::HashMap; use std::sync::{Arc, Mutex}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; + use common::model::PluginId; pub struct RunStatusHolder { - running_plugins: Arc>> + running_plugins: Arc>> } impl RunStatusHolder { pub fn new() -> Self { Self { - running_plugins: Arc::new(Mutex::new(HashSet::new())) + running_plugins: Arc::new(Mutex::new(HashMap::new())) } } pub fn start_block(&self, plugin_id: PluginId) -> RunStatusGuard { let mut running_plugins = self.running_plugins.lock().expect("lock is poisoned"); - running_plugins.insert(plugin_id.clone()); + running_plugins.insert(plugin_id.clone(), CancellationToken::new()); RunStatusGuard { running_plugins: self.running_plugins.clone(), id: plugin_id, @@ -25,13 +27,34 @@ impl RunStatusHolder { pub fn is_plugin_running(&self, plugin_id: &PluginId) -> bool { let running_plugins = self.running_plugins.lock().expect("lock is poisoned"); - running_plugins.contains(plugin_id) + running_plugins.contains_key(plugin_id) + } + + pub fn stop_plugin(&self, plugin_id: &PluginId) { + let mut running_plugins = self.running_plugins.lock().expect("lock is poisoned"); + + running_plugins + .get(plugin_id) + .expect("value should always exist for specified id") + .cancel() } } pub struct RunStatusGuard { id: PluginId, - running_plugins: Arc>>, + running_plugins: Arc>>, +} + +impl RunStatusGuard { + pub fn stopped(&self) -> WaitForCancellationFutureOwned { + let mut running_plugins = self.running_plugins.lock().expect("lock is poisoned"); + + running_plugins + .get(&self.id) + .expect("value should always exist for specified id") + .clone() + .cancelled_owned() + } } impl Drop for RunStatusGuard {