diff --git a/Cargo.lock b/Cargo.lock index 4d5e286..cead6cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,11 +44,13 @@ version = "0.1.0" dependencies = [ "async-trait", "bech32 0.11.0", + "built", "clap", "envpath", "gasket", "hex", "indicatif", + "indoc", "insta", "miette 7.2.0", "opentelemetry", @@ -524,6 +526,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "built" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" +dependencies = [ + "git2", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -1226,6 +1237,19 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "glob" version = "0.3.1" @@ -1615,6 +1639,12 @@ dependencies = [ "web-time", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "insta" version = "1.41.1" @@ -1744,6 +1774,18 @@ version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.8.5" @@ -1793,6 +1835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" dependencies = [ "cc", + "libc", "pkg-config", "vcpkg", ] diff --git a/crates/amaru/Cargo.toml b/crates/amaru/Cargo.toml index 1b7886e..8eee069 100644 --- a/crates/amaru/Cargo.toml +++ b/crates/amaru/Cargo.toml @@ -10,6 +10,7 @@ homepage = "https://github.com/pragma-org/amaru" documentation = "https://docs.rs/amaru" readme = "README.md" rust-version = "1.81.0" +build = "build.rs" [dependencies] async-trait = "0.1.83" @@ -17,6 +18,7 @@ clap = { version = "4.5.20", features = ["derive"] } gasket = { version = "0.8.0", features = ["derive"] } hex = "0.4.3" miette = "7.2.0" +indoc = "2.0" ouroboros = { git = "https://github.com/pragma-org/ouroboros", rev = "ca1d447a6c106e421e6c2b1c7d9d59abf5ca9589" } ouroboros-praos = { git = "https://github.com/pragma-org/ouroboros", rev = "ca1d447a6c106e421e6c2b1c7d9d59abf5ca9589" } pallas-addresses = "0.31.0" @@ -39,10 +41,17 @@ serde = "1.0.215" bech32 = "0.11.0" opentelemetry = { version = "0.27.1" } opentelemetry_sdk = { version = "0.27.1", features = ["async-std", "rt-tokio"] } -opentelemetry-otlp = { version = "0.27.0", features = ["grpc-tonic", "http-proto", "reqwest-client"] } +opentelemetry-otlp = { version = "0.27.0", features = [ + "grpc-tonic", + "http-proto", + "reqwest-client", +] } tracing-opentelemetry = { version = "0.28.0" } [dev-dependencies] envpath = { version = "0.0.1-beta.3", features = ["rand"] } insta = { version = "1.41.1", features = ["json"] } proptest = "1.5.0" + +[build-dependencies] +built = { version = "0.7.1", features = ["git2"] } diff --git a/crates/amaru/build.rs b/crates/amaru/build.rs new file mode 100644 index 0000000..d8f91cb --- /dev/null +++ b/crates/amaru/build.rs @@ -0,0 +1,3 @@ +fn main() { + built::write_built_file().expect("Failed to acquire build-time information"); +} diff --git a/crates/amaru/src/bin/amaru/main.rs b/crates/amaru/src/bin/amaru/main.rs index 79e278f..f64706f 100644 --- a/crates/amaru/src/bin/amaru/main.rs +++ b/crates/amaru/src/bin/amaru/main.rs @@ -1,10 +1,12 @@ use clap::{Parser, Subcommand}; use opentelemetry::metrics::Counter; +use panic::panic_handler; use std::env; mod cmd; mod config; mod exit; +mod panic; pub const SERVICE_NAME: &str = "amaru"; @@ -28,6 +30,8 @@ struct Cli { #[tokio::main] async fn main() -> miette::Result<()> { + panic_handler(); + let counter = setup_tracing(); let args = Cli::parse(); diff --git a/crates/amaru/src/bin/amaru/panic.rs b/crates/amaru/src/bin/amaru/panic.rs new file mode 100644 index 0000000..f96e360 --- /dev/null +++ b/crates/amaru/src/bin/amaru/panic.rs @@ -0,0 +1,106 @@ +/// Installs a panic handler that prints some useful diagnostics and +/// asks the user to report the issue. +pub fn panic_handler() { + std::panic::set_hook(Box::new(move |info| { + let message = info + .payload() + .downcast_ref::<&str>() + .map(|s| (*s).to_string()) + .or_else(|| { + info.payload() + .downcast_ref::() + .map(|s| s.to_string()) + }) + .unwrap_or_else(|| "unknown error".to_string()); + + let location = info.location().map_or_else( + || "".into(), + |location| { + format!( + "{}:{}:{}\n\n ", + location.file(), + location.line(), + location.column(), + ) + }, + ); + + // We present the user with a helpful and welcoming error message; + // Block producing nodes should be considered mission critical software, and so + // They should endeavor *never* to crash, and should always handle and recover from errors. + // So if the process panics, it should be treated as a bug, and we very much want the user to report it. + // TODO(pi): We could go a step further, and prefill some issue details like title, body, labels, etc. + // using query parameters: https://github.com/sindresorhus/new-github-issue-url?tab=readme-ov-file#api + let error_message = indoc::formatdoc! { + r#"{fatal} + Whoops! The Amaru process panicked, rather than handling the error it encountered gracefully. + + This is almost certainly a bug, and we'd appreciate a report so we can improve Amaru. + + Please report this error at https://github.com/pragma-org/amaru/issues/new. + + In your bug report please provide the information below and if possible the code + that produced it. + {info} + + {location}{message}"#, + info = node_info(), + fatal = "amaru::fatal::error", + location = location, + }; + + println!("\n{}", indent(&error_message, 3)); + })); +} + +// TODO: pulled from aiken; should we have our own utility crate for pretty printing? +// https://github.com/aiken-lang/aiken/blob/main/crates/aiken-project/src/pretty.rs#L126C1-L134C2 +pub fn indent(lines: &str, n: usize) -> String { + let tab = pad_left(String::new(), n, " "); + lines + .lines() + .map(|line| format!("{tab}{line}")) + .collect::>() + .join("\n") +} + +pub fn pad_left(mut text: String, n: usize, delimiter: &str) -> String { + let diff = n as i32 - text.len() as i32; + if diff.is_positive() { + for _ in 0..diff { + text.insert_str(0, delimiter); + } + } + text +} + +// TODO: pulled from aiken; should we have our own config utility crate? +// https://github.com/aiken-lang/aiken/blob/main/crates/aiken-project/src/config.rs#L382C1-L393C2 +mod built_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + +pub fn node_info() -> String { + format!( + r#" +Operating System: {} +Architecture: {} +Version: {}"#, + built_info::CFG_OS, + built_info::CFG_TARGET_ARCH, + node_version(true), + ) +} + +pub fn node_version(include_commit_hash: bool) -> String { + let version = built_info::PKG_VERSION; + let suffix = if include_commit_hash { + format!( + "+{}", + built_info::GIT_COMMIT_HASH_SHORT.unwrap_or("unknown") + ) + } else { + "".to_string() + }; + format!("v{version}{suffix}") +}