From d461c982ecdef5ac7e3f06713aee2c1655e26f06 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 1 Sep 2026 09:14:50 -0700 Subject: [PATCH] Don't block window creation on plugin runtime boot (#616) --- crates-tauri/yaak-app-client/src/grpc.rs | 5 +- .../yaak-app-client/src/http_request.rs | 3 +- crates-tauri/yaak-app-client/src/import.rs | 3 +- crates-tauri/yaak-app-client/src/lib.rs | 24 +++-- .../yaak-app-client/src/plugin_events.rs | 7 +- .../yaak-app-client/src/plugins_ext.rs | 95 +++++++++++++++---- crates-tauri/yaak-app-client/src/rpc_ext.rs | 81 +++++++++------- crates-tauri/yaak-app-client/src/updates.rs | 6 +- .../yaak-app-client/src/uri_scheme.rs | 3 +- crates-tauri/yaak-app-client/src/ws_ext.rs | 6 +- crates/yaak-commands/src/host.rs | 7 +- crates/yaak-commands/src/render.rs | 2 +- crates/yaak-commands/src/templates.rs | 4 +- crates/yaak-commands/tests/test_host.rs | 7 +- crates/yaak-plugins/src/manager.rs | 35 +++---- 15 files changed, 183 insertions(+), 105 deletions(-) diff --git a/crates-tauri/yaak-app-client/src/grpc.rs b/crates-tauri/yaak-app-client/src/grpc.rs index 0f95403a..52add513 100644 --- a/crates-tauri/yaak-app-client/src/grpc.rs +++ b/crates-tauri/yaak-app-client/src/grpc.rs @@ -3,11 +3,10 @@ use std::collections::BTreeMap; use crate::PluginContextExt; use crate::error::Result; use KeyAndValueRef::{Ascii, Binary}; -use tauri::{Manager, Runtime, WebviewWindow}; +use tauri::{Runtime, WebviewWindow}; use yaak_grpc::{KeyAndValueRef, MetadataMap}; use yaak_models::models::GrpcRequest; use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader}; -use yaak_plugins::manager::PluginManager; pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap { let mut entries = BTreeMap::new(); @@ -26,7 +25,7 @@ pub(crate) async fn build_metadata( request: &GrpcRequest, authentication_context_id: &str, ) -> Result> { - let plugin_manager = window.state::(); + let plugin_manager = crate::plugins_ext::plugin_manager(window).await?; let mut metadata = BTreeMap::new(); // Add the rest of metadata diff --git a/crates-tauri/yaak-app-client/src/http_request.rs b/crates-tauri/yaak-app-client/src/http_request.rs index efa6cfbc..6e010d9a 100644 --- a/crates-tauri/yaak-app-client/src/http_request.rs +++ b/crates-tauri/yaak-app-client/src/http_request.rs @@ -14,7 +14,6 @@ use yaak_http::manager::HttpConnectionManager; use yaak_models::models::{CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseState}; use yaak_models::util::UpdateSource; use yaak_plugins::events::PluginContext; -use yaak_plugins::manager::PluginManager; /// Context for managing response state during HTTP transactions. /// Handles both persisted responses (stored in DB) and ephemeral responses (in-memory only). @@ -149,7 +148,7 @@ async fn send_http_request_inner( response_ctx: &mut ResponseContext, ) -> Result { let app_handle = window.app_handle().clone(); - let plugin_manager = Arc::new((*app_handle.state::()).clone()); + let plugin_manager = Arc::new(crate::plugins_ext::plugin_manager(&app_handle).await?); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let connection_manager = app_handle.state::(); let environment_id = environment.map(|e| e.id); diff --git a/crates-tauri/yaak-app-client/src/import.rs b/crates-tauri/yaak-app-client/src/import.rs index 1da6d0f5..f7c0bbfd 100644 --- a/crates-tauri/yaak-app-client/src/import.rs +++ b/crates-tauri/yaak-app-client/src/import.rs @@ -7,7 +7,6 @@ use tauri::{Manager, Runtime, WebviewWindow}; use yaak::import::{self, PlanImportDataParams}; use yaak_api::{ApiClientKind, yaak_api_client}; use yaak_models::util::{BatchUpsertResult, ImportDestination, ImportPlan}; -use yaak_plugins::manager::PluginManager; pub(crate) async fn import_data( window: &WebviewWindow, @@ -40,7 +39,7 @@ async fn plan_import_contents( contents: &str, destination: ImportDestination, ) -> Result { - let plugin_manager = window.state::(); + let plugin_manager = crate::plugins_ext::plugin_manager(window).await?; let query_manager = window.db_manager(); let plugin_context = window.plugin_context(); diff --git a/crates-tauri/yaak-app-client/src/lib.rs b/crates-tauri/yaak-app-client/src/lib.rs index 4b35864d..dec37c7f 100644 --- a/crates-tauri/yaak-app-client/src/lib.rs +++ b/crates-tauri/yaak-app-client/src/lib.rs @@ -45,7 +45,6 @@ use yaak_plugins::events::{ Color, ErrorResponse, FilterResponse, InternalEvent, InternalEventPayload, PluginContext, RenderPurpose, ShowToastRequest, }; -use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_rpc_schema::{AppMetaData, EphemeralHttpResponse}; use yaak_sse::sse::ServerSentEvent; @@ -250,7 +249,7 @@ async fn cmd_grpc_reflect( let resolved_settings = app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?; - let plugin_manager = Arc::new((*app_handle.state::()).clone()); + let plugin_manager = Arc::new(crate::plugins_ext::plugin_manager(&app_handle).await?); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let req = render_grpc_request( &resolved_request, @@ -310,7 +309,7 @@ async fn cmd_grpc_go( let resolved_settings = app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?; - let plugin_manager = Arc::new((*app_handle.state::()).clone()); + let plugin_manager = Arc::new(crate::plugins_ext::plugin_manager(&app_handle).await?); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let request = render_grpc_request( &resolved_request, @@ -962,7 +961,6 @@ async fn cmd_format_graphql(text: &str) -> YaakResult { async fn cmd_http_response_body( window: WebviewWindow, - plugin_manager: State<'_, PluginManager>, response_id: &str, filter: Option<&str>, ) -> YaakResult { @@ -977,7 +975,8 @@ async fn cmd_http_response_body( .ok_or(GenericError("Failed to find response body".to_string()))?; match filter { - Some(filter) if !filter.is_empty() => Ok(plugin_manager + Some(filter) if !filter.is_empty() => Ok(plugins_ext::plugin_manager(&window) + .await? .filter_data(&window.plugin_context(), filter, &body, content_type) .await?), _ => Ok(FilterResponse { content: body, error: None }), @@ -1450,7 +1449,10 @@ fn safe_uri(endpoint: &str) -> String { fn monitor_plugin_events(app_handle: &AppHandle) { let app_handle = app_handle.clone(); tauri::async_runtime::spawn(async move { - let plugin_manager: State<'_, PluginManager> = app_handle.state(); + let plugin_manager = match plugins_ext::plugin_manager(&app_handle).await { + Ok(pm) => pm, + Err(_) => return, // The runtime failed to boot; there are no events + }; let (rx_id, mut rx) = plugin_manager.subscribe("app").await; while let Some(event) = rx.recv().await { @@ -1491,9 +1493,13 @@ fn monitor_plugin_events(app_handle: &AppHandle) { } }; - let plugin_manager: State<'_, PluginManager> = app_handle.state(); - if let Err(e) = plugin_manager.reply(&event, &ev).await { - warn!("Failed to reply to plugin manager: {:?}", e) + match plugins_ext::plugin_manager(&app_handle).await { + Ok(pm) => { + if let Err(e) = pm.reply(&event, &ev).await { + warn!("Failed to reply to plugin manager: {:?}", e) + } + } + Err(e) => warn!("Failed to get plugin manager for reply: {e:?}"), } }); } diff --git a/crates-tauri/yaak-app-client/src/plugin_events.rs b/crates-tauri/yaak-app-client/src/plugin_events.rs index 3049a2a2..9071313c 100644 --- a/crates-tauri/yaak-app-client/src/plugin_events.rs +++ b/crates-tauri/yaak-app-client/src/plugin_events.rs @@ -32,7 +32,6 @@ use yaak_plugins::events::{ ShowToastRequest, TemplateRenderResponse, WindowInfoResponse, WindowNavigateEvent, WorkspaceInfo, }; -use yaak_plugins::manager::PluginManager; use yaak_plugins::plugin_handle::PluginHandle; use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_tauri_utils::window::WorkspaceWindowTrait; @@ -205,7 +204,7 @@ async fn handle_host_plugin_request( req.grpc_request.folder_id.as_deref(), environment_id.as_deref(), )?; - let plugin_manager = Arc::new((*app_handle.state::()).clone()); + let plugin_manager = Arc::new(crate::plugins_ext::plugin_manager(app_handle).await?); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let cb = PluginTemplateCallback::new( plugin_manager, @@ -231,7 +230,7 @@ async fn handle_host_plugin_request( req.http_request.folder_id.as_deref(), environment_id.as_deref(), )?; - let plugin_manager = Arc::new((*app_handle.state::()).clone()); + let plugin_manager = Arc::new(crate::plugins_ext::plugin_manager(app_handle).await?); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let cb = PluginTemplateCallback::new( plugin_manager, @@ -267,7 +266,7 @@ async fn handle_host_plugin_request( folder_id.as_deref(), environment_id.as_deref(), )?; - let plugin_manager = Arc::new((*app_handle.state::()).clone()); + let plugin_manager = Arc::new(crate::plugins_ext::plugin_manager(app_handle).await?); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let cb = PluginTemplateCallback::new( plugin_manager, diff --git a/crates-tauri/yaak-app-client/src/plugins_ext.rs b/crates-tauri/yaak-app-client/src/plugins_ext.rs index a9a5d246..bff3c6ed 100644 --- a/crates-tauri/yaak-app-client/src/plugins_ext.rs +++ b/crates-tauri/yaak-app-client/src/plugins_ext.rs @@ -31,10 +31,43 @@ use yaak_plugins::api::{ use yaak_plugins::events::{Color, PluginContext, ShowToastRequest}; use yaak_plugins::install::{delete_and_uninstall, download_and_install}; use yaak_plugins::manager::PluginManager; +use yaak_plugins::error::Error::PluginErr; use yaak_plugins::plugin_meta::get_plugin_meta; static EXITING: AtomicBool = AtomicBool::new(false); +// ============================================================================ +// Plugin Manager Handle +// ============================================================================ + +/// The plugin runtime boots in the background so startup doesn't wait on it. +/// This handle is the only way to reach the manager: [`PluginManagerHandle::get`] +/// resolves once boot completes, so callers can never observe a +/// partially-initialized runtime. +#[derive(Clone)] +pub struct PluginManagerHandle { + rx: tokio::sync::watch::Receiver>>, +} + +impl PluginManagerHandle { + pub async fn get(&self) -> yaak_plugins::error::Result { + let mut rx = self.rx.clone(); + let result = rx + .wait_for(|v| v.is_some()) + .await + .map_err(|_| PluginErr("Plugin runtime boot task died".to_string()))?; + result.clone().unwrap().map_err(PluginErr) + } +} + +/// Wait for the plugin runtime to finish booting and return the manager. +pub async fn plugin_manager( + manager: &impl Manager, +) -> yaak_plugins::error::Result { + let handle = manager.state::().inner().clone(); + handle.get().await +} + // ============================================================================ // Plugin Updater // ============================================================================ @@ -146,7 +179,7 @@ pub async fn cmd_plugins_install( name: &str, version: Option, ) -> Result<()> { - let plugin_manager = Arc::new((*window.state::()).clone()); + let plugin_manager = Arc::new(plugin_manager(&window).await?); let app_version = window.app_handle().package_info().version.to_string(); let http_client = yaak_api_client(ApiClientKind::App, &app_version)?; let query_manager = window.state::(); @@ -167,6 +200,9 @@ pub async fn cmd_plugins_install_from_directory( window: WebviewWindow, directory: &str, ) -> Result { + // Resolve the manager before writing the row so startup's plugin snapshot + // can't include it and boot it a second time + let plugin_manager = Arc::new(plugin_manager(&window).await?); let plugin = window.db().upsert_plugin( &Plugin { directory: directory.into(), @@ -178,7 +214,6 @@ pub async fn cmd_plugins_install_from_directory( &UpdateSource::from_window_label(window.label()), )?; - let plugin_manager = Arc::new((*window.state::()).clone()); plugin_manager.add_plugin(&window.plugin_context(), &plugin).await?; Ok(plugin) @@ -188,7 +223,7 @@ pub async fn cmd_plugins_uninstall( plugin_id: &str, window: WebviewWindow, ) -> Result { - let plugin_manager = Arc::new((*window.state::()).clone()); + let plugin_manager = Arc::new(plugin_manager(&window).await?); let query_manager = window.state::(); let plugin_context = window.plugin_context(); Ok(delete_and_uninstall(plugin_manager, &query_manager, &plugin_context, plugin_id).await?) @@ -217,7 +252,7 @@ pub async fn cmd_plugins_update_all( return Ok(Vec::new()); } - let plugin_manager = Arc::new((*window.state::()).clone()); + let plugin_manager = Arc::new(plugin_manager(&window).await?); let query_manager = window.state::(); let plugin_context = window.plugin_context(); @@ -300,20 +335,38 @@ pub fn init() -> TauriPlugin { let query_manager = app_handle.state::().inner().clone(); - // Create plugin manager asynchronously + // Boot the plugin runtime in the background so the window shows + // immediately. Everything that needs plugins resolves the handle, + // which waits for this task to finish. + let (tx, rx) = tokio::sync::watch::channel(None); + app_handle.manage(PluginManagerHandle { rx }); let app_handle_clone = app_handle.clone(); - tauri::async_runtime::block_on(async move { - let manager = PluginManager::new( - vendored_plugin_dir, - installed_plugin_dir, - node_bin_path, - plugin_runtime_main, - &query_manager, - &PluginContext::new_empty(), - dev_mode, + tauri::async_runtime::spawn(async move { + let result = tokio::time::timeout( + Duration::from_secs(60), + PluginManager::new( + vendored_plugin_dir, + installed_plugin_dir, + node_bin_path, + plugin_runtime_main, + &query_manager, + &PluginContext::new_empty(), + dev_mode, + ), ) .await - .expect("Failed to start plugin runtime"); + .unwrap_or_else(|_| Err(yaak_plugins::error::Error::PluginErr( + "Timed out starting the plugin runtime".to_string(), + ))); + + let manager = match result { + Ok(manager) => manager, + Err(e) => { + error!("Failed to start plugin runtime: {e:?}"); + let _ = tx.send(Some(Err(e.to_string()))); + return; + } + }; // Surface unexpected runtime crashes to the user let mut crash_rx = manager.runtime_crash_rx(); @@ -339,7 +392,7 @@ pub fn init() -> TauriPlugin { } }); - app_handle_clone.manage(manager); + let _ = tx.send(Some(Ok(manager))); }); let plugin_updater = PluginUpdater::new(); @@ -355,8 +408,14 @@ pub fn init() -> TauriPlugin { api.prevent_exit(); tauri::async_runtime::block_on(async move { info!("Exiting plugin runtime due to app exit"); - let manager: State = app.state(); - manager.terminate().await; + // Bound the wait in case the exit comes while boot is still + // in flight + let get_manager = plugin_manager(app); + if let Ok(Ok(manager)) = + tokio::time::timeout(Duration::from_secs(5), get_manager).await + { + manager.terminate().await; + } app.exit(0); }); } diff --git a/crates-tauri/yaak-app-client/src/rpc_ext.rs b/crates-tauri/yaak-app-client/src/rpc_ext.rs index ee9a7434..5aa8ceb7 100644 --- a/crates-tauri/yaak-app-client/src/rpc_ext.rs +++ b/crates-tauri/yaak-app-client/src/rpc_ext.rs @@ -109,10 +109,11 @@ impl Host for ClientCtx { } impl ClientCtx { - /// The plugin runtime this window talks to. Only the `PluginHost` impl - /// below uses it; everything else goes through the trait. - fn pm(&self) -> State<'_, PluginManager> { - self.window.state::() + /// The plugin runtime this window talks to, once it finishes booting. + /// Only the `PluginHost` impl below uses it; everything else goes through + /// the trait. + async fn pm(&self) -> yaak_plugins::error::Result { + crate::plugins_ext::plugin_manager(&self.window).await } } @@ -122,35 +123,40 @@ impl ClientCtx { /// providing them. impl PluginHost for ClientCtx { async fn loaded_plugin_metadata(&self, directory: &str) -> Option { - let handle = self.pm().get_plugin_by_dir(directory).await?; + let handle = self.pm().await.ok()?.get_plugin_by_dir(directory).await?; Some(handle.info()) } async fn take_plugin_init_errors(&self) -> Vec<(String, String)> { - self.pm().take_init_errors().await + match self.pm().await { + Ok(pm) => pm.take_init_errors().await, + Err(_) => Vec::new(), + } } async fn resolve_plugins(&self, plugins: Vec) -> Vec { - self.pm().resolve_plugins_for_runtime_from_db(plugins).await + match self.pm().await { + Ok(pm) => pm.resolve_plugins_for_runtime_from_db(plugins).await, + Err(_) => plugins, + } } - fn template_callback(&self, purpose: RenderPurpose) -> impl TemplateCallback { - PluginTemplateCallback::new( - Arc::new((*self.pm()).clone()), + async fn template_callback( + &self, + purpose: RenderPurpose, + ) -> yaak_commands::Result { + Ok(PluginTemplateCallback::new( + Arc::new(self.pm().await?), Arc::new(self.encryption_manager().clone()), &self.plugin_context(), purpose, - ) + )) } async fn template_function_summaries( &self, ) -> yaak_commands::Result> { - Ok(self - .window - .state::() - .get_template_function_summaries(&self.plugin_context()) - .await?) + Ok(self.pm().await?.get_template_function_summaries(&self.plugin_context()).await?) } async fn template_function_config( @@ -160,81 +166,81 @@ impl PluginHost for ClientCtx { model_id: &str, ) -> yaak_commands::Result { Ok(self - .window - .state::() + .pm() + .await? .get_template_function_config(&self.plugin_context(), function_name, values, model_id) .await?) } async fn themes(&self) -> yaak_commands::Result> { - Ok(self.pm().get_themes(&self.plugin_context()).await?) + Ok(self.pm().await?.get_themes(&self.plugin_context()).await?) } async fn http_request_actions( &self, ) -> yaak_commands::Result> { - Ok(self.pm().get_http_request_actions(&self.plugin_context()).await?) + Ok(self.pm().await?.get_http_request_actions(&self.plugin_context()).await?) } async fn websocket_request_actions( &self, ) -> yaak_commands::Result> { - Ok(self.pm().get_websocket_request_actions(&self.plugin_context()).await?) + Ok(self.pm().await?.get_websocket_request_actions(&self.plugin_context()).await?) } async fn grpc_request_actions( &self, ) -> yaak_commands::Result> { - Ok(self.pm().get_grpc_request_actions(&self.plugin_context()).await?) + Ok(self.pm().await?.get_grpc_request_actions(&self.plugin_context()).await?) } async fn workspace_actions(&self) -> yaak_commands::Result> { - Ok(self.pm().get_workspace_actions(&self.plugin_context()).await?) + Ok(self.pm().await?.get_workspace_actions(&self.plugin_context()).await?) } async fn folder_actions(&self) -> yaak_commands::Result> { - Ok(self.pm().get_folder_actions(&self.plugin_context()).await?) + Ok(self.pm().await?.get_folder_actions(&self.plugin_context()).await?) } async fn call_http_request_action( &self, req: CallHttpRequestActionRequest, ) -> yaak_commands::Result<()> { - Ok(self.pm().call_http_request_action(&self.plugin_context(), req).await?) + Ok(self.pm().await?.call_http_request_action(&self.plugin_context(), req).await?) } async fn call_grpc_request_action( &self, req: CallGrpcRequestActionRequest, ) -> yaak_commands::Result<()> { - Ok(self.pm().call_grpc_request_action(&self.plugin_context(), req).await?) + Ok(self.pm().await?.call_grpc_request_action(&self.plugin_context(), req).await?) } async fn call_websocket_request_action( &self, req: CallWebsocketRequestActionRequest, ) -> yaak_commands::Result<()> { - Ok(self.pm().call_websocket_request_action(&self.plugin_context(), req).await?) + Ok(self.pm().await?.call_websocket_request_action(&self.plugin_context(), req).await?) } async fn call_workspace_action( &self, req: CallWorkspaceActionRequest, ) -> yaak_commands::Result<()> { - Ok(self.pm().call_workspace_action(&self.plugin_context(), req).await?) + Ok(self.pm().await?.call_workspace_action(&self.plugin_context(), req).await?) } async fn call_folder_action( &self, req: CallFolderActionRequest, ) -> yaak_commands::Result<()> { - Ok(self.pm().call_folder_action(&self.plugin_context(), req).await?) + Ok(self.pm().await?.call_folder_action(&self.plugin_context(), req).await?) } async fn http_authentication_summaries( &self, ) -> yaak_commands::Result> { - let results = self.pm().get_http_authentication_summaries(&self.plugin_context()).await?; + let results = self.pm().await?.get_http_authentication_summaries(&self.plugin_context()).await?; Ok(results.into_iter().map(|(_, a)| a).collect()) } @@ -246,6 +252,7 @@ impl PluginHost for ClientCtx { ) -> yaak_commands::Result { Ok(self .pm() + .await? .get_http_authentication_config(&self.plugin_context(), auth_name, values, model_id) .await?) } @@ -259,6 +266,7 @@ impl PluginHost for ClientCtx { ) -> yaak_commands::Result<()> { Ok(self .pm() + .await? .call_http_authentication_action( &self.plugin_context(), auth_name, @@ -270,15 +278,18 @@ impl PluginHost for ClientCtx { } async fn import_data(&self, content: &str) -> yaak_commands::Result { - Ok(self.pm().import_data(&self.plugin_context(), content).await?) + Ok(self.pm().await?.import_data(&self.plugin_context(), content).await?) } async fn reload_plugins(&self, plugins: Vec) -> Vec<(String, String)> { - self.pm().initialize_all_plugins(plugins, &self.plugin_context()).await + match self.pm().await { + Ok(pm) => pm.initialize_all_plugins(plugins, &self.plugin_context()).await, + Err(e) => vec![("*".to_string(), e.to_string())], + } } async fn encrypt_secure_template(&self, template: &str) -> yaak_commands::Result { - let plugin_manager = Arc::new((*self.pm()).clone()); + let plugin_manager = Arc::new(self.pm().await?); let encryption_manager = Arc::new(self.encryption_manager().clone()); Ok(encrypt_secure_template_function( plugin_manager, @@ -422,7 +433,7 @@ async fn cmd_format_graphql(_ctx: ClientCtx, req: CmdFormatGraphq } async fn cmd_http_response_body(ctx: ClientCtx, req: CmdHttpResponseBodyReq) -> Result { - Ok(crate::cmd_http_response_body(ctx.window.clone(), ctx.window.app_handle().state::(), &req.response_id, req.filter.as_deref()).await?) + Ok(crate::cmd_http_response_body(ctx.window.clone(), &req.response_id, req.filter.as_deref()).await?) } async fn cmd_http_response_body_path(ctx: ClientCtx, req: CmdHttpResponseBodyPathReq) -> Result> { @@ -817,7 +828,7 @@ async fn cmd_ws_close(ctx: ClientCtx, req: CmdWsCloseReq) -> Resu } async fn cmd_ws_connect(ctx: ClientCtx, req: CmdWsConnectReq) -> Result { - Ok(crate::ws_ext::cmd_ws_connect(&req.request_id, req.environment_id.as_deref(), req.cookie_jar_id.as_deref(), ctx.window.app_handle().clone(), ctx.window.clone(), ctx.window.app_handle().state::(), ctx.window.app_handle().state::>()).await?) + Ok(crate::ws_ext::cmd_ws_connect(&req.request_id, req.environment_id.as_deref(), req.cookie_jar_id.as_deref(), ctx.window.app_handle().clone(), ctx.window.clone(), ctx.window.app_handle().state::>()).await?) } async fn cmd_plugins_search(ctx: ClientCtx, req: CmdPluginsSearchReq) -> Result { diff --git a/crates-tauri/yaak-app-client/src/updates.rs b/crates-tauri/yaak-app-client/src/updates.rs index 9f771607..ffef3a00 100644 --- a/crates-tauri/yaak-app-client/src/updates.rs +++ b/crates-tauri/yaak-app-client/src/updates.rs @@ -14,7 +14,6 @@ use tokio::task::block_in_place; use tokio::time::sleep; use ts_rs::TS; use yaak_models::util::generate_id; -use yaak_plugins::manager::PluginManager; use url::Url; use yaak_api::get_system_proxy_url; @@ -98,8 +97,9 @@ impl YaakUpdater { block_in_place(|| { tauri::async_runtime::block_on(async move { info!("Shutting down plugin manager before update"); - let plugin_manager = w.state::(); - plugin_manager.terminate().await; + if let Ok(plugin_manager) = crate::plugins_ext::plugin_manager(&w).await { + plugin_manager.terminate().await; + } }); }); }) diff --git a/crates-tauri/yaak-app-client/src/uri_scheme.rs b/crates-tauri/yaak-app-client/src/uri_scheme.rs index d540afff..874ad2cd 100644 --- a/crates-tauri/yaak-app-client/src/uri_scheme.rs +++ b/crates-tauri/yaak-app-client/src/uri_scheme.rs @@ -12,7 +12,6 @@ use yaak_api::{ApiClientKind, yaak_api_client}; use yaak_models::util::generate_id; use yaak_plugins::events::{Color, ShowToastRequest}; use yaak_plugins::install::download_and_install; -use yaak_plugins::manager::PluginManager; pub(crate) async fn handle_deep_link( app_handle: &AppHandle, @@ -44,7 +43,7 @@ pub(crate) async fn handle_deep_link( return Ok(()); } - let plugin_manager = Arc::new((*window.state::()).clone()); + let plugin_manager = Arc::new(crate::plugins_ext::plugin_manager(window).await?); let query_manager = app_handle.db_manager(); let app_version = app_handle.package_info().version.to_string(); let http_client = yaak_api_client(ApiClientKind::App, &app_version)?; diff --git a/crates-tauri/yaak-app-client/src/ws_ext.rs b/crates-tauri/yaak-app-client/src/ws_ext.rs index ef8a4e36..7ccba1fe 100644 --- a/crates-tauri/yaak-app-client/src/ws_ext.rs +++ b/crates-tauri/yaak-app-client/src/ws_ext.rs @@ -22,7 +22,6 @@ use yaak_models::models::{ }; use yaak_models::util::UpdateSource; use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose}; -use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_templates::strip_json_comments::maybe_strip_json_comments; use yaak_templates::{RenderErrorBehavior, RenderOptions}; @@ -77,7 +76,7 @@ async fn send_websocket_message( )?; let (resolved_request, _auth_context_id) = resolve_websocket_request(&window.db(), &unrendered_request)?; - let plugin_manager = Arc::new((*app_handle.state::()).clone()); + let plugin_manager = Arc::new(crate::plugins_ext::plugin_manager(app_handle).await?); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let request = render_websocket_request( &resolved_request, @@ -142,7 +141,6 @@ pub async fn cmd_ws_connect( cookie_jar_id: Option<&str>, app_handle: AppHandle, window: WebviewWindow, - _plugin_manager: State<'_, PluginManager>, ws_manager: State<'_, Mutex>, ) -> Result { let unrendered_request = app_handle.db().get_websocket_request(request_id)?; @@ -156,7 +154,7 @@ pub async fn cmd_ws_connect( let settings = app_handle.db().get_settings(); let (resolved_request, auth_context_id) = resolve_websocket_request(&window.db(), &unrendered_request)?; - let plugin_manager = Arc::new((*app_handle.state::()).clone()); + let plugin_manager = Arc::new(crate::plugins_ext::plugin_manager(&app_handle).await?); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let request = render_websocket_request( &resolved_request, diff --git a/crates/yaak-commands/src/host.rs b/crates/yaak-commands/src/host.rs index fb78df5a..5d8a54b1 100644 --- a/crates/yaak-commands/src/host.rs +++ b/crates/yaak-commands/src/host.rs @@ -121,7 +121,12 @@ pub trait PluginHost: Host { /// a render — the variables come from the environment chain, which is an /// ordinary database read — so handing back the callback keeps the rest of /// rendering shared instead of pushing whole commands behind this trait. - fn template_callback(&self, purpose: RenderPurpose) -> impl TemplateCallback; + /// Async so hosts that finish booting their plugin runtime in the + /// background can wait for it here. + fn template_callback( + &self, + purpose: RenderPurpose, + ) -> impl Future>; /// Every template function the installed plugins expose, for the /// autocomplete menu. diff --git a/crates/yaak-commands/src/render.rs b/crates/yaak-commands/src/render.rs index 5d652700..230fc7ac 100644 --- a/crates/yaak-commands/src/render.rs +++ b/crates/yaak-commands/src/render.rs @@ -66,7 +66,7 @@ pub(crate) async fn render_form_values( let environment_chain = host.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?; - let cb = host.template_callback(purpose); + let cb = host.template_callback(purpose).await?; let rendered = render_json_value(serde_json::to_value(&values)?, environment_chain, &cb, options).await?; diff --git a/crates/yaak-commands/src/templates.rs b/crates/yaak-commands/src/templates.rs index 926090a5..9044c893 100644 --- a/crates/yaak-commands/src/templates.rs +++ b/crates/yaak-commands/src/templates.rs @@ -21,7 +21,7 @@ pub async fn cmd_render_template( ) -> Result { let environment_chain = host.db().resolve_environments(&req.workspace_id, None, req.environment_id.as_deref())?; - let cb = host.template_callback(req.purpose.unwrap_or(RenderPurpose::Preview)); + let cb = host.template_callback(req.purpose.unwrap_or(RenderPurpose::Preview)).await?; let options = RenderOptions { // A preview that throws would show the user an error where they expect // to see the value so far, so callers rendering *into the UI* ask for @@ -41,7 +41,7 @@ pub async fn cmd_template_tokens_to_string( host: H, req: CmdTemplateTokensToStringReq, ) -> Result { - let cb = host.template_callback(RenderPurpose::Preview); + let cb = host.template_callback(RenderPurpose::Preview).await?; Ok(transform_args(req.tokens, &cb)?.to_string()) } diff --git a/crates/yaak-commands/tests/test_host.rs b/crates/yaak-commands/tests/test_host.rs index 6a485d1f..15440c8c 100644 --- a/crates/yaak-commands/tests/test_host.rs +++ b/crates/yaak-commands/tests/test_host.rs @@ -250,8 +250,11 @@ impl PluginHost for SingleThreadedHost { Err(yaak_commands::Error::Generic("no plugin runtime on this host".into())) } - fn template_callback(&self, _purpose: RenderPurpose) -> impl TemplateCallback { - NoTemplateFunctions + async fn template_callback( + &self, + _purpose: RenderPurpose, + ) -> yaak_commands::Result { + Ok(NoTemplateFunctions) } async fn template_function_summaries( diff --git a/crates/yaak-plugins/src/manager.rs b/crates/yaak-plugins/src/manager.rs index cf7ad6c2..2e05dd8b 100644 --- a/crates/yaak-plugins/src/manager.rs +++ b/crates/yaak-plugins/src/manager.rs @@ -187,24 +187,25 @@ impl PluginManager { } let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?; - let db = query_manager.connect(); - for dir in &bundled_dirs { - if db.get_plugin_by_directory(dir).is_none() { - db.upsert_plugin( - &Plugin { - directory: dir.clone(), - enabled: true, - url: None, - source: PluginSource::Bundled, - ..Default::default() - }, - &UpdateSource::Background, - )?; + // Scope the db connection so the future stays Send across the await below + let plugins = { + let db = query_manager.connect(); + for dir in &bundled_dirs { + if db.get_plugin_by_directory(dir).is_none() { + db.upsert_plugin( + &Plugin { + directory: dir.clone(), + enabled: true, + url: None, + source: PluginSource::Bundled, + ..Default::default() + }, + &UpdateSource::Background, + )?; + } } - } - - let plugins = db.list_plugins()?; - drop(db); + db.list_plugins()? + }; let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await; if !init_errors.is_empty() {