use std::{
env,
fmt::Write as _,
process,
sync::{Arc, OnceLock},
};
use clap::Parser;
use eyre::{eyre, Report};
use semver::{BuildMetadata, Version};
use tokio::sync::watch;
use zakura_state::{state_database_format_version_in_code, state_database_format_version_on_disk};
use crate::{
commands::EntryPoint,
components::{metrics, tracing::Tracing},
config::ZakuradConfig,
};
lazy_static::lazy_static! {
pub static ref LAST_WARN_ERROR_LOG_SENDER: watch::Sender<Option<(String, tracing::Level, chrono::DateTime<chrono::Utc>)>> = watch::Sender::new(None);
}
pub fn build_version() -> Version {
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
let fallback_version = CARGO_PKG_VERSION.parse().unwrap_or_else(|error| {
panic!(
"unexpected invalid CARGO_PKG_VERSION: {error:?} in {CARGO_PKG_VERSION:?}, \
should have been checked by cargo"
)
});
let mut version = vergen_build_version().unwrap_or(fallback_version);
if version.build == BuildMetadata::EMPTY {
if let Some(git_commit) = git_commit() {
let short_commit: String = git_commit.chars().take(12).collect();
if let Ok(build) = BuildMetadata::new(&format!("g{short_commit}")) {
version.build = build;
}
}
}
version
}
pub fn clap_build_version() -> &'static str {
static BUILD_VERSION: OnceLock<String> = OnceLock::new();
BUILD_VERSION
.get_or_init(|| build_version().to_string())
.as_str()
}
fn vergen_build_version() -> Option<Version> {
const VERGEN_GIT_DESCRIBE: Option<&str> = option_env!("VERGEN_GIT_DESCRIBE");
let vergen_git_describe = VERGEN_GIT_DESCRIBE?;
let vergen_git_describe = vergen_git_describe.replace("dirty", "modified");
let mut vergen_git_describe = vergen_git_describe.split('-').peekable();
let mut version = vergen_git_describe.next()?;
version = version.strip_prefix('v').unwrap_or(version);
if Version::parse(version).is_err() {
return None;
}
let mut semver = version.to_string();
let Some(part) = vergen_git_describe.peek() else {
return semver.parse().ok();
};
if part.starts_with(char::is_alphabetic) {
semver.push('-');
semver.push_str(part);
let _ = vergen_git_describe.next();
}
let Some(build) = vergen_git_describe.peek() else {
return semver.parse().ok();
};
if !build.starts_with(char::is_numeric) {
return None;
}
let build_parts: Vec<_> = vergen_git_describe.collect();
let build_parts = build_parts.join(".");
semver.push('+');
semver.push_str(&build_parts);
semver.parse().ok()
}
pub fn git_commit() -> Option<&'static str> {
const GIT_COMMIT_GCLOUD: Option<&str> = option_env!("SHORT_SHA");
const GIT_COMMIT_VERGEN: Option<&str> = option_env!("VERGEN_GIT_SHA");
GIT_COMMIT_GCLOUD.or(GIT_COMMIT_VERGEN)
}
pub fn release_version() -> Version {
let mut release_version = build_version();
release_version.build = BuildMetadata::EMPTY;
release_version
}
pub fn user_agent() -> String {
let release_version = release_version();
format!("/Zakura:{release_version}/")
}
pub fn boot() -> ! {
let args =
EntryPoint::process_cli_args(env::args_os().collect()).unwrap_or_else(|err| err.exit());
let command = EntryPoint::try_parse_from(args).unwrap_or_else(|err| err.exit());
let status = run(&command);
process::exit(status)
}
fn run(command: &EntryPoint) -> i32 {
let (config, _tracing) = match start(command) {
Ok(started) => started,
Err(error) => {
report_fatal_error(&error);
return 1;
}
};
match command.cmd().run(config) {
Ok(()) => 0,
Err(_) => 1,
}
}
#[allow(clippy::print_stderr)]
fn report_fatal_error(error: &Report) {
eprintln!("error: {} fatal error: {error}", env!("CARGO_PKG_NAME"));
}
fn install_panic_hook(metadata_section: String) {
let default_hook = std::panic::take_hook();
#[allow(clippy::print_stderr)]
std::panic::set_hook(Box::new(move |info| {
default_hook(info);
eprintln!("\n{metadata_section}");
}));
}
#[allow(clippy::print_stderr, clippy::unwrap_in_result)]
fn start(command: &EntryPoint) -> Result<(Arc<ZakuradConfig>, Tracing), Report> {
let config = match ZakuradConfig::load(command.config_path()) {
Ok(config) => config,
Err(_e) if command.cmd().should_ignore_load_config_error() => Default::default(),
Err(e) => {
eprintln!(
"error: Zakura could not load the provided configuration file and/or environment variables.\
This might mean you are using a deprecated format of the file, or are attempting to
configure deprecated or unknown fields via environment variables.\
You can generate a valid config by running \"zakurad generate\", \
and diff it against yours to examine any format inconsistencies."
);
return Err(eyre!("I/O operation failed: Configuration error: {e}"));
}
};
let config = command.process_config(config)?;
let disk_db_version = match state_database_format_version_on_disk(
&config.mirrored_state_config(),
&config.network.network,
) {
Ok(Some(version)) => version.to_string(),
Ok(None) => "creating.new.database".to_string(),
Err(error) => {
let mut error = format!("error: {error:?}");
error.truncate(100);
error
}
};
let app_metadata = [
("version", build_version().to_string()),
("Zcash network", config.network.network.to_string()),
(
"running state version",
state_database_format_version_in_code().to_string(),
),
("initial disk state version", disk_db_version),
("features", env!("VERGEN_CARGO_FEATURES").to_string()),
];
let git_metadata: &[(_, Option<_>)] = &[
("branch", option_env!("VERGEN_GIT_BRANCH")),
("git commit", git_commit()),
(
"commit timestamp",
option_env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
),
];
let git_metadata: Vec<(_, String)> = git_metadata
.iter()
.filter_map(|(k, v)| Some((k, (*v)?)))
.map(|(k, v)| (*k, v.to_string()))
.collect();
let build_metadata: Vec<_> = [
("target triple", env!("VERGEN_CARGO_TARGET_TRIPLE")),
("rust compiler", env!("VERGEN_RUSTC_SEMVER")),
("rust release date", env!("VERGEN_RUSTC_COMMIT_DATE")),
("optimization level", env!("VERGEN_CARGO_OPT_LEVEL")),
("debug checks", env!("VERGEN_CARGO_DEBUG")),
]
.iter()
.map(|(k, v)| (*k, v.to_string()))
.collect();
let panic_metadata: Vec<_> = app_metadata
.iter()
.chain(git_metadata.iter())
.chain(build_metadata.iter())
.collect();
let mut metadata_section = "Diagnostic metadata:".to_string();
for (k, v) in panic_metadata {
write!(&mut metadata_section, "\n{k}: {v}").expect("unexpected failure writing to string");
}
install_panic_hook(metadata_section.clone());
rayon::ThreadPoolBuilder::new()
.num_threads(config.sync.parallel_cpu_threads)
.thread_name(|thread_index| format!("rayon {thread_index}"))
.build_global()
.expect("unable to initialize rayon thread pool");
let default_filter = command.cmd().default_tracing_filter(command.verbose);
let is_server = command.cmd().is_server();
let mut tracing_config = config.tracing.clone();
if is_server {
tracing_config.filter = tracing_config
.filter
.clone()
.or_else(|| Some(default_filter.to_owned()));
} else {
tracing_config.filter = Some(default_filter.to_owned());
}
let tracing = Tracing::new(
&config.network.network,
tracing_config,
command.cmd().uses_intro(),
)?;
if is_server {
info!("{metadata_section}");
if command.config_path().is_some() {
info!("Using config file at: {:?}", command.config_path().unwrap());
} else {
info!("No config file provided, using default configuration");
}
info!(
network = %config.network.network,
p2p_stack = ?config.network.p2p_stack.resolve(&config.network.network),
peerset_initial_target_size = config.network.peerset_initial_target_size,
ephemeral_state = config.state.ephemeral,
"loaded node configuration",
);
if let Some(miner_address) = &config.mining.miner_address {
info!(%miner_address, "configured miner address");
}
}
let net = config.network.network.to_string();
let net = match net.as_str() {
default_net_name @ ("Testnet" | "Mainnet") => &default_net_name[..4],
other_net_name => other_net_name,
};
let global_span = if let Some(git_commit) = git_commit() {
error_span!("", zakurad = git_commit, net)
} else {
error_span!("", net)
};
let global_guard = global_span.enter();
std::mem::forget(global_guard);
tracing::info!(
num_threads = rayon::current_num_threads(),
"initialized rayon thread pool for CPU-bound tasks",
);
if is_server {
metrics::start_endpoint(&config.metrics);
}
Ok((Arc::new(config), tracing))
}