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
+290
View File
@@ -0,0 +1,290 @@
use std::{collections::HashMap, sync::Arc};
use futures::{channel::oneshot, future::FutureExt, io::BufReader, AsyncRead, AsyncWrite};
use warpui::r#async::executor::Background;
use crate::{platform::client::connect_client, protocol::Request};
use super::{
protocol::{
receive_message, send_message, ConnectionAddress, ProtocolError, RequestId, Response,
},
service::service_id,
Service,
};
#[derive(Debug)]
pub enum InitializationError {
Io(std::io::Error),
UnsupportedPlatform,
}
#[derive(thiserror::Error, Debug)]
pub enum ClientError {
#[error("Failed to initialize client: {0:?}")]
Initialization(InitializationError),
#[error("Connection was dropped.")]
Disconnected,
#[error("Internal error occurred: {0:?}")]
InternalProtocol(#[from] ProtocolError),
#[error("The channel for receiving the response from the inbound message task is closed.")]
ResponseChannelClosed,
#[error(
"The channel for transmitting pending request info to the inbound message task is closed."
)]
PendingRequestInfoChannelClosed,
}
pub type Result<T> = std::result::Result<T, ClientError>;
#[derive(Debug)]
struct PendingRequestInfo {
/// The ID of the in-flight request.
request_id: RequestId,
/// A sender for relaying the response bytes back to the caller of `send_request()`.
response_result_tx: oneshot::Sender<Result<Vec<u8>>>,
}
#[derive(Debug)]
struct OutboundRequest {
// The request to be sent to the server.
request: Request,
// A sender for relaying any error that occurs when sending the request.
//
// If the request is sent successfully, this sender is moved to the new `PendingRequestInfo`
// created for the request, where it is eventually used to relay response bytes back to the
// caller.
response_result_tx: oneshot::Sender<Result<Vec<u8>>>,
}
pub struct Client {
/// A sender for relaying requests from `Self::send_request()` to the background task
/// responsible for actually writing requests to the socket.
outbound_message_tx: async_channel::Sender<OutboundRequest>,
/// A receiver for a single-message bounded channel that emits an event when the server
/// connection is dropped.
disconnect_rx: async_channel::Receiver<()>,
/// A reference to the background executor so that we don't drop it while waiting on tasks
/// that use it to run to completion. Otherwise, it can hang when all references are dropped.
_background_executor: Arc<Background>,
}
impl Client {
/// Creates a client connected to a server corresponding to the given `connection_address`.
///
/// If successful, spawns background tasks to send requests and receive responses.
pub async fn connect(
connection_address: ConnectionAddress,
background_executor: Arc<Background>,
) -> Result<Self> {
let (reader, writer) = connect_client(connection_address).await?;
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
let (pending_request_info_tx, pending_request_info_rx) = async_channel::unbounded();
let disconnect_tx_clone = disconnect_tx.clone();
background_executor
.spawn(async move {
Self::handle_incoming_responses(reader, pending_request_info_rx).await;
let _ = disconnect_tx_clone.try_send(());
})
.detach();
let (outbound_message_tx, outbound_message_rx) = async_channel::unbounded();
background_executor
.spawn(async move {
Self::handle_outgoing_requests(
writer,
outbound_message_rx,
pending_request_info_tx,
)
.await;
let _ = disconnect_tx.try_send(());
})
.detach();
Ok(Self {
outbound_message_tx,
disconnect_rx,
_background_executor: background_executor,
})
}
pub async fn wait_for_disconnect(&self) {
let _ = self.disconnect_rx.recv().await;
}
/// Schedules the given message to be written to the underlying transport.
pub(super) async fn send_request<S: Service>(&self, request_bytes: Vec<u8>) -> Result<Vec<u8>> {
let request = Request::new(service_id::<S>(), request_bytes);
// Create a channel for the response result. The sending end is sent to the outbound
// message task. The outbound meessage task uses it to relay any error that might occur
// when sending the message. If the message is sent successfully, the sending end is
// forwarded to the _inbound_ message task, which will eventually use it to relay the
// response bytes.
let (response_result_tx, response_result_rx) = oneshot::channel();
if self
.outbound_message_tx
.send(OutboundRequest {
request,
response_result_tx,
})
.await
.is_err()
{
// The background inbound traffic processing task exited, so we must be disconnected.
return Err(ClientError::Disconnected);
}
match response_result_rx.await {
Ok(response_result) => response_result,
Err(_) => Err(ClientError::ResponseChannelClosed),
}
}
/// Handles incoming response messages and relays them back to the caller via a
/// request-specific async channel.
async fn handle_incoming_responses(
reader: impl AsyncRead + Unpin,
pending_request_info_rx: async_channel::Receiver<PendingRequestInfo>,
) {
let mut reader = BufReader::new(reader);
// Map from request ID to async channel sender, through which we should relay the
// corresponding response bytes.
let mut response_senders = HashMap::<RequestId, oneshot::Sender<Result<Vec<u8>>>>::new();
loop {
futures::select! {
pending_request_info = pending_request_info_rx.recv().fuse() => {
// TODO(zachbai): Because we're asynchronously receiving `PendingRequestInfo`
// from the outbound request task, it's possible that the response is actually
// received before the pending_request_info is received and handled by this
// block. We should hold onto unmatched responses for some small amount of time
// and check if new `PendingRequestInfo`s match the recently received responses.
// Similarly, its possible the server never responds to a request with a
// `PendingRequestInfo` -- we should implement timed cleanups of
// `PendingRequestInfo` (a request timeout) to address the possible memory leak.
match pending_request_info {
Ok(PendingRequestInfo {
request_id, response_result_tx
}) => {
// We've just sent a request, so update the `response_senders` map
// so we can relay the response back.
response_senders.insert(request_id, response_result_tx);
}
Err(_) => {
// This happens when the channel is closed, which implies the client
// was `Drop`ped, so break and exit.
break;
}
}
}
response = receive_message(&mut reader).fuse() => {
match response {
Ok(response) => {
let (request_id, response_result) = match response {
Response::Success {
request_id,
bytes: response_bytes,
..
} => {
(request_id, Ok(response_bytes))
}
Response::Failure {
request_id,
error_message,
} => {
(request_id, Err(ClientError::InternalProtocol(ProtocolError::Other(error_message))))
}
};
if let Some(response_result_tx) = response_senders.remove(&request_id) {
// The channel might be closed if the task that called
// `send_message` has been dropped, but that's ok.
let _ = response_result_tx.send(response_result);
} else {
// When there is no corresponding response_senders
// entry for the message's request ID, we weren't
// expecting it.
log::warn!("Received unexpected message with id {request_id}.");
}
}
Err(e) => {
match e {
ProtocolError::Disconnected(_)=> {
// The server was disconnected, so break and exit.
break;
}
e => {
log::warn!("Error occurred while receiving message: {e:?}");
}
}
}
}
}
complete => break,
}
}
}
/// Polls `outbound_message_rx` for request messages and sends them over the IPC transport.
///
/// If a request is sent successfully, the `response_result_tx` from the corresponding
/// `OutboundRequest` is sent to the _inbound_ response task, which sends the response through
/// it once received.
async fn handle_outgoing_requests(
mut writer: impl AsyncWrite + Unpin,
outbound_request_rx: async_channel::Receiver<OutboundRequest>,
pending_request_info_tx: async_channel::Sender<PendingRequestInfo>,
) {
while let Ok(OutboundRequest {
request,
response_result_tx,
}) = outbound_request_rx.recv().await
{
let request_id = *request.id();
match send_message(&mut writer, request).await {
Ok(()) => {
if pending_request_info_tx.is_closed() {
// The channel might be closed if the task that called
// `send_message` has been dropped, but that's ok.
let _ = response_result_tx
.send(Err(ClientError::PendingRequestInfoChannelClosed));
} else {
// Let the inbound traffic task know that we successfully sent a
// request, so it can relay the response back to the caller.
//
// We pass on the `response_result_tx` from the `OutboundRequest`
// object, which will eventually be used to relay the response.
let pending_request_info = PendingRequestInfo {
request_id,
response_result_tx,
};
let _ = pending_request_info_tx.send(pending_request_info).await;
}
}
Err(ProtocolError::Disconnected(_)) => {
// The channel might be closed if the task that called
// `send_message` has been dropped, but that's ok.
let _ = response_result_tx.send(Err(ClientError::Disconnected));
break;
}
Err(e) => {
// The channel might be closed if the task that called
// `send_message` has been dropped, but that's ok.
let _ = response_result_tx.send(Err(ClientError::InternalProtocol(e)));
}
}
}
}
}
+60
View File
@@ -0,0 +1,60 @@
//! This crate provides an ipmlementation of a basic IPC request/response protocol.
//!
//! Users may instantiate a server that implements any number of [`Service`]s as well as
//! corresponding typed "clients" ([`ServiceCaller`]s) which provide a typed interface to call the
//! services across process boundaries.
//!
//! This is intended to initially be used to support communication between the Warp app and
//! third-party plugins running in a separate "plugin host" process, but is designed generically to
//! be extended to other use cases (such as the terminal server). Where possible,
//! transport-specific details are abstracted out to eventually support the same protocol on top of
//! the WebWorkers `MessagePort` API in the browser for Warp on Web.
//!
//! On native platforms, this is implemented on top of the `interprocess` crate, which uses
//! Unix Domain Sockets on Unix platforms and named pipes on Windows as the underlying transport.
//!
//! WASM (wasm32-unknown-unknown) is currently unsupported.
//!
//!
//! Basic usage is like so:
//!
//! ```ignore
//! // In the server's process...
//! let background_executor = ctx.background_executor();
//!
//! // `MyServiceImpl` implements `ServiceImpl<Service = MyService>`.
//! let my_service_impl = MyServiceImpl::new();
//! let (server, connection_address) = ServerBuilder::default()
//! .with_service(my_service_impl)
//! .build_and_run(background_executor)
//! .expect("Failed to instantiate server");
//!
//! // In the client process, passing the same connection address returned from the server
//! // instantiation (possibly as an environment variable set in the client process).
//! let client = Arc::new(
//! Client::connect(connection_address, background_executor)
//! .await
//! .expect("Failed to connect client"),
//! );
//! let my_service_stub = service_caller::<MyService>(client);
//! let response = my_service_stub.call(MyServiceRequest { .. }).await;
//! ```
mod client;
mod protocol;
mod server;
mod service;
// Platform-specific implementations of the underlying transport for both server and client. For
// native platforms, this uses the `interprocess` crate. On wasm, we plan to use the WebWorkers
// MessagePort API, but this is not yet implemented.
#[cfg_attr(not(target_family = "wasm"), path = "native.rs")]
#[cfg_attr(target_family = "wasm", path = "wasm.rs")]
mod platform;
pub use client::{Client, ClientError};
pub use protocol::ConnectionAddress;
pub use server::{Server, ServerBuilder};
pub use service::{service_caller, Service, ServiceCaller, ServiceImpl};
#[cfg(test)]
pub mod testing;
+63
View File
@@ -0,0 +1,63 @@
//! This module implements IPC transport on top of the `interprocess` crate, which uses Unix Domain
//! Sockets on Unix platforms and named pipes on Windows under the hood.
use async_compat::CompatExt as _;
use futures::{AsyncRead, AsyncWrite};
use crate::ConnectionAddress;
pub(crate) mod client {
use super::*;
use crate::client::{ClientError, InitializationError, Result};
use interprocess::local_socket::tokio::LocalSocketStream;
/// Returns a tuple containing structs for reading and writing to a local socket, which is the
/// underlying IPC transport for native (non-wasm) platforms.
pub async fn connect_client(
connection_address: ConnectionAddress,
) -> Result<(impl AsyncRead + Unpin, impl AsyncWrite + Unpin)> {
let stream = LocalSocketStream::connect(connection_address.0.as_str())
.compat()
.await
.map_err(|e| ClientError::Initialization(InitializationError::Io(e)))?;
Ok(stream.into_split())
}
}
pub(crate) mod server {
use super::*;
use crate::server::{InitializationError, Result, ServerError};
use interprocess::local_socket::tokio::{LocalSocketListener, LocalSocketStream};
pub struct ConnectionImpl {
stream: LocalSocketStream,
}
impl ConnectionImpl {
pub fn into_split(self) -> (impl AsyncRead + Unpin, impl AsyncWrite + Unpin) {
self.stream.into_split()
}
}
pub struct ConnectionListenerImpl {
listener: LocalSocketListener,
}
impl ConnectionListenerImpl {
pub fn new(connection_address: ConnectionAddress) -> Result<Self> {
let listener = warpui::r#async::block_on(
async move { LocalSocketListener::bind(connection_address.to_string()) }.compat(),
)
.map_err(|e| ServerError::Initialization(InitializationError::Io(e)))?;
Ok(Self { listener })
}
pub async fn accept_connection(&self) -> Result<ConnectionImpl> {
self.listener
.accept()
.compat()
.await
.map(|stream| ConnectionImpl { stream })
.map_err(ServerError::AcceptConnection)
}
}
}
+191
View File
@@ -0,0 +1,191 @@
use std::{
fmt::{Debug, Display},
marker::Unpin,
};
use futures::{
io::{AsyncReadExt, AsyncWriteExt, BufReader},
AsyncRead, AsyncWrite,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use uuid::Uuid;
use super::service::ServiceId;
/// The size of a usize, in bytes.
const USIZE_SIZE: usize = std::mem::size_of::<usize>();
/// Unique "address" for a server/client connection.
///
/// In the case of this local socket implementation, this is a socket address (path on the
/// filesystem). Conceptually, this somewhat similar to an IP address + port.
#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct ConnectionAddress(pub(super) String);
impl ConnectionAddress {
/// Returns a `ConnectionAddress` containing a path for a socket address.
pub(super) fn new() -> Self {
Self(format!("/tmp/warp-ipc-{}.sock", rand::random::<i64>()))
}
}
impl Display for ConnectionAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for ConnectionAddress {
fn from(value: String) -> Self {
ConnectionAddress(value)
}
}
/// A unique ID for each request message.
///
/// The corresponding response for the request should contain the same ID.
pub(super) type RequestId = Uuid;
/// Trait for arbitrary messages that may be sent across the 'wire' (the socket).
pub trait Message: 'static + Send + Sync + Debug + Clone + DeserializeOwned + Serialize {}
impl<T> Message for T where T: 'static + Send + Sync + Debug + Clone + DeserializeOwned + Serialize {}
/// Request message sent by clients and received by servers.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub(super) struct Request {
/// A unique ID for the request.
pub(super) id: RequestId,
/// The ID of the service to which this request belongs.
pub(super) service_id: ServiceId,
/// The actual request payload.
pub(super) bytes: Vec<u8>,
}
impl Request {
/// Constructs a `Request`, generating a unique request ID in the process.
pub(super) fn new(service_id: ServiceId, bytes: Vec<u8>) -> Self {
Self {
id: Uuid::new_v4(),
service_id,
bytes,
}
}
pub(super) fn id(&self) -> &RequestId {
&self.id
}
}
/// Response message sent by servers and received by clients.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub(super) enum Response {
/// For responses produced "successfully". "Successful" only pertains to the frameworks ability
/// to successfully execute the `Service` handler and produce a response. `Service`s may
/// internally implement their own error types/response schemas.
Success {
/// The ID of the request for which this is a response.
request_id: RequestId,
/// The ID of the service to which this response belongs.
service_id: ServiceId,
/// The actual response payload.
bytes: Vec<u8>,
},
/// For responses that failed due to a framework-level issue. For example, the client attempted
/// to call a service that wasn't registered in the server.
Failure {
/// The ID of the request for which this is a response.
request_id: RequestId,
error_message: String,
},
}
impl Response {
/// Constructs a "success" response for the request with the given `request_id`.
pub(super) fn success(request_id: RequestId, service_id: ServiceId, bytes: Vec<u8>) -> Self {
Self::Success {
request_id,
service_id,
bytes,
}
}
/// Constructs a "failure" response for the request with the given `request_id`.
pub(super) fn failure(request_id: RequestId, error_message: String) -> Self {
Self::Failure {
request_id,
error_message,
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ProtocolError {
/// An error occurred when serializing the request or response.
#[error(transparent)]
Serialization(#[from] bincode::Error),
/// The connection was dropped.
#[error(transparent)]
Disconnected(#[from] std::io::Error),
#[error("Unknown error occurred: {0}")]
Other(String),
}
/// Writes the given message to the given `writer`.
pub(super) async fn send_message<M, W>(writer: &mut W, message: M) -> Result<(), ProtocolError>
where
M: Message,
W: AsyncWrite + Unpin,
{
let serialized_msg = bincode::serialize(&message)?;
// Create a buffer to hold the data to be written.
let mut buf = Vec::with_capacity(serialized_msg.len() + USIZE_SIZE);
// First, add a message "header" - a usize representing the length of the
// serialized payload, in bytes.
buf.extend_from_slice(&serialized_msg.len().to_be_bytes());
// Next, add the serialized payload itself.
buf.extend(serialized_msg);
// Finally, write the buffer to the underlying transport.
Ok(writer.write_all(&buf[..]).await?)
}
/// Reads the next message from the given `reader`.
pub(super) async fn receive_message<M, R>(reader: &mut BufReader<R>) -> Result<M, ProtocolError>
where
M: Message,
R: AsyncRead + Unpin,
{
// Start by allocating a buffer that is only large enough to receive the
// message header, to ensure we don't accidentally receive multiple messages
// in a single read.
let mut header_buf = [0; USIZE_SIZE];
// Read the message "header" from the socket.
reader.read_exact(&mut header_buf[..]).await?;
// Parse the message header - we convert the bytes back into a usize, which
// tells us the size of the serialized message, in bytes. We add the size
// of a usize to get the total number of bytes we expect to read off the
// wire.
let payload_len = usize::from_be_bytes(header_buf);
// Grow the initial buffer to a sufficient size and read the rest of the
// message from the socket.
let mut payload_buf = vec![0; payload_len];
reader.read_exact(&mut payload_buf).await?;
// Deserialize the message.
let message: M = bincode::deserialize(&payload_buf[..])?;
Ok(message)
}
+329
View File
@@ -0,0 +1,329 @@
use std::{collections::HashMap, sync::Arc};
use async_channel::{Receiver, Sender};
use async_trait::async_trait;
use futures::{io::BufReader, AsyncRead, AsyncWrite};
use warpui::r#async::executor::{Background, BackgroundTask};
use crate::{
platform::server::{ConnectionImpl, ConnectionListenerImpl},
service::ServiceImpl,
};
use super::{
protocol::{
receive_message, send_message, ConnectionAddress, ProtocolError, Request, Response,
},
service::{service_id, Service, ServiceId},
};
/// Helper trait to enable storing a polymorphic collection of `ServiceImpl` implementions in
/// `Server`.
///
/// This is akin to the `AnyView` and `AnyModel` traits used by the UI framework to similarly store
/// `View` callbacks that are actually parameterized by the type of the actual `View`
/// implementation.
#[async_trait]
pub(super) trait AnyServiceImpl: Send + Sync {
async fn handle_request(&self, request: &[u8]) -> Vec<u8>;
fn clone_service(&self) -> Box<dyn AnyServiceImpl>;
}
#[async_trait]
impl<I, S> AnyServiceImpl for I
where
S: Service,
I: ServiceImpl<Service = S> + Clone + Sized,
{
async fn handle_request(&self, request_bytes: &[u8]) -> Vec<u8> {
let request: S::Request =
bincode::deserialize(request_bytes).expect("Failed to deserialize request bytes.");
bincode::serialize::<S::Response>(&I::handle_request(self, request).await)
.expect("Should be able to serialize response.")
}
fn clone_service(&self) -> Box<dyn AnyServiceImpl> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn AnyServiceImpl> {
fn clone(&self) -> Self {
self.clone_service()
}
}
#[derive(Debug)]
pub enum InitializationError {
Io(std::io::Error),
UnsupportedPlatform,
}
#[derive(thiserror::Error, Debug)]
pub enum ServerError {
#[error("Failed to initialize server: {0:?}")]
Initialization(InitializationError),
#[error("Failed to accept connection: {0:?}")]
AcceptConnection(std::io::Error),
}
pub type Result<T> = std::result::Result<T, ServerError>;
/// A wrapper struct for abstracting-away platform-specific implementations for the server
/// functionality that listens for and accepts new connections.
struct ConnectionListener(ConnectionListenerImpl);
impl ConnectionListener {
fn new(connection_address: ConnectionAddress) -> Result<Self> {
ConnectionListenerImpl::new(connection_address).map(Self)
}
/// Waits until a client connects and returns the connection.
async fn accept_connection(&self) -> Result<Connection> {
self.0.accept_connection().await.map(Connection)
}
}
/// A wrapper struct for abstracting-away platform-specific implementations of the underlying
/// transport for the IPC connection.
///
/// The main property of a [`Connection`] is that it can be consumed to create read and write
/// 'halves' which can be used to asynchronously read/write bytes to/from the transport.
struct Connection(ConnectionImpl);
impl Connection {
/// Returns an `AsyncRead` impl to read bytes from the transport and `AsyncWrite` to write
/// bytes to the transport.
fn into_split(self) -> (impl AsyncRead + Unpin, impl AsyncWrite + Unpin) {
self.0.into_split()
}
}
/// Helper struct for building and running a server.
///
/// Usage:
///
/// ```ignore
/// let (server, connection_address) = ServerBuilder::default()
/// // Implements `ServiceImpl<MyService>`.
/// .with_service(MyServiceImpl::new())
/// .build_and_run()
/// .expect("Failed to run server.");
/// ```
#[derive(Default)]
pub struct ServerBuilder {
services: HashMap<ServiceId, Box<dyn AnyServiceImpl>>,
fixed_connection_address: Option<ConnectionAddress>,
}
impl ServerBuilder {
pub fn with_service<S: ServiceImpl + Sized>(mut self, service_impl: S) -> Self {
self.services
.insert(service_id::<S::Service>(), Box::new(service_impl));
self
}
/// Use a fixed address name instead of a randomly generated one.
pub fn with_fixed_address(mut self, fixed_address: String) -> Self {
self.fixed_connection_address = Some(ConnectionAddress::from(fixed_address));
self
}
/// Instantiates a `Server` which listens for incoming client connections.
///
/// If the server instantiation fails, returns an error.
pub fn build_and_run(
self,
background_executor: Arc<Background>,
) -> Result<(Server, ConnectionAddress)> {
let connection_address =
if let Some(fixed_connection_address) = self.fixed_connection_address {
fixed_connection_address
} else {
ConnectionAddress::new()
};
Server::run(
connection_address.clone(),
self.services,
background_executor,
)
.map(|server| (server, connection_address))
}
}
/// Serves registered `Service` implementations over platform-specific IPC transport.
///
/// Two background tasks are spawned for each client connection -- one for processing incoming
/// requests and one for sending outgoing responses.
pub struct Server {
_tasks: Vec<BackgroundTask>,
}
impl Server {
/// Runs the main server tasks.
///
/// Two main tasks are spawned immediately -- one for listening for incoming client connections
/// and one for "accepting" connections that were found. When "accepting" a connection, two
/// additional connection-specific tasks are spawned -- one for processing incoming requests
/// and one for sending outbound responses.
fn run(
connection_address: ConnectionAddress,
services: HashMap<ServiceId, Box<dyn AnyServiceImpl>>,
background_executor: Arc<Background>,
) -> Result<Self> {
let listener = ConnectionListener::new(connection_address)?;
// Spawn two separate background tasks. The first is responsible for listening for new
// client connections and passing them to the second, which itself spawns tasks to process
// inbound requests and outbound responses from each connection.
//
// A channel is used to pass connections between the two tasks.
let (new_connection_tx, new_connection_rx) = async_channel::unbounded();
let tasks = vec![
background_executor.spawn(Self::listen_for_new_connections(
listener,
new_connection_tx,
)),
background_executor.spawn(Self::accept_new_connections(
services,
new_connection_rx,
background_executor.clone(),
)),
];
Ok(Self { _tasks: tasks })
}
/// Listens for new connections on `listener`, relaying them through the given sender.
async fn listen_for_new_connections(
listener: ConnectionListener,
new_connection_tx: Sender<Connection>,
) {
loop {
match listener.accept_connection().await {
Ok(stream) => {
if new_connection_tx.send(stream).await.is_err() {
// The task responsible for handling new connections has
// exited, so break and exit too.
return;
}
}
Err(e) => {
log::warn!("Could not establish connection with client: {e:?}");
}
}
}
}
/// Receives new connections from the given `Receiver` and spawns dedicated background tasks
/// for processing incoming request messages and outgoing response messages.
async fn accept_new_connections(
services: HashMap<ServiceId, Box<dyn AnyServiceImpl>>,
new_connection_rx: Receiver<Connection>,
background_executor: Arc<Background>,
) {
// Maintain references to the task handles so they're cancelled when this is dropped.
let mut tasks = vec![];
loop {
let Ok(connection) = new_connection_rx.recv().await else {
// The task responsible for listening for new connections has exited, so
// break and exit too.
return;
};
let (reader, writer) = connection.into_split();
let (response_tx, response_rx) = async_channel::unbounded::<Response>();
tasks.push(background_executor.spawn(Self::handle_incoming_requests(
reader,
services.clone(),
response_tx,
)));
tasks.push(
background_executor.spawn(Self::handle_outgoing_responses(writer, response_rx)),
);
}
}
/// Processes incoming request messages.
///
/// This includes deserializing the request message into a `Service`-specific request type,
/// dispatching the request to the `Service` itself, and sending the resulting response thru
/// the given `response_tx`.
///
/// The receiving end of the `response_tx` channel is processed in a separate task dedicated to
/// sending outbound messages back to the client.
async fn handle_incoming_requests(
reader: impl AsyncRead + Unpin,
services: HashMap<ServiceId, Box<dyn AnyServiceImpl>>,
response_tx: Sender<Response>,
) {
let mut reader = BufReader::new(reader);
loop {
match receive_message(&mut reader).await {
Ok(Request {
id,
service_id,
bytes,
}) => {
let response_message = match services.get(&service_id) {
Some(service) => {
let response_bytes = service.handle_request(&bytes[..]).await;
Response::success(id, service_id, response_bytes)
}
None => {
Response::failure(id, format!("No such service (ID: {service_id})"))
}
};
if response_tx.send(response_message).await.is_err() {
// This means the response_tx channel is closed, which probably
// means the outgoing messages task has exited. So this task should
// exit too.
break;
}
}
Err(e) => {
match e {
ProtocolError::Serialization(e) => {
log::warn!("Failed to deserialize request: {e:?}");
}
ProtocolError::Disconnected(_) => {
// The socket is disconnected, so exit.
log::warn!("IPC server disconnected unexpectedly.");
break;
}
e => {
log::warn!("Unknown error occurred when receiving request: {e:?}");
}
}
}
}
}
}
/// Process outgoing response messages, received from the given `response_rx` receiver.
async fn handle_outgoing_responses(
mut writer: impl AsyncWrite + Unpin,
response_rx: Receiver<Response>,
) {
while let Ok(message) = response_rx.recv().await {
if let Err(e) = send_message(&mut writer, message).await {
match e {
ProtocolError::Serialization(e) => {
log::warn!("Failed to serialize response: {e:?}");
}
ProtocolError::Disconnected(_) => {
// The socket is disconnected, so exit.
break;
}
e => {
log::warn!("Unknown error occurred when sending response: {e:?}");
}
}
}
}
}
}
+97
View File
@@ -0,0 +1,97 @@
use std::{marker::PhantomData, sync::Arc};
use async_trait::async_trait;
use crate::{protocol::Message, Client, ClientError};
pub(crate) type ServiceId = String;
/// Returns a unique ID for the `Service` implementation specified as a type parameter.
pub(crate) fn service_id<S: Service>() -> ServiceId {
std::any::type_name::<S>().to_owned()
}
/// A typed IPC service interface.
///
/// Implementations should implement `ServiceImpl` and be registered on the server, while clients
/// can use `ServiceCaller` to call the service.
#[async_trait]
pub trait Service: Send + Sync + 'static {
type Request: Message + 'static;
type Response: Message + 'static;
}
/// To be implemented for each IPC service, where a service has a defined request/response type.
///
/// This should be implemented and is registered on the "Server" side.
///
/// Though it is technically up to users to determine whether to use a collection of `Service`s
/// or implement a single service that delegates internally, prefer the former.
#[async_trait]
pub trait ServiceImpl: 'static + Send + Sync + Clone {
type Service: Service;
async fn handle_request(
&self,
request: <<Self as ServiceImpl>::Service as Service>::Request,
) -> <<Self as ServiceImpl>::Service as Service>::Response;
}
/// Provides an typed interface to call an underlying `Service`.
///
/// Usage:
///
/// ```ignore
/// let client = Arc::new(
/// Client::connect(connection_address, executor)
/// .await
/// .expect("Failed to connect client."),
/// );
/// let foo = ServiceCaller::<FooService>::new(client);
/// let response = foo.call(FooRequest {}).await;
/// ```
#[async_trait]
pub trait ServiceCaller<S: Service>: Send + Sync {
async fn call(&self, request: S::Request) -> Result<S::Response, ClientError>;
}
/// Returns a `ServiceCaller` implementation for the service `S`.
pub fn service_caller<S: Service>(client: Arc<Client>) -> Box<dyn ServiceCaller<S>> {
Box::new(RealServiceCaller::new(client))
}
/// Real `ServiceCaller` implementation.
struct RealServiceCaller<S> {
client: Arc<Client>,
_service_type_marker: PhantomData<S>,
}
impl<S> RealServiceCaller<S>
where
S: Service,
{
fn new(client: Arc<Client>) -> Self {
Self {
client,
_service_type_marker: PhantomData,
}
}
}
#[async_trait]
impl<S> ServiceCaller<S> for RealServiceCaller<S>
where
S: Service,
{
/// Sends the given request and returns a `Result` containing its response.
async fn call(&self, request: S::Request) -> Result<S::Response, ClientError> {
let request_bytes = bincode::serialize(&request).expect("Failed to serialize request.");
self.client
.send_request::<S>(request_bytes)
.await
.map(|response_bytes| {
bincode::deserialize::<S::Response>(&response_bytes[..])
.expect("Failed to deserialize response.")
})
}
}
+91
View File
@@ -0,0 +1,91 @@
//! Mock implementation of `ServiceCaller` for use in tests.
//!
//! This requires service request to implement `Hash`, `PartialEq`, `Eq` to make assertions about
//! expected requests.
//!
//! If any unexpected request is called, then the caller panics. Additionally, if any expected
//! requests were _not_ actually called, the caller panics upon being dropped.
//!
//! Usage:
//!
//! ```
//! let mock_caller = MockServiceCaller::<MyService>::new();
//! mock_caller.expect_response(MyRequest { foo: "bar" }, MyResponse { bar: "baz" });
//! ```
use std::collections::HashMap;
use std::hash::Hash;
use async_trait::async_trait;
use itertools::Itertools;
use parking_lot::Mutex;
use crate::{service::service_id, ClientError, Service, ServiceCaller};
// Use a `Mutex` so we can satisfy the immutable `&self` in the implementation of `ServiceCaller`.
type ExpectationsMap<T, U> = Mutex<HashMap<T, Result<U, ClientError>>>;
#[derive(Default)]
pub struct MockServiceCaller<S>
where
S: Service,
<S as Service>::Request: Hash + PartialEq + Eq,
<S as Service>::Response: Hash + PartialEq + Eq,
{
expectations: ExpectationsMap<S::Request, S::Response>,
}
impl<S> MockServiceCaller<S>
where
S: Service,
<S as Service>::Request: Hash + PartialEq + Eq,
<S as Service>::Response: Hash + PartialEq + Eq,
{
pub fn new() -> Self {
Self {
expectations: Mutex::new(HashMap::default()),
}
}
pub fn expect_response(
&mut self,
expected_request: S::Request,
response: Result<S::Response, ClientError>,
) {
self.expectations.lock().insert(expected_request, response);
}
}
#[async_trait]
impl<S> ServiceCaller<S> for MockServiceCaller<S>
where
S: Service,
<S as Service>::Request: Hash + PartialEq + Eq,
<S as Service>::Response: Hash + PartialEq + Eq,
{
async fn call(&self, request: S::Request) -> Result<S::Response, ClientError> {
let response = self.expectations.lock().remove(&request);
match response {
Some(result) => result,
None => {
panic!("Unexpected IPC call with request: {:?}", &request);
}
}
}
}
impl<S> Drop for MockServiceCaller<S>
where
S: Service,
<S as Service>::Request: Hash + PartialEq + Eq,
<S as Service>::Response: Hash + PartialEq + Eq,
{
fn drop(&mut self) {
if !self.expectations.lock().is_empty() {
panic!(
"ServiceCaller for {} has unmet expectations: {:?}",
service_id::<S>(),
self.expectations.lock().drain().collect_vec()
);
}
}
}
+61
View File
@@ -0,0 +1,61 @@
//! This module provides a fake, "placeholder" implementation of IPC transport for wasm targets.
//!
//! Eventually, this module will implement transport on top of the WebWorkers MessagePort API.
use futures::{AsyncRead, AsyncWrite};
use crate::ConnectionAddress;
pub(crate) mod client {
use crate::client::{ClientError, InitializationError, Result};
use super::*;
pub async fn connect_client(
_connection_address: ConnectionAddress,
) -> Result<(futures::io::Empty, futures::io::Sink)> {
Err(ClientError::Initialization(
InitializationError::UnsupportedPlatform,
))
}
}
pub(crate) mod server {
use super::*;
use crate::server::{InitializationError, Result, ServerError};
/// "Fake" implementation. Note that because a `ConnectionListenerImpl` can't be instantiated,
/// a `ConnectionImpl` cannot actually be instantiated either.
pub struct ConnectionImpl {
/// A dummy placeholder field to prevent instantiation of a Connection because this crate
/// currently doesn't support wasm.
_marker: bool,
}
impl ConnectionImpl {
pub fn into_split(self) -> (impl AsyncRead + Unpin, impl AsyncWrite + Unpin) {
(futures::io::empty(), futures::io::sink())
}
}
/// "Fake" implementation that cannot actually be initialized.
pub struct ConnectionListenerImpl {
/// A dummy placeholder field to prevent instantiation of a ConnectionListener because this crate
/// currently doesn't support wasm.
_marker: bool,
}
impl ConnectionListenerImpl {
/// Returns an unsupported platform error, since this crate currently doesn't support wasm.
pub fn new(_connection_address: ConnectionAddress) -> Result<Self> {
Err(ServerError::Initialization(
InitializationError::UnsupportedPlatform,
))
}
pub async fn accept_connection(&self) -> Result<ConnectionImpl> {
// This can never be called because its impossible to instantiate a ConnectionListener (on
// wasm).
unreachable!("ConnectionListener cannot be instantiated when targeting wasm.")
}
}
}