mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-21 08:59:07 +01:00
365 lines
13 KiB
Rust
365 lines
13 KiB
Rust
//! 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<Instant>,
|
|
}
|
|
|
|
#[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<PluginUpdateInfo>,
|
|
}
|
|
|
|
#[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<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<bool> {
|
|
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<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<bool> {
|
|
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<R: Runtime>(
|
|
app_handle: AppHandle<R>,
|
|
query: &str,
|
|
) -> Result<PluginSearchResponse> {
|
|
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<R: Runtime>(
|
|
window: WebviewWindow<R>,
|
|
name: &str,
|
|
version: Option<String>,
|
|
) -> Result<()> {
|
|
let plugin_manager = Arc::new((*window.state::<PluginManager>()).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::<yaak_models::query_manager::QueryManager>();
|
|
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<R: Runtime>(
|
|
plugin_id: &str,
|
|
window: WebviewWindow<R>,
|
|
) -> Result<Plugin> {
|
|
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
|
|
let query_manager = window.state::<yaak_models::query_manager::QueryManager>();
|
|
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<R: Runtime>(
|
|
app_handle: AppHandle<R>,
|
|
) -> Result<PluginUpdatesResponse> {
|
|
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<R: Runtime>(
|
|
window: WebviewWindow<R>,
|
|
) -> Result<Vec<PluginNameVersion>> {
|
|
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::<PluginManager>()).clone());
|
|
let query_manager = window.state::<yaak_models::query_manager::QueryManager>();
|
|
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<R: Runtime>() -> TauriPlugin<R> {
|
|
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<PluginManager> = 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<PluginUpdater>> = h.state();
|
|
if let Err(e) = val.lock().await.maybe_check(&w).await {
|
|
warn!("Failed to check for plugin updates {e:?}");
|
|
}
|
|
});
|
|
}
|
|
_ => {}
|
|
})
|
|
.build()
|
|
}
|