Milestone 1: workspace skeleton, wire protocol, socket round-trip
Three-crate workspace per SPECS.md. varde-proto defines the full JSON Lines protocol (requests, envelopes, structured errors, events) with string-typed hashes so the crate carries no iroh dependency. The daemon binds its unix socket, loads config with flags > env > file > defaults precedence, and answers status/list; everything else returns a structured "unimplemented" error. varde-ctl maps subcommands 1:1 onto requests and round-trips status against a real daemon in the tests. Dependencies: serde/serde_json (wire format), tokio (async runtime and unix sockets), tracing/tracing-subscriber (structured logging), toml (config file), anyhow (binary-edge errors), thiserror (reserved for library errors), clap (ctl flag parsing, per spec), tempfile (dev-only, ephemeral test dirs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
//! Unix socket server: JSON Lines request/response plus event streaming.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tracing::{debug, info, warn};
|
||||
use varde_proto::{Envelope, ErrorCode, Request};
|
||||
|
||||
use crate::daemon::Daemon;
|
||||
|
||||
/// Bind the API socket, replacing a stale socket file if the previous
|
||||
/// daemon did not shut down cleanly.
|
||||
pub fn bind_socket(path: &Path) -> Result<UnixListener> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("creating socket directory {}", parent.display()))?;
|
||||
}
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(()) => debug!(path = %path.display(), "removed stale socket"),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => {
|
||||
return Err(e).with_context(|| format!("removing stale socket {}", path.display()))
|
||||
}
|
||||
}
|
||||
UnixListener::bind(path).with_context(|| format!("binding socket {}", path.display()))
|
||||
}
|
||||
|
||||
/// Accept connections until cancelled. Each connection is served by its
|
||||
/// own task; a connection failure never takes the daemon down.
|
||||
pub async fn serve(listener: UnixListener, daemon: Arc<Daemon>) -> Result<()> {
|
||||
info!("listening for clients");
|
||||
loop {
|
||||
let (stream, _addr) = listener.accept().await.context("accepting client")?;
|
||||
let daemon = daemon.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_connection(stream, daemon).await {
|
||||
debug!(error = %e, "client connection ended with error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(stream: UnixStream, daemon: Arc<Daemon>) -> Result<()> {
|
||||
let (read_half, mut write_half) = stream.into_split();
|
||||
let mut lines = BufReader::new(read_half).lines();
|
||||
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let request: Request = match serde_json::from_str(&line) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
let reply = Envelope::err(ErrorCode::BadRequest, format!("invalid request: {e}"));
|
||||
write_line(&mut write_half, &reply).await?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
debug!(?request, "request");
|
||||
|
||||
let subscribe = matches!(request, Request::Subscribe {});
|
||||
let reply = daemon.handle(request).await;
|
||||
write_line(&mut write_half, &reply).await?;
|
||||
|
||||
// Subscribe flips this connection into a one-way event stream;
|
||||
// no further requests are read from it.
|
||||
if subscribe && reply.ok {
|
||||
let mut events = daemon.subscribe_events();
|
||||
loop {
|
||||
match events.recv().await {
|
||||
Ok(event) => write_line(&mut write_half, &event).await?,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
|
||||
warn!(missed = n, "event subscriber lagged, events dropped");
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_line<T: serde::Serialize>(
|
||||
writer: &mut (impl AsyncWriteExt + Unpin),
|
||||
value: &T,
|
||||
) -> Result<()> {
|
||||
let mut buf = serde_json::to_vec(value)?;
|
||||
buf.push(b'\n');
|
||||
writer.write_all(&buf).await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user