From 7a18fb29e49c69cf3e3f97af01f57aab36347be6 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 15 Jun 2024 22:13:01 -0700 Subject: [PATCH] More dynamic plugin access --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/deno.rs | 132 ++++++++++++++++++++++++++++++---------- src-tauri/src/lib.rs | 77 +++++++++++++---------- src-tauri/src/plugin.rs | 121 +++++++++++++++++++++++++----------- 5 files changed, 230 insertions(+), 102 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b7b39d85..916414b3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8172,6 +8172,7 @@ dependencies = [ "tauri-plugin-updater", "tauri-plugin-window-state", "templates", + "thiserror", "tokio", "tokio-stream", "uuid", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6abed543..df2f3249 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -56,3 +56,4 @@ tauri-plugin-window-state = "2.0.0-beta" tokio = { version = "1.36.0", features = ["sync"] } tokio-stream = "0.1.15" uuid = "1.7.0" +thiserror = "1.0.61" diff --git a/src-tauri/src/deno.rs b/src-tauri/src/deno.rs index f2c4253e..e9d15123 100644 --- a/src-tauri/src/deno.rs +++ b/src-tauri/src/deno.rs @@ -9,7 +9,6 @@ use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; -use crate::deno_ops::op_yaml_parse; use anyhow::anyhow; use anyhow::bail; use anyhow::Context; @@ -32,6 +31,9 @@ use deno_core::SourceMapGetter; use deno_core::{resolve_import, v8}; use tokio::task::block_in_place; +use crate::deno_ops::op_yaml_parse; +use crate::plugin::PluginCapability; + #[derive(Clone)] struct SourceMapStore(Rc>>>); @@ -134,13 +136,13 @@ impl ModuleLoader for TypescriptModuleLoader { } } -pub fn run_plugin_deno_block( +pub fn run_plugin_block( plugin_index_file: &str, fn_name: &str, fn_args: Vec, ) -> Result { block_in_place(|| { - tauri::async_runtime::block_on(run_plugin_deno_2(plugin_index_file, fn_name, fn_args)) + tauri::async_runtime::block_on(run_plugin(plugin_index_file, fn_name, fn_args)) }) } @@ -151,39 +153,13 @@ deno_core::extension!( esm = [dir "src/plugin-runtime", "yaml.js"] ); -async fn run_plugin_deno_2( +async fn run_plugin( plugin_index_file: &str, fn_name: &str, fn_args: Vec, ) -> Result { - let source_map_store = SourceMapStore(Rc::new(RefCell::new(HashMap::new()))); - - let mut ext_console = deno_console::deno_console::init_ops_and_esm(); - ext_console.esm_entry_point = Some("ext:deno_console/01_console.js"); - - let ext_yaak = yaak_runtime::init_ops_and_esm(); - - let mut js_runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(Rc::new(TypescriptModuleLoader { - source_maps: source_map_store.clone(), - })), - source_map_getter: Some(Rc::new(source_map_store)), - extensions: vec![ext_console, ext_yaak], - ..Default::default() - }); - - let main_module = resolve_path( - plugin_index_file, - &std::env::current_dir().context("Unable to get CWD")?, - )?; - - // Load the main module so we can do stuff with it - let mod_id = js_runtime.load_main_es_module(&main_module).await?; - let result = js_runtime.mod_evaluate(mod_id); - js_runtime.run_event_loop(Default::default()).await?; - result.await?; - - let module_namespace = js_runtime.get_module_namespace(mod_id).unwrap(); + let mut js_runtime = load_js_runtime()?; + let module_namespace = load_main_module(&mut js_runtime, plugin_index_file).await?; let scope = &mut js_runtime.handle_scope(); let module_namespace = v8::Local::::new(scope, module_namespace); @@ -235,3 +211,95 @@ async fn run_plugin_deno_2( } } } + +pub fn get_plugin_capabilities_block(plugin_index_file: &str) -> Result, Error> { + block_in_place(|| tauri::async_runtime::block_on(get_plugin_capabilities(plugin_index_file))) +} + +pub async fn get_plugin_capabilities( + plugin_index_file: &str, +) -> Result, Error> { + let mut js_runtime = load_js_runtime()?; + let module_namespace = load_main_module(&mut js_runtime, plugin_index_file).await?; + let scope = &mut js_runtime.handle_scope(); + let module_namespace = v8::Local::::new(scope, module_namespace); + + let property_names = + match module_namespace.get_own_property_names(scope, v8::GetPropertyNamesArgs::default()) { + None => return Ok(Vec::new()), + Some(names) => names, + }; + + let mut capabilities: Vec = Vec::new(); + for i in 0..property_names.length() { + let name = property_names.get_index(scope, i); + let name = match name { + Some(name) => name, + None => return Ok(Vec::new()), + }; + + match name.to_rust_string_lossy(scope).as_str() { + "pluginHookImport" => _ = capabilities.push(PluginCapability::Import), + "pluginHookExport" => _ = capabilities.push(PluginCapability::Export), + "pluginHookResponseFilter" => _ = capabilities.push(PluginCapability::Filter), + _ => {} + }; + } + + Ok(capabilities) +} + +async fn load_main_module( + js_runtime: &mut JsRuntime, + plugin_index_file: &str, +) -> Result, Error> { + let main_module = resolve_path( + plugin_index_file, + &std::env::current_dir().context("Unable to get CWD")?, + )?; + + // Load the main module so we can do stuff with it + let mod_id = js_runtime.load_main_es_module(&main_module).await?; + let result = js_runtime.mod_evaluate(mod_id); + js_runtime.run_event_loop(Default::default()).await?; + result.await?; + + let module_namespace = js_runtime.get_module_namespace(mod_id).unwrap(); + + Ok(module_namespace) +} + +fn load_js_runtime<'s>() -> Result { + let source_map_store = SourceMapStore(Rc::new(RefCell::new(HashMap::new()))); + + let mut ext_console = deno_console::deno_console::init_ops_and_esm(); + ext_console.esm_entry_point = Some("ext:deno_console/01_console.js"); + + let ext_yaak = yaak_runtime::init_ops_and_esm(); + + let js_runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(Rc::new(TypescriptModuleLoader { + source_maps: source_map_store.clone(), + })), + source_map_getter: Some(Rc::new(source_map_store)), + extensions: vec![ext_console, ext_yaak], + ..Default::default() + }); + + // let main_module = resolve_path( + // plugin_index_file.to_str().unwrap(), + // &std::env::current_dir().context("Unable to get CWD")?, + // )?; + // + // // Load the main module so we can do stuff with it + // let mod_id = js_runtime.load_main_es_module(&main_module).await?; + // let result = js_runtime.mod_evaluate(mod_id); + // js_runtime.run_event_loop(Default::default()).await?; + // result.await?; + // + // let module_namespace = js_runtime.get_module_namespace(mod_id).unwrap(); + // let scope = &mut js_runtime.handle_scope(); + // let module_namespace = v8::Local::::new(scope, module_namespace); + + Ok(js_runtime) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b3282949..68999c37 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,7 +6,7 @@ extern crate objc; use std::collections::HashMap; use std::env::current_dir; use std::fs; -use std::fs::{create_dir_all, read_to_string, File}; +use std::fs::{create_dir_all, File, read_to_string}; use std::path::PathBuf; use std::process::exit; use std::str::FromStr; @@ -17,43 +17,46 @@ use fern::colors::ColoredLevelConfig; use log::{debug, error, info, warn}; use rand::random; use serde_json::{json, Value}; +use sqlx::{Pool, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; use sqlx::sqlite::SqliteConnectOptions; use sqlx::types::Json; -use sqlx::{Pool, Sqlite, SqlitePool}; +use tauri::{AppHandle, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow}; +use tauri::{Manager, WindowEvent}; use tauri::path::BaseDirectory; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; -use tauri::{AppHandle, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow}; -use tauri::{Manager, WindowEvent}; use tauri_plugin_log::{fern, Target, TargetKind}; use tauri_plugin_shell::ShellExt; use tokio::sync::Mutex; +use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition}; use ::grpc::manager::{DynamicMessage, GrpcHandle}; -use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition}; use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::grpc::metadata_to_map; use crate::http_request::send_http_request; use crate::models::{ - cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, - 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_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, - generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection, + cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, + create_http_response, 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_http_response, delete_workspace, duplicate_grpc_request, + duplicate_http_request, Environment, EnvironmentVariable, Folder, generate_model_id, + get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, - get_or_create_settings, get_workspace, get_workspace_export_resources, list_cookie_jars, - list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, - list_http_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id, - update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, - upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, - Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, - GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace, + get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent, + GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, KeyValue, + list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, + list_grpc_requests, list_http_requests, list_responses, list_workspaces, ModelType, + set_key_value_raw, Settings, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, + upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace, WorkspaceExportResources, }; use crate::notifications::YaakNotifier; -use crate::plugin::{run_plugin_export_curl, run_plugin_import, ImportResult}; +use crate::plugin::{ + find_plugins, get_plugin, ImportResult, PluginCapability, + run_plugin_export_curl, run_plugin_filter, run_plugin_import, +}; use crate::render::render_request; use crate::updates::{UpdateMode, YaakUpdater}; use crate::window_menu::app_menu; @@ -735,7 +738,11 @@ async fn cmd_filter_response( }; let body = read_to_string(response.body_path.unwrap()).unwrap(); - let filter_result = plugin::run_plugin_filter(&w.app_handle(), plugin_name, filter, &body) + let plugin = match get_plugin(&w.app_handle(), plugin_name).map_err(|e| e.to_string())? { + None => return Err("Failed to get plugin".into()), + Some(p) => p, + }; + let filter_result = run_plugin_filter(&plugin, filter, &body) .await .expect("Failed to run filter"); Ok(filter_result.filtered) @@ -748,26 +755,23 @@ async fn cmd_import_data( _workspace_id: &str, ) -> Result { let mut result: Option = None; - let plugins = vec![ - "importer-postman", - "importer-insomnia", - "importer-yaak", - "importer-curl", - ]; let file = read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path)); let file_contents = file.as_str(); - for plugin_name in plugins { - let v = run_plugin_import(&w.app_handle(), plugin_name, file_contents) + let plugins = find_plugins(w.app_handle(), &PluginCapability::Import) + .await + .map_err(|e| e.to_string())?; + for plugin in plugins { + let v = run_plugin_import(&plugin, file_contents) .await .map_err(|e| e.to_string())?; if let Some(r) = v { - info!("Imported data using {}", plugin_name); + info!("Imported data using {}", plugin.name); analytics::track_event( &w.app_handle(), AnalyticsResource::App, AnalyticsAction::Import, - Some(json!({ "plugin": plugin_name })), + Some(json!({ "plugin": plugin.name })), ) .await; result = Some(r); @@ -907,10 +911,17 @@ async fn cmd_request_to_curl( } #[tauri::command] -async fn cmd_curl_to_request(app_handle: AppHandle, command: &str, workspace_id: &str) -> Result { - let v = run_plugin_import(&app_handle, "importer-curl", command) - .await - .map_err(|e| e.to_string()); +async fn cmd_curl_to_request( + app_handle: AppHandle, + command: &str, + workspace_id: &str, +) -> Result { + let plugin = match get_plugin(&app_handle, "importer-curl").map_err(|e| e.to_string())? { + None => return Err("Failed to find plugin".into()), + Some(p) => p, + }; + + let v = run_plugin_import(&plugin, command).await; match v { Ok(Some(r)) => r .resources diff --git a/src-tauri/src/plugin.rs b/src-tauri/src/plugin.rs index 53dcfc81..ebc160ba 100644 --- a/src-tauri/src/plugin.rs +++ b/src-tauri/src/plugin.rs @@ -1,13 +1,24 @@ -use std::path; +use std::{fs, io}; use log::error; use serde::{Deserialize, Serialize}; use tauri::path::BaseDirectory; use tauri::{AppHandle, Manager}; +use thiserror::Error; -use crate::deno::run_plugin_deno_block; +use crate::deno::{get_plugin_capabilities_block, run_plugin_block}; use crate::models::{HttpRequest, WorkspaceExportResources}; +#[derive(Error, Debug)] +pub enum PluginError { + #[error("directory not found")] + DirectoryNotFound(#[from] io::Error), + #[error("anyhow error")] + V8(#[from] anyhow::Error), + // #[error("unknown data store error")] + // Unknown, +} + #[derive(Default, Debug, Deserialize, Serialize)] pub struct FilterResult { pub filtered: String, @@ -18,21 +29,71 @@ pub struct ImportResult { pub resources: WorkspaceExportResources, } -pub async fn run_plugin_filter( +#[derive(Eq, PartialEq, Hash, Clone)] +pub enum PluginCapability { + Export, + Import, + Filter, +} + +pub struct PluginDef { + pub name: String, + pub path: String, + pub capabilities: Vec, +} + +pub fn scan_plugins(app_handle: &AppHandle) -> Result, PluginError> { + let plugins_dir = app_handle + .path() + .resolve("plugins", BaseDirectory::Resource) + .expect("failed to resolve plugin directory resource"); + + let plugin_entries = fs::read_dir(plugins_dir)?; + + let mut plugins = Vec::new(); + for entry in plugin_entries { + let plugin_dir_entry = match entry { + Err(_) => continue, + Ok(entry) => entry, + }; + + let plugin_index_file = plugin_dir_entry.path().join("index.mjs"); + let capabilities = get_plugin_capabilities_block(&plugin_index_file.to_str().unwrap())?; + + plugins.push(PluginDef { + name: plugin_dir_entry.file_name().to_string_lossy().to_string(), + path: plugin_index_file.to_string_lossy().to_string(), + capabilities, + }); + } + + Ok(plugins) +} + +pub async fn find_plugins( app_handle: &AppHandle, - plugin_name: &str, + capability: &PluginCapability, +) -> Result, PluginError> { + let plugins = scan_plugins(app_handle)? + .into_iter() + .filter(|p| p.capabilities.contains(capability)) + .collect(); + Ok(plugins) +} + +pub fn get_plugin(app_handle: &AppHandle, name: &str) -> Result, PluginError> { + Ok(scan_plugins(app_handle)? + .into_iter() + .find(|p| p.name == name)) +} + +pub async fn run_plugin_filter( + plugin: &PluginDef, response_body: &str, filter: &str, ) -> Option { - let plugin_dir = app_handle - .path() - .resolve("plugins", BaseDirectory::Resource) - .expect("failed to resolve plugin directory resource") - .join(plugin_name); - let plugin_index_file = plugin_dir.join("index.mjs"); - - let result = run_plugin_deno_block( - plugin_index_file.to_str().unwrap(), + let result = run_plugin_block( + &plugin.path, "pluginHookResponseFilter", vec![ serde_json::to_value(response_body).unwrap(), @@ -43,7 +104,7 @@ pub async fn run_plugin_filter( .expect("Failed to run plugin"); if result.is_null() { - error!("Plugin {} failed to run", plugin_name); + error!("Plugin {} failed to run", plugin.name); return None; } @@ -56,39 +117,25 @@ pub fn run_plugin_export_curl( app_handle: &AppHandle, request: &HttpRequest, ) -> Result { - let plugin_dir = app_handle - .path() - .resolve("plugins", BaseDirectory::Resource) - .expect("failed to resolve plugin directory resource") - .join("exporter-curl"); - let plugin_index_file = plugin_dir.join("index.mjs"); + let plugin = match get_plugin(app_handle, "exporter-curl").map_err(|e| e.to_string())? { + None => return Err("Failed to get plugin".into()), + Some(p) => p, + }; let request_json = serde_json::to_value(request).map_err(|e| e.to_string())?; - let result = run_plugin_deno_block( - plugin_index_file.to_str().unwrap(), - "pluginHookExport", - vec![request_json], - ) - .map_err(|e| e.to_string())?; + let result = run_plugin_block(&plugin.path, "pluginHookExport", vec![request_json]) + .map_err(|e| e.to_string())?; let export_str: String = serde_json::from_value(result).map_err(|e| e.to_string())?; Ok(export_str) } pub async fn run_plugin_import( - app_handle: &AppHandle, - plugin_name: &str, + plugin: &PluginDef, file_contents: &str, ) -> Result, String> { - let plugin_dir = app_handle - .path() - .resolve("plugins", BaseDirectory::Resource) - .expect("failed to resolve plugin directory resource") - .join(plugin_name); - let plugin_index_file = plugin_dir.join("index.mjs"); - - let result = run_plugin_deno_block( - plugin_index_file.to_str().unwrap(), + let result = run_plugin_block( + &plugin.path, "pluginHookImport", vec![serde_json::to_value(file_contents).map_err(|e| e.to_string())?], )