// LocalBridge - Unix socket IPC implementation // Implements D-020 subprocess/IPC architecture // Deterministic client-server communication via Unix domain sockets use super::{decode_inbound, BridgeError, Inbound, ObserverSnapshot, SimBridge}; use crate::atlas::atlas_data_proxy::{CityNamesResponse, StarMapResponse}; use crate::atlas::browse_proxy::BrowseResponse; use crate::atlas::layer_proxy::AtlasLayerResponse; use crate::atlas::step_canvas::StepCanvasResponse; use crate::bridge::framing::{read_framed, write_framed}; use std::fs; use std::io::{BufReader, BufWriter}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::sync::Mutex; /// LocalBridge: Unix socket transport for client-server IPC pub struct LocalBridge { reader: Mutex>, writer: Mutex>, socket_path: PathBuf, } impl LocalBridge { /// Server-side: create a Unix socket listener and accept one connection. /// Removes any stale socket file before binding. pub fn accept(path: &Path) -> Result { // Remove stale socket if it exists if path.exists() { fs::remove_file(path).map_err(|e| { BridgeError::Transport(format!("failed to remove stale socket: {}", e)) })?; } tracing::info!("LocalBridge listening on {:?}", path); let listener = UnixListener::bind(path) .map_err(|e| BridgeError::Transport(format!("failed to bind Unix socket: {}", e)))?; // Accept one connection let (stream, _addr) = listener .accept() .map_err(|e| BridgeError::Transport(format!("failed to accept connection: {}", e)))?; tracing::info!("LocalBridge accepted connection on {:?}", path); // Clone stream for reader and writer let reader_stream = stream.try_clone().map_err(|e| { BridgeError::Transport(format!("failed to clone stream for reader: {}", e)) })?; Ok(Self { reader: Mutex::new(BufReader::new(reader_stream)), writer: Mutex::new(BufWriter::new(stream)), socket_path: path.to_path_buf(), }) } /// Client-side: connect to an existing Unix socket. pub fn connect(path: &Path) -> Result { tracing::info!("LocalBridge connecting to {:?}", path); let stream = UnixStream::connect(path).map_err(|e| { BridgeError::Transport(format!("failed to connect to Unix socket: {}", e)) })?; tracing::trace!("LocalBridge connected to {:?}", path); // Clone stream for reader and writer let reader_stream = stream.try_clone().map_err(|e| { BridgeError::Transport(format!("failed to clone stream for reader: {}", e)) })?; Ok(Self { reader: Mutex::new(BufReader::new(reader_stream)), writer: Mutex::new(BufWriter::new(stream)), socket_path: path.to_path_buf(), }) } pub fn socket_path(&self) -> &Path { &self.socket_path } } impl SimBridge for LocalBridge { fn receive_startup(&self) -> Result { let mut reader = self .reader .lock() .map_err(|e| BridgeError::MutexPoisoned(format!("reader: {}", e)))?; match read_framed(reader.get_mut())? { Some(payload) => { let msg: super::StartupMessage = rmp_serde::from_slice(&payload)?; tracing::info!("received startup message: world_seed={}", msg.world_seed); Ok(msg) } None => Err(BridgeError::Disconnected), } } fn send_handshake(&self) -> Result<(), BridgeError> { // D-192: HandshakeMessage carries no version. Send an empty marker so the // client knows to begin the startup sequence (send StartupMessage next). use super::types::HandshakeMessage; let msg = HandshakeMessage {}; let payload = rmp_serde::to_vec_named(&msg)?; let mut writer = self .writer .lock() .map_err(|e| BridgeError::MutexPoisoned(format!("writer: {}", e)))?; write_framed(writer.get_mut(), &payload)?; tracing::info!("sent handshake"); Ok(()) } fn send_snapshot(&self, snapshot: &ObserverSnapshot) -> Result<(), BridgeError> { let payload = rmp_serde::to_vec_named(snapshot)?; let mut writer = self .writer .lock() .map_err(|e| BridgeError::MutexPoisoned(format!("writer: {}", e)))?; write_framed(writer.get_mut(), &payload)?; tracing::trace!("sent snapshot: tick={}", snapshot.tick); Ok(()) } fn receive(&self) -> Result, BridgeError> { let mut reader = self .reader .lock() .map_err(|e| BridgeError::MutexPoisoned(format!("reader: {}", e)))?; match read_framed(reader.get_mut())? { Some(payload) => decode_inbound(&payload).map(Some), None => Err(BridgeError::Disconnected), } } fn send_atlas_response(&self, resp: &AtlasLayerResponse) -> Result<(), BridgeError> { let payload = rmp_serde::to_vec_named(resp)?; let mut writer = self .writer .lock() .map_err(|e| BridgeError::MutexPoisoned(format!("writer: {}", e)))?; write_framed(writer.get_mut(), &payload)?; Ok(()) } fn send_star_map_response(&self, resp: &StarMapResponse) -> Result<(), BridgeError> { let payload = rmp_serde::to_vec_named(resp)?; let mut writer = self .writer .lock() .map_err(|e| BridgeError::MutexPoisoned(format!("writer: {}", e)))?; write_framed(writer.get_mut(), &payload)?; Ok(()) } fn send_city_names_response(&self, resp: &CityNamesResponse) -> Result<(), BridgeError> { let payload = rmp_serde::to_vec_named(resp)?; let mut writer = self .writer .lock() .map_err(|e| BridgeError::MutexPoisoned(format!("writer: {}", e)))?; write_framed(writer.get_mut(), &payload)?; Ok(()) } fn send_browse_response(&self, resp: &BrowseResponse) -> Result<(), BridgeError> { let payload = rmp_serde::to_vec_named(resp)?; let mut writer = self .writer .lock() .map_err(|e| BridgeError::MutexPoisoned(format!("writer: {}", e)))?; write_framed(writer.get_mut(), &payload)?; Ok(()) } fn send_step_canvas_response(&self, resp: &StepCanvasResponse) -> Result<(), BridgeError> { let payload = rmp_serde::to_vec_named(resp)?; let mut writer = self .writer .lock() .map_err(|e| BridgeError::MutexPoisoned(format!("writer: {}", e)))?; write_framed(writer.get_mut(), &payload)?; Ok(()) } } impl Drop for LocalBridge { fn drop(&mut self) { // Best-effort socket cleanup if self.socket_path.exists() { let _ = fs::remove_file(&self.socket_path); tracing::trace!("removed socket file {:?}", self.socket_path); } } }