Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
mod service_impl;
|
||||
|
||||
use command::blocking::Command;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use warpui::{Entity, ModelContext, SingletonEntity};
|
||||
|
||||
use super::{PLUGIN_HOST_ADDRESS_ENV_VAR, PLUGIN_HOST_FLAG};
|
||||
use service_impl::{LogServiceImpl, PluginHostBootstrapServiceImpl};
|
||||
|
||||
/// Singleton model responsible for spawning the plugin host child process and initializing IPC
|
||||
/// server and clients for communication between the app and plugin host processes.
|
||||
pub struct PluginHost {
|
||||
/// A handle on the actual plugin host process.
|
||||
///
|
||||
/// This is `None` if we fail to spawn the plugin host process.
|
||||
host_process: Option<std::process::Child>,
|
||||
|
||||
/// The IPC server that serves app services ([`ipc::Service`] implementations) to the plugin
|
||||
/// host process.
|
||||
///
|
||||
/// This is `None` if server initialization fails.
|
||||
_server: Option<ipc::Server>,
|
||||
|
||||
/// An IPC client for sending requests to the plugin host process.
|
||||
///
|
||||
/// This is `None` if the IPC handshake for relaying the plugin host process's connection
|
||||
/// address fails.
|
||||
host_client: Option<Arc<ipc::Client>>,
|
||||
}
|
||||
|
||||
impl PluginHost {
|
||||
#[cfg_attr(not(feature = "plugin_host"), allow(dead_code))]
|
||||
pub fn new(ctx: &mut ModelContext<Self>) -> Result<Self> {
|
||||
let plugin_host_bootstrap_service = PluginHostBootstrapServiceImpl::new();
|
||||
let connection_address_rx = plugin_host_bootstrap_service.connection_address_rx();
|
||||
|
||||
// Schedule a task that awaits a request containing the connection address for the plugin
|
||||
// host process and uses it to instantiate a Client when it's received.
|
||||
let background_executor = ctx.background_executor();
|
||||
ctx.spawn(
|
||||
async move {
|
||||
match connection_address_rx.recv().await {
|
||||
Ok(connection_address) => {
|
||||
match ipc::Client::connect(connection_address, background_executor).await {
|
||||
Ok(client) => Some(client),
|
||||
Err(e) => {
|
||||
log::error!("Failed to instantiate LocalSocketClient: {e:?}.");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to receive connection address for PluginHost services: {e:?}."
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
|me, client, _| {
|
||||
me.host_client = client.map(Arc::new);
|
||||
},
|
||||
);
|
||||
|
||||
let server_builder = ipc::ServerBuilder::default()
|
||||
.with_service(plugin_host_bootstrap_service)
|
||||
.with_service(LogServiceImpl::new());
|
||||
|
||||
#[cfg(feature = "completions_v2")]
|
||||
let server_builder =
|
||||
server_builder.with_service(service_impl::RegisterCommandSignatureServiceImpl::new(
|
||||
warp_completer::signatures::CommandRegistry::global_instance(),
|
||||
));
|
||||
|
||||
let (server, plugin_host_process) =
|
||||
match server_builder.build_and_run(ctx.background_executor()) {
|
||||
Ok((server, connection_address)) => {
|
||||
log::info!("Successfully initialized plugin app server.");
|
||||
|
||||
// Spawn the plugin host process if the app server was successfully initialized.
|
||||
let program = std::env::current_exe()
|
||||
.context("Failed to determine path to current executable.")?;
|
||||
let plugin_host_process = Command::new(program)
|
||||
.args(std::env::args().skip(1))
|
||||
.arg(PLUGIN_HOST_FLAG)
|
||||
.env(PLUGIN_HOST_ADDRESS_ENV_VAR, connection_address.to_string())
|
||||
.spawn()
|
||||
.context("Failed to spawn plugin host process.")?;
|
||||
log::info!("Successfully spawned plugin host process.");
|
||||
|
||||
(Some(server), Some(plugin_host_process))
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Could not initialize server: {e:?}.");
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
host_process: plugin_host_process,
|
||||
_server: server,
|
||||
host_client: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an `ipc::ServiceCaller` for the service specified as `S`.
|
||||
///
|
||||
/// `S` is assumed to be served by the plugin host process; the returned service caller directs
|
||||
/// requests over the IPC connection to the plugin host process.
|
||||
pub fn plugin_service_caller<S: ipc::Service>(&self) -> Option<Box<dyn ipc::ServiceCaller<S>>> {
|
||||
self.host_client.clone().map(ipc::service_caller::<S>)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PluginHost {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut host_process) = self.host_process.take() {
|
||||
if let Ok(Some(exit_status)) = host_process.try_wait() {
|
||||
log::error!("Plugin host process had exited early with status: {exit_status:?}");
|
||||
} else {
|
||||
// Calling `wait()` is necessary for the OS to release process resources on some
|
||||
// systems; processes that have exited but not been `wait`-ed upon are "zombie"
|
||||
// processes that can exhaust OS resources.
|
||||
//
|
||||
// See https://doc.rust-lang.org/std/process/struct.Child.html#warning for more
|
||||
// context.
|
||||
let _ = host_process.kill();
|
||||
let _ = host_process.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for PluginHost {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl SingletonEntity for PluginHost {}
|
||||
@@ -0,0 +1,44 @@
|
||||
//! The implementation of `RegisterCommandSignatureService` to be served by the app process to the
|
||||
//! plugin host process.
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use warp_completer::signatures::CommandRegistry;
|
||||
|
||||
use crate::plugin::service::{
|
||||
RegisterCommandSignatureRequest, RegisterCommandSignatureResponse,
|
||||
RegisterCommandSignatureService,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RegisterCommandSignatureServiceImpl {
|
||||
/// A handle on the command registry in which command signatures may be registered.
|
||||
registry: Arc<CommandRegistry>,
|
||||
}
|
||||
|
||||
impl RegisterCommandSignatureServiceImpl {
|
||||
pub fn new(registry: Arc<CommandRegistry>) -> Self {
|
||||
Self { registry }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for RegisterCommandSignatureServiceImpl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RegisterCommandSignatureService").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ipc::ServiceImpl for RegisterCommandSignatureServiceImpl {
|
||||
type Service = RegisterCommandSignatureService;
|
||||
|
||||
async fn handle_request(
|
||||
&self,
|
||||
request: RegisterCommandSignatureRequest,
|
||||
) -> RegisterCommandSignatureResponse {
|
||||
for serialized_signature in request.signatures {
|
||||
self.registry.register_signature(serialized_signature)
|
||||
}
|
||||
RegisterCommandSignatureResponse { success: true }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! The implementation of `LogService` to be served by the app process to the plugin host process.
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::plugin::service::{LogService, LogServiceRequest, LogServiceResponse};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LogServiceImpl {}
|
||||
|
||||
impl LogServiceImpl {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ipc::ServiceImpl for LogServiceImpl {
|
||||
type Service = LogService;
|
||||
|
||||
async fn handle_request(&self, request: LogServiceRequest) -> LogServiceResponse {
|
||||
let LogServiceRequest {
|
||||
level,
|
||||
target,
|
||||
message,
|
||||
} = request;
|
||||
let log_fn = || {
|
||||
log::log!(target: target.as_str(), level, "{message}");
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "crash_reporting")] {
|
||||
// Explicitly write the log line in the context of the main
|
||||
// Sentry hub; this log receiver thread is spawned before
|
||||
// Sentry is configured, so the thread-local hub doesn't
|
||||
// have the appropriate client and scope configuration.
|
||||
sentry::Hub::run(sentry::Hub::main(), log_fn);
|
||||
} else {
|
||||
log_fn();
|
||||
}
|
||||
}
|
||||
LogServiceResponse { success: true }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "completions_v2")] {
|
||||
mod completions;
|
||||
pub use completions::*;
|
||||
}
|
||||
}
|
||||
mod logging;
|
||||
mod plugin_host_bootstrap;
|
||||
|
||||
pub(super) use logging::*;
|
||||
pub(super) use plugin_host_bootstrap::*;
|
||||
@@ -0,0 +1,48 @@
|
||||
//! The implementation of `PluginHostBootstrapService` to be served by the app process to the plugin
|
||||
//! host process.
|
||||
use async_channel::{Receiver, Sender};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::plugin::service::{
|
||||
PluginHostBootstrapRequest, PluginHostBootstrapResponse, PluginHostBootstrapService,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginHostBootstrapServiceImpl {
|
||||
connection_address_tx: Sender<ipc::ConnectionAddress>,
|
||||
connection_address_rx: Receiver<ipc::ConnectionAddress>,
|
||||
}
|
||||
|
||||
impl PluginHostBootstrapServiceImpl {
|
||||
pub fn new() -> Self {
|
||||
let (connection_key_tx, connection_key_rx) = async_channel::bounded(1);
|
||||
Self {
|
||||
connection_address_tx: connection_key_tx,
|
||||
connection_address_rx: connection_key_rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a receiver that emits the connection address received in an incoming request.
|
||||
pub fn connection_address_rx(&self) -> Receiver<ipc::ConnectionAddress> {
|
||||
self.connection_address_rx.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ipc::ServiceImpl for PluginHostBootstrapServiceImpl {
|
||||
type Service = PluginHostBootstrapService;
|
||||
|
||||
async fn handle_request(
|
||||
&self,
|
||||
request: PluginHostBootstrapRequest,
|
||||
) -> PluginHostBootstrapResponse {
|
||||
match self
|
||||
.connection_address_tx
|
||||
.send(request.connection_address)
|
||||
.await
|
||||
{
|
||||
Ok(_) => PluginHostBootstrapResponse { success: true },
|
||||
Err(_) => PluginHostBootstrapResponse { success: false },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
pub(crate) mod app;
|
||||
pub(crate) mod service;
|
||||
|
||||
#[cfg_attr(not(target_family = "wasm"), path = "host/native/mod.rs")]
|
||||
#[cfg_attr(target_family = "wasm", path = "host/wasm/mod.rs")]
|
||||
mod host;
|
||||
|
||||
pub(crate) use app::PluginHost;
|
||||
pub use host::run as run_plugin_host;
|
||||
|
||||
/// Flag to be passed to the warp executable when executing the warp binary as the plugin host
|
||||
/// process rather than the main app.
|
||||
pub const PLUGIN_HOST_FLAG: &str = "--plugin_host";
|
||||
|
||||
/// The name of the environment variable used to pass connection address for the app server to the
|
||||
/// plugin host process.
|
||||
const PLUGIN_HOST_ADDRESS_ENV_VAR: &str = "WARP_PLUGIN_HOST_ADDRESS";
|
||||
@@ -0,0 +1,42 @@
|
||||
//! IPC service for calling JS functions registered by Warp plugins.
|
||||
//!
|
||||
//! This service is hosted by the plugin host process and used by the main app process to call
|
||||
//! plugin functions defined in JS.
|
||||
//!
|
||||
//! Functions are called by a given `JsFunctionId`; this service does not enforce correctness of
|
||||
//! given IDs. Valid IDs are expected to be passed from plugin host process to app process by some
|
||||
//! other means (e.g. some other service). For example, IDs for Command Signature generator
|
||||
//! functions are contained within `CommandSignature` structs passed from plugin host to app via
|
||||
//! the `RegisterCommandSignature` service.
|
||||
//!
|
||||
//! Similarly, this service does not enforce correctness of input/output types; the caller is
|
||||
//! expected to call a function with its expected input type and deserialize its output bytes
|
||||
//! correctly.
|
||||
//!
|
||||
//! To serialize input/deserialize output, callers are expected to use `bincode`.
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use warp_js::{JsFunctionId, SerializedJsValue};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct CallJsFunctionRequest {
|
||||
/// The `JsFunctionId` of the function to call.
|
||||
pub id: JsFunctionId,
|
||||
|
||||
/// The function's input, serialized to bytes.
|
||||
pub serialized_input: SerializedJsValue,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub enum CallJsFunctionResponse {
|
||||
Success(SerializedJsValue),
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
pub struct CallJsFunctionService {}
|
||||
|
||||
#[async_trait]
|
||||
impl ipc::Service for CallJsFunctionService {
|
||||
type Request = CallJsFunctionRequest;
|
||||
type Response = CallJsFunctionResponse;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! This module contains defines an IPC service to register JS Command Signatures in the global
|
||||
//! CommandRegistry.
|
||||
//!
|
||||
//! This IPC is hosted by the rust app process and called by the plugin process when plugins
|
||||
//! register command signatures.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use warp_completer::signatures::CommandSignature;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct RegisterCommandSignatureRequest {
|
||||
/// Command signatures to be registered with the service's CommandRegistry.
|
||||
pub signatures: Vec<CommandSignature>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct RegisterCommandSignatureResponse {
|
||||
/// `true` if the request succeeded, `false` otherwise.
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
/// IPC service to register Command Signatures in the given [`CommandRegistry`].
|
||||
pub struct RegisterCommandSignatureService {}
|
||||
|
||||
impl ipc::Service for RegisterCommandSignatureService {
|
||||
type Request = RegisterCommandSignatureRequest;
|
||||
type Response = RegisterCommandSignatureResponse;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct LogServiceRequest {
|
||||
pub level: log::Level,
|
||||
pub target: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct LogServiceResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
/// A generic service for relaying log messages over IPC.
|
||||
pub struct LogService {}
|
||||
|
||||
impl ipc::Service for LogService {
|
||||
type Request = LogServiceRequest;
|
||||
type Response = LogServiceResponse;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "completions_v2")] {
|
||||
mod completions;
|
||||
pub use completions::*;
|
||||
}
|
||||
}
|
||||
mod call_js_function;
|
||||
mod logging;
|
||||
mod plugin_host_bootstrap;
|
||||
|
||||
pub use call_js_function::*;
|
||||
pub use logging::*;
|
||||
pub use plugin_host_bootstrap::*;
|
||||
@@ -0,0 +1,23 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PluginHostBootstrapRequest {
|
||||
pub connection_address: ipc::ConnectionAddress,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PluginHostBootstrapResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
/// This is a service implemented by the app process and called by the plugin host process to
|
||||
/// send a single request at startup containing the connection address for the plugin host server.
|
||||
///
|
||||
/// The connection address is used to instantiate an `ipc::Client` that can then be used to call
|
||||
/// plugin host services.
|
||||
pub struct PluginHostBootstrapService {}
|
||||
|
||||
impl ipc::Service for PluginHostBootstrapService {
|
||||
type Request = PluginHostBootstrapRequest;
|
||||
type Response = PluginHostBootstrapResponse;
|
||||
}
|
||||
Reference in New Issue
Block a user