//! Tauri-specific plugin management code. //! //! This module contains all Tauri integration for the plugin system: //! - Plugin initialization and lifecycle management //! - Tauri commands for plugin search/install/uninstall //! - Plugin update checking use crate::PluginContextExt; use crate::error::Result; use crate::models_ext::QueryManagerExt; use log::{error, info, warn}; use serde::Serialize; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; use tauri::path::BaseDirectory; use tauri::plugin::{Builder, TauriPlugin}; use tauri::{ AppHandle, Emitter, Manager, RunEvent, Runtime, State, WebviewWindow, WindowEvent, command, is_dev, }; use tokio::sync::Mutex; use ts_rs::TS; use yaak_api::yaak_api_client; use yaak_models::models::Plugin; use yaak_models::util::UpdateSource; use yaak_plugins::api::{ PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates, search_plugins, }; use yaak_plugins::events::{Color, Icon, PluginContext, ShowToastRequest}; use yaak_plugins::install::{delete_and_uninstall, download_and_install}; use yaak_plugins::manager::PluginManager; use yaak_plugins::plugin_meta::get_plugin_meta; static EXITING: AtomicBool = AtomicBool::new(false); // ============================================================================ // Plugin Updater // ============================================================================ const MAX_UPDATE_CHECK_HOURS: u64 = 12; pub struct PluginUpdater { last_check: Option, } #[derive(Debug, Clone, PartialEq, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "index.ts")] pub struct PluginUpdateNotification { pub update_count: usize, pub plugins: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "index.ts")] pub struct PluginUpdateInfo { pub name: String, pub current_version: String, pub latest_version: String, } impl PluginUpdater { pub fn new() -> Self { Self { last_check: None } } pub async fn check_now(&mut self, window: &WebviewWindow) -> Result { self.last_check = Some(Instant::now()); info!("Checking for plugin updates"); let app_version = window.app_handle().package_info().version.to_string(); let http_client = yaak_api_client(&app_version)?; let plugins = window.app_handle().db().list_plugins()?; let updates = check_plugin_updates(&http_client, plugins.clone()).await?; if updates.plugins.is_empty() { info!("No plugin updates available"); return Ok(false); } // Get current plugin versions to build notification let mut update_infos = Vec::new(); for update in &updates.plugins { if let Some(plugin) = plugins.iter().find(|p| { if let Ok(meta) = get_plugin_meta(&std::path::Path::new(&p.directory)) { meta.name == update.name } else { false } }) { if let Ok(meta) = get_plugin_meta(&std::path::Path::new(&plugin.directory)) { update_infos.push(PluginUpdateInfo { name: update.name.clone(), current_version: meta.version, latest_version: update.version.clone(), }); } } } let notification = PluginUpdateNotification { update_count: update_infos.len(), plugins: update_infos }; info!("Found {} plugin update(s)", notification.update_count); if let Err(e) = window.emit_to(window.label(), "plugin_updates_available", ¬ification) { error!("Failed to emit plugin_updates_available event: {}", e); } Ok(true) } pub async fn maybe_check(&mut self, window: &WebviewWindow) -> Result { let update_period_seconds = MAX_UPDATE_CHECK_HOURS * 60 * 60; if let Some(i) = self.last_check && i.elapsed().as_secs() < update_period_seconds { return Ok(false); } self.check_now(window).await } } // ============================================================================ // Tauri Commands // ============================================================================ #[command] pub async fn cmd_plugins_search( app_handle: AppHandle, query: &str, ) -> Result { let app_version = app_handle.package_info().version.to_string(); let http_client = yaak_api_client(&app_version)?; Ok(search_plugins(&http_client, query).await?) } #[command] pub async fn cmd_plugins_install( window: WebviewWindow, name: &str, version: Option, ) -> Result<()> { let plugin_manager = Arc::new((*window.state::()).clone()); let app_version = window.app_handle().package_info().version.to_string(); let http_client = yaak_api_client(&app_version)?; let query_manager = window.state::(); let plugin_context = window.plugin_context(); download_and_install( plugin_manager, &query_manager, &http_client, &plugin_context, name, version, ) .await?; Ok(()) } #[command] pub async fn cmd_plugins_uninstall( plugin_id: &str, window: WebviewWindow, ) -> Result { let plugin_manager = Arc::new((*window.state::()).clone()); let query_manager = window.state::(); let plugin_context = window.plugin_context(); Ok(delete_and_uninstall(plugin_manager, &query_manager, &plugin_context, plugin_id).await?) } #[command] pub async fn cmd_plugins_updates( app_handle: AppHandle, ) -> Result { let app_version = app_handle.package_info().version.to_string(); let http_client = yaak_api_client(&app_version)?; let plugins = app_handle.db().list_plugins()?; Ok(check_plugin_updates(&http_client, plugins).await?) } #[command] pub async fn cmd_plugins_update_all( window: WebviewWindow, ) -> Result> { let app_version = window.app_handle().package_info().version.to_string(); let http_client = yaak_api_client(&app_version)?; let plugins = window.db().list_plugins()?; // Get list of available updates (already filtered to only registry plugins) let updates = check_plugin_updates(&http_client, plugins).await?; if updates.plugins.is_empty() { return Ok(Vec::new()); } let plugin_manager = Arc::new((*window.state::()).clone()); let query_manager = window.state::(); let plugin_context = window.plugin_context(); let mut updated = Vec::new(); for update in updates.plugins { info!("Updating plugin: {} to version {}", update.name, update.version); match download_and_install( plugin_manager.clone(), &query_manager, &http_client, &plugin_context, &update.name, Some(update.version.clone()), ) .await { Ok(_) => { info!("Successfully updated plugin: {}", update.name); updated.push(update.clone()); } Err(e) => { log::error!("Failed to update plugin {}: {:?}", update.name, e); } } } Ok(updated) } // ============================================================================ // Tauri Plugin Initialization // ============================================================================ pub fn init() -> TauriPlugin { Builder::new("yaak-plugins") .setup(|app_handle, _| { // Resolve paths for plugin manager let vendored_plugin_dir = app_handle .path() .resolve("vendored/plugins", BaseDirectory::Resource) .expect("failed to resolve plugin directory resource"); let installed_plugin_dir = app_handle .path() .app_data_dir() .expect("failed to get app data dir") .join("installed-plugins"); #[cfg(target_os = "windows")] let node_bin_name = "yaaknode.exe"; #[cfg(not(target_os = "windows"))] let node_bin_name = "yaaknode"; let node_bin_path = app_handle .path() .resolve(format!("vendored/node/{}", node_bin_name), BaseDirectory::Resource) .expect("failed to resolve yaaknode binary"); let plugin_runtime_main = app_handle .path() .resolve("vendored/plugin-runtime", BaseDirectory::Resource) .expect("failed to resolve plugin runtime") .join("index.cjs"); let dev_mode = is_dev(); // Create plugin manager asynchronously 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, dev_mode, ) .await; // Initialize all plugins after manager is created let bundled_dirs = manager .list_bundled_plugin_dirs() .await .expect("Failed to list bundled plugins"); // Ensure all bundled plugins make it into the database let db = app_handle_clone.db(); 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, ..Default::default() }, &UpdateSource::Background, ) .expect("Failed to upsert bundled plugin"); } } // Get all plugins from database and initialize let plugins = db.list_plugins().expect("Failed to list plugins from database"); drop(db); // Explicitly drop the connection before await let errors = manager.initialize_all_plugins(plugins, &PluginContext::new_empty()).await; // Show toast for any failed plugins for (plugin_dir, error_msg) in errors { let plugin_name = plugin_dir.split('/').last().unwrap_or(&plugin_dir); let toast = ShowToastRequest { message: format!("Failed to start plugin '{}': {}", plugin_name, error_msg), color: Some(Color::Danger), icon: Some(Icon::AlertTriangle), timeout: Some(10000), }; if let Err(emit_err) = app_handle_clone.emit("show_toast", toast) { error!("Failed to emit toast for plugin error: {emit_err:?}"); } } app_handle_clone.manage(manager); }); let plugin_updater = PluginUpdater::new(); app_handle.manage(Mutex::new(plugin_updater)); Ok(()) }) .on_event(|app, e| match e { RunEvent::ExitRequested { api, .. } => { if EXITING.swap(true, Ordering::SeqCst) { return; // Only exit once to prevent infinite recursion } 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; app.exit(0); }); } RunEvent::WindowEvent { event: WindowEvent::Focused(true), label, .. } => { // Check for plugin updates on window focus let w = app.get_webview_window(&label).unwrap(); let h = app.clone(); tauri::async_runtime::spawn(async move { tokio::time::sleep(Duration::from_secs(3)).await; let val: State<'_, Mutex> = h.state(); if let Err(e) = val.lock().await.maybe_check(&w).await { warn!("Failed to check for plugin updates {e:?}"); } }); } _ => {} }) .build() }