Refactor plugin manager and gRPC server (#96)

This commit is contained in:
Gregory Schier
2024-09-19 05:58:12 -07:00
committed by GitHub
parent 844d795014
commit a3b64423fd
27 changed files with 661 additions and 614 deletions

8
package-lock.json generated
View File

@@ -27,7 +27,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.1", "@tauri-apps/plugin-log": "^2.0.0-rc.1",
"@tauri-apps/plugin-os": "^2.0.0-rc.1", "@tauri-apps/plugin-os": "^2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc.1", "@tauri-apps/plugin-shell": "^2.0.0-rc.1",
"@yaakapp/api": "^0.2.0", "@yaakapp/api": "^0.2.3",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"cm6-graphql": "^0.0.9", "cm6-graphql": "^0.0.9",
@@ -3322,9 +3322,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@yaakapp/api": { "node_modules/@yaakapp/api": {
"version": "0.2.0", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.2.3.tgz",
"integrity": "sha512-DskPYRQ0Hk3KcOIi8O5drbpK0wUwvpUcMvsYfPPz90jfwb9tSpprfFKFUiCVartrA/VO6R0skKscWz74QTKaSA==", "integrity": "sha512-LKLk1EErWF0LyFj70yhZZzk2ZwwpC7xT3y3zPofgxUqKis9gW7lwevsTdyb1Acv18BY6IL2u8as7dzIN2p85ew==",
"dependencies": { "dependencies": {
"@types/node": "^22.5.4" "@types/node": "^22.5.4"
} }

View File

@@ -46,7 +46,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.1", "@tauri-apps/plugin-log": "^2.0.0-rc.1",
"@tauri-apps/plugin-os": "^2.0.0-rc.1", "@tauri-apps/plugin-os": "^2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc.1", "@tauri-apps/plugin-shell": "^2.0.0-rc.1",
"@yaakapp/api": "^0.2.0", "@yaakapp/api": "^0.2.3",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"cm6-graphql": "^0.0.9", "cm6-graphql": "^0.0.9",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@yaakapp/api", "name": "@yaakapp/api",
"version": "0.2.0", "version": "0.2.3",
"main": "lib/index.js", "main": "lib/index.js",
"typings": "./lib/index.d.ts", "typings": "./lib/index.d.ts",
"files": [ "files": [

View File

@@ -1,55 +1,27 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BootRequest } from './BootRequest'; import type { BootRequest } from "./BootRequest";
import type { BootResponse } from './BootResponse'; import type { BootResponse } from "./BootResponse";
import type { CallHttpRequestActionRequest } from './CallHttpRequestActionRequest'; import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest";
import type { CallTemplateFunctionRequest } from './CallTemplateFunctionRequest'; import type { CallTemplateFunctionRequest } from "./CallTemplateFunctionRequest";
import type { CallTemplateFunctionResponse } from './CallTemplateFunctionResponse'; import type { CallTemplateFunctionResponse } from "./CallTemplateFunctionResponse";
import type { CopyTextRequest } from './CopyTextRequest'; import type { CopyTextRequest } from "./CopyTextRequest";
import type { ExportHttpRequestRequest } from './ExportHttpRequestRequest'; import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest";
import type { ExportHttpRequestResponse } from './ExportHttpRequestResponse'; import type { ExportHttpRequestResponse } from "./ExportHttpRequestResponse";
import type { FilterRequest } from './FilterRequest'; import type { FilterRequest } from "./FilterRequest";
import type { FilterResponse } from './FilterResponse'; import type { FilterResponse } from "./FilterResponse";
import type { FindHttpResponsesRequest } from './FindHttpResponsesRequest'; import type { FindHttpResponsesRequest } from "./FindHttpResponsesRequest";
import type { FindHttpResponsesResponse } from './FindHttpResponsesResponse'; import type { FindHttpResponsesResponse } from "./FindHttpResponsesResponse";
import type { GetHttpRequestActionsRequest } from './GetHttpRequestActionsRequest'; import type { GetHttpRequestActionsRequest } from "./GetHttpRequestActionsRequest";
import type { GetHttpRequestActionsResponse } from './GetHttpRequestActionsResponse'; import type { GetHttpRequestActionsResponse } from "./GetHttpRequestActionsResponse";
import type { GetHttpRequestByIdRequest } from './GetHttpRequestByIdRequest'; import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest";
import type { GetHttpRequestByIdResponse } from './GetHttpRequestByIdResponse'; import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
import type { GetTemplateFunctionsResponse } from './GetTemplateFunctionsResponse'; import type { GetTemplateFunctionsResponse } from "./GetTemplateFunctionsResponse";
import type { ImportRequest } from './ImportRequest'; import type { ImportRequest } from "./ImportRequest";
import type { ImportResponse } from './ImportResponse'; import type { ImportResponse } from "./ImportResponse";
import type { RenderHttpRequestRequest } from './RenderHttpRequestRequest'; import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest";
import type { RenderHttpRequestResponse } from './RenderHttpRequestResponse'; import type { RenderHttpRequestResponse } from "./RenderHttpRequestResponse";
import type { SendHttpRequestRequest } from './SendHttpRequestRequest'; import type { SendHttpRequestRequest } from "./SendHttpRequestRequest";
import type { SendHttpRequestResponse } from './SendHttpRequestResponse'; import type { SendHttpRequestResponse } from "./SendHttpRequestResponse";
import type { ShowToastRequest } from './ShowToastRequest'; import type { ShowToastRequest } from "./ShowToastRequest";
export type InternalEventPayload = export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } | { "type": "reload_response" } | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" };
| ({ type: 'boot_request' } & BootRequest)
| ({ type: 'boot_response' } & BootResponse)
| { type: 'reload_request' }
| { type: 'reload_response' }
| ({ type: 'import_request' } & ImportRequest)
| ({ type: 'import_response' } & ImportResponse)
| ({ type: 'filter_request' } & FilterRequest)
| ({ type: 'filter_response' } & FilterResponse)
| ({ type: 'export_http_request_request' } & ExportHttpRequestRequest)
| ({ type: 'export_http_request_response' } & ExportHttpRequestResponse)
| ({ type: 'send_http_request_request' } & SendHttpRequestRequest)
| ({ type: 'send_http_request_response' } & SendHttpRequestResponse)
| ({ type: 'get_http_request_actions_request' } & GetHttpRequestActionsRequest)
| ({ type: 'get_http_request_actions_response' } & GetHttpRequestActionsResponse)
| ({ type: 'call_http_request_action_request' } & CallHttpRequestActionRequest)
| { type: 'get_template_functions_request' }
| ({ type: 'get_template_functions_response' } & GetTemplateFunctionsResponse)
| ({ type: 'call_template_function_request' } & CallTemplateFunctionRequest)
| ({ type: 'call_template_function_response' } & CallTemplateFunctionResponse)
| ({ type: 'copy_text_request' } & CopyTextRequest)
| ({ type: 'render_http_request_request' } & RenderHttpRequestRequest)
| ({ type: 'render_http_request_response' } & RenderHttpRequestResponse)
| ({ type: 'show_toast_request' } & ShowToastRequest)
| ({ type: 'get_http_request_by_id_request' } & GetHttpRequestByIdRequest)
| ({ type: 'get_http_request_by_id_response' } & GetHttpRequestByIdResponse)
| ({ type: 'find_http_responses_request' } & FindHttpResponsesRequest)
| ({ type: 'find_http_responses_response' } & FindHttpResponsesResponse)
| { type: 'empty_response' };

View File

@@ -18,6 +18,10 @@ export class PluginHandle {
this.#worker.postMessage(event); this.#worker.postMessage(event);
} }
async terminate() {
await this.#worker.terminate();
}
#createWorker(): Worker { #createWorker(): Worker {
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs'); const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
const worker = new Worker(workerPath, { const worker = new Worker(workerPath, {

View File

@@ -22,13 +22,19 @@ const plugins: Record<string, PluginHandle> = {};
plugins[pluginEvent.pluginRefId] = plugin; plugins[pluginEvent.pluginRefId] = plugin;
} }
// Once booted, forward all events to plugin's worker // Once booted, forward all events to the plugin worker
const plugin = plugins[pluginEvent.pluginRefId]; const plugin = plugins[pluginEvent.pluginRefId];
if (!plugin) { if (!plugin) {
console.warn('Failed to get plugin for event by', pluginEvent.pluginRefId); console.warn('Failed to get plugin for event by', pluginEvent.pluginRefId);
continue; continue;
} }
if (pluginEvent.payload.type === 'terminate_request') {
await plugin.terminate();
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
delete plugins[pluginEvent.pluginRefId];
}
plugin.sendToWorker(pluginEvent); plugin.sendToWorker(pluginEvent);
} }
console.log('Stream ended'); console.log('Stream ended');

View File

