diff --git a/Cargo.lock b/Cargo.lock index f30014c10e..5c930fc018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2305,6 +2305,7 @@ dependencies = [ "serde_with", "serde_yaml", "sha3", + "snafu", "sysinfo", "tempfile", "tokio", @@ -3206,6 +3207,27 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "snafu" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418b8136fec49956eba89be7da2847ec1909df92a9ae4178b5ff0ff092c8d95e" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a4812a669da00d17d8266a0439eddcacbc88b17f732f927e52eeb9d196f7fb5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.61", +] + [[package]] name = "socket2" version = "0.4.10" diff --git a/Cargo.toml b/Cargo.toml index cff7161390..9c905a56fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ serde_json = { version = "1.0.81", features = ["preserve_order"] } serde_with = "3.7.0" serde_yaml = "0.9.17" sha3 = "0.10.8" +snafu = "0.8.3" sysinfo = "0.30.3" tempfile = "3.2.0" tokio = { version = "1.17.0", features = ["rt-multi-thread"] } diff --git a/src/arguments.rs b/src/arguments.rs index 2e9b0bd2da..d9df72d868 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -20,29 +20,30 @@ pub(crate) struct Arguments { } impl Arguments { - pub(crate) fn run(self) -> SubcommandResult { + pub(crate) fn run(self) -> SnafuResult>> { let mut env: BTreeMap = BTreeMap::new(); - for (var, value) in env::vars_os() { - let Some(var) = var.to_str() else { + for (variable, value) in env::vars_os() { + let Some(variable) = variable.to_str() else { continue; }; - let Some(key) = var.strip_prefix("ORD_") else { + let Some(key) = variable.strip_prefix("ORD_") else { continue; }; env.insert( key.into(), - value.into_string().map_err(|value| { - anyhow!( - "environment variable `{var}` not valid unicode: `{}`", - value.to_string_lossy() - ) - })?, + value + .into_string() + .map_err(|value| SnafuError::EnvVarUnicode { + backtrace: Backtrace::capture(), + value, + variable: variable.into(), + })?, ); } - self.subcommand.run(Settings::load(self.options)?) + Ok(self.subcommand.run(Settings::load(self.options)?)?) } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000000..09254db1af --- /dev/null +++ b/src/error.rs @@ -0,0 +1,66 @@ +use super::*; + +#[derive(Debug, Snafu)] +#[snafu(context(suffix(false)), visibility(pub(crate)))] +pub(crate) enum SnafuError { + #[snafu(display("{err}"))] + Anyhow { err: anyhow::Error }, + #[snafu(display("environment variable `{variable}` not valid unicode: `{}`", value.to_string_lossy()))] + EnvVarUnicode { + backtrace: Backtrace, + value: OsString, + variable: String, + }, + #[snafu(display("I/O error at `{}`", path.display()))] + Io { + backtrace: Backtrace, + path: PathBuf, + source: io::Error, + }, +} + +impl From for SnafuError { + fn from(err: Error) -> SnafuError { + Self::Anyhow { err } + } +} + +/// We currently use `anyhow` for error handling but are migrating to typed +/// errors using `snafu`. This trait exists to provide access to +/// `snafu::ResultExt::{context, with_context}`, which are otherwise shadowed +/// by `anhow::Context::{context, with_context}`. Once the migration is +/// complete, this trait can be deleted, and `snafu::ResultExt` used directly. +pub(crate) trait ResultExt: Sized { + fn snafu_context(self, context: C) -> Result + where + C: snafu::IntoError, + E2: std::error::Error + snafu::ErrorCompat; + + #[allow(unused)] + fn with_snafu_context(self, context: F) -> Result + where + F: FnOnce(&mut E) -> C, + C: snafu::IntoError, + E2: std::error::Error + snafu::ErrorCompat; +} + +impl ResultExt for std::result::Result { + fn snafu_context(self, context: C) -> Result + where + C: snafu::IntoError, + E2: std::error::Error + snafu::ErrorCompat, + { + use snafu::ResultExt; + self.context(context) + } + + fn with_snafu_context(self, context: F) -> Result + where + F: FnOnce(&mut E) -> C, + C: snafu::IntoError, + E2: std::error::Error + snafu::ErrorCompat, + { + use snafu::ResultExt; + self.with_context(context) + } +} diff --git a/src/index.rs b/src/index.rs index 1fa43efad4..b17f2d321d 100644 --- a/src/index.rs +++ b/src/index.rs @@ -215,12 +215,9 @@ impl Index { let path = settings.index().to_owned(); - if let Err(err) = fs::create_dir_all(path.parent().unwrap()) { - bail!( - "failed to create data dir `{}`: {err}", - path.parent().unwrap().display() - ); - } + let data_dir = path.parent().unwrap(); + + fs::create_dir_all(data_dir).snafu_context(error::Io { path: data_dir })?; let index_cache_size = settings.index_cache_size(); diff --git a/src/lib.rs b/src/lib.rs index 6ca3b0819f..b3334ee2b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,7 @@ use { chrono::{DateTime, TimeZone, Utc}, ciborium::Value, clap::{ArgGroup, Parser}, + error::{ResultExt, SnafuError}, html_escaper::{Escape, Trusted}, http::HeaderMap, lazy_static::lazy_static, @@ -58,10 +59,13 @@ use { reqwest::Url, serde::{Deserialize, Deserializer, Serialize}, serde_with::{DeserializeFromStr, SerializeDisplay}, + snafu::{Backtrace, ErrorCompat, Snafu}, std::{ + backtrace::BacktraceStatus, cmp::{self, Reverse}, collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, env, + ffi::OsString, fmt::{self, Display, Formatter}, fs, io::{self, Cursor, Read}, @@ -104,6 +108,7 @@ mod blocktime; pub mod chain; pub mod decimal; mod deserialize_from_str; +mod error; mod fee_rate; pub mod index; mod inscriptions; @@ -122,6 +127,7 @@ pub mod templates; pub mod wallet; type Result = std::result::Result; +type SnafuResult = std::result::Result; const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); @@ -260,15 +266,39 @@ pub fn main() { match args.run() { Err(err) => { eprintln!("error: {err}"); - err - .chain() - .skip(1) - .for_each(|cause| eprintln!("because: {cause}")); - if env::var_os("RUST_BACKTRACE") - .map(|val| val == "1") - .unwrap_or_default() - { - eprintln!("{}", err.backtrace()); + + if let SnafuError::Anyhow { err } = err { + for (i, err) in err.chain().skip(1).enumerate() { + if i == 0 { + eprintln!(); + eprintln!("because:"); + } + + eprintln!("- {err}"); + } + + if env::var_os("RUST_BACKTRACE") + .map(|val| val == "1") + .unwrap_or_default() + { + eprintln!("{}", err.backtrace()); + } + } else { + for (i, err) in err.iter_chain().skip(1).enumerate() { + if i == 0 { + eprintln!(); + eprintln!("because:"); + } + + eprintln!("- {err}"); + } + + if let Some(backtrace) = err.backtrace() { + if backtrace.status() == BacktraceStatus::Captured { + eprintln!("backtrace:"); + eprintln!("{backtrace}"); + } + } } gracefully_shut_down_indexer(); diff --git a/tests/settings.rs b/tests/settings.rs index da228f2176..d5d310e9ea 100644 --- a/tests/settings.rs +++ b/tests/settings.rs @@ -65,7 +65,7 @@ fn config_invalid_error_message() { fs::write(&config, "foo").unwrap(); CommandBuilder::new(format!("--config {} settings", config.to_str().unwrap())) - .stderr_regex("error: failed to deserialize config file `.*ord.yaml`\nbecause:.*") + .stderr_regex("error: failed to deserialize config file `.*ord.yaml`\n\nbecause:.*") .expected_exit_code(1) .run_and_extract_stdout(); } @@ -77,7 +77,7 @@ fn config_not_found_error_message() { let config = tempdir.path().join("ord.yaml"); CommandBuilder::new(format!("--config {} settings", config.to_str().unwrap())) - .stderr_regex("error: failed to open config file `.*ord.yaml`\nbecause:.*") + .stderr_regex("error: failed to open config file `.*ord.yaml`\n\nbecause:.*") .expected_exit_code(1) .run_and_extract_stdout(); } diff --git a/tests/wallet/sats.rs b/tests/wallet/sats.rs index 44e8650bb5..e15dfb369a 100644 --- a/tests/wallet/sats.rs +++ b/tests/wallet/sats.rs @@ -91,6 +91,6 @@ fn sats_from_tsv_file_not_found() { .core(&core) .ord(&ord) .expected_exit_code(1) - .stderr_regex("error: I/O error reading `.*`\nbecause: .*\n") + .stderr_regex("error: I/O error reading `.*`\n\nbecause:.*") .run_and_extract_stdout(); }