logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

43
crates/rpc/Cargo.toml Normal file
View File

@@ -0,0 +1,43 @@
[package]
description = "Shared logic for communication between the Zed app and the zed.dev server"
edition.workspace = true
name = "rpc"
version = "0.1.0"
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/rpc.rs"
doctest = false
[features]
gpui = ["dep:gpui"]
test-support = ["collections/test-support", "gpui/test-support", "proto/test-support"]
[dependencies]
anyhow.workspace = true
async-tungstenite.workspace = true
base64.workspace = true
collections.workspace = true
futures.workspace = true
gpui = { workspace = true, optional = true }
parking_lot.workspace = true
proto.workspace = true
rand.workspace = true
rsa.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
strum.workspace = true
tracing = { version = "0.1.34", features = ["log"] }
util.workspace = true
zstd.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
proto = { workspace = true, features = ["test-support"] }
zlog.workspace = true

1
crates/rpc/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

240
crates/rpc/src/auth.rs Normal file
View File

@@ -0,0 +1,240 @@
use anyhow::{Context as _, Result};
use base64::prelude::*;
use rand::prelude::*;
use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey};
use rsa::traits::PaddingScheme;
use rsa::{Oaep, Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};
use sha2::Sha256;
use std::convert::TryFrom;
fn oaep_sha256_padding() -> impl PaddingScheme {
Oaep::new::<Sha256>()
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum EncryptionFormat {
/// The original encryption format.
///
/// This is using [`Pkcs1v15Encrypt`], which is vulnerable to side-channel attacks.
/// As such, we're in the process of phasing it out.
///
/// See [here](https://people.redhat.com/~hkario/marvin/) for more details.
V0,
/// The new encryption key format using Optimal Asymmetric Encryption Padding (OAEP) with a SHA-256 digest.
V1,
}
pub struct PublicKey(RsaPublicKey);
pub struct PrivateKey(RsaPrivateKey);
/// Generate a public and private key for asymmetric encryption.
pub fn keypair() -> Result<(PublicKey, PrivateKey)> {
let mut rng = RsaRngCompat::new();
let bits = 2048;
let private_key = RsaPrivateKey::new(&mut rng, bits)?;
let public_key = RsaPublicKey::from(&private_key);
Ok((PublicKey(public_key), PrivateKey(private_key)))
}
/// Generate a random 64-character base64 string.
pub fn random_token() -> String {
let mut rng = rand::rng();
let mut token_bytes = [0; 48];
for byte in token_bytes.iter_mut() {
*byte = rng.random();
}
BASE64_URL_SAFE.encode(token_bytes)
}
impl PublicKey {
/// Convert a string to a base64-encoded string that can only be decoded with the corresponding
/// private key.
pub fn encrypt_string(&self, string: &str, format: EncryptionFormat) -> Result<String> {
let mut rng = RsaRngCompat::new();
let bytes = string.as_bytes();
let encrypted_bytes = match format {
EncryptionFormat::V0 => self.0.encrypt(&mut rng, Pkcs1v15Encrypt, bytes),
EncryptionFormat::V1 => self.0.encrypt(&mut rng, oaep_sha256_padding(), bytes),
}
.context("failed to encrypt string with public key")?;
let encrypted_string = BASE64_URL_SAFE.encode(&encrypted_bytes);
Ok(encrypted_string)
}
}
impl PrivateKey {
/// Decrypt a base64-encoded string that was encrypted by the corresponding public key.
pub fn decrypt_string(&self, encrypted_string: &str) -> Result<String> {
let encrypted_bytes = BASE64_URL_SAFE
.decode(encrypted_string)
.context("failed to base64-decode encrypted string")?;
let bytes = self
.0
.decrypt(oaep_sha256_padding(), &encrypted_bytes)
.or_else(|_err| {
// If we failed to decrypt using the new format, try decrypting with the old
// one to handle mismatches between the client and server.
self.0.decrypt(Pkcs1v15Encrypt, &encrypted_bytes)
})
.context("failed to decrypt string with private key")?;
let string = String::from_utf8(bytes).context("decrypted content was not valid utf8")?;
Ok(string)
}
}
impl TryFrom<PublicKey> for String {
type Error = anyhow::Error;
fn try_from(key: PublicKey) -> Result<Self> {
let bytes = key
.0
.to_pkcs1_der()
.context("failed to serialize public key")?;
let string = BASE64_URL_SAFE.encode(&bytes);
Ok(string)
}
}
impl TryFrom<String> for PublicKey {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self> {
let bytes = BASE64_URL_SAFE
.decode(&value)
.context("failed to base64-decode public key string")?;
let key = Self(RsaPublicKey::from_pkcs1_der(&bytes).context("failed to parse public key")?);
Ok(key)
}
}
// TODO: remove once we rsa v0.10 is released.
struct RsaRngCompat(rand::rngs::ThreadRng);
impl RsaRngCompat {
fn new() -> Self {
Self(rand::rng())
}
}
impl rsa::signature::rand_core::RngCore for RsaRngCompat {
fn next_u32(&mut self) -> u32 {
self.0.next_u32()
}
fn next_u64(&mut self) -> u64 {
self.0.next_u64()
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rsa::signature::rand_core::Error> {
self.fill_bytes(dest);
Ok(())
}
}
impl rsa::signature::rand_core::CryptoRng for RsaRngCompat {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_encrypt_and_decrypt_token() {
// CLIENT:
// * generate a keypair for asymmetric encryption
// * serialize the public key to send it to the server.
let (public, private) = keypair().unwrap();
let public_string = String::try_from(public).unwrap();
assert_printable(&public_string);
// SERVER:
// * parse the public key
// * generate a random token.
// * encrypt the token using the public key.
let public = PublicKey::try_from(public_string).unwrap();
let token = random_token();
let encrypted_token = public.encrypt_string(&token, EncryptionFormat::V1).unwrap();
assert_eq!(token.len(), 64);
assert_ne!(encrypted_token, token);
assert_printable(&token);
assert_printable(&encrypted_token);
// CLIENT:
// * decrypt the token using the private key.
let decrypted_token = private.decrypt_string(&encrypted_token).unwrap();
assert_eq!(decrypted_token, token);
}
#[test]
fn test_generate_encrypt_and_decrypt_token_with_v0_encryption_format() {
// CLIENT:
// * generate a keypair for asymmetric encryption
// * serialize the public key to send it to the server.
let (public, private) = keypair().unwrap();
let public_string = String::try_from(public).unwrap();
assert_printable(&public_string);
// SERVER:
// * parse the public key
// * generate a random token.
// * encrypt the token using the public key.
let public = PublicKey::try_from(public_string).unwrap();
let token = random_token();
let encrypted_token = public.encrypt_string(&token, EncryptionFormat::V0).unwrap();
assert_eq!(token.len(), 64);
assert_ne!(encrypted_token, token);
assert_printable(&token);
assert_printable(&encrypted_token);
// CLIENT:
// * decrypt the token using the private key.
let decrypted_token = private.decrypt_string(&encrypted_token).unwrap();
assert_eq!(decrypted_token, token);
}
#[test]
fn test_encode_and_decode_base64_public_key() {
// A base64-encoded public key.
//
// We're using a literal string to ensure that encoding and decoding works across differences in implementations.
let encoded_public_key = "MIGJAoGBAMPvufou8wOuUIF1Wlkbtn0ZMM9nC55QJ06nTZvgMfZv5esFVU9-cQO_JC1P9ZoEcMDJweFERnQuQLqzsrMDLFbkdgL128ZU43WOLiQraxaICFIZsPUeTtWMKp2D5bPWsNxs-lnCma7vCAry6fpXuj5AKQdk7cTZJNucgvZQ0uUfAgMBAAE=".to_string();
// Make sure we can parse the public key.
let public_key = PublicKey::try_from(encoded_public_key.clone()).unwrap();
// Make sure we re-encode to the same format.
assert_eq!(encoded_public_key, String::try_from(public_key).unwrap());
}
#[test]
fn test_tokens_are_always_url_safe() {
for _ in 0..5 {
let token = random_token();
let (public_key, _) = keypair().unwrap();
let encrypted_token = public_key
.encrypt_string(&token, EncryptionFormat::V1)
.unwrap();
let public_key_str = String::try_from(public_key).unwrap();
assert_printable(&token);
assert_printable(&public_key_str);
assert_printable(&encrypted_token);
}
}
fn assert_printable(token: &str) {
for c in token.chars() {
assert!(
c.is_ascii_graphic(),
"token {:?} has non-printable char {}",
token,
c
);
assert_ne!(c, '/', "token {:?} is not URL-safe", token);
assert_ne!(c, '&', "token {:?} is not URL-safe", token);
}
}
}

102
crates/rpc/src/conn.rs Normal file
View File

@@ -0,0 +1,102 @@
use async_tungstenite::tungstenite::Message as WebSocketMessage;
use futures::{SinkExt as _, StreamExt as _};
pub struct Connection {
pub(crate) tx:
Box<dyn 'static + Send + Unpin + futures::Sink<WebSocketMessage, Error = anyhow::Error>>,
pub(crate) rx:
Box<dyn 'static + Send + Unpin + futures::Stream<Item = anyhow::Result<WebSocketMessage>>>,
}
impl Connection {
pub fn new<S>(stream: S) -> Self
where
S: 'static
+ Send
+ Unpin
+ futures::Sink<WebSocketMessage, Error = anyhow::Error>
+ futures::Stream<Item = anyhow::Result<WebSocketMessage>>,
{
let (tx, rx) = stream.split();
Self {
tx: Box::new(tx),
rx: Box::new(rx),
}
}
pub async fn send(&mut self, message: WebSocketMessage) -> anyhow::Result<()> {
self.tx.send(message).await
}
#[cfg(any(test, feature = "test-support"))]
pub fn in_memory(
executor: gpui::BackgroundExecutor,
) -> (Self, Self, std::sync::Arc<std::sync::atomic::AtomicBool>) {
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering::SeqCst},
};
let killed = Arc::new(AtomicBool::new(false));
let (a_tx, a_rx) = channel(killed.clone(), executor.clone());
let (b_tx, b_rx) = channel(killed.clone(), executor);
return (
Self { tx: a_tx, rx: b_rx },
Self { tx: b_tx, rx: a_rx },
killed,
);
#[allow(clippy::type_complexity)]
fn channel(
killed: Arc<AtomicBool>,
executor: gpui::BackgroundExecutor,
) -> (
Box<dyn Send + Unpin + futures::Sink<WebSocketMessage, Error = anyhow::Error>>,
Box<dyn Send + Unpin + futures::Stream<Item = anyhow::Result<WebSocketMessage>>>,
) {
use anyhow::anyhow;
use futures::channel::mpsc;
use std::io::Error;
let (tx, rx) = mpsc::unbounded::<WebSocketMessage>();
let tx = tx.sink_map_err(|error| anyhow!(error)).with({
let killed = killed.clone();
let executor = executor.clone();
move |msg| {
let killed = killed.clone();
let executor = executor.clone();
Box::pin(async move {
executor.simulate_random_delay().await;
// Writes to a half-open TCP connection will error.
if killed.load(SeqCst) {
std::io::Result::Err(Error::other("connection lost"))?;
}
Ok(msg)
})
}
});
let rx = rx.then({
move |msg| {
let killed = killed.clone();
let executor = executor.clone();
Box::pin(async move {
executor.simulate_random_delay().await;
// Reads from a half-open TCP connection will hang.
if killed.load(SeqCst) {
futures::future::pending::<()>().await;
}
Ok(msg)
})
}
});
(Box::new(tx), Box::new(rx))
}
}
}