@@ -155,6 +155,14 @@ async function initialize() {
return; return;
} }
if (payload.type === 'terminate_request') {
const payload: InternalEventPayload = {
type: 'terminate_response',
};
sendPayload(payload, replyId);
return;
}
if (payload.type === 'import_request' && typeof mod.pluginHookImport === 'function') { if (payload.type === 'import_request' && typeof mod.pluginHookImport === 'function') {
const reply: ImportResponse | null = await mod.pluginHookImport(ctx, payload.content); const reply: ImportResponse | null = await mod.pluginHookImport(ctx, payload.content);
if (reply != null) { if (reply != null) {

View File

@@ -47,9 +47,9 @@ use yaak_models::queries::{
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response, cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, delete_http_response, delete_plugin, delete_workspace, duplicate_grpc_request,
generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection, duplicate_http_request, generate_model_id, get_cookie_jar, get_environment, get_folder,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments, get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments,
list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
list_http_responses, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, list_http_responses, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id,
@@ -57,12 +57,12 @@ use yaak_models::queries::{
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
}; };
use yaak_plugin_runtime::events::{ use yaak_plugin_runtime::events::{
CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse, BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse,
InternalEvent, InternalEventPayload, BootResponse, RenderHttpRequestResponse, InternalEvent, InternalEventPayload, RenderHttpRequestResponse, SendHttpRequestResponse,
SendHttpRequestResponse, ShowToastRequest, ToastVariant, ShowToastRequest, ToastVariant,
}; };
use yaak_plugin_runtime::handle::PluginHandle; use yaak_plugin_runtime::plugin_handle::PluginHandle;
use yaak_templates::{Parser, Tokens}; use yaak_templates::{Parser, Tokens};
mod analytics; mod analytics;
@@ -82,6 +82,7 @@ const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
const MIN_WINDOW_WIDTH: f64 = 300.0; const MIN_WINDOW_WIDTH: f64 = 300.0;
const MIN_WINDOW_HEIGHT: f64 = 300.0; const MIN_WINDOW_HEIGHT: f64 = 300.0;
const MAIN_WINDOW_PREFIX: &str = "main_";
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
@@ -1172,12 +1173,18 @@ async fn cmd_create_workspace(name: &str, w: WebviewWindow) -> Result<Workspace,
} }
#[tauri::command] #[tauri::command]
async fn cmd_create_plugin( async fn cmd_install_plugin(
directory: &str, directory: &str,
url: Option<String>, url: Option<String>,
plugin_manager: State<'_, PluginManager>,
w: WebviewWindow, w: WebviewWindow,
) -> Result<Plugin, String> { ) -> Result<Plugin, String> {
upsert_plugin( plugin_manager
.add_plugin_by_dir(&directory)
.await
.map_err(|e| e.to_string())?;
let plugin = upsert_plugin(
&w, &w,
Plugin { Plugin {
directory: directory.into(), directory: directory.into(),
@@ -1186,7 +1193,27 @@ async fn cmd_create_plugin(
}, },
) )
.await .await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())?;
Ok(plugin)
}
#[tauri::command]
async fn cmd_uninstall_plugin(
plugin_id: &str,
plugin_manager: State<'_, PluginManager>,
w: WebviewWindow,
) -> Result<Plugin, String> {
let plugin = delete_plugin(&w, plugin_id)
.await
.map_err(|e| e.to_string())?;
plugin_manager
.uninstall(plugin.directory.as_str())
.await
.map_err(|e| e.to_string())?;
Ok(plugin)
} }
#[tauri::command] #[tauri::command]
@@ -1463,8 +1490,14 @@ async fn cmd_list_plugins(w: WebviewWindow) -> Result<Vec<Plugin>, String> {
} }
#[tauri::command] #[tauri::command]
async fn cmd_reload_plugins(plugin_manager: State<'_, PluginManager>) -> Result<(), String> { async fn cmd_reload_plugins(
plugin_manager.reload_all().await; app_handle: AppHandle,
plugin_manager: State<'_, PluginManager>,
) -> Result<(), String> {
plugin_manager
.initialize_all_plugins(&app_handle)
.await
.map_err(|e| e.to_string())?;
Ok(()) Ok(())
} }
@@ -1476,9 +1509,12 @@ async fn cmd_plugin_info(
) -> Result<BootResponse, String> { ) -> Result<BootResponse, String> {
let plugin = get_plugin(&w, id).await.map_err(|e| e.to_string())?; let plugin = get_plugin(&w, id).await.map_err(|e| e.to_string())?;
plugin_manager plugin_manager
.get_plugin_info(plugin.directory.as_str()) .get_plugin_by_dir(plugin.directory.as_str())
.await .await
.ok_or("Failed to find plugin info".to_string()) .ok_or("Failed to find plugin info".to_string())?
.info()
.await
.ok_or("Failed to find plugin".to_string())
} }
#[tauri::command] #[tauri::command]
@@ -1719,8 +1755,7 @@ pub fn run() {
let plugin_cb = PluginTemplateCallback::new(app.app_handle().clone()); let plugin_cb = PluginTemplateCallback::new(app.app_handle().clone());
app.manage(plugin_cb); app.manage(plugin_cb);
let app_handle = app.app_handle().clone(); monitor_plugin_events(&app.app_handle().clone());
monitor_plugin_events(&app_handle);
Ok(()) Ok(())
}) })
@@ -1732,7 +1767,7 @@ pub fn run() {
cmd_create_folder, cmd_create_folder,
cmd_create_grpc_request, cmd_create_grpc_request,
cmd_create_http_request, cmd_create_http_request,
cmd_create_plugin, cmd_install_plugin,
cmd_create_workspace, cmd_create_workspace,
cmd_curl_to_request, cmd_curl_to_request,
cmd_delete_all_grpc_connections, cmd_delete_all_grpc_connections,
@@ -1744,6 +1779,7 @@ pub fn run() {
cmd_delete_grpc_request, cmd_delete_grpc_request,
cmd_delete_http_request, cmd_delete_http_request,
cmd_delete_http_response, cmd_delete_http_response,
cmd_uninstall_plugin,
cmd_delete_workspace, cmd_delete_workspace,
cmd_dismiss_notification, cmd_dismiss_notification,
cmd_duplicate_grpc_request, cmd_duplicate_grpc_request,
@@ -1909,7 +1945,7 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
handle.set_menu(menu).expect("Failed to set app menu"); handle.set_menu(menu).expect("Failed to set app menu");
let window_num = handle.webview_windows().len(); let window_num = handle.webview_windows().len();
let label = format!("main_{}", window_num); let label = format!("{MAIN_WINDOW_PREFIX}{window_num}");
info!("Create new window label={label}"); info!("Create new window label={label}");
let mut win_builder = let mut win_builder =
tauri::WebviewWindowBuilder::new(handle, label, WebviewUrl::App(url.into())) tauri::WebviewWindowBuilder::new(handle, label, WebviewUrl::App(url.into()))
@@ -2005,14 +2041,20 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
let app_handle = app_handle.clone(); let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let plugin_manager: State<'_, PluginManager> = app_handle.state(); let plugin_manager: State<'_, PluginManager> = app_handle.state();
let (_rx_id, mut rx) = plugin_manager.subscribe().await; let (rx_id, mut rx) = plugin_manager.subscribe().await;
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
let app_handle = app_handle.clone(); let app_handle = app_handle.clone();
let plugin = plugin_manager let plugin = match plugin_manager
.get_plugin(event.plugin_ref_id.as_str()) .get_plugin_by_ref_id(event.plugin_ref_id.as_str())
.await .await
.unwrap(); {
None => {
warn!("Failed to get plugin for event {:?}", event);
continue;
}
Some(p) => p,
};
// We might have recursive back-and-forth calls between app and plugin, so we don't // We might have recursive back-and-forth calls between app and plugin, so we don't
// want to block here // want to block here
@@ -2020,6 +2062,7 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
handle_plugin_event(&app_handle, &event, &plugin).await; handle_plugin_event(&app_handle, &event, &plugin).await;
}); });
} }
plugin_manager.unsubscribe(rx_id.as_str()).await;
}); });
} }
@@ -2062,19 +2105,19 @@ async fn handle_plugin_event<R: Runtime>(
)) ))
} }
InternalEventPayload::RenderHttpRequestRequest(req) => { InternalEventPayload::RenderHttpRequestRequest(req) => {
let w = get_focused_window_no_lock(app_handle).expect("No focused window"); let window = get_focused_window_no_lock(app_handle).expect("No focused window");
let workspace = get_workspace(app_handle, req.http_request.workspace_id.as_str()) let workspace = get_workspace(app_handle, req.http_request.workspace_id.as_str())
.await .await
.expect("Failed to get workspace for request"); .expect("Failed to get workspace for request");
let url = w.url().unwrap(); let url = window.url().unwrap();
let mut query_pairs = url.query_pairs(); let mut query_pairs = url.query_pairs();
let environment_id = query_pairs let environment_id = query_pairs
.find(|(k, _v)| k == "environment_id") .find(|(k, _v)| k == "environment_id")
.map(|(_k, v)| v.to_string()); .map(|(_k, v)| v.to_string());
let environment = match environment_id { let environment = match environment_id {
None => None, None => None,
Some(id) => get_environment(&w, id.as_str()).await.ok(), Some(id) => get_environment(&window, id.as_str()).await.ok(),
}; };
let cb = &*app_handle.state::<PluginTemplateCallback>(); let cb = &*app_handle.state::<PluginTemplateCallback>();
let rendered_http_request = let rendered_http_request =
@@ -2086,28 +2129,22 @@ async fn handle_plugin_event<R: Runtime>(
)) ))
} }
InternalEventPayload::ReloadResponse => { InternalEventPayload::ReloadResponse => {
let w = get_focused_window_no_lock(app_handle).expect("No focused window"); let window = get_focused_window_no_lock(app_handle).expect("No focused window");
let plugins = list_plugins(&w).await.unwrap(); let plugins = list_plugins(&window).await.unwrap();
for plugin in plugins { for plugin in plugins {
if plugin.directory != plugin_handle.dir { if plugin.directory != plugin_handle.dir {
continue; continue;
} }
upsert_plugin( let new_plugin = Plugin {
&w, updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
Plugin { ..plugin
// TODO: Add reloaded_at field to use instead };
updated_at: Utc::now().naive_utc(), upsert_plugin(&window, new_plugin).await.unwrap();
..plugin
},
)
.await
.unwrap();
} }
let plugin_name = plugin_handle.info().await.unwrap().name;
let toast_event = plugin_handle.build_event_to_send( let toast_event = plugin_handle.build_event_to_send(
&InternalEventPayload::ShowToastRequest(ShowToastRequest { &InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_name), message: format!("Reloaded plugin {}", plugin_handle.dir),
variant: ToastVariant::Info, variant: ToastVariant::Info,
}), }),
None, None,
@@ -2174,18 +2211,25 @@ async fn handle_plugin_event<R: Runtime>(
fn get_focused_window_no_lock<R: Runtime>(app_handle: &AppHandle<R>) -> Option<WebviewWindow<R>> { fn get_focused_window_no_lock<R: Runtime>(app_handle: &AppHandle<R>) -> Option<WebviewWindow<R>> {
// TODO: Getting the focused window doesn't seem to work on Windows, so // TODO: Getting the focused window doesn't seem to work on Windows, so
// we'll need to pass the window label into plugin events instead. // we'll need to pass the window label into plugin events instead.
if app_handle.webview_windows().len() == 1 { let main_windows = app_handle
let w = app_handle
.webview_windows()
.iter()
.next()
.map(|w| w.1.clone());
return w;
}
app_handle
.webview_windows() .webview_windows()
.iter() .iter()
.find(|w| w.1.is_focused().unwrap_or(false)) .filter_map(|(_, w)| {
.map(|w| w.1.clone()) if w.label().starts_with(MAIN_WINDOW_PREFIX) {
Some(w.to_owned())
} else {
None
}
})
.collect::<Vec<WebviewWindow<R>>>();
if main_windows.len() == 1 {
return main_windows.iter().next().map(|w| w.clone())
}
main_windows
.iter()
.cloned()
.find(|w| w.is_focused().unwrap_or(false))
.map(|w| w.clone())
} }

View File

@@ -34,7 +34,7 @@ impl TemplateCallback for PluginTemplateCallback {
} else { } else {
fn_name fn_name
}; };
let plugin_manager = self.app_handle.state::<PluginManager>(); let plugin_manager = self.app_handle.state::<PluginManager>();
let function = plugin_manager let function = plugin_manager
.get_template_functions() .get_template_functions()
@@ -46,7 +46,7 @@ impl TemplateCallback for PluginTemplateCallback {
.ok_or("")?; .ok_or("")?;
let mut args_with_defaults = args.clone(); let mut args_with_defaults = args.clone();
// Fill in default values for all args // Fill in default values for all args
for a_def in function.args { for a_def in function.args {
let base = match a_def { let base = match a_def {

View File

@@ -74,7 +74,7 @@ impl YaakUpdater {
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
info!("Shutting down plugin manager before update"); info!("Shutting down plugin manager before update");
let plugin_manager = h.state::<PluginManager>(); let plugin_manager = h.state::<PluginManager>();
plugin_manager.cleanup().await; plugin_manager.terminate().await;
}); });
}); });
}) })

