Improved error handling.

Signed-off-by: Daira Hopwood <daira@jacaranda.org>
This commit is contained in:
Daira Hopwood 2022-03-07 15:00:59 +00:00
parent 25792cba93
commit 6f5efcbb0f
3 changed files with 104 additions and 45 deletions

7
Cargo.lock generated
View File

@ -67,6 +67,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
[[package]]
name = "arrayref"
version = "0.3.6"
@ -921,6 +927,7 @@ checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db"
name = "librustzcash"
version = "0.2.0"
dependencies = [
"anyhow",
"backtrace",
"bellman",
"blake2b_simd 1.0.0",

View File

@ -65,6 +65,8 @@ thiserror = "1"
tokio = { version = "1.0", features = ["rt", "net", "time", "macros"] }
# Wallet tool
# (also depends on thiserror)
anyhow = "1.0"
backtrace = "0.3"
clearscreen = "1.0"
gumdrop = "0.8"

View File

@ -10,9 +10,11 @@ use std::process::{self, Command, Output};
use std::str::from_utf8;
use std::time::SystemTime;
use anyhow::{self, Context};
use backtrace::Backtrace;
use gumdrop::{Options, ParsingStyle};
use rand::{thread_rng, Rng};
use thiserror::Error;
use time::macros::format_description;
use time::OffsetDateTime;
@ -104,6 +106,24 @@ impl CliOptions {
}
}
#[derive(Debug, Error)]
enum WalletToolError {
#[error("zcash-cli executable not found")]
ZcashCliNotFound,
#[error("Unexpected response from zcash-cli or zcashd")]
UnexpectedResponse,
#[error("Could not connect to zcashd")]
ZcashdConnection,
#[error("zcashd -exportdir option not set")]
ExportDirNotSet,
#[error("Could not parse a recovery phrase from the export file")]
RecoveryPhraseNotFound,
}
pub fn main() {
// Allow either Bitcoin-style or GNU-style arguments.
let mut args = env::args();
@ -151,7 +171,7 @@ pub fn main() {
}
}
fn run(opts: &CliOptions) -> Result<(), io::Error> {
fn run(opts: &CliOptions) -> anyhow::Result<()> {
let cli_options: Vec<String> = opts.to_zcash_cli_options();
println!(concat!(
@ -170,13 +190,12 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
cli_args.extend_from_slice(&["z_exportwallet".to_string(), "\x01".to_string()]);
let out = exec(&zcash_cli, &cli_args, opts.debug)?;
let cli_err: Vec<_> = from_utf8(&out.stderr)
.map_err(|_| io::Error::from(ErrorKind::InvalidData))?
.with_context(|| "Output from zcash-cli was not UTF-8")?
.lines()
.map(|s| s.trim_end_matches('\r'))
.collect();
if cli_err.len() < 3 || cli_err[0] != "error code: -4" || cli_err[1] != "error message:" {
// TODO: distinguish not running case more precisely
if !cli_err.is_empty() && cli_err[0].starts_with("error: couldn't connect") {
println!(concat!(
"\nNo, we could not connect. zcashd might not be running; in that case\n",
"please start it. The '-exportdir' option should be set to the absolute\n",
@ -190,18 +209,37 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
"use the same connection options for zcashd-wallet-tool (see '--help' for\n",
"accepted options) as for zcash-cli.\n"
));
return Err(io::Error::from(ErrorKind::Other));
return Err(WalletToolError::ZcashdConnection.into());
}
if cli_err[2].contains("zcashd -exportdir") {
if !cli_err.is_empty() && cli_err[0] == "error code: -28" {
println!(concat!(
"\nIt looks like zcashd is running without the '-exportdir' option.\n\n",
"Please start or restart zcashd with '-exportdir' set to the absolute\n",
"\nNo, we could not connect. zcashd seems to be initializing; please try\n",
"again once it has finished.\n",
));
return Err(WalletToolError::ZcashdConnection.into());
}
if cli_err.len() < 3
|| cli_err[0] != "error code: -4"
|| !cli_err[2].starts_with("Filename is invalid")
{
let e = if cli_err[2].contains("zcashd -exportdir") {
println!("\nIt looks like zcashd is running without the '-exportdir' option.");
WalletToolError::ExportDirNotSet
} else {
println!(
"\nThere was an unexpected response from zcashd:\n> {}",
cli_err.join("\n> ")
);
WalletToolError::UnexpectedResponse
};
println!(concat!(
"\nPlease start or restart zcashd with '-exportdir' set to the absolute\n",
"path of the directory you want to save the wallet export file to.\n",
"(Don't forget to restart zcashd without '-exportdir' after finishing\n",
"the backup, if running it long-term with that option is not desired\n",
"or would be a security hazard in your environment.)"
"or would be a security hazard in your environment.)\n",
));
return Err(io::Error::from(ErrorKind::Other));
return Err(e.into());
}
println!("Yes, and it is running with the '-exportdir' option as required.");
@ -238,7 +276,7 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
cli_args.extend_from_slice(&["z_exportwallet".to_string(), filename.to_string()]);
let out = exec(&zcash_cli, &cli_args, opts.debug)?;
let cli_err: Vec<_> = from_utf8(&out.stderr)
.map_err(|_| io::Error::from(ErrorKind::InvalidData))?
.with_context(|| "Output from zcash-cli was not UTF-8")?
.lines()
.map(|s| s.trim_end_matches('\r'))
.collect();
@ -261,7 +299,7 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
};
let cli_out: Vec<_> = from_utf8(&out.stdout)
.map_err(|_| io::Error::from(ErrorKind::InvalidData))?
.with_context(|| "Output from zcash-cli was not UTF-8")?
.lines()
.map(|s| s.trim_end_matches('\r'))
.collect();
@ -269,15 +307,15 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
eprintln!("DEBUG: stdout {:?}", cli_out);
}
if cli_out.is_empty() {
return Err(io::Error::from(ErrorKind::InvalidData));
return Err(WalletToolError::UnexpectedResponse.into());
}
let export_path = cli_out[0];
println!("\nSaved the export file to '{}'.", export_path);
println!("IMPORTANT: This file contains secrets that allow spending all wallet funds.\n");
// TODO: better handling of file not found and permission errors.
let export_file = File::open(export_path)?;
let export_file = File::open(export_path)
.with_context(|| format!("Could not open {:?} for reading", export_path))?;
let phrase_line: Vec<_> = io::BufReader::new(export_file)
.lines()
.filter(|s| {
@ -287,7 +325,7 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
})
.collect();
if phrase_line.len() != 1 || phrase_line[0].is_err() {
return Err(io::Error::from(ErrorKind::InvalidData));
return Err(WalletToolError::RecoveryPhraseNotFound.into());
}
let phrase = phrase_line[0]
.as_ref()
@ -307,7 +345,7 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
eprintln!("\nPanic: {}\n{:?}", s, Backtrace::new());
}));
let res = (|| -> io::Result<()> {
let res = (|| -> anyhow::Result<()> {
println!("The recovery phrase is:\n");
const WORDS_PER_LINE: usize = 3;
@ -380,7 +418,7 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
exec(&zcash_cli, &cli_args, false)
.and_then(|out| {
let cli_err: Vec<_> = from_utf8(&out.stderr)
.map_err(|_| io::Error::from(ErrorKind::InvalidData))?
.with_context(|| "Output from zcash-cli was not UTF-8")?
.lines()
.map(|s| s.trim_end_matches('\r'))
.collect();
@ -388,10 +426,17 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
eprintln!("DEBUG: stderr {:?}", cli_err);
}
if !cli_err.is_empty() {
// TODO: distinguish errors: cannot connect, vs incorrect passphrase
// (RPC_WALLET_PASSPHRASE_INCORRECT), vs other errors.
Err(io::Error::from(ErrorKind::Other))
if cli_err[0].starts_with("error: couldn't connect") {
println!("\nWe could not connect to zcashd; it may have exited.");
return Err(WalletToolError::ZcashdConnection.into());
} else {
println!(
"\nThere was an unexpected response from zcashd:\n> {}",
cli_err.join("\n> "),
);
return Err(WalletToolError::UnexpectedResponse.into());
}
}
println!(concat!(
"\nThe backup of the emergency recovery phrase for the zcashd\n",
"wallet has been successfully confirmed 🙂. You can now use the\n",
@ -400,7 +445,6 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
"to be backed up separately.\n"
));
Ok(())
}
})
.map_err(|e| {
println!(concat!(
@ -410,16 +454,22 @@ fn run(opts: &CliOptions) -> Result<(), io::Error> {
"help or try to use 'zcash-cli -stdin walletconfirmbackup' manually.\n"
));
e
})
})?;
Ok(())
}
fn prompt<'a>(input: &mut Stdin, buf: &'a mut String) -> io::Result<&'a str> {
let n = input.read_line(buf)?;
if n == 0 {
return Err(io::Error::from(ErrorKind::UnexpectedEof));
}
fn prompt<'a>(input: &mut Stdin, buf: &'a mut String) -> anyhow::Result<&'a str> {
input
.read_line(buf)
.with_context(|| "Error reading from stdin")?;
if !buf.ends_with('\n') {
Err(io::Error::from(ErrorKind::UnexpectedEof))
.with_context(|| "End of file reading from stdin")
} else {
Ok(buf.trim_end_matches(|c| c == '\r' || c == '\n').trim())
}
}
fn ordinal(num: usize) -> String {
let suffix = if (11..=13).contains(&(num % 100)) {
@ -435,9 +485,10 @@ fn ordinal(num: usize) -> String {
format!("{}{}", num, suffix)
}
fn zcash_cli_path(debug: bool) -> io::Result<PathBuf> {
fn zcash_cli_path(debug: bool) -> anyhow::Result<PathBuf> {
// First look for `zcash_cli[.exe]` as a sibling of the executable.
let mut exe = env::current_exe()?;
let mut exe = env::current_exe()
.with_context(|| "Cannot determine the path of the running executable")?;
exe.set_file_name("zcash-cli");
exe.set_extension(EXE_EXTENSION);
if debug {
@ -454,7 +505,7 @@ fn zcash_cli_path(debug: bool) -> io::Result<PathBuf> {
// or in `../../src/zcash_cli[.exe]` under the same proviso
exe.pop(); // ../..
if exe.file_name() != Some(OsStr::new("target")) {
return Err(io::Error::from(ErrorKind::NotFound));
return Err(WalletToolError::ZcashCliNotFound.into());
}
}
// Replace 'target/' with 'src/'.
@ -464,18 +515,17 @@ fn zcash_cli_path(debug: bool) -> io::Result<PathBuf> {
if debug {
eprintln!("DEBUG: Testing for zcash-cli at {:?}", exe);
}
if exe.exists() {
Ok(exe)
} else {
Err(io::Error::from(ErrorKind::NotFound))
if !exe.exists() {
return Err(WalletToolError::ZcashCliNotFound.into());
}
Ok(exe)
}
fn exec(exe_path: &Path, args: &[String], debug: bool) -> Result<Output, io::Error> {
fn exec(exe_path: &Path, args: &[String], debug: bool) -> anyhow::Result<Output> {
if debug {
eprintln!("DEBUG: Running {:?} {:?}", exe_path, args);
}
Command::new(exe_path).args(args).output()
Ok(Command::new(exe_path).args(args).output()?)
}
fn default_filename_base() -> String {