Skip to content

Commit dfd2525

Browse files
authored
feat: improve cli error handling and add git commit info (#1246)
1 parent 9aa3519 commit dfd2525

File tree

7 files changed

+128
-42
lines changed

7 files changed

+128
-42
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
fn main() {
2+
let desc = std::process::Command::new("git")
3+
.args(["describe", "--always", "--dirty", "--exclude", "*"])
4+
.output()
5+
.ok()
6+
.and_then(|r| String::from_utf8(r.stdout).ok())
7+
.map(|d| format!(" {d}"))
8+
.unwrap_or_default();
9+
println!("cargo:rustc-env=AXON_GIT_DESCRIPTION={desc}");
10+
}

core/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ semver = "1.0"
1010
serde = { version = "1.0", features = ["derive"] }
1111
serde_json = "1.0"
1212
tempfile = "3.6"
13+
thiserror = "1.0"
1314

1415
common-config-parser = { path = "../../common/config-parser" }
1516
common-logger = { path = "../../common/logger" }

core/cli/src/lib.rs

Lines changed: 94 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,58 @@
11
use std::io::{self, Write};
22
use std::path::Path;
33

4-
use clap::builder::{IntoResettable, Str};
54
use clap::{Arg, ArgMatches, Command};
5+
use protocol::ProtocolError;
66
use semver::Version;
7+
use thiserror::Error;
78

8-
use common_config_parser::{parse_file, types::Config};
9+
use common_config_parser::{parse_file, types::Config, ParseError};
910
use core_run::{Axon, KeyProvider, SecioKeyPair};
1011
use protocol::types::RichBlock;
1112

13+
#[non_exhaustive]
14+
#[derive(Error, Debug)]
15+
pub enum Error {
16+
// Boxing so the error type isn't too large (clippy::result-large-err).
17+
#[error(transparent)]
18+
CheckingVersion(Box<CheckingVersionError>),
19+
#[error("reading data version: {0}")]
20+
ReadingVersion(#[source] io::Error),
21+
#[error("writing data version: {0}")]
22+
WritingVersion(#[source] io::Error),
23+
24+
#[error("parsing config: {0}")]
25+
ParsingConfig(#[source] ParseError),
26+
#[error("getting parent directory of config file")]
27+
GettingParent,
28+
#[error("parsing genesis: {0}")]
29+
ParsingGenesis(#[source] ParseError),
30+
31+
#[error(transparent)]
32+
Running(ProtocolError),
33+
}
34+
35+
#[non_exhaustive]
36+
#[derive(Error, Debug)]
37+
#[cfg_attr(test, derive(PartialEq, Eq))]
38+
#[error("data version({data}) is not compatible with the current axon version({current}), version >= {least_compatible} is supported")]
39+
pub struct CheckingVersionError {
40+
pub current: Version,
41+
pub data: Version,
42+
pub least_compatible: Version,
43+
}
44+
45+
pub type Result<T, E = Error> = std::result::Result<T, E>;
46+
1247
pub struct AxonCli {
1348
version: Version,
1449
matches: ArgMatches,
1550
}
1651

1752
impl AxonCli {
18-
pub fn init(ver: impl IntoResettable<Str>) -> Self {
53+
pub fn init(axon_version: Version, cli_version: &'static str) -> Self {
1954
let matches = Command::new("axon")
20-
.version(ver)
55+
.version(cli_version)
2156
.arg(
2257
Arg::new("config_path")
2358
.short('c')
@@ -37,19 +72,24 @@ impl AxonCli {
3772
.subcommand(Command::new("run").about("Run axon process"));
3873

3974
AxonCli {
40-
version: Version::parse(matches.get_version().unwrap()).unwrap(),
75+
version: axon_version,
4176
matches: matches.get_matches(),
4277
}
4378
}
4479

45-
pub fn start(&self) {
80+
pub fn start(&self) -> Result<()> {
4681
self.start_with_custom_key_provider::<SecioKeyPair>(None)
4782
}
4883

49-
pub fn start_with_custom_key_provider<K: KeyProvider>(&self, key_provider: Option<K>) {
84+
pub fn start_with_custom_key_provider<K: KeyProvider>(
85+
&self,
86+
key_provider: Option<K>,
87+
) -> Result<()> {
5088
let config_path = self.matches.get_one::<String>("config_path").unwrap();
51-
let path = Path::new(&config_path).parent().unwrap();
52-
let mut config: Config = parse_file(config_path, false).unwrap();
89+
let path = Path::new(&config_path)
90+
.parent()
91+
.ok_or(Error::GettingParent)?;
92+
let mut config: Config = parse_file(config_path, false).map_err(Error::ParsingConfig)?;
5393

5494
if let Some(ref mut f) = config.rocksdb.options_file {
5595
*f = path.join(&f)
@@ -58,47 +98,53 @@ impl AxonCli {
5898
self.matches.get_one::<String>("genesis_path").unwrap(),
5999
true,
60100
)
61-
.unwrap();
101+
.map_err(Error::ParsingGenesis)?;
62102

63-
self.check_version(&config);
103+
self.check_version(&config)?;
64104

65105
register_log(&config);
66106

67-
Axon::new(config, genesis).run(key_provider).unwrap();
107+
Axon::new(config, genesis)
108+
.run(key_provider)
109+
.map_err(Error::Running)?;
110+
Ok(())
68111
}
69112

70-
fn check_version(&self, config: &Config) {
71-
if !config.data_path.exists() {
72-
std::fs::create_dir_all(&config.data_path).unwrap();
73-
}
74-
113+
fn check_version(&self, config: &Config) -> Result<()> {
114+
// Won't panic because parent of data_path_for_version() is data_path.
75115
check_version(
76116
&config.data_path_for_version(),
77117
&self.version,
78-
&latest_compatible_version(),
79-
);
118+
latest_compatible_version(),
119+
)
80120
}
81121
}
82122

83-
fn check_version(p: &Path, current: &Version, least_compatible: &Version) {
123+
/// # Panics
124+
///
125+
/// If p.parent() is None.
126+
fn check_version(p: &Path, current: &Version, least_compatible: Version) -> Result<()> {
84127
let ver_str = match std::fs::read_to_string(p) {
85128
Ok(x) => x,
86129
Err(e) if e.kind() == io::ErrorKind::NotFound => "".into(),
87-
Err(e) => panic!("failed to read version: {e}"),
130+
Err(e) => return Err(Error::ReadingVersion(e)),
88131
};
89132

90133
if ver_str.is_empty() {
91-
return atomic_write(p, current.to_string().as_bytes()).unwrap();
134+
atomic_write(p, current.to_string().as_bytes()).map_err(Error::WritingVersion)?;
135+
return Ok(());
92136
}
93137

94138
let prev_version = Version::parse(&ver_str).unwrap();
95-
if prev_version < *least_compatible {
96-
panic!(
97-
"The previous version {} is not compatible with the current version {}",
98-
prev_version, current
99-
);
139+
if prev_version < least_compatible {
140+
return Err(Error::CheckingVersion(Box::new(CheckingVersionError {
141+
least_compatible,
142+
data: prev_version,
143+
current: current.clone(),
144+
})));
100145
}
101-
atomic_write(p, current.to_string().as_bytes()).unwrap();
146+
atomic_write(p, current.to_string().as_bytes()).map_err(Error::WritingVersion)?;
147+
Ok(())
102148
}
103149

104150
/// Write content to p atomically. Create the parent directory if it doesn't
@@ -107,7 +153,7 @@ fn check_version(p: &Path, current: &Version, least_compatible: &Version) {
107153
/// # Panics
108154
///
109155
/// if p.parent() is None.
110-
fn atomic_write(p: &Path, content: &[u8]) -> std::io::Result<()> {
156+
fn atomic_write(p: &Path, content: &[u8]) -> io::Result<()> {
111157
let parent = p.parent().unwrap();
112158

113159
std::fs::create_dir_all(parent)?;
@@ -147,28 +193,39 @@ mod tests {
147193
use super::*;
148194

149195
#[test]
150-
fn test_check_version() {
196+
fn test_check_version() -> Result<()> {
151197
let tmp = NamedTempFile::new().unwrap();
152198
let p = tmp.path();
153199
// We just want NamedTempFile to delete the file on drop. We want to
154200
// start with the file not exist.
155201
std::fs::remove_file(p).unwrap();
156202

157-
let least_compatible = "0.1.0-alpha.9".parse().unwrap();
203+
let latest_compatible: Version = "0.1.0-alpha.9".parse().unwrap();
158204

159-
check_version(p, &"0.1.15".parse().unwrap(), &least_compatible);
205+
check_version(p, &"0.1.15".parse().unwrap(), latest_compatible.clone())?;
160206
assert_eq!(std::fs::read_to_string(p).unwrap(), "0.1.15");
161207

162-
check_version(p, &"0.2.0".parse().unwrap(), &least_compatible);
208+
check_version(p, &"0.2.0".parse().unwrap(), latest_compatible)?;
163209
assert_eq!(std::fs::read_to_string(p).unwrap(), "0.2.0");
210+
211+
Ok(())
164212
}
165213

166-
#[should_panic = "The previous version"]
167214
#[test]
168-
fn test_check_version_failure() {
215+
fn test_check_version_failure() -> Result<()> {
169216
let tmp = NamedTempFile::new().unwrap();
170217
let p = tmp.path();
171-
check_version(p, &"0.1.0".parse().unwrap(), &"0.1.0".parse().unwrap());
172-
check_version(p, &"0.2.0".parse().unwrap(), &"0.2.0".parse().unwrap());
218+
check_version(p, &"0.1.0".parse().unwrap(), "0.1.0".parse().unwrap())?;
219+
let err =
220+
check_version(p, &"0.2.2".parse().unwrap(), "0.2.0".parse().unwrap()).unwrap_err();
221+
match err {
222+
Error::CheckingVersion(e) => assert_eq!(*e, CheckingVersionError {
223+
current: "0.2.2".parse().unwrap(),
224+
least_compatible: "0.2.0".parse().unwrap(),
225+
data: "0.1.0".parse().unwrap(),
226+
}),
227+
e => panic!("unexpected error {e}"),
228+
}
229+
Ok(())
173230
}
174231
}

examples/custom_chain.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,9 @@ impl KeyProvider for CustomKey {
4949
}
5050

5151
fn main() {
52-
axon::run(CustomFeeAllocator::default(), CustomKey::default())
52+
let result = axon::run(CustomFeeAllocator::default(), CustomKey::default(), "0.1.0");
53+
if let Err(e) = result {
54+
eprintln!("Error {e}");
55+
std::process::exit(1);
56+
}
5357
}

src/lib.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ pub use protocol::{
77

88
use std::sync::Arc;
99

10-
use core_cli::AxonCli;
10+
use core_cli::{AxonCli, Result};
1111
use core_executor::FEE_ALLOCATOR;
1212

13-
pub fn run(fee_allocator: impl FeeAllocate + 'static, key_provider: impl KeyProvider) {
13+
pub fn run(
14+
fee_allocator: impl FeeAllocate + 'static,
15+
key_provider: impl KeyProvider,
16+
cli_version: &'static str,
17+
) -> Result<()> {
1418
FEE_ALLOCATOR.swap(Arc::new(Box::new(fee_allocator)));
15-
AxonCli::init(clap::crate_version!()).start_with_custom_key_provider(Some(key_provider));
19+
AxonCli::init(clap::crate_version!().parse().unwrap(), cli_version)
20+
.start_with_custom_key_provider(Some(key_provider))
1621
}

src/main.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
use core_cli::AxonCli;
22

33
fn main() {
4-
AxonCli::init(clap::crate_version!()).start();
4+
let result = AxonCli::init(
5+
clap::crate_version!().parse().unwrap(),
6+
concat!(clap::crate_version!(), env!("AXON_GIT_DESCRIPTION")),
7+
)
8+
.start();
9+
if let Err(e) = result {
10+
eprintln!("Error {e}");
11+
std::process::exit(1);
12+
}
513
}

0 commit comments

Comments
 (0)