View File

@@ -954,6 +954,21 @@ pub async fn upsert_plugin<R: Runtime>(
Ok(emit_upserted_model(window, m)) Ok(emit_upserted_model(window, m))
} }
pub async fn delete_plugin<R: Runtime>(window: &WebviewWindow<R>, id: &str) -> Result<Plugin> {
let plugin = get_plugin(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::delete()
.from_table(PluginIden::Table)
.cond_where(Expr::col(PluginIden::Id).eq(id))
.build_rusqlite(SqliteQueryBuilder);
db.execute(sql.as_str(), &*params.as_params())?;
emit_deleted_model(window, plugin)
}
pub async fn get_folder<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Folder> { pub async fn get_folder<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Folder> {
let dbm = &*mgr.state::<SqliteConnection>(); let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap(); let db = dbm.0.lock().await.get().unwrap();

View File

@@ -5,22 +5,35 @@ use crate::server::plugin_runtime::EventStreamEvent;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("IO error")] #[error("IO error: {0}")]
IoErr(#[from] io::Error), IoErr(#[from] io::Error),
#[error("Tauri error")]
#[error("Tauri error: {0}")]
TauriErr(#[from] tauri::Error), TauriErr(#[from] tauri::Error),
#[error("Tauri shell error")]
#[error("Tauri shell error: {0}")]
TauriShellErr(#[from] tauri_plugin_shell::Error), TauriShellErr(#[from] tauri_plugin_shell::Error),
#[error("Grpc transport error")]
#[error("Grpc transport error: {0}")]
GrpcTransportErr(#[from] tonic::transport::Error), GrpcTransportErr(#[from] tonic::transport::Error),
#[error("Grpc send error")]
#[error("Grpc send error: {0}")]
GrpcSendErr(#[from] SendError<tonic::Result<EventStreamEvent>>), GrpcSendErr(#[from] SendError<tonic::Result<EventStreamEvent>>),
#[error("JSON error")]
#[error("JSON error: {0}")]
JsonErr(#[from] serde_json::Error), JsonErr(#[from] serde_json::Error),
#[error("Plugin not found: {0}")] #[error("Plugin not found: {0}")]
PluginNotFoundErr(String), PluginNotFoundErr(String),
#[error("Plugin error: {0}")] #[error("Plugin error: {0}")]
PluginErr(String), PluginErr(String),
#[error("Client not initialized error")]
ClientNotInitializedErr,
#[error("Unknown event received")]
UnknownEventErr,
} }
impl Into<String> for Error { impl Into<String> for Error {

View File

@@ -27,6 +27,9 @@ pub enum InternalEventPayload {
ReloadRequest, ReloadRequest,
ReloadResponse, ReloadResponse,
TerminateRequest,
TerminateResponse,
ImportRequest(ImportRequest), ImportRequest(ImportRequest),
ImportResponse(ImportResponse), ImportResponse(ImportResponse),

View File

@@ -4,5 +4,5 @@ pub mod manager;
mod nodejs; mod nodejs;
pub mod plugin; pub mod plugin;
mod server; mod server;
pub mod handle; pub mod plugin_handle;
mod util; mod util;

View File

@@ -1,57 +1,236 @@
use crate::error::Error::{ClientNotInitializedErr, PluginErr, PluginNotFoundErr, UnknownEventErr};
use crate::error::Result; use crate::error::Result;
use crate::events::{ use crate::events::{
BootResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs, BootRequest, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse, CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse,
GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose,
}; };
use std::collections::HashMap;
use crate::error::Error::PluginErr;
use crate::nodejs::start_nodejs_plugin_runtime; use crate::nodejs::start_nodejs_plugin_runtime;
use crate::plugin::start_server; use crate::plugin_handle::PluginHandle;
use crate::server::PluginRuntimeGrpcServer; use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntimeServer;
use crate::server::PluginRuntimeServerImpl;
use log::{info, warn};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tauri::{AppHandle, Runtime}; use tauri::path::BaseDirectory;
use tokio::sync::mpsc; use tauri::{AppHandle, Manager, Runtime};
use tokio::sync::watch::Sender; use tokio::fs::read_dir;
use crate::handle::PluginHandle; use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex};
use tonic::codegen::tokio_stream;
use tonic::transport::Server;
use yaak_models::queries::{generate_id, list_plugins};
#[derive(Clone)]
pub struct PluginManager { pub struct PluginManager {
kill_tx: Sender<bool>, subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
server: PluginRuntimeGrpcServer, plugins: Arc<Mutex<Vec<PluginHandle>>>,
kill_tx: tokio::sync::watch::Sender<bool>,
server: Arc<PluginRuntimeServerImpl>,
} }
impl PluginManager { impl PluginManager {
pub async fn new<R: Runtime>( pub fn new<R: Runtime>(app_handle: AppHandle<R>) -> PluginManager {
app_handle: &AppHandle<R>, let (events_tx, mut events_rx) = mpsc::channel(128);
plugin_dirs: Vec<String>, let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
) -> PluginManager {
let (server, addr) = start_server(plugin_dirs)
.await
.expect("Failed to start plugin runtime server");
let (kill_tx, kill_rx) = tokio::sync::watch::channel(false); let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
start_nodejs_plugin_runtime(app_handle, addr, &kill_rx) let (client_connect_tx, mut client_connect_rx) = tokio::sync::watch::channel(false);
.await let server =
.expect("Failed to start plugin runtime"); PluginRuntimeServerImpl::new(events_tx, client_disconnect_tx, client_connect_tx);
PluginManager { kill_tx, server } let plugin_manager = PluginManager {
plugins: Arc::new(Mutex::new(Vec::new())),
subscribers: Arc::new(Mutex::new(HashMap::new())),
server: Arc::new(server.clone()),
kill_tx: kill_server_tx,
};
// Forward events to subscribers
let subscribers = plugin_manager.subscribers.clone();
tauri::async_runtime::spawn(async move {
while let Some(event) = events_rx.recv().await {
for (tx_id, tx) in subscribers.lock().await.iter_mut() {
if let Err(e) = tx.try_send(event.clone()) {
warn!("Failed to send event to subscriber {tx_id} {e:?}");
}
}
}
});
// Handle when client plugin runtime disconnects
tauri::async_runtime::spawn(async move {
while let Some(_) = client_disconnect_rx.recv().await {
info!("Plugin runtime client disconnected! TODO: Handle this case");
}
});
info!("Starting plugin server");
let svc = PluginRuntimeServer::new(server.to_owned());
let listen_addr = match option_env!("PORT") {
None => "localhost:0".to_string(),
Some(port) => format!("localhost:{port}"),
};
let listener = tauri::async_runtime::block_on(async move {
TcpListener::bind(listen_addr)
.await
.expect("Failed to bind TCP listener")
});
let addr = listener.local_addr().expect("Failed to get local address");
// 1. Reload all plugins when the Node.js runtime connects
{
let plugin_manager = plugin_manager.clone();
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
match client_connect_rx.changed().await {
Ok(_) => {
info!("Plugin runtime client connected!");
plugin_manager
.initialize_all_plugins(&app_handle)
.await
.expect("Failed to reload plugins");
}
Err(e) => {
warn!("Failed to receive from client connection rx {e:?}");
}
}
});
};
// 1. Spawn server in the background
info!("Starting gRPC plugin server on {addr}");
tauri::async_runtime::spawn(async move {
Server::builder()
.timeout(Duration::from_secs(10))
.add_service(svc)
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
.expect("grpc plugin runtime server failed to start");
});
// 2. Start Node.js runtime and initialize plugins
tauri::async_runtime::block_on(async move {
start_nodejs_plugin_runtime(&app_handle, addr, &kill_server_rx)
.await
.unwrap();
});
plugin_manager
} }
pub async fn reload_all(&self) { pub async fn list_plugin_dirs<R: Runtime>(&self, app_handle: &AppHandle<R>) -> Vec<String> {
self.server.reload_plugins().await let plugins_dir = app_handle
.path()
.resolve("plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource");
let bundled_plugin_dirs = read_plugins_dir(&plugins_dir)
.await
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str());
let plugins = list_plugins(app_handle).await.unwrap_or_default();
let installed_plugin_dirs = plugins
.iter()
.map(|p| p.directory.to_owned())
.collect::<Vec<String>>();
let plugin_dirs = [bundled_plugin_dirs, installed_plugin_dirs].concat();
plugin_dirs
}
pub async fn uninstall(&self, dir: &str) -> Result<()> {
let plugin = self
.get_plugin_by_dir(dir)
.await
.ok_or(PluginNotFoundErr(dir.to_string()))?;
self.remove_plugin(&plugin).await
}
async fn remove_plugin(&self, plugin: &PluginHandle) -> Result<()> {
let mut plugins = self.plugins.lock().await;
// Terminate the plugin
plugin.terminate().await?;
// Remove the plugin from the list
let pos = plugins.iter().position(|p| p.ref_id == plugin.ref_id);
if let Some(pos) = pos {
plugins.remove(pos);
}
Ok(())
}
pub async fn add_plugin_by_dir(&self, dir: &str) -> Result<()> {
info!("Adding plugin by dir {dir}");
let maybe_tx = self.server.app_to_plugin_events_tx.lock().await;
let tx = match &*maybe_tx {
None => return Err(ClientNotInitializedErr),
Some(tx) => tx,
};
let ph = PluginHandle::new(dir, tx.clone());
self.plugins.lock().await.push(ph.clone());
let plugin = self
.get_plugin_by_dir(dir)
.await
.ok_or(PluginNotFoundErr(dir.to_string()))?;
// Boot the plugin
let event = self
.send_to_plugin_and_wait(
&plugin,
&InternalEventPayload::BootRequest(BootRequest {
dir: dir.to_string(),
}),
)
.await?;
let resp = match event.payload {
InternalEventPayload::BootResponse(resp) => resp,
_ => return Err(UnknownEventErr),
};
plugin.set_boot_response(&resp).await;
Ok(())
}
pub async fn initialize_all_plugins<R: Runtime>(
&self,
app_handle: &AppHandle<R>,
) -> Result<()> {
for dir in self.list_plugin_dirs(app_handle).await {
// First remove the plugin if it exists
if let Some(plugin) = self.get_plugin_by_dir(dir.as_str()).await {
if let Err(e) = self.remove_plugin(&plugin).await {
warn!("Failed to remove plugin {dir} {e:?}");
}
}
if let Err(e) = self.add_plugin_by_dir(dir.as_str()).await {
warn!("Failed to add plugin {dir} {e:?}");
}
}
Ok(())
} }
pub async fn subscribe(&self) -> (String, mpsc::Receiver<InternalEvent>) { pub async fn subscribe(&self) -> (String, mpsc::Receiver<InternalEvent>) {
self.server.subscribe().await let (tx, rx) = mpsc::channel(128);
let rx_id = generate_id();
self.subscribers.lock().await.insert(rx_id.clone(), tx);
(rx_id, rx)
} }
pub async fn unsubscribe(&self, rx_id: &str) { pub async fn unsubscribe(&self, rx_id: &str) {
self.server.unsubscribe(rx_id).await self.subscribers.lock().await.remove(rx_id);
} }
pub async fn cleanup(&self) { pub async fn terminate(&self) {
self.kill_tx.send_replace(true); self.kill_tx.send_replace(true);
// Give it a bit of time to kill // Give it a bit of time to kill
@@ -64,22 +243,115 @@ impl PluginManager {
payload: &InternalEventPayload, payload: &InternalEventPayload,
) -> Result<()> { ) -> Result<()> {
let reply_id = Some(source_event.clone().id); let reply_id = Some(source_event.clone().id);
self.server let plugin = self
.send(&payload, source_event.plugin_ref_id.as_str(), reply_id) .get_plugin_by_ref_id(source_event.plugin_ref_id.as_str())
.await
.ok_or(PluginNotFoundErr(source_event.plugin_ref_id.to_string()))?;
let event = plugin.build_event_to_send(&payload, reply_id);
plugin.send(&event).await
}
pub async fn get_plugin_by_ref_id(&self, ref_id: &str) -> Option<PluginHandle> {
self.plugins
.lock()
.await
.iter()
.find(|p| p.ref_id == ref_id)
.cloned()
}
pub async fn get_plugin_by_dir(&self, dir: &str) -> Option<PluginHandle> {
self.plugins
.lock()
.await
.iter()
.find(|p| p.dir == dir)
.cloned()
}
pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> {
for plugin in self.plugins.lock().await.iter().cloned() {
let info = plugin.info().await?;
if info.name == name {
return Some(plugin);
}
}
None
}
async fn send_to_plugin_and_wait(
&self,
plugin: &PluginHandle,
payload: &InternalEventPayload,
) -> Result<InternalEvent> {
let events = self
.send_to_plugins_and_wait(payload, vec![plugin.to_owned()])
.await?;
Ok(events.first().unwrap().to_owned())
}
async fn send_and_wait(&self, payload: &InternalEventPayload) -> Result<Vec<InternalEvent>> {
self.send_to_plugins_and_wait(payload, self.plugins.lock().await.clone())
.await .await
} }
pub async fn get_plugin_info(&self, dir: &str) -> Option<BootResponse> { async fn send_to_plugins_and_wait(
self.server.plugin_by_dir(dir).await.ok()?.info().await &self,
} payload: &InternalEventPayload,
plugins: Vec<PluginHandle>,
) -> Result<Vec<InternalEvent>> {
let (rx_id, mut rx) = self.subscribe().await;
pub async fn get_plugin(&self, ref_id: &str) -> Result<PluginHandle> { // 1. Build the events with IDs and everything
self.server.plugin_by_ref_id(ref_id).await let events_to_send = plugins
.iter()
.map(|p| p.build_event_to_send(payload, None))
.collect::<Vec<InternalEvent>>();
// 2. Spawn thread to subscribe to incoming events and check reply ids
let send_events_fut = {
let events_to_send = events_to_send.clone();
tokio::spawn(async move {
let mut found_events = Vec::new();
while let Some(event) = rx.recv().await {
if events_to_send
.iter()
.find(|e| Some(e.id.to_owned()) == event.reply_id)
.is_some()
{
found_events.push(event.clone());
};
if found_events.len() == events_to_send.len() {
break;
}
}
found_events
})
};
// 3. Send the events
for event in events_to_send {
let plugin = plugins
.iter()
.find(|p| p.ref_id == event.plugin_ref_id)
.expect("Didn't find plugin in list");
plugin.send(&event).await?
}
// 4. Join on the spawned thread
let events = send_events_fut.await.expect("Thread didn't succeed");
// 5. Unsubscribe
self.unsubscribe(rx_id.as_str()).await;
Ok(events)
} }
pub async fn get_http_request_actions(&self) -> Result<Vec<GetHttpRequestActionsResponse>> { pub async fn get_http_request_actions(&self) -> Result<Vec<GetHttpRequestActionsResponse>> {
let reply_events = self let reply_events = self
.server
.send_and_wait(&InternalEventPayload::GetHttpRequestActionsRequest( .send_and_wait(&InternalEventPayload::GetHttpRequestActionsRequest(
GetHttpRequestActionsRequest {}, GetHttpRequestActionsRequest {},
)) ))
@@ -97,7 +369,6 @@ impl PluginManager {
pub async fn get_template_functions(&self) -> Result<Vec<GetTemplateFunctionsResponse>> { pub async fn get_template_functions(&self) -> Result<Vec<GetTemplateFunctionsResponse>> {
let reply_events = self let reply_events = self
.server
.send_and_wait(&InternalEventPayload::GetTemplateFunctionsRequest) .send_and_wait(&InternalEventPayload::GetTemplateFunctionsRequest)
.await?; .await?;
@@ -112,10 +383,11 @@ impl PluginManager {
} }
pub async fn call_http_request_action(&self, req: CallHttpRequestActionRequest) -> Result<()> { pub async fn call_http_request_action(&self, req: CallHttpRequestActionRequest) -> Result<()> {
let ref_id = req.plugin_ref_id.clone();
let plugin = self let plugin = self
.server .get_plugin_by_ref_id(ref_id.as_str())
.plugin_by_ref_id(req.plugin_ref_id.as_str()) .await
.await?; .ok_or(PluginNotFoundErr(ref_id))?;
let event = plugin.build_event_to_send( let event = plugin.build_event_to_send(
&InternalEventPayload::CallHttpRequestActionRequest(req), &InternalEventPayload::CallHttpRequestActionRequest(req),
None, None,
@@ -139,7 +411,6 @@ impl PluginManager {
}; };
let events = self let events = self
.server
.send_and_wait(&InternalEventPayload::CallTemplateFunctionRequest(req)) .send_and_wait(&InternalEventPayload::CallTemplateFunctionRequest(req))
.await?; .await?;
@@ -155,7 +426,6 @@ impl PluginManager {
pub async fn import_data(&self, content: &str) -> Result<(ImportResponse, String)> { pub async fn import_data(&self, content: &str) -> Result<(ImportResponse, String)> {
let reply_events = self let reply_events = self
.server
.send_and_wait(&InternalEventPayload::ImportRequest(ImportRequest { .send_and_wait(&InternalEventPayload::ImportRequest(ImportRequest {
content: content.to_string(), content: content.to_string(),
})) }))
@@ -172,9 +442,12 @@ impl PluginManager {
"No importers found for file contents".to_string(), "No importers found for file contents".to_string(),
)), )),
Some((resp, ref_id)) => { Some((resp, ref_id)) => {
let plugin = self.server.plugin_by_ref_id(ref_id.as_str()).await?; let plugin = self
let plugin_name = plugin.name().await; .get_plugin_by_ref_id(ref_id.as_str())
Ok((resp, plugin_name)) .await
.ok_or(PluginNotFoundErr(ref_id))?;
let info = plugin.info().await.unwrap();
Ok((resp, info.name))
} }
} }
} }
@@ -191,10 +464,14 @@ impl PluginManager {
"filter-xpath" "filter-xpath"
}; };
let plugin = self
.get_plugin_by_dir(plugin_name)
.await
.ok_or(PluginNotFoundErr(plugin_name.to_string()))?;
let event = self let event = self
.server
.send_to_plugin_and_wait( .send_to_plugin_and_wait(
plugin_name, &plugin,
&InternalEventPayload::FilterRequest(FilterRequest { &InternalEventPayload::FilterRequest(FilterRequest {
filter: filter.to_string(), filter: filter.to_string(),
content: content.to_string(), content: content.to_string(),
@@ -211,3 +488,39 @@ impl PluginManager {
} }
} }
} }
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
let mut result = read_dir(dir).await?;
let mut dirs: Vec<String> = vec![];
while let Ok(Some(entry)) = result.next_entry().await {
if entry.path().is_dir() {
#[cfg(target_os = "windows")]
dirs.push(fix_windows_paths(&entry.path()));
#[cfg(not(target_os = "windows"))]
dirs.push(entry.path().to_string_lossy().to_string());
}
}
Ok(dirs)
}
#[cfg(target_os = "windows")]
fn fix_windows_paths(p: &PathBuf) -> String {
use dunce;
use path_slash::PathBufExt;
use regex::Regex;
// 1. Remove UNC prefix for Windows paths to pass to sidecar
let safe_path = dunce::simplified(p.as_path()).to_string_lossy().to_string();
// 2. Remove the drive letter
let safe_path = Regex::new("^[a-zA-Z]:")
.unwrap()
.replace(safe_path.as_str(), "");
// 3. Convert backslashes to forward
let safe_path = PathBuf::from(safe_path.to_string())
.to_slash_lossy()
.to_string();
safe_path
}

View File

@@ -39,7 +39,7 @@ pub async fn start_nodejs_plugin_runtime<R: Runtime>(
.args(&[plugin_runtime_main]); .args(&[plugin_runtime_main]);
let (mut child_rx, child) = cmd.spawn()?; let (mut child_rx, child) = cmd.spawn()?;
println!("Spawned plugin runtime"); info!("Spawned plugin runtime");
let mut kill_rx = kill_rx.clone(); let mut kill_rx = kill_rx.clone();

View File

@@ -1,46 +1,16 @@
use crate::error::Result;
use crate::events::{InternalEvent, InternalEventPayload};
use crate::manager::PluginManager; use crate::manager::PluginManager;
use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntimeServer;
use crate::server::PluginRuntimeGrpcServer;
use log::info; use log::info;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::time::Duration;
use tauri::path::BaseDirectory;
use tauri::plugin::{Builder, TauriPlugin}; use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Manager, RunEvent, Runtime, State}; use tauri::{Manager, RunEvent, Runtime, State};
use tokio::fs::read_dir;
use tokio::net::TcpListener;
use tonic::codegen::tokio_stream;
use tonic::transport::Server;
use yaak_models::queries::list_plugins;
pub fn init<R: Runtime>() -> TauriPlugin<R> { pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak_plugin_runtime") Builder::new("yaak_plugin_runtime")
.setup(|app_handle, _| { .setup(|app_handle, _| {
let plugins_dir = app_handle let manager = PluginManager::new(app_handle.clone());
.path() app_handle.manage(manager.clone());
.resolve("plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource");
tauri::async_runtime::block_on(async move { Ok(())
let bundled_plugin_dirs = read_plugins_dir(&plugins_dir)
.await
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str());
let plugins = list_plugins(app_handle).await.unwrap_or_default();
let installed_plugin_dirs = plugins
.iter()
.map(|p| p.directory.to_owned())
.collect::<Vec<String>>();
let plugin_dirs = [installed_plugin_dirs, bundled_plugin_dirs].concat();
let manager = PluginManager::new(&app_handle, plugin_dirs).await;
app_handle.manage(manager);
Ok(())
})
}) })
.on_event(|app, e| match e { .on_event(|app, e| match e {
// TODO: Also exit when app is force-quit (eg. cmd+r in IntelliJ runner) // TODO: Also exit when app is force-quit (eg. cmd+r in IntelliJ runner)
@@ -49,94 +19,11 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
info!("Exiting plugin runtime due to app exit"); info!("Exiting plugin runtime due to app exit");
let manager: State<PluginManager> = app.state(); let manager: State<PluginManager> = app.state();
manager.cleanup().await; manager.terminate().await;
exit(0); exit(0);
}); });
} }
_ => {} _ => {}
}) })
.build() .build()
} }
pub async fn start_server(
plugin_dirs: Vec<String>,
) -> Result<(PluginRuntimeGrpcServer, SocketAddr)> {
println!("Starting plugin server with {plugin_dirs:?}");
let server = PluginRuntimeGrpcServer::new(plugin_dirs);
let svc = PluginRuntimeServer::new(server.clone());
let listen_addr = match option_env!("PORT") {
None => "localhost:0".to_string(),
Some(port) => format!("localhost:{port}"),
};
{
let server = server.clone();
tokio::spawn(async move {
let (rx_id, mut rx) = server.subscribe().await;
while let Some(event) = rx.recv().await {
match event.clone() {
InternalEvent {
payload: InternalEventPayload::BootResponse(resp),
plugin_ref_id,
..
} => {
server.boot_plugin(plugin_ref_id.as_str(), &resp).await;
}
_ => {}
};
}
server.unsubscribe(rx_id.as_str()).await;
});
};
let listener = TcpListener::bind(listen_addr).await?;
let addr = listener.local_addr()?;
println!("Starting gRPC plugin server on {addr}");
tokio::spawn(async move {
Server::builder()
.timeout(Duration::from_secs(10))
.add_service(svc)
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
.expect("grpc plugin runtime server failed to start");
});
Ok((server, addr))
}
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
let mut result = read_dir(dir).await?;
let mut dirs: Vec<String> = vec![];
while let Ok(Some(entry)) = result.next_entry().await {
if entry.path().is_dir() {
#[cfg(target_os = "windows")]
dirs.push(fix_windows_paths(&entry.path()));
#[cfg(not(target_os = "windows"))]
dirs.push(entry.path().to_string_lossy().to_string());
}
}
Ok(dirs)
}
#[cfg(target_os = "windows")]
fn fix_windows_paths(p: &PathBuf) -> String {
use dunce;
use path_slash::PathBufExt;
use regex::Regex;
// 1. Remove UNC prefix for Windows paths to pass to sidecar
let safe_path = dunce::simplified(p.as_path()).to_string_lossy().to_string();
// 2. Remove the drive letter
let safe_path = Regex::new("^[a-zA-Z]:")
.unwrap()
.replace(safe_path.as_str(), "");
// 3. Convert backslashes to forward
let safe_path = PathBuf::from(safe_path.to_string())
.to_slash_lossy()
.to_string();
safe_path
}

View File

@@ -1,7 +1,9 @@
use crate::error::Result;
use crate::events::{BootResponse, InternalEvent, InternalEventPayload}; use crate::events::{BootResponse, InternalEvent, InternalEventPayload};
use crate::server::plugin_runtime::EventStreamEvent; use crate::server::plugin_runtime::EventStreamEvent;
use crate::util::gen_id; use crate::util::gen_id;
use std::sync::Arc; use std::sync::Arc;
use log::info;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
#[derive(Clone)] #[derive(Clone)]
@@ -13,10 +15,14 @@ pub struct PluginHandle {
} }
impl PluginHandle { impl PluginHandle {
pub async fn name(&self) -> String { pub fn new(dir: &str, tx: mpsc::Sender<tonic::Result<EventStreamEvent>>) -> Self {
match &*self.boot_resp.lock().await { let ref_id = gen_id();
None => "__NOT_BOOTED__".to_string(),
Some(r) => r.name.to_owned(), PluginHandle {
ref_id: ref_id.clone(),
dir: dir.to_string(),
to_plugin_tx: Arc::new(Mutex::new(tx)),
boot_resp: Arc::new(Mutex::new(None)),
} }
} }
@@ -38,28 +44,33 @@ impl PluginHandle {
} }
} }
pub async fn reload(&self) -> crate::error::Result<()> { pub async fn terminate(&self) -> Result<()> {
let event = self.build_event_to_send(&InternalEventPayload::ReloadRequest, None); info!("Terminating plugin {}", self.dir);
let event = self.build_event_to_send(&InternalEventPayload::TerminateRequest, None);
self.send(&event).await self.send(&event).await
} }
pub async fn send(&self, event: &InternalEvent) -> crate::error::Result<()> { pub async fn send(&self, event: &InternalEvent) -> Result<()> {
// info!(
// "Sending event to plugin {} {:?}",
// event.id,
// self.name().await
// );
self.to_plugin_tx self.to_plugin_tx
.lock() .lock()
.await .await
.send(Ok(EventStreamEvent { .send(Ok(EventStreamEvent {
event: serde_json::to_string(&event)?, event: serde_json::to_string(event)?,
})) }))
.await?; .await?;
Ok(()) Ok(())
} }
pub async fn boot(&self, resp: &BootResponse) { pub async fn send_payload(
&self,
payload: &InternalEventPayload,
reply_id: Option<String>,
) -> Result<()> {
let event = self.build_event_to_send(payload, reply_id);
self.send(&event).await
}
pub async fn set_boot_response(&self, resp: &BootResponse) {
let mut boot_resp = self.boot_resp.lock().await; let mut boot_resp = self.boot_resp.lock().await;
*boot_resp = Some(resp.clone()); *boot_resp = Some(resp.clone());
} }