73
crates/rpc/src/macros.rs Normal file
View File

@@ -0,0 +1,73 @@
#[macro_export]
macro_rules! messages {
($(($name:ident, $priority:ident)),* $(,)?) => {
pub fn build_typed_envelope(sender_id: ConnectionId, received_at: Instant, envelope: Envelope) -> Option<Box<dyn AnyTypedEnvelope>> {
match envelope.payload {
$(Some(envelope::Payload::$name(payload)) => {
Some(Box::new(TypedEnvelope {
sender_id,
original_sender_id: envelope.original_sender_id.map(|original_sender| PeerId {
owner_id: original_sender.owner_id,
id: original_sender.id
}),
message_id: envelope.id,
payload,
received_at,
}))
}, )*
_ => None
}
}
$(
impl EnvelopedMessage for $name {
const NAME: &'static str = std::stringify!($name);
const PRIORITY: MessagePriority = MessagePriority::$priority;
fn into_envelope(
self,
id: u32,
responding_to: Option<u32>,
original_sender_id: Option<PeerId>,
) -> Envelope {
Envelope {
id,
responding_to,
original_sender_id,
payload: Some(envelope::Payload::$name(self)),
}
}
fn from_envelope(envelope: Envelope) -> Option<Self> {
if let Some(envelope::Payload::$name(msg)) = envelope.payload {
Some(msg)
} else {
None
}
}
}
)*
};
}
#[macro_export]
macro_rules! request_messages {
($(($request_name:ident, $response_name:ident)),* $(,)?) => {
$(impl RequestMessage for $request_name {
type Response = $response_name;
})*
};
}
#[macro_export]
macro_rules! entity_messages {
({$id_field:ident, $entity_type:ty}, $($name:ident),* $(,)?) => {
$(impl EntityMessage for $name {
type Entity = $entity_type;
fn remote_entity_id(&self) -> u64 {
self.$id_field
}
})*
};
}

