diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-09-24 12:56:56 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-09-24 12:56:56 +0200 |
| commit | 533140831d825829afaf864d940c435056788d3e (patch) | |
| tree | 9fe6b7da3955663c8373776499f85addfd044d40 /zellij-utils/src/ipc.rs | |
| parent | refactor(client-server-contract): config + layout (#4383) (diff) | |
| download | sl0ppij-533140831d825829afaf864d940c435056788d3e.tar.gz sl0ppij-533140831d825829afaf864d940c435056788d3e.tar.xz sl0ppij-533140831d825829afaf864d940c435056788d3e.zip | |
fix(ipc): make sessions compatible between versions (#4439)
* translate ipc tuple enum variants to struct enum variants
* initial client/server contract
* tests and missing types
* migrate web server and remove rmp_serde
* change socket folder
* fix crash on unknown messages
* fix client logout on unknown message
* fix web client logout on unknown message
* rustfmt
* add comment and change log level
* address compilation warnings
* style(fmt): rustfmt
* fix plugin system test assertions
* add pr
Diffstat (limited to 'zellij-utils/src/ipc.rs')
| -rw-r--r-- | zellij-utils/src/ipc.rs | 274 |
1 files changed, 226 insertions, 48 deletions
diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 57a31cd0..fdffaa07 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -1,7 +1,7 @@ //! IPC stuff for starting to split things into a client and server model. use crate::{ data::{ClientId, ConnectToSession, KeyWithModifier, Style}, - errors::{get_current_ctx, prelude::*, ErrorContext}, + errors::{prelude::*, ErrorContext}, input::{actions::Action, cli_assets::CliAssets}, pane_size::{Size, SizeInPixels}, }; @@ -11,11 +11,23 @@ use nix::unistd::dup; use serde::{Deserialize, Serialize}; use std::{ fmt::{Display, Error, Formatter}, - io::{self, Write}, + io::{self, Read, Write}, marker::PhantomData, os::unix::io::{AsRawFd, FromRawFd}, }; +// Protobuf imports +use crate::client_server_contract::client_server_contract::{ + ClientToServerMsg as ProtoClientToServerMsg, ServerToClientMsg as ProtoServerToClientMsg, +}; +use prost::Message; + +mod enum_conversions; +mod protobuf_conversion; + +#[cfg(test)] +mod tests; + type SessionId = u64; #[derive(PartialEq, Eq, Serialize, Deserialize, Hash)] @@ -47,6 +59,18 @@ pub struct PixelDimensions { pub character_cell_size: Option<SizeInPixels>, } +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct PaneReference { + pub pane_id: u32, + pub is_plugin: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct ColorRegister { + pub index: usize, + pub color: String, +} + impl PixelDimensions { pub fn merge(&mut self, other: PixelDimensions) { if let Some(text_area_size) = other.text_area_size { @@ -60,52 +84,93 @@ impl PixelDimensions { // Types of messages sent from the client to the server #[allow(clippy::large_enum_variant)] -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum ClientToServerMsg { - DetachSession(Vec<ClientId>), - TerminalPixelDimensions(PixelDimensions), - BackgroundColor(String), - ForegroundColor(String), - ColorRegisters(Vec<(usize, String)>), - TerminalResize(Size), - FirstClientConnected( - CliAssets, - bool, // is_web_client - ), - AttachClient( - CliAssets, - Option<usize>, // tab position to focus - Option<(u32, bool)>, // (pane_id, is_plugin) => pane id to focus - bool, // is_web_client - ), - Action(Action, Option<u32>, Option<ClientId>), // u32 is the terminal id - Key(KeyWithModifier, Vec<u8>, bool), // key, raw_bytes, is_kitty_keyboard_protocol + DetachSession { + client_ids: Vec<ClientId>, + }, + TerminalPixelDimensions { + pixel_dimensions: PixelDimensions, + }, + BackgroundColor { + color: String, + }, + ForegroundColor { + color: String, + }, + ColorRegisters { + color_registers: Vec<ColorRegister>, + }, + TerminalResize { + new_size: Size, + }, + FirstClientConnected { + cli_assets: CliAssets, + is_web_client: bool, + }, + AttachClient { + cli_assets: CliAssets, + tab_position_to_focus: Option<usize>, + pane_to_focus: Option<PaneReference>, + is_web_client: bool, + }, + Action { + action: Action, + terminal_id: Option<u32>, + client_id: Option<ClientId>, + }, + Key { + key: KeyWithModifier, + raw_bytes: Vec<u8>, + is_kitty_keyboard_protocol: bool, + }, ClientExited, KillSession, ConnStatus, - WebServerStarted(String), // String -> base_url - FailedToStartWebServer(String), + WebServerStarted { + base_url: String, + }, + FailedToStartWebServer { + error: String, + }, } // Types of messages sent from the server to the client -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum ServerToClientMsg { - Render(String), + Render { + content: String, + }, UnblockInputThread, - Exit(ExitReason), + Exit { + exit_reason: ExitReason, + }, Connected, - Log(Vec<String>), - LogError(Vec<String>), - SwitchSession(ConnectToSession), - UnblockCliPipeInput(String), // String -> pipe name - CliPipeOutput(String, String), // String -> pipe name, String -> Output + Log { + lines: Vec<String>, + }, + LogError { + lines: Vec<String>, + }, + SwitchSession { + connect_to_session: ConnectToSession, + }, + UnblockCliPipeInput { + pipe_name: String, + }, + CliPipeOutput { + pipe_name: String, + output: String, + }, QueryTerminalSize, StartWebServer, - RenamedSession(String), // String -> new session name + RenamedSession { + name: String, + }, ConfigFileUpdated, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum ExitReason { Normal, NormalDetached, @@ -177,17 +242,22 @@ impl<T: Serialize> IpcSenderWithContext<T> { } } - /// Sends an event, along with the current [`ErrorContext`], on this [`IpcSenderWithContext`]'s socket. - pub fn send(&mut self, msg: T) -> Result<()> { - let err_ctx = get_current_ctx(); - if rmp_serde::encode::write(&mut self.sender, &(msg, err_ctx)).is_err() { - Err(anyhow!("failed to send message to client")) - } else { - if let Err(e) = self.sender.flush() { - log::error!("Failed to flush ipc sender: {}", e); - } - Ok(()) + pub fn send_client_msg(&mut self, msg: ClientToServerMsg) -> Result<()> { + let proto_msg: ProtoClientToServerMsg = msg.into(); + write_protobuf_message(&mut self.sender, &proto_msg)?; + if let Err(e) = self.sender.flush() { + log::error!("Failed to flush ipc sender: {}", e); + } + Ok(()) + } + + pub fn send_server_msg(&mut self, msg: ServerToClientMsg) -> Result<()> { + let proto_msg: ProtoServerToClientMsg = msg.into(); + write_protobuf_message(&mut self.sender, &proto_msg)?; + if let Err(e) = self.sender.flush() { + log::error!("Failed to flush ipc sender: {}", e); } + Ok(()) } /// Returns an [`IpcReceiverWithContext`] with the same socket as this sender. @@ -220,12 +290,33 @@ where } } - /// Receives an event, along with the current [`ErrorContext`], on this [`IpcReceiverWithContext`]'s socket. - pub fn recv(&mut self) -> Option<(T, ErrorContext)> { - match rmp_serde::decode::from_read(&mut self.receiver) { - Ok(msg) => Some(msg), + pub fn recv_client_msg(&mut self) -> Option<(ClientToServerMsg, ErrorContext)> { + match read_protobuf_message::<ProtoClientToServerMsg>(&mut self.receiver) { + Ok(proto_msg) => match proto_msg.try_into() { + Ok(rust_msg) => Some((rust_msg, ErrorContext::default())), + Err(e) => { + warn!("Error converting protobuf to ClientToServerMsg: {:?}", e); + None + }, + }, Err(e) => { - warn!("Error in IpcReceiver.recv(): {:?}", e); + warn!("Error in protobuf IpcReceiver.recv_client_msg(): {:?}", e); + None + }, + } + } + + pub fn recv_server_msg(&mut self) -> Option<(ServerToClientMsg, ErrorContext)> { + match read_protobuf_message::<ProtoServerToClientMsg>(&mut self.receiver) { + Ok(proto_msg) => match proto_msg.try_into() { + Ok(rust_msg) => Some((rust_msg, ErrorContext::default())), + Err(e) => { + warn!("Error converting protobuf to ServerToClientMsg: {:?}", e); + None + }, + }, + Err(e) => { + warn!("Error in protobuf IpcReceiver.recv_server_msg(): {:?}", e); None }, } @@ -239,3 +330,90 @@ where IpcSenderWithContext::new(socket) } } + +// Protobuf wire format utilities +fn read_protobuf_message<T: Message + Default>(reader: &mut impl Read) -> Result<T> { + // Read length-prefixed protobuf message + let mut len_bytes = [0u8; 4]; + reader.read_exact(&mut len_bytes)?; + let len = u32::from_le_bytes(len_bytes) as usize; + + let mut buf = vec![0u8; len]; + reader.read_exact(&mut buf)?; + + T::decode(&buf[..]).map_err(Into::into) +} + +fn write_protobuf_message<T: Message>(writer: &mut impl Write, msg: &T) -> Result<()> { + let encoded = msg.encode_to_vec(); + let len = encoded.len() as u32; + + // we measure the length of the message and transmit it first so that the reader will be able + // to first read exactly 4 bytes (representing this length) and then read that amount of bytes + // as the actual message - this is so that we are able to distinct whole messages over the wire + // stream + writer.write_all(&len.to_le_bytes())?; + writer.write_all(&encoded)?; + Ok(()) +} + +// Protobuf helper functions +pub fn send_protobuf_client_to_server( + sender: &mut IpcSenderWithContext<ClientToServerMsg>, + msg: ClientToServerMsg, +) -> Result<()> { + let proto_msg: ProtoClientToServerMsg = msg.into(); + write_protobuf_message(&mut sender.sender, &proto_msg)?; + if let Err(e) = sender.sender.flush() { + log::error!("Failed to flush ipc sender: {}", e); + } + Ok(()) +} + +pub fn send_protobuf_server_to_client( + sender: &mut IpcSenderWithContext<ServerToClientMsg>, + msg: ServerToClientMsg, +) -> Result<()> { + let proto_msg: ProtoServerToClientMsg = msg.into(); + write_protobuf_message(&mut sender.sender, &proto_msg)?; + if let Err(e) = sender.sender.flush() { + log::error!("Failed to flush ipc sender: {}", e); + } + Ok(()) +} + +pub fn recv_protobuf_client_to_server( + receiver: &mut IpcReceiverWithContext<ClientToServerMsg>, +) -> Option<(ClientToServerMsg, ErrorContext)> { + match read_protobuf_message::<ProtoClientToServerMsg>(&mut receiver.receiver) { + Ok(proto_msg) => match proto_msg.try_into() { + Ok(rust_msg) => Some((rust_msg, ErrorContext::default())), + Err(e) => { + warn!("Error converting protobuf message: {:?}", e); + None + }, + }, + Err(e) => { + warn!("Error reading protobuf message: {:?}", e); + None + }, + } +} + +pub fn recv_protobuf_server_to_client( + receiver: &mut IpcReceiverWithContext<ServerToClientMsg>, +) -> Option<(ServerToClientMsg, ErrorContext)> { + match read_protobuf_message::<ProtoServerToClientMsg>(&mut receiver.receiver) { + Ok(proto_msg) => match proto_msg.try_into() { + Ok(rust_msg) => Some((rust_msg, ErrorContext::default())), + Err(e) => { + warn!("Error converting protobuf message: {:?}", e); + None + }, + }, + Err(e) => { + warn!("Error reading protobuf message: {:?}", e); + None + }, + } +} |
