#![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))]
use std::{net::SocketAddr, path::PathBuf, time::Duration};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;
use tsoracle_driver_file::FileDriver;
use tsoracle_server::Server;
#[derive(Parser, Debug)]
#[command(name = "tsoracle", version, about = "Standalone timestamp oracle")]
struct Cli {
#[command(subcommand)]
cmd: Option<Cmd>,
#[command(flatten)]
serve: ServeArgs,
}
#[derive(Subcommand, Debug)]
enum Cmd {
Serve(ServeArgs),
Init(InitArgs),
}
#[derive(Parser, Debug, Clone)]
struct ServeArgs {
#[arg(long, default_value = "127.0.0.1:50551")]
listen: SocketAddr,
#[arg(long, default_value = "./tsoracle-data")]
state_dir: PathBuf,
#[arg(long, value_parser = parse_duration, default_value = "3s")]
window_ahead: Duration,
#[arg(long, value_parser = parse_duration, default_value = "1s")]
failover_advance: Duration,
#[arg(long, default_value = "info")]
log: String,
}
#[derive(Parser, Debug)]
struct InitArgs {
#[arg(long, default_value = "./tsoracle-data")]
state_dir: PathBuf,
#[arg(long)]
seed_physical_ms: u64,
}
fn parse_duration(input: &str) -> std::result::Result<Duration, String> {
humantime::parse_duration(input).map_err(|e| e.to_string())
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let cmd = cli.cmd.unwrap_or(Cmd::Serve(cli.serve));
match cmd {
Cmd::Init(args) => run_init(args),
Cmd::Serve(args) => run_serve(args).await,
}
}
fn run_init(args: InitArgs) -> Result<()> {
FileDriver::init_seeded(&args.state_dir, args.seed_physical_ms)
.with_context(|| format!("init state_dir={}", args.state_dir.display()))?;
println!(
"Initialized {} at seed physical_ms={}",
args.state_dir.display(),
args.seed_physical_ms
);
Ok(())
}
async fn run_serve(args: ServeArgs) -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_new(&args.log).unwrap_or_else(|_| EnvFilter::new("info")))
.init();
let driver = FileDriver::open_or_init(&args.state_dir)
.with_context(|| format!("open state_dir={}", args.state_dir.display()))?;
let server = Server::builder()
.consensus_driver(driver)
.window_ahead(args.window_ahead)
.failover_advance(args.failover_advance)
.build()
.context("server build")?;
let listener = tokio::net::TcpListener::bind(args.listen)
.await
.with_context(|| format!("bind {}", args.listen))?;
let local_addr = listener.local_addr().context("listener.local_addr()")?;
println!("serving on {local_addr}");
tracing::info!(
addr = %local_addr,
state_dir = %args.state_dir.display(),
"tsoracle serving"
);
server
.serve_with_listener(listener, shutdown_signal())
.await
.context("serve")?;
Ok(())
}
#[cfg(unix)]
async fn shutdown_signal() {
use tokio::signal::unix::{SignalKind, signal};
let mut sigterm = match signal(SignalKind::terminate()) {
Ok(stream) => stream,
Err(error) => {
tracing::warn!(%error, "could not install SIGTERM handler; only Ctrl-C will trigger shutdown");
let _ = tokio::signal::ctrl_c().await;
tracing::info!(signal = "SIGINT", "shutdown signal received");
return;
}
};
let signal_name = tokio::select! {
_ = tokio::signal::ctrl_c() => "SIGINT",
_ = sigterm.recv() => "SIGTERM",
};
tracing::info!(signal = signal_name, "shutdown signal received");
}
#[cfg(not(unix))]
async fn shutdown_signal() {
let _ = tokio::signal::ctrl_c().await;
tracing::info!(signal = "SIGINT", "shutdown signal received");
}