use anyhow::{Context, Result};
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(feature = "profile-heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
#[cfg(all(feature = "jemalloc", not(feature = "profile-heap")))]
#[global_allocator]
static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[cfg(all(feature = "jemalloc", not(feature = "profile-heap")))]
#[allow(non_upper_case_globals)]
#[export_name = "malloc_conf"]
pub static MALLOC_CONF: &[u8] =
b"background_thread:true,dirty_decay_ms:1000,muzzy_decay_ms:0,abort_conf:true\0";
use x0x::server::{DaemonConfig, InstanceName, ServeOptions};
fn resolve_instance_startup(
cli_name: Option<InstanceName>,
config_name: Option<String>,
connect_acl_override: Option<&Path>,
) -> Result<(Option<InstanceName>, Option<PathBuf>)> {
let instance_name = match cli_name {
Some(name) => Some(name),
None => config_name.map(InstanceName::try_from).transpose()?,
};
let connect_acl_path = match (connect_acl_override, instance_name.as_ref()) {
(Some(path), _) => Some(path.to_path_buf()),
(None, Some(name)) => Some(x0x::connect::default_connect_acl_path_for(name)),
(None, None) => None,
};
Ok((instance_name, connect_acl_path))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
#[cfg(feature = "profile-heap")]
let _dhat_profiler = {
let dir = std::env::var("DHAT_OUT_DIR").unwrap_or_else(|_| ".".to_string());
let path = format!("{}/dhat-heap-{}.json", dir, std::process::id());
eprintln!("dhat: writing heap dump to {} on exit", path);
dhat::Profiler::builder().file_name(&path).build()
};
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--version" || a == "-V") {
println!("x0xd {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if args.iter().any(|a| a == "--help" || a == "-h") {
println!("x0xd {} — x0x agent daemon", env!("CARGO_PKG_VERSION"));
println!();
println!("USAGE:");
println!(" x0xd [OPTIONS]");
println!();
println!("OPTIONS:");
println!(" --config <PATH> Path to config file (TOML)");
println!(" --name <NAME> Instance name for multi-instance support");
println!(" --api-port <PORT> Override API server port");
println!(
" --no-hard-coded-bootstrap Skip embedded bootstrap peers (config peers kept)"
);
println!(" --disable-peer-cache Do not load or save cached peers");
println!(" --exec-acl <PATH> Override default exec ACL path");
println!(" --connect-acl <PATH> Override default connect ACL path");
println!(" --check Check configuration and exit");
println!(" --check-updates Check for updates and exit");
println!(" --skip-update-check Skip update check on startup");
println!(" --doctor Run diagnostics");
println!(" --version, -V Print version and exit");
println!(" --help, -h Print this help and exit");
return Ok(());
}
let config_path = if let Some(idx) = args.iter().position(|a| a == "--config") {
Some(
args.get(idx + 1)
.context("--config requires a path argument")?
.clone(),
)
} else {
None
};
let exec_acl_override = if let Some(idx) = args.iter().position(|a| a == "--exec-acl") {
Some(PathBuf::from(
args.get(idx + 1)
.context("--exec-acl requires a path argument")?
.clone(),
))
} else {
None
};
let exec_acl_load_mode = if exec_acl_override.is_some() {
x0x::exec::LoadMode::ExplicitPath
} else {
x0x::exec::LoadMode::DefaultPath
};
let connect_acl_override = if let Some(idx) = args.iter().position(|a| a == "--connect-acl") {
Some(PathBuf::from(
args.get(idx + 1)
.context("--connect-acl requires a path argument")?
.clone(),
))
} else {
None
};
let connect_acl_load_mode = if connect_acl_override.is_some() {
x0x::connect::LoadMode::ExplicitPath
} else {
x0x::connect::LoadMode::DefaultPath
};
let check_only = args.contains(&"--check".to_string());
let check_updates_only = args.contains(&"--check-updates".to_string());
let skip_update_check = args.contains(&"--skip-update-check".to_string());
let doctor_mode = args.iter().any(|arg| arg == "doctor" || arg == "--doctor");
let no_hard_coded_bootstrap = args.contains(&"--no-hard-coded-bootstrap".to_string());
let legacy_no_bootstrap = args.contains(&"--no-bootstrap".to_string());
if legacy_no_bootstrap {
eprintln!("warning: --no-bootstrap is deprecated; use --no-hard-coded-bootstrap");
}
let disable_configured_bootstrap = no_hard_coded_bootstrap || legacy_no_bootstrap;
let cli_no_port_mapping = args.contains(&"--no-port-mapping".to_string());
let cli_disable_peer_cache = args.contains(&"--disable-peer-cache".to_string());
let api_port_override = if let Some(idx) = args.iter().position(|a| a == "--api-port") {
let port_str = args
.get(idx + 1)
.context("--api-port requires a port number")?;
let port: u16 = port_str
.parse()
.context("--api-port value must be a valid port number (0-65535)")?;
Some(port)
} else {
None
};
let cli_instance_name = if let Some(idx) = args.iter().position(|a| a == "--name") {
let name = args
.get(idx + 1)
.context("--name requires an instance name")?
.clone();
Some(InstanceName::try_from(name)?)
} else {
None
};
if args.contains(&"--list".to_string()) {
x0x::server::list_instances().await?;
return Ok(());
}
let mut config = match &config_path {
Some(path) => load_config(path).await?,
None => {
let config_dir_name = match &cli_instance_name {
Some(name) => format!("x0x-{}", name.as_str()),
None => "x0x".to_string(),
};
let default_path = dirs::config_dir()
.map(|d| d.join(&config_dir_name).join("config.toml"))
.unwrap_or_else(|| PathBuf::from("/etc/x0x/config.toml"));
if default_path.exists() {
load_config(default_path.to_str().unwrap_or("/etc/x0x/config.toml")).await?
} else {
DaemonConfig::default()
}
}
};
let (instance_name, effective_connect_acl_path) = resolve_instance_startup(
cli_instance_name,
config.instance_name.clone(),
connect_acl_override.as_deref(),
)?;
if let Some(ref name) = instance_name {
let default_data_dir = x0x::server::default_data_dir();
if config.data_dir == default_data_dir {
config.data_dir = dirs::data_dir()
.map(|d| d.join(format!("x0x-{}", name.as_str())))
.unwrap_or_else(|| PathBuf::from(format!("/var/lib/x0x-{}", name.as_str())));
}
if config.api_address == x0x::server::default_api_address() {
config.api_address = SocketAddr::from(([127, 0, 0, 1], 0));
}
if config.bind_address == x0x::server::default_bind_address() {
config.bind_address = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 0));
}
config.instance_name = Some(name.as_str().to_owned());
}
if let Some(port) = api_port_override {
config.api_address.set_port(port);
}
if disable_configured_bootstrap && config.bootstrap_peers.is_none() {
config.bootstrap_peers = Some(Vec::new());
}
config
.gossip
.validate()
.map_err(|e| anyhow::anyhow!("invalid gossip config: {e}"))?;
init_logging(&config.log_level, &config.log_format)?;
let exec_policy = x0x::exec::load_exec_policy(exec_acl_override.as_deref(), exec_acl_load_mode)
.await
.context("failed to load exec ACL")?;
let connect_policy = x0x::connect::load_connect_policy(
effective_connect_acl_path.as_deref(),
connect_acl_load_mode,
)
.await
.context("failed to load connect ACL")?;
tracing::info!(
target: "x0x::startup",
path = %effective_connect_acl_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| x0x::connect::default_connect_acl_path().display().to_string()),
explicit = connect_acl_override.is_some(),
"Resolved connect-ACL policy path"
);
if doctor_mode {
return run_doctor(&config).await;
}
if check_only {
println!("Configuration is valid");
println!("{:#?}", config);
println!("Exec ACL summary: {:#?}", exec_policy.summary());
println!("Connect ACL summary: {:#?}", connect_policy.summary());
return Ok(());
}
if check_updates_only {
return x0x::server::run_update_check_and_report(&config, skip_update_check).await;
}
let self_update_enabled = config.update_enabled();
let options = ServeOptions {
skip_update_check,
cli_no_port_mapping,
cli_disable_peer_cache,
instance_name: instance_name.map(InstanceName::into_string),
exec_policy,
connect_policy,
self_update_enabled,
};
let handle = x0x::server::serve_with_options(config, options).await?;
let cancel = handle.cancellation_token();
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
cancel.cancel();
}
});
#[cfg(unix)]
{
let cancel = handle.cancellation_token();
tokio::spawn(async move {
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
Ok(mut sigterm) => {
if sigterm.recv().await.is_some() {
cancel.cancel();
}
}
Err(e) => tracing::warn!("failed to install SIGTERM handler: {e}"),
}
});
}
handle.wait().await
}
async fn run_doctor(config: &DaemonConfig) -> Result<()> {
let mut warnings = 0usize;
let mut failures = 0usize;
let print_pass = |msg: &str| println!("PASS {msg}");
let mut print_warn = |msg: &str| {
warnings += 1;
println!("WARN {msg}");
};
let mut print_fail = |msg: &str| {
failures += 1;
println!("FAIL {msg}");
};
println!("x0xd doctor");
println!("-----------");
match std::env::current_exe() {
Ok(path) => print_pass(&format!("binary: {}", path.display())),
Err(err) => print_warn(&format!("could not determine binary path: {err}")),
}
let in_path = std::env::var_os("PATH")
.map(|paths| std::env::split_paths(&paths).any(|p| p.join("x0xd").exists()))
.unwrap_or(false);
if in_path {
print_pass("x0xd found on PATH");
} else {
print_warn("x0xd not found on PATH");
}
print_pass("configuration loaded");
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(2))
.build()
.context("failed to build HTTP client")?;
let base = format!("http://{}", config.api_address);
let mut daemon_reachable = false;
match client.get(format!("{base}/health")).send().await {
Ok(resp) if resp.status().is_success() => {
daemon_reachable = true;
print_pass(&format!("daemon reachable at {}", config.api_address));
match resp.json::<serde_json::Value>().await {
Ok(body) if body.get("ok").and_then(|v| v.as_bool()) == Some(true) => {
print_pass("/health ok=true");
}
Ok(body) => print_warn(&format!("/health unexpected payload: {body}")),
Err(err) => print_warn(&format!("/health invalid JSON: {err}")),
}
}
Ok(resp) => print_warn(&format!("/health HTTP {}", resp.status())),
Err(err) => print_warn(&format!(
"daemon not reachable at {}: {err}",
config.api_address
)),
}
if daemon_reachable {
if let Ok(resp) = client.get(format!("{base}/agent")).send().await {
if resp.status().is_success() {
if let Ok(body) = resp.json::<serde_json::Value>().await {
let has_id = body
.get("agent_id")
.and_then(|v| v.as_str())
.is_some_and(|v| !v.is_empty());
if has_id {
print_pass("/agent returned agent_id");
} else {
print_warn("/agent response missing agent_id");
}
}
}
}
if let Ok(resp) = client.get(format!("{base}/status")).send().await {
if resp.status().is_success() {
if let Ok(body) = resp.json::<serde_json::Value>().await {
let state = body
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
print_pass(&format!("/status connectivity: {state}"));
}
}
}
if let Ok(resp) = client
.get(format!("{base}/diagnostics/connectivity"))
.send()
.await
{
if resp.status().is_success() {
if let Ok(body) = resp.json::<serde_json::Value>().await {
match body.get("transport_environment") {
Some(env)
if env.get("degraded").and_then(|v| v.as_bool()) == Some(true) =>
{
let guidance = env
.get("guidance")
.and_then(|v| v.as_str())
.unwrap_or("transport path is degraded");
print_warn(&format!("transport environment degraded: {guidance}"));
if let Some(reasons) = env.get("reasons").and_then(|v| v.as_array()) {
for reason in reasons.iter().filter_map(|r| r.as_str()) {
println!(" • {reason}");
}
}
}
Some(_) => print_pass("transport environment: healthy"),
None => {}
}
}
}
}
} else {
match tokio::net::TcpListener::bind(config.api_address).await {
Ok(listener) => {
drop(listener);
print_warn(&format!(
"daemon not running (port {} is free)",
config.api_address.port()
));
}
Err(err) => {
print_fail(&format!(
"port {} in use by another process: {err}",
config.api_address.port()
));
}
}
}
println!("-----------");
if failures > 0 {
println!("FAIL {failures} failure(s), {warnings} warning(s)");
anyhow::bail!("doctor detected failures")
} else if warnings > 0 {
println!("WARN {warnings} warning(s)");
Ok(())
} else {
println!("PASS all checks passed");
Ok(())
}
}
async fn load_config(path: &str) -> Result<DaemonConfig> {
let content = tokio::fs::read_to_string(path)
.await
.with_context(|| format!("failed to read config file: {path}"))?;
toml::from_str(&content).with_context(|| format!("failed to parse config file: {path}"))
}
fn init_logging(level: &str, format: &str) -> Result<()> {
use tracing_subscriber::EnvFilter;
let fallback = level.to_lowercase();
let fallback_directive = match fallback.as_str() {
"trace" | "debug" | "info" | "warn" | "error" => fallback.as_str(),
_ => "warn",
};
let (filter, source) = match std::env::var("RUST_LOG") {
Ok(val) if !val.trim().is_empty() => match EnvFilter::try_new(&val) {
Ok(f) => (f, format!("RUST_LOG={val}")),
Err(e) => (
EnvFilter::new(fallback_directive),
format!("RUST_LOG invalid ({e}), falling back to {fallback_directive}"),
),
},
_ => (
EnvFilter::new(fallback_directive),
format!("config log_level={fallback_directive}"),
),
};
let log_dir = std::env::var_os("X0X_LOG_DIR")
.map(std::path::PathBuf::from)
.filter(|p| !p.as_os_str().is_empty());
let file_writer = match log_dir.as_ref() {
Some(dir) => match std::fs::create_dir_all(dir) {
Ok(()) => {
let path = dir.join(format!("x0xd-{}.log", std::process::id()));
match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
Ok(f) => Some((path, f)),
Err(e) => {
eprintln!(
"x0xd: X0X_LOG_DIR set but could not open {}: {e}",
path.display()
);
None
}
}
}
Err(e) => {
eprintln!(
"x0xd: X0X_LOG_DIR set but mkdir -p {} failed: {e}",
dir.display()
);
None
}
},
None => None,
};
let file_path_for_log = file_writer.as_ref().map(|(p, _)| p.display().to_string());
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
let stdout_layer: Box<dyn tracing_subscriber::Layer<_> + Send + Sync + 'static> =
if format == "json" {
Box::new(
tracing_subscriber::fmt::layer()
.json()
.with_writer(std::io::stdout),
)
} else {
Box::new(tracing_subscriber::fmt::layer().with_writer(std::io::stdout))
};
let file_layer: Option<Box<dyn tracing_subscriber::Layer<_> + Send + Sync + 'static>> =
match file_writer {
Some((_, f)) => {
let writer = std::sync::Mutex::new(f);
if format == "json" {
Some(Box::new(
tracing_subscriber::fmt::layer().json().with_writer(writer),
))
} else {
Some(Box::new(
tracing_subscriber::fmt::layer().with_writer(writer),
))
}
}
None => None,
};
let registry = tracing_subscriber::registry()
.with(filter)
.with(stdout_layer);
if let Some(layer) = file_layer {
registry.with(layer).init();
} else {
registry.init();
}
if let Some(path) = file_path_for_log.as_deref() {
tracing::info!(
target: "x0x::startup",
source = %source,
log_file = %path,
"tracing subscriber initialised (file sink active)"
);
} else {
tracing::info!(
target: "x0x::startup",
source = %source,
"tracing subscriber initialised"
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use x0x::server::InstanceName;
fn valid_name(raw: &str) -> InstanceName {
InstanceName::try_from(raw.to_owned())
.unwrap_or_else(|e| panic!("{raw:?}: expected valid instance name, got {e}"))
}
#[test]
fn cli_name_wins_over_invalid_config_loser() {
let cli = valid_name("prod");
let expected_path = x0x::connect::default_connect_acl_path_for(&cli);
let (instance, path) =
resolve_instance_startup(Some(cli.clone()), Some("../etc/passwd".to_owned()), None)
.expect("valid CLI name must shadow an invalid config loser");
assert_eq!(
instance,
Some(cli),
"CLI name must win the instance identity"
);
assert_eq!(
path,
Some(expected_path),
"named CLI instance must derive its plane-scoped ACL path"
);
}
#[test]
fn config_only_traversal_name_is_rejected_with_canonical_error() {
let bad = "../etc/passwd".to_owned();
let resolver_err = resolve_instance_startup(None, Some(bad.clone()), None)
.expect_err("traversal config name must be rejected before path derivation")
.to_string();
let canonical_err = InstanceName::try_from(bad)
.expect_err("traversal name must be rejected by the grammar")
.to_string();
assert_eq!(
resolver_err, canonical_err,
"resolver must propagate the canonical InstanceName error unchanged"
);
assert!(
resolver_err.contains("alphanumeric"),
"expected the grammar error, got {resolver_err:?}"
);
}
#[test]
fn unnamed_instance_has_no_default_acl_path() {
let (instance, path) =
resolve_instance_startup(None, None, None).expect("no inputs resolves trivially");
assert_eq!(instance, None, "no name ⇒ no instance identity");
assert_eq!(path, None, "no name and no override ⇒ no derived ACL path");
}
#[test]
fn connect_acl_override_shadows_named_instance() {
let cli = valid_name("prod");
let override_path = std::path::Path::new("/etc/x0x/custom-connect.toml");
let (instance, path) = resolve_instance_startup(
Some(cli.clone()),
Some("testnet".to_owned()), Some(override_path),
)
.expect("override + valid names resolve");
assert_eq!(instance, Some(cli), "CLI name owns the instance identity");
assert_eq!(
path.as_deref(),
Some(override_path),
"explicit override must shadow the named default path"
);
}
#[test]
fn valid_config_name_resolves_when_no_cli_name() {
let resolved = valid_name("testnet");
let expected_path = x0x::connect::default_connect_acl_path_for(&resolved);
let (instance, path) = resolve_instance_startup(None, Some("testnet".to_owned()), None)
.expect("valid config name resolves");
assert_eq!(
instance,
Some(resolved),
"config name wins when no CLI name is present"
);
assert_eq!(
path,
Some(expected_path),
"config name derives its plane-scoped default"
);
}
}