Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+140
View File
@@ -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 }
}
}
+11
View File
@@ -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 },
}
}
}
+91
View File
@@ -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)
}
+62
View File
@@ -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) {}
}
+126
View File
@@ -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()
}
+167
View 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?)
}
}
+61
View File
@@ -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)),
},
}
}
}
+93
View File
@@ -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(())
}
}
+169
View File
@@ -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:?}"),
}
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
use anyhow::{anyhow, Result};
pub fn run() -> Result<()> {
Err(anyhow!("Plugin host unsupported on WASM"))
}
+17
View File
@@ -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;
}
+27
View File
@@ -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;
}
+21
View File
@@ -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;
}
+13
View File
@@ -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;
}