diff --git a/Cargo.lock b/Cargo.lock index b331bc1..10a2574 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,12 +46,14 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "const_format", "dfu-core", "dfu-libusb", "env_logger", "indicatif", "log", "rusb", + "rustc_version", "thiserror", ] @@ -109,6 +111,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "const_format" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0936ffe6d0c8d6a51b3b0a73b2acbe925d786f346cf45bfddc8341d79fb7dc8a" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef196d5d972878a48da7decb7686eded338b4858fbabeed513d63a7c98b2b82d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "dfu-core" version = "0.2.1" @@ -312,6 +334,21 @@ dependencies = [ "libusb1-sys", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" + [[package]] name = "syn" version = "1.0.91" diff --git a/Cargo.toml b/Cargo.toml index e91b85a..c6feaa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,14 @@ name = "bmputil" version = "0.1.0" edition = "2021" +[features] +# Enable backtraces for errors. Requires nightly toolchain. +# Automatically enabled if detect-backtrace feature is enabled (default). +backtrace = [] +# Autoamtically detect if backtrace feature should be enabled by detecting the channel we're being compiled on. +detect-backtrace = [] +default = ["detect-backtrace"] + [dependencies] clap = { version = "3.0", default-features = false, features = ["std", "color"] } env_logger = "0.9" @@ -10,10 +18,14 @@ dfu-core = { version = "0.2", features = ["std"] } dfu-libusb = "0.2" rusb = "0.9" log = "0.4" +const_format = "0.2" anyhow = "1.0" thiserror = "1.0" indicatif = "0.16" +[build-dependencies] +rustc_version = "0.4" + [profile.release] lto = "fat" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..8fb05d6 --- /dev/null +++ b/build.rs @@ -0,0 +1,23 @@ + +use rustc_version::{version_meta, Channel}; + +fn main() +{ + // If detect-backtrace is enabled (default), detect if we're on nightly or not. + // If we're on nightly, enable backtraces automatically. + match std::env::var_os("CARGO_FEATURE_DETECT_BACKTRACE") { + Some(_val) => { + match version_meta() { + Ok(version_meta) => { + if version_meta.channel == Channel::Nightly { + println!("cargo:rustc-cfg=feature=\"backtrace\""); + } + }, + Err(e) => { + println!("cargo:warning=error detecting rustc version: {}", e); + } + } + }, + None => (), + } +} diff --git a/src/bmp.rs b/src/bmp.rs index 7a48d4a..42c0aad 100644 --- a/src/bmp.rs +++ b/src/bmp.rs @@ -6,14 +6,15 @@ use std::str::FromStr; use std::time::{Duration, Instant}; use std::fmt::{self, Display, Formatter}; -use crate::{BmputilError, libusb_cannot_fail}; -use crate::usb::{DfuFunctionalDescriptor, InterfaceClass, InterfaceSubClass, GenericDescriptorRef, DfuRequest}; -use crate::usb::{Vid, Pid, DfuOperatingMode, DfuMatch}; - -use dfu_libusb::DfuLibusb; -use log::{trace, info, warn, error}; use clap::ArgMatches; +use log::{trace, info, warn, error}; use rusb::{UsbContext, Direction, RequestType, Recipient}; +use dfu_libusb::DfuLibusb; + +use crate::libusb_cannot_fail; +use crate::error::{Error, ErrorKind}; +use crate::usb::{DfuFunctionalDescriptor, InterfaceClass, InterfaceSubClass, GenericDescriptorRef, DfuRequest}; +use crate::usb::{Vid, Pid, DfuOperatingMode, DfuMatch}; type UsbDevice = rusb::Device; type UsbHandle = rusb::DeviceHandle; @@ -39,7 +40,7 @@ impl BlackmagicProbeDevice /// Creates a [`BlackmagicProbeDevice`] struct from the first found connected Blackmagic Probe. #[allow(dead_code)] - pub fn first_found() -> Result + pub fn first_found() -> Result { let context = rusb::Context::new()?; let devices = context @@ -51,7 +52,7 @@ impl BlackmagicProbeDevice // Alas, this is the probably best way until Iterator::try_find is stable. // (https://github.com/rust-lang/rust/issues/63178). let (device, vid, pid) = loop { - let dev = devices.next().ok_or(BmputilError::DeviceNotFoundError)?; + let dev = devices.next().ok_or_else(|| ErrorKind::DeviceNotFound.error())?; let desc = dev.device_descriptor() .expect(libusb_cannot_fail!("libusb_get_device_descriptor()")); @@ -86,9 +87,9 @@ impl BlackmagicProbeDevice /// using the first device for which `matcher_fn` returns `true`. /// However, it is not assumed that `matcher_fn` is in fact a valid Blackmagic Probe device. /// If the device matched by `matcher_fn` does not seem to be a valid Blackmagic Probe device, - /// this function returns `Err(BmputilError::DeviceNotFoundError)`. + /// this function returns `ErrorKind::DeviceNotFoundError`. #[allow(dead_code)] - pub fn from_matching(matcher_fn: MatcherT) -> Result + pub fn from_matching(matcher_fn: MatcherT) -> Result where MatcherT: Fn(&UsbDevice) -> bool, { let context = rusb::Context::new()?; @@ -97,7 +98,7 @@ impl BlackmagicProbeDevice .unwrap() .iter() .find(matcher_fn) - .ok_or(BmputilError::DeviceNotFoundError)?; + .ok_or_else(|| ErrorKind::DeviceNotFound.error())?; let desc = device.device_descriptor() .expect(libusb_cannot_fail!("libusb_get_device_descriptor()")); @@ -109,7 +110,7 @@ impl BlackmagicProbeDevice let mode = BmpVidPid::mode_from_vid_pid(vid, pid).ok_or_else(|| { warn!("Matcher function given to find a BMP device does not seem to have returned a BMP device!"); warn!("The matcher function passed to BlackmagicProbeDevice::from_matching() is probably incorrect!"); - BmputilError::DeviceNotFoundError + ErrorKind::DeviceNotFound.error() })?; let handle = device.open()?; @@ -122,7 +123,7 @@ impl BlackmagicProbeDevice }) } - pub fn from_usb_device(device: UsbDevice) -> Result + pub fn from_usb_device(device: UsbDevice) -> Result { let desc = device.device_descriptor() .expect(libusb_cannot_fail!("libusb_get_device_descriptor()")); @@ -130,7 +131,7 @@ impl BlackmagicProbeDevice let mode = BmpVidPid::mode_from_vid_pid(vid, pid).ok_or_else(|| { warn!("Device passed to BlackmagicProbeDevice::from_usb_device() does not seem to be a BMP device!"); warn!("The logic for finding this device is probably incorrect!"); - BmputilError::DeviceNotFoundError + ErrorKind::DeviceNotFound.error() })?; let handle = device.open()?; @@ -182,7 +183,7 @@ impl BlackmagicProbeDevice /// This struct caches the serial number in an [`std::cell::RefCell`], /// and thus returns a `Ref` rather than the `&str` directly. /// Feel free to clone the result if you want a directly referenceable value. - pub fn serial_number(&self) -> Result, BmputilError> + pub fn serial_number(&self) -> Result, Error> { let serial = self.serial.borrow(); if serial.is_some() { @@ -194,10 +195,10 @@ impl BlackmagicProbeDevice let languages = self.handle.read_languages(Duration::from_secs(2))?; if languages.is_empty() { - return Err(BmputilError::DeviceSeemsInvalidError { - source: None, - invalid_thing: String::from("no string descriptor languages"), - }); + return Err( + ErrorKind::DeviceSeemsInvalid(String::from("no string descriptor languages")) + .error() + ); } let language = languages.first().unwrap(); // Okay as we proved len > 0. @@ -224,7 +225,7 @@ impl BlackmagicProbeDevice /// /// This does not execute any requests to the device, and only uses information already /// available from libusb's device structures. - pub fn dfu_descriptors(&self) -> Result<(u8, DfuFunctionalDescriptor), BmputilError> + pub fn dfu_descriptors(&self) -> Result<(u8, DfuFunctionalDescriptor), Error> { let configuration = match self.device.active_config_descriptor() { Ok(d) => d, @@ -244,10 +245,11 @@ impl BlackmagicProbeDevice match self.device.config_descriptor(1) { Ok(d) => d, Err(e) => { - return Err(BmputilError::DeviceSeemsInvalidError { - source: Some(e.into()), - invalid_thing: String::from("no configuration descriptor exists"), - }); + return Err( + ErrorKind::DeviceSeemsInvalid( + String::from("no configuration descriptor exists") + ).error_from(e) + ); }, } }, @@ -269,10 +271,7 @@ impl BlackmagicProbeDevice desc.sub_class_code() == InterfaceSubClass::DFU.0 }) - .ok_or_else(|| BmputilError::DeviceSeemsInvalidError { - source: None, - invalid_thing: String::from("no DFU interfaces"), - })?; + .ok_or_else(|| ErrorKind::DeviceSeemsInvalid(String::from("no DFU interfaces")).error())?; // Get the data for all the "extra" descriptors that follow the interface descriptor. let extra_descriptors: Vec<_> = GenericDescriptorRef::multiple_from_bytes(dfu_interface_descriptor.extra()); @@ -287,16 +286,16 @@ impl BlackmagicProbeDevice .unwrap(); // Unwrap fine as we already set the length two lines above. let dfu_func_desc = DfuFunctionalDescriptor::copy_from_bytes(dfu_func_desc_bytes) - .map_err(|desc_convert_err| BmputilError::DeviceSeemsInvalidError { - source: Some(desc_convert_err.into()), - invalid_thing: String::from("DFU functional descriptor"), + .map_err(|source| { + ErrorKind::DeviceSeemsInvalid(String::from("DFU functional descriptor")) + .error_from(source) })?; Ok((dfu_interface_descriptor.interface_number(), dfu_func_desc)) } /// Requests the device to leave DFU mode, using the DefuSe extensions. - fn leave_dfu_mode(&mut self) -> Result<(), BmputilError> + fn leave_dfu_mode(&mut self) -> Result<(), Error> { let (iface_number, _func_desc) = self.dfu_descriptors()?; self.handle.claim_interface(iface_number)?; @@ -348,7 +347,7 @@ impl BlackmagicProbeDevice } /// Performs a DFU_DETACH request to enter DFU mode. - fn enter_dfu_mode(&mut self) -> Result<(), BmputilError> + fn enter_dfu_mode(&mut self) -> Result<(), Error> { let (iface_number, func_desc) = self.dfu_descriptors()?; self.handle.claim_interface(iface_number)?; @@ -386,7 +385,7 @@ impl BlackmagicProbeDevice /// if the device successfully detached. Further requests will fail, and functions like /// `dfu_descriptors()` may return now-incorrect data. /// - pub unsafe fn request_detach(&mut self) -> Result<(), BmputilError> + pub unsafe fn request_detach(&mut self) -> Result<(), Error> { use DfuOperatingMode::*; let res = match self.mode { @@ -403,11 +402,13 @@ impl BlackmagicProbeDevice /// Requests the Black Magic Probe to detach, and re-initializes this struct with the new /// device. - pub fn detach_and_enumerate(&mut self) -> Result<(), BmputilError> + pub fn detach_and_enumerate(&mut self) -> Result<(), Error> { // Make sure we have our serial number before we try to detach, // so that we can find the DFU-mode device when it re-enumerates. - let serial = self.serial_number()?.to_string(); + let serial = self.serial_number() + .map_err(|e| e.with_ctx("reading device serial number"))? + .to_string(); unsafe { self.request_detach()? }; // Now that we've detached, try to find the device again with the same serial number. @@ -425,7 +426,7 @@ impl BlackmagicProbeDevice /// /// Currently there is not a way to recover this instance if this function errors. /// You'll just have to create another one. - pub fn detach_and_destroy(mut self) -> Result<(), BmputilError> + pub fn detach_and_destroy(mut self) -> Result<(), Error> { unsafe { self.request_detach()? }; @@ -436,7 +437,7 @@ impl BlackmagicProbeDevice /// /// `progress` is a callback of the form `fn(just_written: usize)`, for callers to keep track of /// the flashing process. - pub fn download(&mut self, firmware: R, length: u32, progress: P) -> Result<(), BmputilError> + pub fn download(&mut self, firmware: R, length: u32, progress: P) -> Result<(), Error> where R: Read, P: Fn(usize) + 'static, @@ -457,7 +458,19 @@ impl BlackmagicProbeDevice info!("Performing flash..."); - dfu_dev.download(firmware, length)?; + dfu_dev.download(firmware, length) + .map_err(|source| { + match source { + dfu_libusb::Error::LibUsb(rusb::Error::NoDevice) => { + error!("Black Magic Probe device disconnected during the flash process!"); + warn!( + "If the device now fails to enumerate, try holding down the button while plugging the device in order to enter the bootloader." + ); + ErrorKind::DeviceDisconnectDuringOperation.error_from(source) + } + _ => source.into(), + } + })?; info!("Flash complete!"); @@ -588,13 +601,13 @@ pub struct BlackmagicProbeMatchResults { pub found: Vec, pub filtered_out: Vec, - pub errors: Vec, + pub errors: Vec, } impl BlackmagicProbeMatchResults { /// Pops all found devices, handling printing error and warning cases. - pub(crate) fn pop_all(&mut self) -> Result, BmputilError> + pub(crate) fn pop_all(&mut self) -> Result, Error> { if self.found.is_empty() { if !self.filtered_out.is_empty() { @@ -605,14 +618,14 @@ impl BlackmagicProbeMatchResults suffix, verb, ); - warn!("Filter arguments (--serial, --index, --port may be incorrect."); + warn!("Filter arguments (--serial, --index, --port) may be incorrect."); } if !self.errors.is_empty() { warn!("Device not found and errors occurred when searching for devices."); warn!("One of these may be why the Blackmagic Probe device was not found: {:?}", self.errors.as_slice()); } - return Err(BmputilError::DeviceNotFoundError); + return Err(ErrorKind::DeviceNotFound.error()); } if !self.errors.is_empty() { @@ -621,11 +634,11 @@ impl BlackmagicProbeMatchResults warn!("Other device errors: {:?}", self.errors.as_slice()); } - return Ok(mem::take(&mut self.found)); + Ok(mem::take(&mut self.found)) } /// Pops a single found device, handling printing error and warning cases. - pub(crate) fn pop_single(&mut self, operation: &str) -> Result + pub(crate) fn pop_single(&mut self, operation: &str) -> Result { if self.found.is_empty() { if !self.filtered_out.is_empty() { @@ -643,7 +656,7 @@ impl BlackmagicProbeMatchResults warn!("Device not found and errors occurred when searching for devices."); warn!("One of these may be why the Blackmagic Probe device was not found: {:?}", self.errors.as_slice()); } - return Err(BmputilError::DeviceNotFoundError); + return Err(ErrorKind::DeviceNotFound.error()); } if self.found.len() > 1 { @@ -653,7 +666,7 @@ impl BlackmagicProbeMatchResults self.found.len() ); error!("Hint: try bmputil info and revise your filter arguments (--serial, --index, --port)."); - return Err(BmputilError::TooManyDevicesError); + return Err(ErrorKind::TooManyDevices.error()); } if !self.errors.is_empty() { @@ -666,15 +679,15 @@ impl BlackmagicProbeMatchResults } /// Like `pop_single()`, but does not print helpful diagnostics for edge cases. - pub(crate) fn pop_single_silent(&mut self) -> Result + pub(crate) fn pop_single_silent(&mut self) -> Result { if self.found.len() > 1 { - return Err(BmputilError::TooManyDevicesError); + return Err(ErrorKind::TooManyDevices.error()); } else if self.found.is_empty() { - return Err(BmputilError::DeviceNotFoundError); + return Err(ErrorKind::DeviceNotFound.error()); } - return Ok(self.found.remove(0)); + Ok(self.found.remove(0)) } } @@ -692,9 +705,6 @@ impl BlackmagicProbeMatchResults /// The `index` matcher *includes* devices that errored when attempting to match them. pub fn find_matching_probes(matcher: &BlackmagicProbeMatcher) -> BlackmagicProbeMatchResults { - //let mut found: Vec = Vec::new(); - //let mut filtered_out: Vec = Vec::new(); - //let mut errors: Vec = Vec::new(); let mut results = BlackmagicProbeMatchResults { found: Vec::new(), filtered_out: Vec::new(), @@ -731,7 +741,7 @@ pub fn find_matching_probes(matcher: &BlackmagicProbeMatcher) -> BlackmagicProbe for (index, dev) in devices.enumerate() { // If we're trying to match against a serial number, try to open the device. - let handle = if let Some(_) = matcher.serial { + let handle = if matcher.serial.is_some() { match dev.open() { Ok(h) => Some(h), Err(e) => { @@ -804,7 +814,7 @@ pub fn find_matching_probes(matcher: &BlackmagicProbeMatcher) -> BlackmagicProbe match BlackmagicProbeDevice::from_usb_device(dev) { Ok(bmpdev) => results.found.push(bmpdev), Err(e) => { - results.errors.push(e.into()); + results.errors.push(e); continue; }, }; @@ -819,7 +829,7 @@ pub fn find_matching_probes(matcher: &BlackmagicProbeMatcher) -> BlackmagicProbe } -pub fn wait_for_probe_reboot(serial: &str, timeout: Duration, operation: &str) -> Result +pub fn wait_for_probe_reboot(serial: &str, timeout: Duration, operation: &str) -> Result { let silence_timeout = timeout / 2; @@ -833,14 +843,14 @@ pub fn wait_for_probe_reboot(serial: &str, timeout: Duration, operation: &str) - let mut dev = find_matching_probes(&matcher).pop_single_silent(); - while let Err(BmputilError::DeviceNotFoundError) = dev { + while let Err(ErrorKind::DeviceNotFound) = dev.as_ref().map_err(|e| &e.kind) { // If it's been more than the timeout length, error out. if start.duration_since(Instant::now()) > timeout { error!( "Timed-out waiting for Black Magic Probe to re-enumerate!" ); - return Err(BmputilError::DeviceRebootError { source: None }); + return Err(ErrorKind::DeviceReboot.error_from(dev.unwrap_err())); } // Wait 200 milliseconds between checks. Hardware is a bottleneck and we diff --git a/src/error.rs b/src/error.rs index dede089..572439c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,85 +1,279 @@ //! Module for error handling code. +use std::fmt::{Display, Formatter}; +#[cfg(feature = "backtrace")] +use std::backtrace::{Backtrace, BacktraceStatus}; +use std::error::Error as StdError; + use thiserror::Error; -#[derive(Debug, Error)] -#[allow(dead_code)] // XXX FIXME -pub enum BmputilError +use crate::S; + +/// More convenient alias for `Box`, +/// which shows up in a few signatures and structs. +type BoxedError = Box; + +/// Kinds of errors for [Error]. Use [ErrorKind::new] and [ErrorKind::error_from] to generate the +/// [Error] value for this ErrorKind. +#[derive(Debug)] +pub enum ErrorKind { - #[error("Failed to read firmware file {filename}")] - FirmwareFileIOError + /// Failed to read firmware file. + FirmwareFileIo(Option), + + /// Current operation only supports one Black Magic Probe but more tha none device was found. + TooManyDevices, + + /// Black Magic Probe device not found. + DeviceNotFound, + + /// Black Magic Probe found disconnected during an ongoing operation. + DeviceDisconnectDuringOperation, + + /// Black Magic Probe device did not come back online (e.g. after switching to DFU mode + /// or flashing firmware). + DeviceReboot, + + /// Black Magic Probe device returned bad data during configuration. + /// + /// This generally shouldn't be possible, but could happen if the cable is bad, the OS is + /// messing with things, or the firmware on the device is corrupted. + DeviceSeemsInvalid(/** invalid thing **/ String), + + /// Unhandled external error. + External(ErrorSource), +} + +impl ErrorKind +{ + /// Creates a new [Error] from this error kind. + /// + /// Enables convenient code like: + /// ``` + /// return Err(ErrorKind::DeviceNotFound.new()); + /// ``` + #[inline(always)] + pub fn error(self) -> Error { - #[source] - source: std::io::Error, - filename: String, - }, + Error::new(self, None) + } - #[error("More than one Blackmagic Probe device was found")] - TooManyDevicesError, + /// Creates a new [Error] from this error kind, with the passed error as the source. + /// + /// Enables convenient code like: + /// ``` + /// # let operation = || std::io::Error::from(std::io::ErrorKind::PermissionDenied); + /// operation().map_err(|e| ErrorKind::DeviceNotFound.error_from(e))?; + /// ``` + #[inline(always)] + pub fn error_from(self, source: E) -> Error + { + Error::new(self, Some(Box::new(source))) + } +} - #[error("No connected Blackmagic Probe device was found! Check connection?")] - DeviceNotFoundError, +/// Constructs an [Error] for this [ErrorKind]. +impl From for Error +{ + /// Constructs an [Error] for this [ErrorKind]. + fn from(other: ErrorKind) -> Self + { + other.error() + } +} - #[error("Access denied when attempting to {operation} to {context}")] - PermissionsError +impl Display for ErrorKind +{ + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - #[source] - source: rusb::Error, + use ErrorKind::*; + match self { + FirmwareFileIo(None) => write!(f, "failed to read firmware file")?, + FirmwareFileIo(Some(filename)) => write!(f, "failed to read firmware file {filename}")?, + TooManyDevices => write!(f, "current operation only supports one Black Magic Probe device but more than one device was found")?, + DeviceNotFound => write!(f, "Black Magic Probe device not found (check connection?)")?, + DeviceDisconnectDuringOperation => write!(f, "Black Magic Probe device found disconnected")?, + DeviceReboot => write!(f, "Black Magic Probe device did not come back online (invalid firmware?)")?, + DeviceSeemsInvalid(thing) => { + write!( + f, + "Black Magic Probe device returned bad data ({}) during configuration.\ + This generally shouldn't be possible. Maybe cable is bad, or OS is messing with things?", + thing, + )?; + }, + External(source) => { + use ErrorSource::*; + match source { + StdIo(e) => { + write!(f, "unhandled std::io::Error: {}", e)?; + }, + Libusb(e) => { + write!(f, "unhandled libusb error: {}", e)?; + }, + DfuLibusb(e) => { + write!(f, "unhandled dfu_libusb error: {}", e)?; + }, + }; + }, + }; - /// The USB/libusb operation that failed (e.g. `"send a control transfer"`). - operation: String, + Ok(()) + } +} - /// The context that operation was being performed in (e.g.: `"read firmware version"`). - context: String, - }, - #[error("Blackmagic Probe device found disconnected when attempting to {operation} to {context}")] - DeviceDisconnectDuringOperationError +#[derive(Debug)] +/// Error type for Black Magic Probe operations. Easily constructed from [ErrorKind]. +pub struct Error +{ + pub kind: ErrorKind, + pub source: Option, + #[cfg(feature = "backtrace")] + pub backtrace: Backtrace, + /// A string for additional context about what was being attempted when this error occurred. + /// + /// Example: "reading current firmware version". + pub context: Option, +} + +impl Error +{ + #[inline(always)] + pub fn new(kind: ErrorKind, source: Option) -> Self { - #[source] - source: rusb::Error, + Self { + kind, + source, + context: None, + #[cfg(feature = "backtrace")] + backtrace: Backtrace::capture(), + } + } - /// The USB/libusb operation that failed (e.g.: `"send a control transfer"`). - operation: String, + #[allow(dead_code)] + /// Add additional context about what was being attempted when this error occurred. + /// + /// Example: "reading current firmware version". + pub fn with_ctx(mut self, ctx: &str) -> Self + { + self.context = Some(ctx.to_string()); + self + } + +} + +impl Display for Error +{ + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result + { + if let Some(ctx) = &self.context { + writeln!(f, "(while {}): {}", ctx, self.kind)?; + } else { + writeln!(f, "{}", self.kind)?; + } - /// The context that operation was being performed in (e.g.: `"read firmware version"`). - context: String, - }, + #[cfg(feature = "backtrace")] + { + if self.backtrace.status() == BacktraceStatus::Captured { + write!(f, "Backtrace:\n{}", self.backtrace)?; + } + } - #[error("Blackmagic Probe device did not re-enumerate after requesting to switch to DFU mode")] - DeviceReconfigureError + if let Some(source) = &self.source { + writeln!(f, "\nCaused by: {}", source)?; + } + + Ok(()) + } +} + +impl StdError for Error +{ + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - /// Source is optional because there may be no libusb error, if detecting connection is - /// done through e.g. checking device lists. - #[source] - source: Option, - }, - - #[allow(dead_code)] // FIXME: this will presumably be used once we, well, actually implement the post-flash check. - #[error("Blackmagic Probe device did not re-enumerate after flashing firmware; firmware may be invalid?")] - DeviceRebootError + + None + } + + #[cfg(feature = "backtrace")] + fn backtrace(&self) -> Option<&Backtrace> { - #[source] - source: Option, - }, + Some(&self.backtrace) + } +} +impl From for Error +{ + fn from(other: rusb::Error) -> Self + { + use ErrorKind::*; + match other { + rusb::Error::NoDevice => DeviceNotFound.error_from(other), + other => External(ErrorSource::Libusb(other)).error() + } + } +} - #[error( - "Blackmagic Probe device returned bad data ({invalid_thing}) during configuration.\ - This generally shouldn't be possible. Maybe cable is bad, or OS is messing with things?" - )] - DeviceSeemsInvalidError +impl From for Error +{ + fn from(other: dfu_libusb::Error) -> Self { - #[source] - source: Option, - invalid_thing: String, - }, + use ErrorKind::*; + use dfu_libusb::Error as Source; + match other { + dfu_libusb::Error::LibUsb(source) => { + External(ErrorSource::Libusb(source)).error_from(other) + }, + dfu_libusb::Error::MemoryLayout(source) => { + DeviceSeemsInvalid(String::from("DFU interface memory layout string")) + .error_from(source) + }, + dfu_libusb::Error::MissingLanguage => { + DeviceSeemsInvalid(S!("no string descriptor languages")) + .error_from(other) + }, + Source::InvalidAlt => { + DeviceSeemsInvalid(S!("DFU interface (alt mode) not found")) + .error_from(other) + }, + Source::InvalidAddress => { + DeviceSeemsInvalid(S!("DFU interface memory layout string")) + .error_from(other) + }, + Source::InvalidInterface => { + DeviceSeemsInvalid(S!("DFU interface not found")) + .error_from(other) + }, + Source::InvalidInterfaceString => { + DeviceSeemsInvalid(S!("DFU interface memory layout string")) + .error_from(other) + }, + Source::FunctionalDescriptor(source) => { + DeviceSeemsInvalid(S!("DFU functional interface descriptor")) + .error_from(source) + }, + anything_else => { + External(ErrorSource::DfuLibusb(anything_else)) + .error() + }, + } + } +} + + +/// Sources of external error in this library. +#[derive(Debug, Error)] +pub enum ErrorSource +{ + #[error(transparent)] + StdIo(#[from] std::io::Error), - #[error("Other/unhandled libusb error (please report this so we can add better handling!)")] - LibusbError(#[from] rusb::Error), + #[error(transparent)] + Libusb(#[from] rusb::Error), - #[error("Other/unhandled dfu_libusb error (please report this so we can add better error handling!")] - DfuLibusbError(#[from] dfu_libusb::Error), + #[error(transparent)] + DfuLibusb(#[from] dfu_libusb::Error), } diff --git a/src/main.rs b/src/main.rs index 09ae143..5dad65a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,7 @@ +#![cfg_attr(feature = "backtrace", feature(backtrace))] +#[cfg(feature = "backtrace")] +use std::backtrace::BacktraceStatus; + use std::rc::Rc; use std::str::FromStr; use std::time::Duration; @@ -6,7 +10,6 @@ use clap::{Command, Arg, ArgMatches}; use indicatif::{ProgressBar, ProgressStyle}; -use anyhow::Result as AResult; use log::error; @@ -15,10 +18,19 @@ mod error; mod bmp; use crate::usb::DfuOperatingMode; use crate::bmp::{BlackmagicProbeDevice, BlackmagicProbeMatcher, find_matching_probes}; -use crate::error::BmputilError; +use crate::error::{Error, ErrorKind, ErrorSource}; + +#[macro_export] +#[doc(hidden)] +macro_rules! S +{ + ($expr:expr) => { + String::from($expr) + }; +} -fn detach_command(matches: &ArgMatches) -> Result<(), BmputilError> +fn detach_command(matches: &ArgMatches) -> Result<(), Error> { let matcher = BlackmagicProbeMatcher::from_clap_matches(matches); let mut results = find_matching_probes(&matcher); @@ -30,28 +42,25 @@ fn detach_command(matches: &ArgMatches) -> Result<(), BmputilError> FirmwareUpgrade => println!("Requesting device detach from DFU mode to runtime mode..."), }; - match dev.detach_and_destroy() { - Ok(()) => (), - Err(e) => { - error!("Device failed to detach!"); - return Err(e); - } - }; + dev.detach_and_destroy() + .map_err(|e| e.with_ctx("detaching device"))?; Ok(()) } -fn flash(matches: &ArgMatches) -> Result<(), BmputilError> +fn flash(matches: &ArgMatches) -> Result<(), Error> { let filename = matches.value_of("firmware_binary") .expect("No firmware file was specified!"); // Should be impossible, thanks to clap. let firmware_file = std::fs::File::open(filename) - .map_err(|source| BmputilError::FirmwareFileIOError { source, filename: filename.to_string() })?; + .map_err(|source| ErrorKind::FirmwareFileIo(Some(filename.to_string())).error_from(source)) + .map_err(|e| e.with_ctx("reading firmware file to flash"))?; let file_size = firmware_file.metadata() - .map_err(|source| BmputilError::FirmwareFileIOError { source, filename: filename.to_string() })? + .map_err(|source| ErrorKind::FirmwareFileIo(Some(filename.to_string())).error_from(source))? .len(); + let file_size = u32::try_from(file_size) .expect("firmware filesize exceeded 32 bits! Firmware binary must be invalid"); @@ -59,13 +68,16 @@ fn flash(matches: &ArgMatches) -> Result<(), BmputilError> let matcher = BlackmagicProbeMatcher::from_clap_matches(matches); let mut results = find_matching_probes(&matcher); let mut dev: BlackmagicProbeDevice = results.pop_single("flash")?; - let serial = dev.serial_number()?.to_string(); + let serial = dev.serial_number() + .map_err(|e| e.with_ctx("reading device serial number"))? + .to_string(); println!("Found: {}", dev); if dev.operating_mode() == DfuOperatingMode::Runtime { println!("Detaching and entering DFU mode..."); - dev.detach_and_enumerate()?; + dev.detach_and_enumerate() + .map_err(|e| e.with_ctx("detaching device to DFU mode"))?; } // We need an Rc as [`dfu_core::sync::DfuSync`] requires `progress` to be 'static, @@ -80,15 +92,18 @@ fn flash(matches: &ArgMatches) -> Result<(), BmputilError> let progress_bar = Rc::new(progress_bar); let enclosed = Rc::clone(&progress_bar); - match dev.download(firmware_file, file_size, move |delta| { + match dev.download(firmware_file, file_size, move |delta| { enclosed.inc(delta as u64); }) { Ok(v) => Ok(v), - Err(BmputilError::DfuLibusbError(dfu_libusb::Error::Io(source))) => Err(BmputilError::FirmwareFileIOError { - source, - filename: filename.to_string(), - }), - Err(e) => Err(e), + Err(e) => match e.kind { + ErrorKind::External(ErrorSource::DfuLibusb(dfu_libusb::Error::Io(source))) => { + Err(ErrorKind::FirmwareFileIo(Some(filename.to_string())).error_from(Box::new(source))) + }, + _ => { + Err(e) + }, + }, }?; progress_bar.finish(); @@ -132,7 +147,7 @@ fn flash(matches: &ArgMatches) -> Result<(), BmputilError> Ok(()) } -fn info_command(matches: &ArgMatches) -> Result<(), BmputilError> +fn info_command(matches: &ArgMatches) -> Result<(), Error> { let matcher = BlackmagicProbeMatcher::from_clap_matches(matches); @@ -156,7 +171,7 @@ fn info_command(matches: &ArgMatches) -> Result<(), BmputilError> } -fn main() -> AResult<()> +fn main() { env_logger::Builder::new() .filter_level(log::LevelFilter::Warn) @@ -217,11 +232,11 @@ fn main() -> AResult<()> let (subcommand, subcommand_matches) = matches.subcommand() .expect("No subcommand given!"); // Should be impossible, thanks to clap. - match subcommand { - "info" => info_command(subcommand_matches)?, - "flash" => flash(subcommand_matches)?, + let res = match subcommand { + "info" => info_command(subcommand_matches), + "flash" => flash(subcommand_matches), "debug" => match subcommand_matches.subcommand().unwrap() { - ("detach", detach_matches) => detach_command(detach_matches)?, + ("detach", detach_matches) => detach_command(detach_matches), _ => unreachable!(), }, @@ -229,5 +244,22 @@ fn main() -> AResult<()> &_ => unimplemented!(), }; - Ok(()) + + // Unfortunately, we have to do the printing ourselves, as we need to print a note + // in the event that backtraces are supported but not enabled. + if let Err(e) = res { + print!("Error: {e}"); + #[cfg(feature = "backtrace")] + { + if e.backtrace.status() == BacktraceStatus::Disabled { + println!("note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace."); + } + } + + if cfg!(not(feature = "backtrace")) { + println!("note: recompile with nightly toolchain and run with `RUST_BACKTRACE=1` environment variable to display a backtrace."); + } + + std::process::exit(1); + } } diff --git a/src/usb.rs b/src/usb.rs index c6b9cc3..bd2267c 100644 --- a/src/usb.rs +++ b/src/usb.rs @@ -243,11 +243,10 @@ pub(crate) const CHECKED_LIBUSB_VERSION: &str = "1.0.26"; macro_rules! libusb_cannot_fail { ($funcname:literal) => { - format!( + const_format::formatcp!( "As of libusb {}, {} cannot fail. This should be unreachable, unless libusb has updated.", - crate::usb::CHECKED_LIBUSB_VERSION, + $crate::usb::CHECKED_LIBUSB_VERSION, $funcname, ) - .as_str() }; }