View File

@@ -0,0 +1,145 @@
#![allow(non_snake_case)]
pub use ::proto::*;
use async_tungstenite::tungstenite::Message as WebSocketMessage;
use futures::{SinkExt as _, StreamExt as _};
use proto::Message as _;
use std::time::Instant;
use std::{fmt::Debug, io};
const KIB: usize = 1024;
const MIB: usize = KIB * 1024;
const MAX_BUFFER_LEN: usize = MIB;
/// A stream of protobuf messages.
pub struct MessageStream<S> {
stream: S,
encoding_buffer: Vec<u8>,
}
#[derive(Debug)]
pub enum Message {
Envelope(Envelope),
Ping,
Pong,
}
impl<S> MessageStream<S> {
pub fn new(stream: S) -> Self {
Self {
stream,
encoding_buffer: Vec::new(),
}
}
}
impl<S> MessageStream<S>
where
S: futures::Sink<WebSocketMessage, Error = anyhow::Error> + Unpin,
{
pub async fn write(&mut self, message: Message) -> anyhow::Result<()> {
#[cfg(any(test, feature = "test-support"))]
const COMPRESSION_LEVEL: i32 = -7;
#[cfg(not(any(test, feature = "test-support")))]
const COMPRESSION_LEVEL: i32 = 4;
match message {
Message::Envelope(message) => {
self.encoding_buffer.reserve(message.encoded_len());
message
.encode(&mut self.encoding_buffer)
.map_err(io::Error::from)?;
let buffer =
zstd::stream::encode_all(self.encoding_buffer.as_slice(), COMPRESSION_LEVEL)
.unwrap();
self.encoding_buffer.clear();
self.encoding_buffer.shrink_to(MAX_BUFFER_LEN);
self.stream
.send(WebSocketMessage::Binary(buffer.into()))
.await?;
}
Message::Ping => {
self.stream
.send(WebSocketMessage::Ping(Default::default()))
.await?;
}
Message::Pong => {
self.stream
.send(WebSocketMessage::Pong(Default::default()))
.await?;
}
}
Ok(())
}
}
impl<S> MessageStream<S>
where
S: futures::Stream<Item = anyhow::Result<WebSocketMessage>> + Unpin,
{
pub async fn read(&mut self) -> anyhow::Result<(Message, Instant)> {
while let Some(bytes) = self.stream.next().await {
let received_at = Instant::now();
match bytes? {
WebSocketMessage::Binary(bytes) => {
zstd::stream::copy_decode(
zstd::zstd_safe::WriteBuf::as_slice(&*bytes),
&mut self.encoding_buffer,
)?;
let envelope = Envelope::decode(self.encoding_buffer.as_slice())
.map_err(io::Error::from)?;
self.encoding_buffer.clear();
self.encoding_buffer.shrink_to(MAX_BUFFER_LEN);
return Ok((Message::Envelope(envelope), received_at));
}
WebSocketMessage::Ping(_) => return Ok((Message::Ping, received_at)),
WebSocketMessage::Pong(_) => return Ok((Message::Pong, received_at)),
WebSocketMessage::Close(_) => break,
_ => {}
}
}
anyhow::bail!("connection closed");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[gpui::test]
async fn test_buffer_size() {
let (tx, rx) = futures::channel::mpsc::unbounded();
let mut sink = MessageStream::new(tx.sink_map_err(|_| anyhow::anyhow!("")));
sink.write(Message::Envelope(Envelope {
payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree {
root_name: "abcdefg".repeat(10),
..Default::default()
})),
..Default::default()
}))
.await
.unwrap();
assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN);
sink.write(Message::Envelope(Envelope {
payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree {
root_name: "abcdefg".repeat(1000000),
..Default::default()
})),
..Default::default()
}))
.await
.unwrap();
assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN);
let mut stream = MessageStream::new(rx.map(anyhow::Ok));
stream.read().await.unwrap();
assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN);
stream.read().await.unwrap();
assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN);
}
}