View File

@@ -1,293 +1,47 @@
use std::collections::HashMap; use log::warn;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use log::warn;
use tokio::sync::mpsc::Receiver;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tonic::codegen::tokio_stream::wrappers::ReceiverStream; use tonic::codegen::tokio_stream::wrappers::ReceiverStream;
use tonic::codegen::tokio_stream::{Stream, StreamExt}; use tonic::codegen::tokio_stream::{Stream, StreamExt};
use tonic::{Request, Response, Status, Streaming}; use tonic::{Request, Response, Status, Streaming};
use crate::error::Error::PluginNotFoundErr; use crate::events::InternalEvent;
use crate::error::Result;
use crate::events::{InternalEvent, InternalEventPayload, BootRequest, BootResponse};
use crate::handle::PluginHandle;
use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntime; use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntime;
use crate::util::gen_id;
use plugin_runtime::EventStreamEvent; use plugin_runtime::EventStreamEvent;
use yaak_models::queries::generate_id;
pub mod plugin_runtime { pub mod plugin_runtime {
tonic::include_proto!("yaak.plugins.runtime"); tonic::include_proto!("yaak.plugins.runtime");
} }
type ResponseStream = type ResponseStream = Pin<Box<dyn Stream<Item = Result<EventStreamEvent, Status>> + Send>>;
Pin<Box<dyn Stream<Item = std::result::Result<EventStreamEvent, Status>> + Send>>;
#[derive(Clone)] #[derive(Clone)]
pub struct PluginRuntimeGrpcServer { pub(crate) struct PluginRuntimeServerImpl {
plugin_ref_to_plugin: Arc<Mutex<HashMap<String, PluginHandle>>>, pub(crate) app_to_plugin_events_tx:
callback_to_plugin_ref: Arc<Mutex<HashMap<String, String>>>, Arc<Mutex<Option<mpsc::Sender<tonic::Result<EventStreamEvent>>>>>,
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>, client_disconnect_tx: mpsc::Sender<bool>,
plugin_dirs: Vec<String>, client_connect_tx: tokio::sync::watch::Sender<bool>,
plugin_to_app_events_tx: mpsc::Sender<InternalEvent>,
} }
impl PluginRuntimeGrpcServer { impl PluginRuntimeServerImpl {
pub fn new(plugin_dirs: Vec<String>) -> Self { pub fn new(
PluginRuntimeGrpcServer { events_tx: mpsc::Sender<InternalEvent>,
plugin_ref_to_plugin: Arc::new(Mutex::new(HashMap::new())), disconnect_tx: mpsc::Sender<bool>,
callback_to_plugin_ref: Arc::new(Mutex::new(HashMap::new())), connect_tx: tokio::sync::watch::Sender<bool>,
subscribers: Arc::new(Mutex::new(HashMap::new())), ) -> Self {
plugin_dirs, PluginRuntimeServerImpl {
app_to_plugin_events_tx: Arc::new(Mutex::new(None)),
client_disconnect_tx: disconnect_tx,
client_connect_tx: connect_tx,
plugin_to_app_events_tx: events_tx,
} }
} }
pub async fn plugins(&self) -> Vec<PluginHandle> {
self.plugin_ref_to_plugin
.lock()
.await
.iter()
.map(|p| p.1.to_owned())
.collect::<Vec<PluginHandle>>()
}
pub async fn subscribe(&self) -> (String, Receiver<InternalEvent>) {
let (tx, rx) = mpsc::channel(128);
let rx_id = generate_id();
self.subscribers.lock().await.insert(rx_id.clone(), tx);
(rx_id, rx)
}
pub async fn unsubscribe(&self, rx_id: &str) {
self.subscribers.lock().await.remove(rx_id);
}
pub async fn remove_plugins(&self, plugin_ids: Vec<String>) {
for plugin_id in plugin_ids {
self.remove_plugin(plugin_id.as_str()).await;
}
}
pub async fn remove_plugin(&self, id: &str) {
match self.plugin_ref_to_plugin.lock().await.remove(id) {
None => println!("Tried to remove non-existing plugin {}", id),
Some(plugin) => println!("Removed plugin {} {}", id, plugin.name().await),
};
}
pub async fn boot_plugin(&self, id: &str, resp: &BootResponse) {
match self.plugin_ref_to_plugin.lock().await.get(id) {
None => println!("Tried booting non-existing plugin {}", id),
Some(plugin) => plugin.clone().boot(resp).await,
}
}
pub async fn add_plugin(
&self,
dir: &str,
tx: mpsc::Sender<tonic::Result<EventStreamEvent>>,
) -> PluginHandle {
let ref_id = gen_id();
let plugin_handle = PluginHandle {
ref_id: ref_id.clone(),
dir: dir.to_string(),
to_plugin_tx: Arc::new(Mutex::new(tx)),
boot_resp: Arc::new(Mutex::new(None)),
};
let _ = self
.plugin_ref_to_plugin
.lock()
.await
.insert(ref_id, plugin_handle.clone());
plugin_handle
}
pub async fn plugin_by_ref_id(&self, ref_id: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
match plugins.get(ref_id) {
None => Err(PluginNotFoundErr(ref_id.into())),
Some(p) => Ok(p.to_owned()),
}
}
pub async fn plugin_by_dir(&self, dir: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
for p in plugins.values() {
if p.dir == dir {
return Ok(p.to_owned());
}
}
Err(PluginNotFoundErr(dir.into()))
}
pub async fn plugin_by_name(&self, plugin_name: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
for p in plugins.values() {
if p.name().await == plugin_name {
return Ok(p.to_owned());
}
}
Err(PluginNotFoundErr(plugin_name.into()))
}
pub async fn send(
&self,
payload: &InternalEventPayload,
plugin_ref_id: &str,
reply_id: Option<String>,
) -> Result<()> {
let plugin = self.plugin_by_ref_id(plugin_ref_id).await?;
let event = plugin.build_event_to_send(payload, reply_id);
plugin.send(&event).await
}
pub async fn send_to_plugin(
&self,
plugin_name: &str,
payload: InternalEventPayload,
) -> Result<InternalEvent> {
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(PluginNotFoundErr(plugin_name.into()));
}
let mut plugin = None;
for p in plugins.values() {
if p.name().await == plugin_name {
plugin = Some(p);
break;
}
}
match plugin {
Some(plugin) => {
let event = plugin.build_event_to_send(&payload, None);
plugin.send(&event).await?;
Ok(event)
}
None => Err(PluginNotFoundErr(plugin_name.into())),
}
}
pub async fn send_to_plugin_and_wait(
&self,
plugin_name: &str,
payload: &InternalEventPayload,
) -> Result<InternalEvent> {
let plugin = self.plugin_by_name(plugin_name).await?;
let events = self.send_to_plugins_and_wait(payload, vec![plugin]).await?;
Ok(events.first().unwrap().to_owned())
}
pub async fn send_and_wait(
&self,
payload: &InternalEventPayload,
) -> Result<Vec<InternalEvent>> {
let plugins = self
.plugin_ref_to_plugin
.lock()
.await
.values()
.cloned()
.collect();
self.send_to_plugins_and_wait(payload, plugins).await
}
async fn send_to_plugins_and_wait(
&self,
payload: &InternalEventPayload,
plugins: Vec<PluginHandle>,
) -> Result<Vec<InternalEvent>> {
// 1. Build the events with IDs and everything
let events_to_send = plugins
.iter()
.map(|p| p.build_event_to_send(payload, None))
.collect::<Vec<InternalEvent>>();
// 2. Spawn thread to subscribe to incoming events and check reply ids
let server = self.clone();
let send_events_fut = {
let events_to_send = events_to_send.clone();
tokio::spawn(async move {
let (rx_id, mut rx) = server.subscribe().await;
let mut found_events = Vec::new();
while let Some(event) = rx.recv().await {
if events_to_send
.iter()
.find(|e| Some(e.id.to_owned()) == event.reply_id)
.is_some()
{
found_events.push(event.clone());
};
if found_events.len() == events_to_send.len() {
break;
}
}
server.unsubscribe(rx_id.as_str()).await;
found_events
})
};
// 3. Send the events
for event in events_to_send {
let plugin = plugins
.iter()
.find(|p| p.ref_id == event.plugin_ref_id)
.expect("Didn't find plugin in list");
plugin.send(&event).await?
}
// 4. Join on the spawned thread
let events = send_events_fut.await.expect("Thread didn't succeed");
Ok(events)
}
pub async fn reload_plugins(&self) {
for (_, plugin) in self.plugin_ref_to_plugin.lock().await.clone() {
if let Err(e) = plugin.reload().await {
warn!("Failed to reload plugin {} {}", plugin.dir, e)
}
}
}
async fn load_plugins(
&self,
to_plugin_tx: mpsc::Sender<tonic::Result<EventStreamEvent>>,
plugin_dirs: Vec<String>,
) -> Vec<String> {
let mut plugin_ids = Vec::new();
for dir in plugin_dirs {
let plugin = self.add_plugin(dir.as_str(), to_plugin_tx.clone()).await;
plugin_ids.push(plugin.clone().ref_id);
let event = plugin.build_event_to_send(
&InternalEventPayload::BootRequest(BootRequest {
dir: dir.to_string(),
}),
None,
);
if let Err(e) = plugin.send(&event).await {
// TODO: Error handling
println!(
"Failed boot plugin {} at {} -> {}",
plugin.ref_id, plugin.dir, e
)
} else {
println!("Loaded plugin {} at {}", plugin.ref_id, plugin.dir)
}
}
plugin_ids
}
} }
#[tonic::async_trait] #[tonic::async_trait]
impl PluginRuntime for PluginRuntimeGrpcServer { impl PluginRuntime for PluginRuntimeServerImpl {
type EventStreamStream = ResponseStream; type EventStreamStream = ResponseStream;
async fn event_stream( async fn event_stream(
@@ -296,51 +50,48 @@ impl PluginRuntime for PluginRuntimeGrpcServer {
) -> tonic::Result<Response<Self::EventStreamStream>> { ) -> tonic::Result<Response<Self::EventStreamStream>> {
let mut in_stream = req.into_inner(); let mut in_stream = req.into_inner();
let (to_plugin_tx, to_plugin_rx) = mpsc::channel(128); let (to_plugin_tx, to_plugin_rx) = mpsc::channel::<tonic::Result<EventStreamEvent>>(128);
let mut app_to_plugin_events_tx = self.app_to_plugin_events_tx.lock().await;
*app_to_plugin_events_tx = Some(to_plugin_tx);
println!("GRPC CLIENT CONNECTED");
let plugin_ids = self let plugin_to_app_events_tx = self.plugin_to_app_events_tx.clone();
.load_plugins(to_plugin_tx, self.plugin_dirs.clone()) let client_disconnect_tx = self.client_disconnect_tx.clone();
.await;
self.client_connect_tx
.send(true)
.expect("Failed to send client ready event");
let callbacks = self.callback_to_plugin_ref.clone();
let server = self.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some(result) = in_stream.next().await { while let Some(result) = in_stream.next().await {
// Received event from plugin runtime
match result { match result {
Ok(v) => { Ok(v) => {
let event: InternalEvent = match serde_json::from_str(v.event.as_str()) { let event: InternalEvent = match serde_json::from_str(v.event.as_str()) {
Ok(pe) => pe, Ok(pe) => pe,
Err(e) => { Err(e) => {
println!("Failed to deserialize event {e:?} -> {}", v.event); warn!("Failed to deserialize event {e:?} -> {}", v.event);
continue; continue;
} }
}; };
let plugin_ref_id = event.plugin_ref_id.clone(); // Send event to subscribers
let reply_id = event.reply_id.clone(); // Emit event to the channel for server to handle
if let Err(e) = plugin_to_app_events_tx.try_send(event.clone()) {
let subscribers = server.subscribers.lock().await; warn!("Failed to send to channel. Receiver probably isn't listening: {:?}", e);
for tx in subscribers.values() {
// Emit event to the channel for server to handle
if let Err(e) = tx.try_send(event.clone()) {
println!("Failed to send to server channel (n={}). Receiver probably isn't listening: {:?}", subscribers.len(), e);
}
}
// Add to callbacks if there's a reply_id
if let Some(reply_id) = reply_id {
callbacks.lock().await.insert(reply_id, plugin_ref_id);
} }
} }
Err(err) => { Err(err) => {
// TODO: Better error handling // TODO: Better error handling
println!("gRPC server error {err}"); warn!("gRPC server error {err}");
break; break;
} }
}; };
} }
server.remove_plugins(plugin_ids).await; if let Err(e) = client_disconnect_tx.send(true).await {
warn!("Failed to send killed event {:?}", e);
}
}); });
// Write the same data that was received // Write the same data that was received

View File

@@ -1,11 +1,11 @@
import type { Plugin } from '@yaakapp/api'; import type { Plugin } from '@yaakapp/api';
import { open } from '@tauri-apps/plugin-shell'; import { open } from '@tauri-apps/plugin-shell';
import React from 'react'; import React from 'react';
import { useCreatePlugin } from '../../hooks/useCreatePlugin'; import { useInstallPlugin } from '../../hooks/useInstallPlugin';
import { useUninstallPlugin } from '../../hooks/useUninstallPlugin';
import { usePluginInfo } from '../../hooks/usePluginInfo'; import { usePluginInfo } from '../../hooks/usePluginInfo';
import { usePlugins, useRefreshPlugins } from '../../hooks/usePlugins'; import { usePlugins, useRefreshPlugins } from '../../hooks/usePlugins';
import { Button } from '../core/Button'; import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { IconButton } from '../core/IconButton'; import { IconButton } from '../core/IconButton';
import { InlineCode } from '../core/InlineCode'; import { InlineCode } from '../core/InlineCode';
import { HStack } from '../core/Stacks'; import { HStack } from '../core/Stacks';
@@ -15,7 +15,7 @@ import { SelectFile } from '../SelectFile';
export function SettingsPlugins() { export function SettingsPlugins() {
const [directory, setDirectory] = React.useState<string | null>(null); const [directory, setDirectory] = React.useState<string | null>(null);
const plugins = usePlugins(); const plugins = usePlugins();
const createPlugin = useCreatePlugin(); const createPlugin = useInstallPlugin();
const refreshPlugins = useRefreshPlugins(); const refreshPlugins = useRefreshPlugins();
return ( return (
<div className="grid grid-rows-[minmax(0,1fr)_auto] h-full"> <div className="grid grid-rows-[minmax(0,1fr)_auto] h-full">
@@ -31,7 +31,6 @@ export function SettingsPlugins() {
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight"> <table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead> <thead>
<tr> <tr>
<th></th>
<th className="py-2 text-left">Plugin</th> <th className="py-2 text-left">Plugin</th>
<th className="py-2 text-right">Version</th> <th className="py-2 text-right">Version</th>
<th></th> <th></th>
@@ -88,14 +87,10 @@ export function SettingsPlugins() {
function PluginInfo({ plugin }: { plugin: Plugin }) { function PluginInfo({ plugin }: { plugin: Plugin }) {
const pluginInfo = usePluginInfo(plugin.id); const pluginInfo = usePluginInfo(plugin.id);
const deletePlugin = useUninstallPlugin(plugin.id);
return ( return (
<tr className="group"> <tr className="group">
<td className="pr-2"> <td className="py-2 select-text cursor-text w-full">{pluginInfo.data?.name}</td>
<Checkbox hideLabel checked={true} title="foo" onChange={() => null} />
</td>
<td className="py-2 select-text cursor-text w-full">
<InlineCode>{pluginInfo.data?.name}</InlineCode>
</td>
<td className="py-2 select-text cursor-text text-right"> <td className="py-2 select-text cursor-text text-right">
<InlineCode>{pluginInfo.data?.version}</InlineCode> <InlineCode>{pluginInfo.data?.version}</InlineCode>
</td> </td>
@@ -105,6 +100,7 @@ function PluginInfo({ plugin }: { plugin: Plugin }) {
icon="trash" icon="trash"
title="Uninstall plugin" title="Uninstall plugin"
className="text-text-subtlest" className="text-text-subtlest"
onClick={() => deletePlugin.mutate()}
/> />
</td> </td>
</tr> </tr>

View File

@@ -5,12 +5,13 @@ import type {
HttpRequest, HttpRequest,
} from '@yaakapp/api'; } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { usePlugins } from './usePlugins'; import { usePluginsKey } from './usePlugins';
export function useHttpRequestActions() { export function useHttpRequestActions() {
const plugins = usePlugins(); const pluginsKey = usePluginsKey();
const httpRequestActions = useQuery({ const httpRequestActions = useQuery({
queryKey: ['http_request_actions', plugins.map((p) => p.updatedAt)], queryKey: ['http_request_actions', pluginsKey],
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
queryFn: async () => { queryFn: async () => {
const responses = (await invokeCmd( const responses = (await invokeCmd(

View File

@@ -2,11 +2,11 @@ import { useMutation } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
export function useCreatePlugin() { export function useInstallPlugin() {
return useMutation<void, unknown, string>({ return useMutation<void, unknown, string>({
mutationKey: ['create_plugin'], mutationKey: ['install_plugin'],
mutationFn: async (directory: string) => { mutationFn: async (directory: string) => {
await invokeCmd('cmd_create_plugin', { directory }); await invokeCmd('cmd_install_plugin', { directory });
}, },
onSettled: () => trackEvent('plugin', 'create'), onSettled: () => trackEvent('plugin', 'create'),
}); });

View File

@@ -1,12 +1,12 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import type { PluginBootResponse } from '@yaakapp/api'; import type { BootResponse } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
export function usePluginInfo(id: string) { export function usePluginInfo(id: string) {
return useQuery({ return useQuery({
queryKey: ['plugin_info', id], queryKey: ['plugin_info', id],
queryFn: async () => { queryFn: async () => {
const info = (await invokeCmd('cmd_plugin_info', { id })) as PluginBootResponse; const info = (await invokeCmd('cmd_plugin_info', { id })) as BootResponse;
return info; return info;
}, },
}); });

View File

@@ -12,6 +12,12 @@ export function usePlugins() {
return useAtomValue(pluginsAtom); return useAtomValue(pluginsAtom);
} }
export function usePluginsKey() {
return useAtomValue(pluginsAtom)
.map((p) => p.id + p.updatedAt)
.join(',');
}
/** /**
* Reload all plugins and refresh the list of plugins * Reload all plugins and refresh the list of plugins
*/ */

View File

@@ -1,10 +1,13 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import type { GetTemplateFunctionsResponse } from '@yaakapp/api'; import type { GetTemplateFunctionsResponse } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { usePluginsKey } from './usePlugins';
export function useTemplateFunctions() { export function useTemplateFunctions() {
const pluginsKey = usePluginsKey();
const result = useQuery({ const result = useQuery({
queryKey: ['template_functions'], queryKey: ['template_functions', pluginsKey],
queryFn: async () => { queryFn: async () => {
const responses = (await invokeCmd( const responses = (await invokeCmd(
'cmd_template_functions', 'cmd_template_functions',
@@ -13,6 +16,5 @@ export function useTemplateFunctions() {
}, },
}); });
const fns = result.data?.flatMap((r) => r.functions) ?? []; return result.data?.flatMap((r) => r.functions) ?? [];
return fns;
} }

View File

@@ -0,0 +1,14 @@
import { useMutation } from '@tanstack/react-query';
import type { Plugin } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
export function useUninstallPlugin(pluginId: string) {
return useMutation<Plugin | null, string>({
mutationKey: ['uninstall_plugin'],
mutationFn: async () => {
return invokeCmd('cmd_uninstall_plugin', { pluginId });
},
onSettled: () => trackEvent('plugin', 'delete'),
});
}

View File

@@ -6,7 +6,6 @@ type TauriCmd =
| 'cmd_check_for_updates' | 'cmd_check_for_updates'
| 'cmd_create_cookie_jar' | 'cmd_create_cookie_jar'
| 'cmd_create_environment' | 'cmd_create_environment'
| 'cmd_create_plugin'
| 'cmd_template_tokens_to_string' | 'cmd_template_tokens_to_string'
| 'cmd_create_folder' | 'cmd_create_folder'
| 'cmd_create_grpc_request' | 'cmd_create_grpc_request'
@@ -40,6 +39,7 @@ type TauriCmd =
| 'cmd_grpc_reflect' | 'cmd_grpc_reflect'
| 'cmd_http_request_actions' | 'cmd_http_request_actions'
| 'cmd_import_data' | 'cmd_import_data'
| 'cmd_install_plugin'
| 'cmd_list_cookie_jars' | 'cmd_list_cookie_jars'
| 'cmd_list_environments' | 'cmd_list_environments'
| 'cmd_list_folders' | 'cmd_list_folders'
@@ -64,6 +64,7 @@ type TauriCmd =
| 'cmd_set_update_mode' | 'cmd_set_update_mode'
| 'cmd_template_functions' | 'cmd_template_functions'
| 'cmd_track_event' | 'cmd_track_event'
| 'cmd_uninstall_plugin'
| 'cmd_update_cookie_jar' | 'cmd_update_cookie_jar'
| 'cmd_update_environment' | 'cmd_update_environment'
| 'cmd_update_folder' | 'cmd_update_folder'