The D-225-discharging wire migration per D-255(c): a new
server/src/atlas/step_canvas.rs subsystem (six-rung StepCanvasRung
ladder — Global variable-extent rung 0 through Chunk 64m — the
StepCanvasRequest/StepCanvasResponse tagged envelope extending the
proven ShapeProbe discriminated-shape pattern, PNG-per-field dense +
MessagePack-native sparse encoding per the T-1179 measured table, and
both server cache tiers: the structurally keep-always GlobalTierCache
and the dual-axis StepCanvasCache with storage TTLs per rung and
SIM_STATE_TTL clock-bucket staleness per the D-227 amendment (1)
formula). Wired through bridge/{mod,tcp,local}.rs (sixth demux shape,
send_step_canvas_response mirroring the five existing senders),
gen_queue.rs (DeriveStepCanvas work item with per-connection-per-rung
coalescing, reusing the shared TerrainAnalysisCache), and plugin.rs
(serve/complete systems, lazy D-206 rung-0 population).
Acceptance gate (mandatory per D-227 amendment (3)):
tests/step_canvas_acceptance_gate.rs — cache-hit == cache-miss
byte-identical for every rung, lossless encode round-trips, cache
round-trips vs fresh derive, distinct-center sanity. 5/5 pass.
Station-spacing cap ADOPTED: course stations floored to District
spacing (2,048 m) at finer rungs — the S2-measured +38-87% chunk/block
course cost had zero display benefit at the same station density
(COURSE_STATION_SPACING_FLOOR_M).
Documented honest gaps, not shortcuts: settlement_id is a proximity
approximation (no footprint polygons exist yet); glaciation/flooded_q
sim-state planes are wire-shape-ready D-253 stubs; rung-0 uses signed
equator-anchored rows (the canonical D-256 core convention — T-1186's
wrong-latitude behavior applies unchanged and unfixed here, by
instruction).
Legacy district_window carrier byte-unchanged: window_derivation_golden
6/6 byte-identical, all district_window suites pass unmodified. Full
suite at implementation time: 2127 passed across 45 binaries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
7.2 KiB
Rust
202 lines
7.2 KiB
Rust
// 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<BufReader<UnixStream>>,
|
|
writer: Mutex<BufWriter<UnixStream>>,
|
|
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<Self, BridgeError> {
|
|
// 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<Self, BridgeError> {
|
|
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<super::StartupMessage, BridgeError> {
|
|
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<Option<Inbound>, 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);
|
|
}
|
|
}
|
|
}
|