View File

@@ -0,0 +1,99 @@
use crate::proto;
use serde::{Deserialize, Serialize};
use serde_json::{Value, map};
use strum::VariantNames;
const KIND: &str = "kind";
const ENTITY_ID: &str = "entity_id";
/// A notification that can be stored, associated with a given recipient.
///
/// This struct is stored in the collab database as JSON, so it shouldn't be
/// changed in a backward-incompatible way. For example, when renaming a
/// variant, add a serde alias for the old name.
///
/// Most notification types have a special field which is aliased to
/// `entity_id`. This field is stored in its own database column, and can
/// be used to query the notification.
#[derive(Debug, Clone, PartialEq, Eq, VariantNames, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Notification {
ContactRequest {
#[serde(rename = "entity_id")]
sender_id: u64,
},
ContactRequestAccepted {
#[serde(rename = "entity_id")]
responder_id: u64,
},
ChannelInvitation {
#[serde(rename = "entity_id")]
channel_id: u64,
channel_name: String,
inviter_id: u64,
},
}
impl Notification {
pub fn to_proto(&self) -> proto::Notification {
let mut value = serde_json::to_value(self).unwrap();
let mut entity_id = None;
let value = value.as_object_mut().unwrap();
let Some(Value::String(kind)) = value.remove(KIND) else {
unreachable!("kind is the enum tag")
};
if let map::Entry::Occupied(e) = value.entry(ENTITY_ID)
&& e.get().is_u64()
{
entity_id = e.remove().as_u64();
}
proto::Notification {
kind,
entity_id,
content: serde_json::to_string(&value).unwrap(),
..Default::default()
}
}
pub fn from_proto(notification: &proto::Notification) -> Option<Self> {
let mut value = serde_json::from_str::<Value>(&notification.content).ok()?;
let object = value.as_object_mut()?;
object.insert(KIND.into(), notification.kind.to_string().into());
if let Some(entity_id) = notification.entity_id {
object.insert(ENTITY_ID.into(), entity_id.into());
}
serde_json::from_value(value).ok()
}
pub fn all_variant_names() -> &'static [&'static str] {
Self::VARIANTS
}
}
#[cfg(test)]
mod tests {
use crate::Notification;
#[test]
fn test_notification() {
// Notifications can be serialized and deserialized.
for notification in [
Notification::ContactRequest { sender_id: 1 },
Notification::ContactRequestAccepted { responder_id: 2 },
Notification::ChannelInvitation {
channel_id: 100,
channel_name: "the-channel".into(),
inviter_id: 50,
},
] {
let message = notification.to_proto();
let deserialized = Notification::from_proto(&message).unwrap();
assert_eq!(deserialized, notification);
}
// When notifications are serialized, the `kind` and `actor_id` fields are
// stored separately, and do not appear redundantly in the JSON.
let notification = Notification::ContactRequest { sender_id: 1 };
assert_eq!(notification.to_proto().content, "{}");
}
}

