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:
Bendik Lynghaug
2026-07-14 20:10:29 +02:00
co-authored by Claude Fable 5
commit 4e1a95613b
19 changed files with 2466 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
//! varde-daemon: content-addressed blob mirror daemon.
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{bail, Context, Result};
use tracing::info;
use varde_daemon::config::{Config, Mode, Overrides};
use varde_daemon::{daemon, server};
const USAGE: &str = "\
varde-daemon — content-addressed blob mirror daemon
USAGE:
varde-daemon [OPTIONS]
OPTIONS:
--config <PATH> Config file (default: /etc/varde/config.toml or
$XDG_CONFIG_HOME/varde/config.toml)
--store <PATH> Store directory override
--socket <PATH> API socket path override
--system Force system-mode default paths
--user Force user-mode default paths
--version Print version and exit
--help Print this help and exit
";
// The spec reserves clap for varde-ctl; the daemon takes five flags, which
// a hand parse covers without the dependency.
fn parse_args() -> Result<(Mode, Overrides)> {
let mut overrides = Overrides::default();
let mut mode = Mode::detect();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
let mut value = |name: &str| -> Result<PathBuf> {
args.next()
.map(PathBuf::from)
.with_context(|| format!("{name} requires a value"))
};
match arg.as_str() {
"--config" => overrides.config_path = Some(value("--config")?),
"--store" => overrides.store_dir = Some(value("--store")?),
"--socket" => overrides.socket_path = Some(value("--socket")?),
"--system" => mode = Mode::System,
"--user" => mode = Mode::User,
"--version" => {
println!("varde-daemon {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
"--help" | "-h" => {
print!("{USAGE}");
std::process::exit(0);
}
other => bail!("unknown argument {other:?}\n\n{USAGE}"),
}
}
Ok((mode, overrides))
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "varde_daemon=info".into()),
)
.init();
let (mode, overrides) = parse_args()?;
let config = Config::load(mode, &overrides)?;
info!(store = %config.store_dir.display(), socket = %config.socket_path.display(), "starting");
std::fs::create_dir_all(&config.store_dir)
.with_context(|| format!("creating store directory {}", config.store_dir.display()))?;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.context("building tokio runtime")?
.block_on(run(config))
}
async fn run(config: Config) -> Result<()> {
let socket_path = config.socket_path.clone();
let listener = server::bind_socket(&socket_path)?;
let daemon = Arc::new(daemon::Daemon::new(config));
let result = tokio::select! {
r = server::serve(listener, daemon) => r,
r = shutdown_signal() => r.map(|signal| info!(signal, "shutting down")),
};
// Best-effort cleanup so the next start doesn't find a stale socket.
let _ = std::fs::remove_file(&socket_path);
result
}
async fn shutdown_signal() -> Result<&'static str> {
use tokio::signal::unix::{signal, SignalKind};
let mut term = signal(SignalKind::terminate()).context("installing SIGTERM handler")?;
let mut int = signal(SignalKind::interrupt()).context("installing SIGINT handler")?;
tokio::select! {
_ = term.recv() => Ok("SIGTERM"),
_ = int.recv() => Ok("SIGINT"),
}
}