Files
zesdex/crates/zesdex-ipc/src/client.rs
T

112 lines
3.6 KiB
Rust
Raw Normal View History

//! IPC client — connects to the daemon's Unix socket and sends/receives
//! framed JSON messages.
//!
//! [`IpcClient`] wraps a [`Connection`] behind a [`Mutex`] so it can be
//! shared across threads (e.g. the TUI event loop and the render task).
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
use crate::conn::Connection;
use anyhow::{Context, Result};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::os::unix::net::UnixStream;
use std::sync::Mutex;
/// A thread-safe IPC client connected to a Zesdex daemon over a Unix
/// socket.
pub struct IpcClient {
/// Inner connection protected by a mutex for shared access.
conn: Mutex<Connection>,
}
impl IpcClient {
/// Connect to the daemon listening at `path` (a Unix socket path).
///
/// # Errors
///
/// Returns an error if the socket path does not exist, the connection
/// is refused, or the caller lacks permission.
pub fn connect_unix(path: &str) -> Result<Self> {
let stream = UnixStream::connect(path)
.with_context(|| format!("failed to connect to Unix socket at {path:?}"))?;
let conn = Connection::new(stream);
Ok(Self {
conn: Mutex::new(conn),
})
}
/// Serialise `msg` to JSON and send it as a length-prefixed frame.
///
/// # Errors
///
/// Delegates to the underlying [`Connection::send`].
pub fn send<T: Serialize>(&self, msg: &T) -> Result<()> {
let mut guard = self
.conn
.lock()
.expect("IpcClient mutex poisoned — the previous operation panicked");
guard.send(msg)
}
/// Read one framed JSON message and deserialise it.
///
/// Returns `Ok(None)` on clean EOF (daemon closed the connection).
///
/// # Errors
///
/// Delegates to the underlying [`Connection::receive`].
pub fn receive<T: DeserializeOwned>(&self) -> Result<Option<T>> {
let mut guard = self
.conn
.lock()
.expect("IpcClient mutex poisoned — the previous operation panicked");
guard.receive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
use std::os::unix::net::UnixListener;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Ping {
seq: u32,
}
#[test]
fn connect_and_round_trip() {
let dir = std::env::temp_dir().join(format!("zesdex-ipc-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let sock_path = dir.join("test.sock");
let sock_path_str = sock_path.to_string_lossy().to_string();
// Start a minimal echo server in a background thread.
let listener = UnixListener::bind(&sock_path).unwrap();
let server_handle = std::thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
let mut conn = Connection::new(stream);
// Echo one message back.
let req: Ping = conn.receive().unwrap().unwrap();
conn.send(&req).unwrap();
});
// Client connects and sends a ping, then receives the echo.
let client = IpcClient::connect_unix(&sock_path_str).unwrap();
client.send(&Ping { seq: 7 }).unwrap();
let resp: Ping = client.receive().unwrap().expect("expected a response");
assert_eq!(resp, Ping { seq: 7 });
server_handle.join().unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
}