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,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"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user