Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
use rquickjs::{Ctx, Function, Object};
|
||||
|
||||
use super::plugin::PluginHandle;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "completions_v2")] {
|
||||
use rquickjs::{prelude::MutFn, Value};
|
||||
use warp_completer::signatures::CommandSignature;
|
||||
use warp_js::FromWarpJs;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a JS object representing the Warp Plugin API exposed to external JavaScript plugins.
|
||||
///
|
||||
/// Currently, the API contains a single "completions" namespace for registering command
|
||||
/// signatures.
|
||||
pub fn warp(
|
||||
#[allow(unused_variables)] plugin: PluginHandle,
|
||||
ctx: Ctx<'_>,
|
||||
) -> rquickjs::Result<Object<'_>> {
|
||||
let api = Object::new(ctx)?;
|
||||
#[cfg(feature = "completions_v2")]
|
||||
api.set("completions", completions(plugin, ctx)?)?;
|
||||
Ok(api)
|
||||
}
|
||||
|
||||
/// Returns a JS object to be used as a the `console` global, implementing `console.log()` and
|
||||
/// `console.err()`.
|
||||
pub fn console(ctx: Ctx<'_>) -> rquickjs::Result<Object<'_>> {
|
||||
let console = Object::new(ctx)?;
|
||||
console.set(
|
||||
"log",
|
||||
Function::new(ctx, |message: String| {
|
||||
log::info!("{message}");
|
||||
}),
|
||||
)?;
|
||||
console.set(
|
||||
"err",
|
||||
Function::new(ctx, |message: String| {
|
||||
log::error!("{message}");
|
||||
}),
|
||||
)?;
|
||||
Ok(console)
|
||||
}
|
||||
|
||||
/// Returns a JS object representing the Completions namespace for the Warp Plugin API.
|
||||
///
|
||||
/// API methods:
|
||||
///
|
||||
/// `registerCommandSignature(signature: CommandSignature[] | CommandSignature)`: Registers
|
||||
/// the given command signature(s) to be used for completions.
|
||||
#[cfg(feature = "completions_v2")]
|
||||
fn completions<'js>(plugin: PluginHandle, ctx: Ctx<'js>) -> rquickjs::Result<Object<'js>> {
|
||||
let completions = Object::new(ctx)?;
|
||||
completions.set(
|
||||
"registerCommandSignature",
|
||||
Function::new(
|
||||
ctx,
|
||||
MutFn::from(move |val: Value<'js>| {
|
||||
if val.is_array() {
|
||||
let mut plugin = plugin.get_mut();
|
||||
match Vec::<CommandSignature>::from_warp_js(
|
||||
ctx,
|
||||
val,
|
||||
plugin.js_function_registry_mut(),
|
||||
) {
|
||||
Ok(signatures) => plugin.register_command_signatures(signatures),
|
||||
Err(e) => {
|
||||
log::warn!("Attempted to register invalid JS CommandSignatures {e:?}")
|
||||
}
|
||||
}
|
||||
} else if val.is_object() {
|
||||
let mut plugin = plugin.get_mut();
|
||||
match CommandSignature::from_warp_js(
|
||||
ctx,
|
||||
val,
|
||||
plugin.js_function_registry_mut(),
|
||||
) {
|
||||
Ok(signature) => {
|
||||
plugin.register_command_signatures(vec![signature]);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Attempted to register invalid JS CommandSignature {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)?;
|
||||
Ok(completions)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use warpui::r#async::{block_on, executor::Background};
|
||||
|
||||
use crate::plugin::service::{LogService, LogServiceRequest};
|
||||
|
||||
/// Initializes logging for the plugin host process. Internally, the logger relays log messages to
|
||||
/// the main app process via the IPC LogService.
|
||||
pub(super) fn initialize_logging(client: &Arc<ipc::Client>, executor: &Arc<Background>) {
|
||||
let log_service = ipc::service_caller::<LogService>(client.clone());
|
||||
log::set_boxed_logger(Box::new(PluginHostLogger::new(
|
||||
log_service,
|
||||
executor.clone(),
|
||||
)))
|
||||
.expect("Logger should only be set once.");
|
||||
log::set_max_level(log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
/// A logger that relays plugin host log messages to the main app process via IPC `LogService`.
|
||||
pub(super) struct PluginHostLogger {
|
||||
request_tx: async_channel::Sender<LogServiceRequest>,
|
||||
}
|
||||
|
||||
impl PluginHostLogger {
|
||||
pub(super) fn new(
|
||||
log_service: Box<dyn ipc::ServiceCaller<LogService>>,
|
||||
background_executor: Arc<Background>,
|
||||
) -> Self {
|
||||
let (request_tx, message_rx) = async_channel::unbounded();
|
||||
background_executor
|
||||
.spawn(async move {
|
||||
while let Ok(message) = message_rx.recv().await {
|
||||
if let Err(err) = log_service.call(message).await {
|
||||
// In failing tests, the app shuts down abruptly and this message pollutes the test
|
||||
// output.
|
||||
if !cfg!(feature = "integration_tests") {
|
||||
eprintln!("Failed to send log record to host process: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self { request_tx }
|
||||
}
|
||||
}
|
||||
|
||||
impl log::Log for PluginHostLogger {
|
||||
fn enabled(&self, _metadata: &log::Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
let _ = block_on(self.request_tx.send(LogServiceRequest {
|
||||
level: record.level(),
|
||||
target: record.target().to_string(),
|
||||
message: record.args().to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
mod js_api;
|
||||
mod logging;
|
||||
mod plugin;
|
||||
mod plugin_caller;
|
||||
mod plugin_ref;
|
||||
mod runner;
|
||||
mod runners;
|
||||
mod service_impl;
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use warpui::r#async::executor::Background;
|
||||
|
||||
use crate::plugin::host::runners::PluginRunners;
|
||||
|
||||
use self::{
|
||||
plugin_caller::PluginCaller, plugin_ref::PluginRef, runner::PLUGIN_ENTRYPOINT_JS_FILE_NAME,
|
||||
service_impl::CallJsFunctionServiceImpl,
|
||||
};
|
||||
use super::{
|
||||
service::{
|
||||
PluginHostBootstrapRequest, PluginHostBootstrapResponse, PluginHostBootstrapService,
|
||||
},
|
||||
PLUGIN_HOST_ADDRESS_ENV_VAR,
|
||||
};
|
||||
use logging::initialize_logging;
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
warpui::r#async::block_on(async move {
|
||||
let executor = Arc::new(Background::default());
|
||||
|
||||
// Initialize a client connection to the warp app process.
|
||||
let connection_address: ipc::ConnectionAddress = std::env::var(PLUGIN_HOST_ADDRESS_ENV_VAR)
|
||||
.context("Failed to retrieve connection key from env var.")?
|
||||
.into();
|
||||
let client = Arc::new(
|
||||
ipc::Client::connect(connection_address.clone(), executor.clone())
|
||||
.await
|
||||
.context("Failed to instantiate LocalSocketClient.")?,
|
||||
);
|
||||
|
||||
// Initialize logging, which internally transmits logs from this process to the warp app
|
||||
// process via `LogService`.
|
||||
initialize_logging(&client, &executor);
|
||||
|
||||
// Spawn plugin runners for each plugin.
|
||||
let (plugin_request_tx, plugin_request_rx) = async_channel::unbounded();
|
||||
let mut plugin_runners =
|
||||
PluginRunners::new(plugin_request_rx, client.clone(), executor.clone());
|
||||
|
||||
#[cfg(feature = "completions_v2")]
|
||||
plugin_runners.run_plugin(PluginRef::BuiltIn(
|
||||
plugin_ref::BuiltInPluginType::Completions,
|
||||
));
|
||||
|
||||
for plugin_path in plugin_paths() {
|
||||
plugin_runners.run_plugin(PluginRef::Path(plugin_path));
|
||||
}
|
||||
|
||||
// Initialize the plugin host server, which serves services implemented by plugins.
|
||||
let (_server, plugin_host_connection_key) = ipc::ServerBuilder::default()
|
||||
.with_service(CallJsFunctionServiceImpl::new(PluginCaller::new(
|
||||
plugin_request_tx,
|
||||
)))
|
||||
.build_and_run(executor.clone())
|
||||
.expect("Failed to instantiate Plugin Host server ");
|
||||
|
||||
// Send the connection address for the plugin host server back to the warp app.
|
||||
let bootstrap_service = ipc::service_caller::<PluginHostBootstrapService>(client.clone());
|
||||
if !matches!(
|
||||
bootstrap_service
|
||||
.call(PluginHostBootstrapRequest {
|
||||
connection_address: plugin_host_connection_key,
|
||||
})
|
||||
.await,
|
||||
Ok(PluginHostBootstrapResponse { success: true })
|
||||
) {
|
||||
return Err(anyhow!(
|
||||
"Handshake for plugin host server connection address failed."
|
||||
));
|
||||
}
|
||||
|
||||
// Wait for the connection to be dropped to exit.
|
||||
client.wait_for_disconnect().await;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a vector of validated plugin directory paths in the plugins directory.
|
||||
///
|
||||
/// This assumes that all plugins are located in ~/.warp/plugins.
|
||||
fn plugin_paths() -> Vec<PathBuf> {
|
||||
const PLUGIN_PATH_SUFFIX: &str = ".warp/plugins";
|
||||
|
||||
dirs::home_dir()
|
||||
.map(|home_dir| home_dir.join(PLUGIN_PATH_SUFFIX))
|
||||
.and_then(|plugins_dir| fs::read_dir(plugins_dir).ok())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
if is_plugin_dir(path.as_path()) {
|
||||
log::info!("Plugin detected at {path:?}");
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns `true` if the directory at the given path is directory containing JS source for a Warp plugin.
|
||||
fn is_plugin_dir(path: &Path) -> bool {
|
||||
if !path.is_dir() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let expected_plugin_entrypoint = path.join(PLUGIN_ENTRYPOINT_JS_FILE_NAME);
|
||||
expected_plugin_entrypoint.is_file()
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use rquickjs::Context;
|
||||
use warp_js::{JsFunctionId, JsFunctionRegistry, SerializedJsValue};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "completions_v2")] {
|
||||
use warp_completer::signatures::CommandSignature;
|
||||
|
||||
use crate::plugin::service::{
|
||||
RegisterCommandSignatureRequest, RegisterCommandSignatureService,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle on a single JS plugin.
|
||||
///
|
||||
/// We use a `Rc<RefCell<_>>` to make it possible for the plugin "API" (JS functions implemented in
|
||||
/// Rust) _and_ native Rust logic to share ownership over the plugin "state". We know that, since
|
||||
/// JS is singlethreaded, the JS-side logic will never actually ask for a mutable borrow at the same
|
||||
/// time as host-side Rust.
|
||||
#[derive(Clone)]
|
||||
pub struct PluginHandle(Rc<RefCell<Plugin>>);
|
||||
|
||||
impl PluginHandle {
|
||||
pub(super) fn new(
|
||||
app_service_callers: AppServiceCallers,
|
||||
on_registered_js_function_callback: impl Fn(JsFunctionId) + 'static,
|
||||
) -> Self {
|
||||
Self(Rc::new(RefCell::new(Plugin::new(
|
||||
app_service_callers,
|
||||
on_registered_js_function_callback,
|
||||
))))
|
||||
}
|
||||
|
||||
pub(super) fn get_mut(&self) -> RefMut<'_, Plugin> {
|
||||
(*self.0).borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Container struct for holding ipc `ServiceCaller` dependencies of `Plugin`.
|
||||
pub(super) struct AppServiceCallers {
|
||||
#[cfg(feature = "completions_v2")]
|
||||
register_command_signatures_caller:
|
||||
Box<dyn ipc::ServiceCaller<RegisterCommandSignatureService>>,
|
||||
}
|
||||
|
||||
impl AppServiceCallers {
|
||||
pub fn new(#[allow(unused_variables)] app_client: Arc<ipc::Client>) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "completions_v2")]
|
||||
register_command_signatures_caller: ipc::service_caller::<
|
||||
RegisterCommandSignatureService,
|
||||
>(app_client),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents logic and functionality implemented by a Plugin in JS.
|
||||
///
|
||||
/// Conceptually, this an intermediate abstraction between plugin JS and "host-side" Rust.
|
||||
///
|
||||
/// Concretely, this can generally be viewed as a wrapper around functions implemented by the
|
||||
/// plugin in JavaScript, exposing APIs to call such JS functions from Rust.
|
||||
///
|
||||
/// This ultimately the main backing data structure behind the JS plugin API exposed to plugins
|
||||
/// (see [`super::js_api`]).
|
||||
pub(super) struct Plugin {
|
||||
app_services: AppServiceCallers,
|
||||
js_function_registry: JsFunctionRegistry,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
/// Handles the given `request` and returns its corresponding response.
|
||||
///
|
||||
/// Generally, should return a `PluginResponse` enum variant that matches the request variant.
|
||||
pub(super) fn handle_request(
|
||||
&mut self,
|
||||
request: PluginRequest,
|
||||
ctx: &Context,
|
||||
) -> anyhow::Result<PluginResponse> {
|
||||
match request {
|
||||
PluginRequest::CallJsFunction { id, input } => self
|
||||
.call_js_function(input, &id, ctx)
|
||||
.map(|serialized_value| PluginResponse::CallJsFunctionResult {
|
||||
output: serialized_value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the given command signatures.
|
||||
#[cfg(feature = "completions_v2")]
|
||||
pub(super) fn register_command_signatures(&mut self, signatures: Vec<CommandSignature>) {
|
||||
if let Err(e) = warpui::r#async::block_on(
|
||||
self.app_services
|
||||
.register_command_signatures_caller
|
||||
.call(RegisterCommandSignatureRequest { signatures }),
|
||||
) {
|
||||
log::warn!("Failed to register command signature: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn js_function_registry_mut(&mut self) -> &mut JsFunctionRegistry {
|
||||
&mut self.js_function_registry
|
||||
}
|
||||
|
||||
/// Calls the js function with the given function_id and input, if registered.
|
||||
///
|
||||
/// The function with the given `function_id` is expected to be registered in this `Plugin`'s
|
||||
/// `JsFunctionRegistry`. If the function_id has no registered function, returns an error.
|
||||
fn call_js_function(
|
||||
&mut self,
|
||||
input: SerializedJsValue,
|
||||
function_id: &JsFunctionId,
|
||||
ctx: &Context,
|
||||
) -> anyhow::Result<SerializedJsValue> {
|
||||
let Some(function) = self.js_function_registry.get_function(function_id) else {
|
||||
return Err(anyhow!(
|
||||
"Attempted to call unregistered JS Function with ID {:?}.",
|
||||
function_id
|
||||
));
|
||||
};
|
||||
Ok(ctx.with(|ctx| function.call(input, &mut self.js_function_registry, ctx))?)
|
||||
}
|
||||
|
||||
fn new(
|
||||
app_services: AppServiceCallers,
|
||||
on_registered_js_function_callback: impl Fn(JsFunctionId) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
js_function_registry: JsFunctionRegistry::new()
|
||||
.on_registered_js_function(on_registered_js_function_callback),
|
||||
app_services,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A request to be served by a plugin.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) enum PluginRequest {
|
||||
/// Request for execution of a JS function with the contained `id` and `input` to be passed as
|
||||
/// a parameter. `input` should be the serialized bytes representation of the
|
||||
/// `IntoPluginJs`-implementing Rust struct to be passed to the JS function.
|
||||
CallJsFunction {
|
||||
id: JsFunctionId,
|
||||
input: SerializedJsValue,
|
||||
},
|
||||
}
|
||||
|
||||
/// The response to a PluginRequest.
|
||||
#[derive(Debug)]
|
||||
pub(super) enum PluginResponse {
|
||||
/// Result of JS function execution requested via `PluginRequest::CallJsFunction`.
|
||||
CallJsFunctionResult {
|
||||
/// The serialized bytes representation of the `FromPluginJs`-implementing Rust value that
|
||||
/// is the return value of the JS function.
|
||||
///
|
||||
/// If this response is coming from a thread that does "host" the JS function (e.g. it was
|
||||
/// registered by a different plugin), this is `None`.
|
||||
output: SerializedJsValue,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use anyhow::Result;
|
||||
use futures::channel::oneshot;
|
||||
|
||||
use super::plugin::{PluginRequest, PluginResponse};
|
||||
|
||||
/// An interface to make asynchronous [`PluginRequest`]s.
|
||||
///
|
||||
/// This is intended to be used to implement IPC `Service`s that rely on plugin execution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct PluginCaller {
|
||||
request_tx: async_channel::Sender<(PluginRequest, oneshot::Sender<PluginResponse>)>,
|
||||
}
|
||||
|
||||
impl PluginCaller {
|
||||
/// Constructs a new [`PluginCaller`] from the sending end of the channel to the
|
||||
/// `PluginRunners` task that broadcasts [`PluginRequest`]s to individual `PluginRunner`s.
|
||||
pub(super) fn new(
|
||||
request_tx: async_channel::Sender<(PluginRequest, oneshot::Sender<PluginResponse>)>,
|
||||
) -> Self {
|
||||
Self { request_tx }
|
||||
}
|
||||
|
||||
pub(super) async fn send_message(&self, request: PluginRequest) -> Result<PluginResponse> {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
self.request_tx.send((request, response_tx)).await?;
|
||||
Ok(response_rx.await?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
pub(super) const PLUGIN_ENTRYPOINT_JS_FILE_NAME: &str = "main.js";
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(super) enum PluginLoadError {
|
||||
#[error("Failed to load plugin: {0:?}")]
|
||||
File(#[from] io::Error),
|
||||
|
||||
#[error("Missing source for builtin plugin: {0:?}")]
|
||||
MissingBuiltin(BuiltInPluginType),
|
||||
}
|
||||
|
||||
/// Represents "Built-in" plugins. Each variant corresponds to a plugin bundled with Warp by
|
||||
/// default (e.g. Completions/Command Signatures)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(super) enum BuiltInPluginType {
|
||||
Completions,
|
||||
}
|
||||
|
||||
impl BuiltInPluginType {
|
||||
#[cfg(feature = "completions_v2")]
|
||||
pub(super) fn plugin_bytes(&self) -> Option<Vec<u8>> {
|
||||
match self {
|
||||
BuiltInPluginType::Completions => {
|
||||
command_signatures_v2::CommandSignaturesJs::get("main.js")
|
||||
.map(|bytes| bytes.data.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "completions_v2"))]
|
||||
pub(super) fn plugin_bytes(&self) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// References a single plugin.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub(super) enum PluginRef {
|
||||
/// Refers to plugin source on disk.
|
||||
Path(PathBuf),
|
||||
|
||||
/// Refers to a "built-in" plugin bundled with the Warp binary.
|
||||
BuiltIn(BuiltInPluginType),
|
||||
}
|
||||
|
||||
impl PluginRef {
|
||||
pub(super) fn plugin_bytes(&self) -> Result<Vec<u8>, PluginLoadError> {
|
||||
match self {
|
||||
PluginRef::Path(path) => {
|
||||
let entrypoint_file_path = path.join(PLUGIN_ENTRYPOINT_JS_FILE_NAME);
|
||||
fs::read(entrypoint_file_path).map_err(PluginLoadError::from)
|
||||
}
|
||||
PluginRef::BuiltIn(builtin_plugin_type) => match builtin_plugin_type.plugin_bytes() {
|
||||
Some(bytes) => Ok(bytes),
|
||||
None => Err(PluginLoadError::MissingBuiltin(*builtin_plugin_type)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use anyhow::{Context as AnyhowContext, Result};
|
||||
use async_channel::Receiver;
|
||||
use rquickjs::{Context, Function, Runtime};
|
||||
use warp_js::JsFunctionId;
|
||||
|
||||
use super::{
|
||||
js_api,
|
||||
plugin::{AppServiceCallers, PluginHandle},
|
||||
plugin_ref::PluginRef,
|
||||
runners::PluginRunnerMessage,
|
||||
};
|
||||
|
||||
/// The name of the entrypoint JS file for a plugin.
|
||||
pub(super) const PLUGIN_ENTRYPOINT_JS_FILE_NAME: &str = "main.js";
|
||||
|
||||
/// A "runner" for a single JS plugin.
|
||||
///
|
||||
/// This struct wraps a single QuickJS runtime. It is primarily responsible for loading, compiling,
|
||||
/// and running the plugin.
|
||||
pub(super) struct PluginRunner {
|
||||
/// The execution context of the QuickJS runtime.
|
||||
ctx: Context,
|
||||
|
||||
/// A handle on the actual JS "plugin" logic (e.g. exports of the plugin JS represented in Rust).
|
||||
plugin: PluginHandle,
|
||||
|
||||
/// A receiver for [`PluginRunnerMessage`]s received from the plugin host main thread.
|
||||
message_rx: Receiver<PluginRunnerMessage>,
|
||||
}
|
||||
|
||||
impl PluginRunner {
|
||||
/// Instantiates a new [`PluginRunner`].
|
||||
pub(super) fn new(
|
||||
message_rx: Receiver<PluginRunnerMessage>,
|
||||
app_service_callers: AppServiceCallers,
|
||||
on_registered_js_function_callback: impl Fn(JsFunctionId) + 'static,
|
||||
) -> Result<Self> {
|
||||
let rt = Runtime::new().context("Could not instantiate runtime")?;
|
||||
let ctx = Context::full(&rt).context("Could not instantiate context.")?;
|
||||
|
||||
Ok(Self {
|
||||
ctx,
|
||||
message_rx,
|
||||
plugin: PluginHandle::new(app_service_callers, on_registered_js_function_callback),
|
||||
})
|
||||
}
|
||||
|
||||
/// Loads and evaluates the plugin source JS, and then runs a blocking "event loop" to serve
|
||||
/// incoming [`PluginRequest`]s.
|
||||
///
|
||||
/// After compiling the plugin module, its exported 'activate()' function is called with an
|
||||
/// instance of the warp API object.
|
||||
///
|
||||
/// After `activate()`, listens for incoming [`PluginRequest`]s from the host main thread and
|
||||
/// serves corresponding responses.
|
||||
pub(super) fn run(&mut self, plugin_ref: &PluginRef) -> Result<()> {
|
||||
let plugin_source_bytes = plugin_ref.plugin_bytes()?;
|
||||
let plugin = self.plugin.clone();
|
||||
self.ctx.with(|ctx| -> Result<()> {
|
||||
let plugin_module = ctx
|
||||
.compile("plugin", plugin_source_bytes)
|
||||
.context("Could not compile plugin source")?;
|
||||
|
||||
ctx.globals().set("console", js_api::console(ctx))?;
|
||||
|
||||
let warp_api = js_api::warp(plugin, ctx);
|
||||
|
||||
let activate_fn: Function = plugin_module
|
||||
.get("activate")
|
||||
.context("Could not resolve activate() function")?;
|
||||
activate_fn
|
||||
.call::<_, ()>((warp_api,))
|
||||
.context("Failed to call activate() function")?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
while let Ok(message) = self.message_rx.recv_blocking() {
|
||||
match message {
|
||||
PluginRunnerMessage::Request(request, response_tx) => {
|
||||
if let Ok(output) = self.plugin.get_mut().handle_request(request, &self.ctx) {
|
||||
// If the consuming end of the output response is dropped, we don't really
|
||||
// care (just means that the caller no longer cares about the response).
|
||||
let _ = response_tx.send(output);
|
||||
}
|
||||
}
|
||||
PluginRunnerMessage::Exit => break,
|
||||
}
|
||||
}
|
||||
log::info!("Plugin thread exiting...");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use async_channel::Receiver;
|
||||
use futures::channel::oneshot;
|
||||
use parking_lot::Mutex;
|
||||
use warp_js::JsFunctionId;
|
||||
use warpui::r#async::executor::Background;
|
||||
|
||||
use super::{
|
||||
plugin::{AppServiceCallers, PluginRequest, PluginResponse},
|
||||
plugin_ref::PluginRef,
|
||||
runner::PluginRunner,
|
||||
};
|
||||
|
||||
/// Message type for messages that may be sent to each `PluginRunner`.
|
||||
///
|
||||
/// This message type only exists for `PluginRunners` -> `PluginRunner` communication.
|
||||
pub(super) enum PluginRunnerMessage {
|
||||
/// A request to execute some plugin logic and send back its output.
|
||||
Request(PluginRequest, oneshot::Sender<PluginResponse>),
|
||||
|
||||
/// The plugin runner that receives this should exit.
|
||||
Exit,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PluginRunnerSender {
|
||||
/// A `Sender` for emitting `PlugginRunnerMessage`s to the plugin runner.
|
||||
runner_tx: async_channel::Sender<PluginRunnerMessage>,
|
||||
|
||||
/// The set of IDs for JsFunctions registered by the plugin runner that owns the receiving end
|
||||
/// of `runner_tx`.
|
||||
registered_js_function_ids: HashSet<JsFunctionId>,
|
||||
}
|
||||
|
||||
/// Responsible for spawning threads for [`PluginRunner`]s, broadcasting incoming `PluginRequest`s
|
||||
/// to them, and aggregating their responses into one to be dispatched back to the `PluginRequest`
|
||||
/// caller.
|
||||
pub(super) struct PluginRunners {
|
||||
plugin_runner_senders: Arc<Mutex<HashMap<PluginRef, PluginRunnerSender>>>,
|
||||
app_client: Arc<ipc::Client>,
|
||||
}
|
||||
|
||||
impl PluginRunners {
|
||||
pub(super) fn new(
|
||||
plugin_request_rx: Receiver<(PluginRequest, oneshot::Sender<PluginResponse>)>,
|
||||
app_client: Arc<ipc::Client>,
|
||||
executor: Arc<Background>,
|
||||
) -> Self {
|
||||
let plugin_runner_senders = Arc::new(Mutex::new(HashMap::new()));
|
||||
executor
|
||||
.spawn(proxy_requests_to_plugin_runners(
|
||||
executor.clone(),
|
||||
plugin_request_rx,
|
||||
plugin_runner_senders.clone(),
|
||||
))
|
||||
.detach();
|
||||
Self {
|
||||
plugin_runner_senders,
|
||||
app_client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a new thread to execute the plugin at the given `path`.
|
||||
///
|
||||
/// If there is an existing runner for a plugin at the given path, kills it and starts a new
|
||||
/// runner. This effectively "reloads" the plugin.
|
||||
pub(super) fn run_plugin(&mut self, plugin_ref: PluginRef) {
|
||||
// If there's a live plugin runner for a plugin at this path, attempt to kill it.
|
||||
if let Some(PluginRunnerSender { runner_tx, .. }) =
|
||||
self.plugin_runner_senders.lock().remove(&plugin_ref)
|
||||
{
|
||||
let _ = runner_tx.try_send(PluginRunnerMessage::Exit);
|
||||
}
|
||||
|
||||
let (message_tx, message_rx) = async_channel::unbounded::<PluginRunnerMessage>();
|
||||
self.plugin_runner_senders.lock().insert(
|
||||
plugin_ref.clone(),
|
||||
PluginRunnerSender {
|
||||
runner_tx: message_tx,
|
||||
registered_js_function_ids: HashSet::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let app_client = self.app_client.clone();
|
||||
let plugin_ref_clone = plugin_ref.clone();
|
||||
let plugin_runner_senders = self.plugin_runner_senders.clone();
|
||||
std::thread::spawn(move || {
|
||||
let registered_js_function_id = move |id: JsFunctionId| {
|
||||
if let Some(registered_js_function_ids) = plugin_runner_senders
|
||||
.lock()
|
||||
.get_mut(&plugin_ref_clone)
|
||||
.map(|sender| &mut sender.registered_js_function_ids)
|
||||
{
|
||||
registered_js_function_ids.insert(id);
|
||||
}
|
||||
};
|
||||
let Ok(mut runner) = PluginRunner::new(
|
||||
message_rx,
|
||||
AppServiceCallers::new(app_client),
|
||||
registered_js_function_id,
|
||||
) else {
|
||||
log::error!(
|
||||
"Failed to instantiate PluginRunner for plugin {:?}.",
|
||||
&plugin_ref
|
||||
);
|
||||
return;
|
||||
};
|
||||
if let Err(e) = runner.run(&plugin_ref) {
|
||||
log::error!("Failed to run plugin: {e:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Proxies [`PluginRequest`]s received via the given `request_rx` to each plugin runner.
|
||||
///
|
||||
/// For each received request, a corresponding `PluginRunnerMessage` is sent to the individual
|
||||
/// plugin runner that registered the requested JS function.
|
||||
async fn proxy_requests_to_plugin_runners(
|
||||
executor: Arc<Background>,
|
||||
request_rx: Receiver<(PluginRequest, oneshot::Sender<PluginResponse>)>,
|
||||
plugin_runner_senders: Arc<Mutex<HashMap<PluginRef, PluginRunnerSender>>>,
|
||||
) {
|
||||
while let Ok((request, response_tx)) = request_rx.recv().await {
|
||||
// Find the plugin runner which hosts the JS function being called.
|
||||
let Some(matching_plugin_runner_tx) = plugin_runner_senders
|
||||
.lock()
|
||||
.values()
|
||||
.find(|sender| match request {
|
||||
PluginRequest::CallJsFunction { id, .. } => {
|
||||
sender.registered_js_function_ids.contains(&id)
|
||||
}
|
||||
})
|
||||
.map(|sender| sender.runner_tx.clone())
|
||||
else {
|
||||
log::warn!("No plugin runner found for request {request:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
executor
|
||||
.spawn(async move {
|
||||
let (runner_response_tx, runner_response_rx) = oneshot::channel();
|
||||
|
||||
if let Err(e) = matching_plugin_runner_tx
|
||||
.send(PluginRunnerMessage::Request(
|
||||
request.clone(),
|
||||
runner_response_tx,
|
||||
))
|
||||
.await
|
||||
{
|
||||
log::warn!("Failed to dispatch request to plugin runner: {e:?}");
|
||||
}
|
||||
|
||||
match runner_response_rx.await {
|
||||
Ok(response) => {
|
||||
let _ = response_tx.send(response);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Received error when awaiting plugin runner response: {e:?}");
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//! Implementation of `CallJsFunctionService` for native platforms running QuickJS, served by the
|
||||
//! plugin host process to the main app process.
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::plugin::service::{
|
||||
CallJsFunctionRequest, CallJsFunctionResponse, CallJsFunctionService,
|
||||
};
|
||||
|
||||
use super::{
|
||||
plugin::{PluginRequest, PluginResponse},
|
||||
plugin_caller::PluginCaller,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct CallJsFunctionServiceImpl {
|
||||
plugin_caller: PluginCaller,
|
||||
}
|
||||
|
||||
impl CallJsFunctionServiceImpl {
|
||||
pub(super) fn new(plugin_caller: PluginCaller) -> Self {
|
||||
Self { plugin_caller }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ipc::ServiceImpl for CallJsFunctionServiceImpl {
|
||||
type Service = CallJsFunctionService;
|
||||
|
||||
async fn handle_request(&self, request: CallJsFunctionRequest) -> CallJsFunctionResponse {
|
||||
let CallJsFunctionRequest {
|
||||
id,
|
||||
serialized_input,
|
||||
} = request;
|
||||
|
||||
match self
|
||||
.plugin_caller
|
||||
.send_message(PluginRequest::CallJsFunction {
|
||||
id,
|
||||
input: serialized_input,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(PluginResponse::CallJsFunctionResult { output }) => {
|
||||
CallJsFunctionResponse::Success(output)
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to receive result for calling JS function");
|
||||
CallJsFunctionResponse::Error {
|
||||
message: format!("Failed with error: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
Err(anyhow!("Plugin host unsupported on WASM"))
|
||||
}
|
||||
Reference in New Issue
Block a user