1371
crates/rpc/src/peer.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,683 @@
use anyhow::{Context, Result};
use collections::HashMap;
use futures::{
Future, FutureExt as _, Stream, StreamExt as _,
channel::oneshot,
future::{BoxFuture, LocalBoxFuture},
stream::BoxStream,
};
use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, BackgroundExecutor, Entity, FutureExt as _};
use parking_lot::Mutex;
use proto::{
AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, LspRequestId, LspRequestMessage,
RequestMessage, TypedEnvelope, error::ErrorExt as _,
};
use std::{
any::{Any, TypeId},
sync::{
Arc, OnceLock,
atomic::{self, AtomicU64},
},
time::Duration,
};
#[derive(Debug, Clone)]
pub struct AnyProtoClient(Arc<State>);
type RequestIds = Arc<
Mutex<
HashMap<
LspRequestId,
oneshot::Sender<
Result<
Option<TypedEnvelope<Vec<proto::ProtoLspResponse<Box<dyn AnyTypedEnvelope>>>>>,
>,
>,
>,
>,
>;
static NEXT_LSP_REQUEST_ID: OnceLock<Arc<AtomicU64>> = OnceLock::new();
static REQUEST_IDS: OnceLock<RequestIds> = OnceLock::new();
struct State {
client: Arc<dyn ProtoClient>,
next_lsp_request_id: Arc<AtomicU64>,
request_ids: RequestIds,
}
impl std::fmt::Debug for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("State")
.field("next_lsp_request_id", &self.next_lsp_request_id)
.field("request_ids", &self.request_ids)
.finish_non_exhaustive()
}
}
pub trait ProtoClient: Send + Sync {
fn request(
&self,
envelope: Envelope,
request_type: &'static str,
) -> BoxFuture<'static, Result<Envelope>>;
fn request_stream(
&self,
envelope: Envelope,
request_type: &'static str,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<Envelope>>>> {
async move {
anyhow::bail!(
"stream requests are not supported for {request_type}: {:?}",
envelope.payload
)
}
.boxed()
}
fn send(&self, envelope: Envelope, message_type: &'static str) -> Result<()>;
fn send_response(&self, envelope: Envelope, message_type: &'static str) -> Result<()>;
fn message_handler_set(&self) -> &parking_lot::Mutex<ProtoMessageHandlerSet>;
fn is_via_collab(&self) -> bool;
fn has_wsl_interop(&self) -> bool;
}
#[derive(Default)]
pub struct ProtoMessageHandlerSet {
pub entity_types_by_message_type: HashMap<TypeId, TypeId>,
pub entities_by_type_and_remote_id: HashMap<(TypeId, u64), EntityMessageSubscriber>,
pub entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
pub entities_by_message_type: HashMap<TypeId, AnyWeakEntity>,
pub message_handlers: HashMap<TypeId, ProtoMessageHandler>,
}
pub type ProtoMessageHandler = Arc<
dyn Send
+ Sync
+ Fn(
AnyEntity,
Box<dyn AnyTypedEnvelope>,
AnyProtoClient,
AsyncApp,
) -> LocalBoxFuture<'static, Result<()>>,
>;
impl ProtoMessageHandlerSet {
pub fn clear(&mut self) {
self.message_handlers.clear();
self.entities_by_message_type.clear();
self.entities_by_type_and_remote_id.clear();
self.entity_id_extractors.clear();
}
fn add_message_handler(
&mut self,
message_type_id: TypeId,
entity: gpui::AnyWeakEntity,
handler: ProtoMessageHandler,
) {
self.entities_by_message_type
.insert(message_type_id, entity);
let prev_handler = self.message_handlers.insert(message_type_id, handler);
if prev_handler.is_some() {
panic!("registered handler for the same message twice");
}
}
fn add_entity_message_handler(
&mut self,
message_type_id: TypeId,
entity_type_id: TypeId,
entity_id_extractor: fn(&dyn AnyTypedEnvelope) -> u64,
handler: ProtoMessageHandler,
) {
self.entity_id_extractors
.entry(message_type_id)
.or_insert(entity_id_extractor);
self.entity_types_by_message_type
.insert(message_type_id, entity_type_id);
let prev_handler = self.message_handlers.insert(message_type_id, handler);
if prev_handler.is_some() {
panic!("registered handler for the same message twice");
}
}
pub fn handle_message(
this: &parking_lot::Mutex<Self>,
message: Box<dyn AnyTypedEnvelope>,
client: AnyProtoClient,
cx: AsyncApp,
) -> Option<LocalBoxFuture<'static, Result<()>>> {
let payload_type_id = message.payload_type_id();
let mut this = this.lock();
let handler = this.message_handlers.get(&payload_type_id)?.clone();
let entity = if let Some(entity) = this.entities_by_message_type.get(&payload_type_id) {
entity.upgrade()?
} else {
let extract_entity_id = *this.entity_id_extractors.get(&payload_type_id)?;
let entity_type_id = *this.entity_types_by_message_type.get(&payload_type_id)?;
let entity_id = (extract_entity_id)(message.as_ref());
match this
.entities_by_type_and_remote_id
.get_mut(&(entity_type_id, entity_id))?
{
EntityMessageSubscriber::Pending(pending) => {
pending.push(message);
return None;
}
EntityMessageSubscriber::Entity { handle } => handle.upgrade()?,
}
};
drop(this);
Some(handler(entity, message, client, cx))
}
}
pub enum EntityMessageSubscriber {
Entity { handle: AnyWeakEntity },
Pending(Vec<Box<dyn AnyTypedEnvelope>>),
}
impl std::fmt::Debug for EntityMessageSubscriber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EntityMessageSubscriber::Entity { handle } => f
.debug_struct("EntityMessageSubscriber::Entity")
.field("handle", handle)
.finish(),
EntityMessageSubscriber::Pending(vec) => f
.debug_struct("EntityMessageSubscriber::Pending")
.field(
"envelopes",
&vec.iter()
.map(|envelope| envelope.payload_type_name())
.collect::<Vec<_>>(),
)
.finish(),
}
}
}
impl<T> From<Arc<T>> for AnyProtoClient
where
T: ProtoClient + 'static,
{
fn from(client: Arc<T>) -> Self {
Self::new(client)
}
}
impl AnyProtoClient {
pub fn new<T: ProtoClient + 'static>(client: Arc<T>) -> Self {
Self(Arc::new(State {
client,
next_lsp_request_id: NEXT_LSP_REQUEST_ID
.get_or_init(|| Arc::new(AtomicU64::new(0)))
.clone(),
request_ids: REQUEST_IDS.get_or_init(RequestIds::default).clone(),
}))
}
pub fn is_via_collab(&self) -> bool {
self.0.client.is_via_collab()
}
pub fn request<T: RequestMessage>(
&self,
request: T,
) -> impl Future<Output = Result<T::Response>> + use<T> {
let envelope = request.into_envelope(0, None, None);
let response = self.0.client.request(envelope, T::NAME);
async move {
T::Response::from_envelope(response.await?)
.context("received response of the wrong type")
}
}
pub fn request_stream<T: RequestMessage>(
&self,
request: T,
) -> impl Future<Output = Result<BoxStream<'static, Result<T::Response>>>> + use<T> {
let envelope = request.into_envelope(0, None, None);
let response_stream = self.0.client.request_stream(envelope, T::NAME);
async move {
Ok(response_stream
.await?
.map(|response| {
T::Response::from_envelope(response?)
.context("received response of the wrong type")
})
.boxed())
}
}
pub fn send<T: EnvelopedMessage>(&self, request: T) -> Result<()> {
let envelope = request.into_envelope(0, None, None);
self.0.client.send(envelope, T::NAME)
}
pub fn send_response<T: EnvelopedMessage>(&self, request_id: u32, request: T) -> Result<()> {
let envelope = request.into_envelope(0, Some(request_id), None);
self.0.client.send(envelope, T::NAME)
}
pub fn request_lsp<T>(
&self,
project_id: u64,
server_id: Option<u64>,
timeout: Duration,
executor: BackgroundExecutor,
request: T,
) -> impl Future<
Output = Result<Option<TypedEnvelope<Vec<proto::ProtoLspResponse<T::Response>>>>>,
> + use<T>
where
T: LspRequestMessage,
{
let new_id = LspRequestId(
self.0
.next_lsp_request_id
.fetch_add(1, atomic::Ordering::Acquire),
);
let (tx, rx) = oneshot::channel();
{
self.0.request_ids.lock().insert(new_id, tx);
}
let query = proto::LspQuery {
project_id,
server_id,
lsp_request_id: new_id.0,
request: Some(request.to_proto_query()),
};
let request = self.request(query);
let request_ids = self.0.request_ids.clone();
async move {
match request.await {
Ok(_request_enqueued) => {}
Err(e) => {
request_ids.lock().remove(&new_id);
return Err(e).context("sending LSP proto request");
}
}
let response = rx.with_timeout(timeout, &executor).await;
{
request_ids.lock().remove(&new_id);
}
match response {
Ok(Ok(response)) => {
let response = response
.context("waiting for LSP proto response")?
.map(|response| {
anyhow::Ok(TypedEnvelope {
payload: response
.payload
.into_iter()
.map(|lsp_response| lsp_response.into_response::<T>())
.collect::<Result<Vec<_>>>()?,
sender_id: response.sender_id,
original_sender_id: response.original_sender_id,
message_id: response.message_id,
received_at: response.received_at,
})
})
.transpose()
.context("converting LSP proto response")?;
Ok(response)
}
Err(_cancelled_due_timeout) => Ok(None),
Ok(Err(_channel_dropped)) => Ok(None),
}
}
}
pub fn send_lsp_response<T: LspRequestMessage>(
&self,
project_id: u64,
lsp_request_id: LspRequestId,
server_responses: HashMap<u64, T::Response>,
) -> Result<()> {
self.send(proto::LspQueryResponse {
project_id,
lsp_request_id: lsp_request_id.0,
responses: server_responses
.into_iter()
.map(|(server_id, response)| proto::LspResponse {
server_id,
response: Some(T::response_to_proto_query(response)),
})
.collect(),
})
}
pub fn handle_lsp_response(&self, mut envelope: TypedEnvelope<proto::LspQueryResponse>) {
let request_id = LspRequestId(envelope.payload.lsp_request_id);
let mut response_senders = self.0.request_ids.lock();
if let Some(tx) = response_senders.remove(&request_id) {
let responses = envelope.payload.responses.drain(..).collect::<Vec<_>>();
tx.send(Ok(Some(proto::TypedEnvelope {
sender_id: envelope.sender_id,
original_sender_id: envelope.original_sender_id,
message_id: envelope.message_id,
received_at: envelope.received_at,
payload: responses
.into_iter()
.filter_map(|response| {
use proto::lsp_response::Response;
let server_id = response.server_id;
let response = match response.response? {
Response::GetReferencesResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetDocumentColorResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetHoverResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetCodeActionsResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetSignatureHelpResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetCodeLensResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetDocumentDiagnosticsResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetDefinitionResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetDeclarationResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetTypeDefinitionResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetImplementationResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::InlayHintsResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::SemanticTokensResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetFoldingRangesResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetDocumentSymbolsResponse(response) => {
to_any_envelope(&envelope, response)
}
};
Some(proto::ProtoLspResponse {
server_id,
response,
})
})
.collect(),
})))
.ok();
}
}
pub fn add_request_handler<M, E, H, F>(&self, entity: gpui::WeakEntity<E>, handler: H)
where
M: RequestMessage,
E: 'static,
H: 'static + Sync + Fn(Entity<E>, TypedEnvelope<M>, AsyncApp) -> F + Send + Sync,
F: 'static + Future<Output = Result<M::Response>>,
{
self.0
.client
.message_handler_set()
.lock()
.add_message_handler(
TypeId::of::<M>(),
entity.into(),
Arc::new(move |entity, envelope, client, cx| {
let entity = entity.downcast::<E>().unwrap();
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
let request_id = envelope.message_id();
handler(entity, *envelope, cx)
.then(move |result| async move {
match result {
Ok(response) => {
client.send_response(request_id, response)?;
Ok(())
}
Err(error) => {
client.send_response(request_id, error.to_proto())?;
Err(error)
}
}
})
.boxed_local()
}),
)
}
pub fn add_entity_request_handler<M, E, H, F>(&self, handler: H)
where
M: EnvelopedMessage + RequestMessage + EntityMessage,
E: 'static,
H: 'static + Sync + Send + Fn(gpui::Entity<E>, TypedEnvelope<M>, AsyncApp) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
let message_type_id = TypeId::of::<M>();
let entity_type_id = TypeId::of::<E>();
let entity_id_extractor = |envelope: &dyn AnyTypedEnvelope| {
(envelope as &dyn Any)
.downcast_ref::<TypedEnvelope<M>>()
.unwrap()
.payload
.remote_entity_id()
};
self.0
.client
.message_handler_set()
.lock()
.add_entity_message_handler(
message_type_id,
entity_type_id,
entity_id_extractor,
Arc::new(move |entity, envelope, client, cx| {
let entity = entity.downcast::<E>().unwrap();
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
let request_id = envelope.message_id();
handler(entity, *envelope, cx)
.then(move |result| async move {
match result {
Ok(response) => {
client.send_response(request_id, response)?;
Ok(())
}
Err(error) => {
client.send_response(request_id, error.to_proto())?;
Err(error)
}
}
})
.boxed_local()
}),
);
}
pub fn add_entity_stream_request_handler<M, E, H, F, S>(&self, handler: H)
where
M: EnvelopedMessage + RequestMessage + EntityMessage,
E: 'static,
H: 'static + Sync + Send + Fn(gpui::Entity<E>, TypedEnvelope<M>, AsyncApp) -> F,
F: 'static + Future<Output = Result<S>>,
S: 'static + Stream<Item = Result<M::Response>>,
{
let message_type_id = TypeId::of::<M>();
let entity_type_id = TypeId::of::<E>();
let entity_id_extractor = |envelope: &dyn AnyTypedEnvelope| {
(envelope as &dyn Any)
.downcast_ref::<TypedEnvelope<M>>()
.unwrap()
.payload
.remote_entity_id()
};
self.0
.client
.message_handler_set()
.lock()
.add_entity_message_handler(
message_type_id,
entity_type_id,
entity_id_extractor,
Arc::new(move |entity, envelope, client, cx| {
let entity = entity.downcast::<E>().unwrap();
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
let request_id = envelope.message_id();
let stream = handler(entity, *envelope, cx);
async move {
// An Error response is itself a terminal stream frame on
// both transports (Peer and ChannelClient), so we don't
// need to follow it with an EndStream.
match stream.await {
Ok(stream) => {
futures::pin_mut!(stream);
while let Some(result) = stream.next().await {
match result {
Ok(response) => {
client.send_response(request_id, response)?
}
Err(error) => {
client.send_response(request_id, error.to_proto())?;
return Err(error);
}
}
}
client.send_response(request_id, proto::EndStream {})?;
Ok(())
}
Err(error) => {
client.send_response(request_id, error.to_proto())?;
Err(error)
}
}
}
.boxed_local()
}),
);
}
pub fn add_entity_message_handler<M, E, H, F>(&self, handler: H)
where
M: EnvelopedMessage + EntityMessage,
E: 'static,
H: 'static + Sync + Send + Fn(gpui::Entity<E>, TypedEnvelope<M>, AsyncApp) -> F,
F: 'static + Future<Output = Result<()>>,
{
let message_type_id = TypeId::of::<M>();
let entity_type_id = TypeId::of::<E>();
let entity_id_extractor = |envelope: &dyn AnyTypedEnvelope| {
(envelope as &dyn Any)
.downcast_ref::<TypedEnvelope<M>>()
.unwrap()
.payload
.remote_entity_id()
};
self.0
.client
.message_handler_set()
.lock()
.add_entity_message_handler(
message_type_id,
entity_type_id,
entity_id_extractor,
Arc::new(move |entity, envelope, _, cx| {
let entity = entity.downcast::<E>().unwrap();
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
handler(entity, *envelope, cx).boxed_local()
}),
);
}
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Entity<E>) {
let id = (TypeId::of::<E>(), remote_id);
let mut message_handlers = self.0.client.message_handler_set().lock();
if message_handlers
.entities_by_type_and_remote_id
.contains_key(&id)
{
panic!("already subscribed to entity");
}
message_handlers.entities_by_type_and_remote_id.insert(
id,
EntityMessageSubscriber::Entity {
handle: entity.downgrade().into(),
},
);
}
pub fn has_wsl_interop(&self) -> bool {
self.0.client.has_wsl_interop()
}
}
fn to_any_envelope<T: EnvelopedMessage>(
envelope: &TypedEnvelope<proto::LspQueryResponse>,
response: T,
) -> Box<dyn AnyTypedEnvelope> {
Box::new(proto::TypedEnvelope {
sender_id: envelope.sender_id,
original_sender_id: envelope.original_sender_id,
message_id: envelope.message_id,
received_at: envelope.received_at,
payload: response,
}) as Box<_>
}
#[cfg(any(test, feature = "test-support"))]
pub struct NoopProtoClient {
handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
}
#[cfg(any(test, feature = "test-support"))]
impl NoopProtoClient {
pub fn new() -> Arc<Self> {
Arc::new(Self {
handler_set: parking_lot::Mutex::new(ProtoMessageHandlerSet::default()),
})
}
}
#[cfg(any(test, feature = "test-support"))]
impl ProtoClient for NoopProtoClient {
fn request(
&self,
_: proto::Envelope,
_: &'static str,
) -> futures::future::BoxFuture<'static, Result<proto::Envelope>> {
unimplemented!()
}
fn send(&self, _: proto::Envelope, _: &'static str) -> Result<()> {
Ok(())
}
fn send_response(&self, _: proto::Envelope, _: &'static str) -> Result<()> {
Ok(())
}
fn message_handler_set(&self) -> &parking_lot::Mutex<ProtoMessageHandlerSet> {
&self.handler_set
}
fn is_via_collab(&self) -> bool {
false
}
fn has_wsl_interop(&self) -> bool {
false
}
}

19
crates/rpc/src/rpc.rs Normal file
View File

@@ -0,0 +1,19 @@
pub mod auth;
mod conn;
mod message_stream;
mod notification;
mod peer;
pub use conn::Connection;
pub use notification::*;
pub use peer::*;
pub use proto;
pub use proto::{Receipt, TypedEnvelope, error::*};
mod macros;
#[cfg(feature = "gpui")]
mod proto_client;
#[cfg(feature = "gpui")]
pub use proto_client::*;
pub const PROTOCOL_VERSION: u32 = 68;