//! Zebrad Abscissa Application //! //! This is the code that starts `zebrad`, and launches its tasks and services. //! See [the crate docs](crate) and [the start docs](crate::commands::start) for more details. use std::{env, fmt::Write as _, io::Write as _, process, sync::Arc}; use abscissa_core::{ application::{self, AppCell}, config::CfgCell, status_err, terminal::{component::Terminal, stderr, stdout, ColorChoice}, Application, Component, Configurable, FrameworkError, Shutdown, StandardPaths, }; use semver::{BuildMetadata, Version}; use zebra_network::constants::PORT_IN_USE_ERROR; use zebra_state::{ constants::LOCK_FILE_ERROR, state_database_format_version_in_code, state_database_format_version_on_disk, }; use crate::{ commands::EntryPoint, components::{sync::end_of_support::EOS_PANIC_MESSAGE_HEADER, tracing::Tracing}, config::ZebradConfig, }; /// See /// Print a fatal error message and exit fn fatal_error(app_name: String, err: &dyn std::error::Error) -> ! { status_err!("{} fatal error: {}", app_name, err); process::exit(1) } /// Application state pub static APPLICATION: AppCell = AppCell::new(); /// Returns the `zebrad` version for this build, in SemVer 2.0 format. /// /// Includes `git describe` build metatata if available: /// - the number of commits since the last version tag, and /// - the git commit. /// /// For details, see pub fn build_version() -> Version { // CARGO_PKG_VERSION is always a valid SemVer 2.0 version. const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); // We're using the same library as cargo uses internally, so this is guaranteed. 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" ) }); vergen_build_version().unwrap_or(fallback_version) } /// Returns the `zebrad` version from this build, if available from `vergen`. fn vergen_build_version() -> Option { // VERGEN_GIT_DESCRIBE should be in the format: // - v1.0.0-rc.9-6-g319b01bb84 // - v1.0.0-6-g319b01bb84 // but sometimes it is just a short commit hash. See #6879 for details. // // Currently it is the output of `git describe --tags --dirty --match='v*.*.*'`, // or whatever is specified in zebrad/build.rs. const VERGEN_GIT_DESCRIBE: Option<&str> = option_env!("VERGEN_GIT_DESCRIBE"); // The SemVer 2.0 format is: // - 1.0.0-rc.9+6.g319b01bb84 // - 1.0.0+6.g319b01bb84 // // Or as a pattern: // - version: major`.`minor`.`patch // - optional pre-release: `-`tag[`.`tag ...] // - optional build: `+`tag[`.`tag ...] // change the git describe format to the semver 2.0 format let vergen_git_describe = VERGEN_GIT_DESCRIBE?; // `git describe` uses "dirty" for uncommitted changes, // but users won't understand what that means. let vergen_git_describe = vergen_git_describe.replace("dirty", "modified"); // Split using "git describe" separators. let mut vergen_git_describe = vergen_git_describe.split('-').peekable(); // Check the "version core" part. let mut version = vergen_git_describe.next()?; // strip the leading "v", if present. version = version.strip_prefix('v').unwrap_or(version); // If the initial version is empty, just a commit hash, or otherwise invalid. if Version::parse(version).is_err() { return None; } let mut semver = version.to_string(); // Check if the next part is a pre-release or build part, // but only consume it if it is a pre-release tag. let Some(part) = vergen_git_describe.peek() else { // No pre-release or build. return semver.parse().ok(); }; if part.starts_with(char::is_alphabetic) { // It's a pre-release tag. semver.push('-'); semver.push_str(part); // Consume the pre-release tag to move on to the build tags, if any. let _ = vergen_git_describe.next(); } // Check if the next part is a build part. let Some(build) = vergen_git_describe.peek() else { // No build tags. return semver.parse().ok(); }; if !build.starts_with(char::is_numeric) { // It's not a valid "commit count" build tag from "git describe". return None; } // Append the rest of the build parts with the correct `+` and `.` separators. let build_parts: Vec<_> = vergen_git_describe.collect(); let build_parts = build_parts.join("."); semver.push('+'); semver.push_str(&build_parts); semver.parse().ok() } /// The Zebra current release version, without any build metadata. pub fn release_version() -> Version { let mut release_version = build_version(); release_version.build = BuildMetadata::EMPTY; release_version } /// The User-Agent string provided by the node. /// /// This must be a valid [BIP 14] user agent. /// /// [BIP 14]: https://github.com/bitcoin/bips/blob/master/bip-0014.mediawiki pub fn user_agent() -> String { let release_version = release_version(); format!("/Zebra:{release_version}/") } /// Zebrad Application #[derive(Debug, Default)] pub struct ZebradApp { /// Application configuration. config: CfgCell, /// Application state. state: application::State, } impl ZebradApp { /// Returns the git commit for this build, if available. /// /// /// # Accuracy /// /// If the user makes changes, but does not commit them, the git commit will /// not match the compiled source code. 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) } } impl Application for ZebradApp { /// Entrypoint command for this application. type Cmd = EntryPoint; /// Application configuration. type Cfg = ZebradConfig; /// Paths to resources within the application. type Paths = StandardPaths; /// Accessor for application configuration. fn config(&self) -> Arc { self.config.read() } /// Borrow the application state immutably. fn state(&self) -> &application::State { &self.state } /// Returns the framework components used by this application. fn framework_components( &mut self, _command: &Self::Cmd, ) -> Result>>, FrameworkError> { // TODO: Open a PR in abscissa to add a TerminalBuilder for opting out // of the `color_eyre::install` part of `Terminal::new` without // ColorChoice::Never? // The Tracing component uses stdout directly and will apply colors automatically. // // Note: It's important to use `ColorChoice::Never` here to avoid panicking in // `register_components()` below if `color_eyre::install()` is called // after `color_spantrace` has been initialized. let terminal = Terminal::new(ColorChoice::Never); Ok(vec![Box::new(terminal)]) } /// Register all components used by this application. /// /// If you would like to add additional components to your application /// beyond the default ones provided by the framework, this is the place /// to do so. #[allow(clippy::print_stderr)] #[allow(clippy::unwrap_in_result)] fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { use crate::components::{ metrics::MetricsEndpoint, tokio::TokioComponent, tracing::TracingEndpoint, }; let mut components = self.framework_components(command)?; // Load config *after* framework components so that we can // report an error to the terminal if it occurs (unless used with a command that doesn't need the config). let config = match command.config_path() { Some(path) => match self.load_config(&path) { Ok(config) => config, // Ignore errors loading the config for some commands. Err(_e) if command.cmd().should_ignore_load_config_error() => Default::default(), Err(e) => { status_err!("Zebra could not parse the provided config file. This might mean you are using a deprecated format of the file. You can generate a valid config by running \"zebrad generate\", and diff it against yours to examine any format inconsistencies."); return Err(e); } }, None => ZebradConfig::default(), }; let config = command.process_config(config)?; let theme = if config.tracing.use_color_stdout_and_stderr() { color_eyre::config::Theme::dark() } else { color_eyre::config::Theme::new() }; // collect the common metadata for the issue URL and panic report, // skipping any env vars that aren't present // reads state disk version file, doesn't open RocksDB database let disk_db_version = match state_database_format_version_on_disk(&config.state, &config.network.network) { Ok(Some(version)) => version.to_string(), // This "version" is specially formatted to match a relaxed version regex in CI Ok(None) => "creating.new.database".to_string(), Err(error) => { let mut error = format!("error: {error:?}"); error.truncate(100); error } }; let app_metadata = [ // build-time constant: cargo or git tag + short commit ("version", build_version().to_string()), // config ("Zcash network", config.network.network.to_string()), // code constant ( "running state version", state_database_format_version_in_code().to_string(), ), // state disk file, doesn't open database ("initial disk state version", disk_db_version), // build-time constant ("features", env!("VERGEN_CARGO_FEATURES").to_string()), ]; // git env vars can be skipped if there is no `.git` during the // build, so they must all be optional let git_metadata: &[(_, Option<_>)] = &[ ("branch", option_env!("VERGEN_GIT_BRANCH")), ("git commit", Self::git_commit()), ( "commit timestamp", option_env!("VERGEN_GIT_COMMIT_TIMESTAMP"), ), ]; // skip missing metadata 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 builder = color_eyre::config::HookBuilder::default(); let mut metadata_section = "Metadata:".to_string(); for (k, v) in panic_metadata { builder = builder.add_issue_metadata(k, v.clone()); write!(&mut metadata_section, "\n{k}: {}", &v) .expect("unexpected failure writing to string"); } builder = builder .theme(theme) .panic_section(metadata_section.clone()) .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new")) .issue_filter(|kind| match kind { color_eyre::ErrorKind::NonRecoverable(error) => { let error_str = match error.downcast_ref::() { Some(as_string) => as_string, None => return true, }; // listener port conflicts if PORT_IN_USE_ERROR.is_match(error_str) { return false; } // RocksDB lock file conflicts if LOCK_FILE_ERROR.is_match(error_str) { return false; } // Don't ask users to report old version panics. if error_str.to_string().contains(EOS_PANIC_MESSAGE_HEADER) { return false; } true } color_eyre::ErrorKind::Recoverable(error) => { // Type checks should be faster than string conversions. // // Don't ask users to create bug reports for timeouts and peer errors. if error.is::() || error.is::() || error.is::() || error.is::() || error.is::() { return false; } // Don't ask users to create bug reports for known timeouts, duplicate blocks, // full disks, or updated binaries. let error_str = error.to_string(); !error_str.contains("timed out") && !error_str.contains("duplicate hash") && !error_str.contains("No space left on device") // abscissa panics like this when the running zebrad binary has been updated && !error_str.contains("error canonicalizing application path") } }); // This MUST happen after `Terminal::new` to ensure our preferred panic // handler is the last one installed let (panic_hook, eyre_hook) = builder.into_hooks(); eyre_hook.install().expect("eyre_hook.install() error"); // The Sentry default config pulls in the DSN from the `SENTRY_DSN` // environment variable. #[cfg(feature = "sentry")] let guard = sentry::init(sentry::ClientOptions { debug: true, release: Some(build_version().to_string().into()), ..Default::default() }); std::panic::set_hook(Box::new(move |panic_info| { let panic_report = panic_hook.panic_report(panic_info); eprintln!("{panic_report}"); #[cfg(feature = "sentry")] { let event = crate::sentry::panic_event_from(panic_report); sentry::capture_event(event); if !guard.close(None) { warn!("unable to flush sentry events during panic"); } } })); // Apply the configured number of threads to the thread pool. // // TODO: // - set rayon panic handler to a function that takes `Box`, // which forwards to sentry. If possible, use eyre's panic report for formatting. // - do we also need to call this code in `zebra_consensus::init()`, // when that crate is being used by itself? 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 cfg_ref = &config; let default_filter = command.cmd().default_tracing_filter(command.verbose); let is_server = command.cmd().is_server(); // Ignore the configured tracing filter for short-lived utility commands let mut tracing_config = cfg_ref.tracing.clone(); let metrics_config = cfg_ref.metrics.clone(); if is_server { // Override the default tracing filter based on the command-line verbosity. tracing_config.filter = tracing_config .filter .clone() .or_else(|| Some(default_filter.to_owned())); } else { // Don't apply the configured filter for short-lived commands. tracing_config.filter = Some(default_filter.to_owned()); tracing_config.flamegraph = None; } components.push(Box::new(Tracing::new( &config.network.network, tracing_config, command.cmd().uses_intro(), )?)); // Log git metadata and platform info when zebrad starts up if is_server { tracing::info!("Diagnostic {}", metadata_section); info!(config_path = ?command.config_path(), config = ?cfg_ref, "loaded zebrad config"); } // Activate the global span, so it's visible when we load the other // components. Space is at a premium here, so we use an empty message, // short commit hash, and the unique part of the network name. let net = &config.network.network.to_string()[..4]; let global_span = if let Some(git_commit) = ZebradApp::git_commit() { error_span!("", zebrad = git_commit, net) } else { error_span!("", net) }; let global_guard = global_span.enter(); // leak the global span, to make sure it stays active std::mem::forget(global_guard); tracing::info!( num_threads = rayon::current_num_threads(), "initialized rayon thread pool for CPU-bound tasks", ); // Launch network and async endpoints only for long-running commands. if is_server { components.push(Box::new(TokioComponent::new()?)); components.push(Box::new(TracingEndpoint::new(cfg_ref)?)); components.push(Box::new(MetricsEndpoint::new(&metrics_config)?)); } self.state.components_mut().register(components)?; // Fire callback to signal state in the application lifecycle self.after_config(config) } /// Load this application's configuration and initialize its components. #[allow(clippy::unwrap_in_result)] fn init(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { // Create and register components with the application. // We do this first to calculate a proper dependency ordering before // application configuration is processed self.register_components(command) } /// Post-configuration lifecycle callback. /// /// Called regardless of whether config is loaded to indicate this is the /// time in app lifecycle when configuration would be loaded if /// possible. fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> { // Configure components self.state.components_mut().after_config(&config)?; self.config.set_once(config); Ok(()) } fn shutdown(&self, shutdown: Shutdown) -> ! { // Some OSes require a flush to send all output to the terminal. // zebrad's logging uses Abscissa, so we flush its streams. // // TODO: // - if this doesn't work, send an empty line as well // - move this code to the tracing component's `before_shutdown()` let _ = stdout().lock().flush(); let _ = stderr().lock().flush(); let shutdown_result = self.state().components().shutdown(self, shutdown); self.state() .components_mut() .get_downcast_mut::() .map(Tracing::shutdown); if let Err(e) = shutdown_result { let app_name = self.name().to_string(); fatal_error(app_name, &e); } match shutdown { Shutdown::Graceful => process::exit(0), Shutdown::Forced => process::exit(1), Shutdown::Crash => process::exit(2), } } } /// Boot the given application, parsing subcommand and options from /// command-line arguments, and terminating when complete. // pub fn boot(app_cell: &'static AppCell) -> ! { let args = EntryPoint::process_cli_args(env::args_os().collect()).unwrap_or_else(|err| err.exit()); ZebradApp::run(app_cell, args); process::exit(0); }