moonfire-nvr/server/base/error.rs
Scott Lamb 64ca096ff3 massive error overhaul
* fully stop using ancient `failure` crate in favor of own error type
* set an `ErrorKind` on everything
2023-07-09 22:04:17 -07:00

459 lines
15 KiB
Rust

// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2018 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
use std::backtrace::Backtrace;
use std::error::Error as StdError;
use std::fmt::{Debug, Display};
//use std::num::NonZeroU16;
pub use coded::ErrorKind;
/// Like [`coded::ToErrKind`] but with more third-party implementations.
///
/// It's not possible to implement those here on that trait because of the orphan rule.
pub trait ToErrKind {
fn err_kind(&self) -> ErrorKind;
}
impl ToErrKind for Error {
#[inline]
fn err_kind(&self) -> ErrorKind {
self.0.kind
}
}
impl ToErrKind for std::io::Error {
#[inline]
fn err_kind(&self) -> ErrorKind {
self.kind().into()
}
}
impl ToErrKind for rusqlite::ErrorCode {
fn err_kind(&self) -> ErrorKind {
use rusqlite::ErrorCode;
// https://www.sqlite.org/rescode.html
match self {
ErrorCode::InternalMalfunction => ErrorKind::Internal,
ErrorCode::PermissionDenied => ErrorKind::PermissionDenied,
ErrorCode::OperationAborted => ErrorKind::Aborted,
// Conflict with another database connection in a process which is accessing
// the database, apparently without using Moonfire NVR's scheme of acquiring
// a lock on the db directory.
// https://www.sqlite.org/wal.html#sometimes_queries_return_sqlite_busy_in_wal_mode
ErrorCode::DatabaseBusy => ErrorKind::Unavailable,
// Conflict within the same database connection. Shouldn't happen for Moonfire.
ErrorCode::DatabaseLocked => ErrorKind::Internal,
ErrorCode::OutOfMemory => ErrorKind::ResourceExhausted,
ErrorCode::ReadOnly => ErrorKind::FailedPrecondition,
ErrorCode::OperationInterrupted => ErrorKind::Aborted,
ErrorCode::SystemIoFailure => ErrorKind::Unavailable,
ErrorCode::DatabaseCorrupt => ErrorKind::DataLoss,
ErrorCode::NotFound => ErrorKind::NotFound,
ErrorCode::DiskFull => ErrorKind::ResourceExhausted,
ErrorCode::CannotOpen => ErrorKind::Unavailable,
// Similar to DatabaseBusy in this implies a conflict with another conn.
ErrorCode::FileLockingProtocolFailed => ErrorKind::Unavailable,
// Likewise: Moonfire NVR should never change the schema
// mid-statement, so the most plausible explanation for
// SchemaChange is another process.
ErrorCode::SchemaChanged => ErrorKind::Unavailable,
ErrorCode::TooBig => ErrorKind::ResourceExhausted,
ErrorCode::ConstraintViolation => ErrorKind::Internal,
ErrorCode::TypeMismatch => ErrorKind::Internal,
ErrorCode::ApiMisuse => ErrorKind::Internal,
ErrorCode::NoLargeFileSupport => ErrorKind::ResourceExhausted,
ErrorCode::AuthorizationForStatementDenied => ErrorKind::Internal,
ErrorCode::ParameterOutOfRange => ErrorKind::Internal,
ErrorCode::NotADatabase => ErrorKind::FailedPrecondition,
_ => ErrorKind::Unknown,
}
}
}
impl ToErrKind for rusqlite::Error {
#[inline]
fn err_kind(&self) -> ErrorKind {
match self {
rusqlite::Error::SqliteFailure(e, _) => e.code.err_kind(),
_ => ErrorKind::Unknown,
}
}
}
impl ToErrKind for rusqlite::types::FromSqlError {
fn err_kind(&self) -> ErrorKind {
match self {
rusqlite::types::FromSqlError::InvalidType => ErrorKind::FailedPrecondition,
rusqlite::types::FromSqlError::OutOfRange(_) => ErrorKind::OutOfRange,
rusqlite::types::FromSqlError::InvalidBlobSize { .. } => ErrorKind::OutOfRange,
/* rusqlite::types::FromSqlError::Other(_) | */ _ => ErrorKind::Unknown,
}
}
}
impl ToErrKind for nix::Error {
fn err_kind(&self) -> ErrorKind {
use nix::Error;
match self {
Error::EACCES | Error::EPERM => ErrorKind::PermissionDenied,
Error::EDQUOT => ErrorKind::ResourceExhausted,
Error::EBUSY
| Error::EEXIST
| Error::ENOTDIR
| Error::EROFS
| Error::EFBIG
| Error::EOVERFLOW
| Error::ENXIO
| Error::ETXTBSY => ErrorKind::FailedPrecondition,
Error::EINVAL | Error::ENAMETOOLONG => ErrorKind::InvalidArgument,
Error::ELOOP => ErrorKind::FailedPrecondition,
Error::EMLINK | Error::ENOMEM | Error::ENOSPC | Error::EMFILE | Error::ENFILE => {
ErrorKind::ResourceExhausted
}
Error::EBADF | Error::EFAULT => ErrorKind::InvalidArgument,
Error::EINTR | Error::EAGAIN => ErrorKind::Aborted,
Error::ENOENT | Error::ENODEV => ErrorKind::NotFound,
Error::EOPNOTSUPP => ErrorKind::Unimplemented,
_ => ErrorKind::Unknown,
}
}
}
pub struct Error(Box<ErrorInner>);
struct ErrorInner {
kind: ErrorKind,
msg: Option<String>,
//http_status: Option<NonZeroU16>,
backtrace: Option<Backtrace>,
source: Option<Box<dyn StdError + Sync + Send>>,
}
pub struct ErrorBuilder(Box<ErrorInner>);
impl Default for ErrorBuilder {
#[inline]
fn default() -> Self {
Self(Box::new(ErrorInner {
kind: ErrorKind::Unknown,
msg: None,
// http_status: None,
backtrace: None,
source: None,
}))
}
}
impl From<ErrorKind> for ErrorBuilder {
#[inline]
fn from(value: ErrorKind) -> Self {
Self::default().kind(value)
}
}
impl ErrorBuilder {
#[inline]
pub fn kind(mut self, kind: ErrorKind) -> Self {
self.0.kind = kind;
self
}
#[inline]
pub fn map<F: Fn(ErrorKind) -> ErrorKind>(mut self, f: F) -> Self {
self.0.kind = f(self.0.kind);
self
}
#[inline]
pub fn msg(mut self, msg: String) -> Self {
self.0.msg = Some(msg);
self
}
#[inline]
pub fn source<S: Into<Box<dyn StdError + Send + Sync + 'static>>>(mut self, source: S) -> Self {
self.0.source = Some(source.into());
self
}
#[inline]
pub fn build(self) -> Error {
Error(self.0)
}
}
macro_rules! cvt {
($t:ty) => {
impl From<$t> for ErrorBuilder {
#[inline]
fn from(t: $t) -> Self {
Self::default().kind(ToErrKind::err_kind(&t)).source(t)
}
}
impl From<$t> for Error {
#[inline(always)]
fn from(t: $t) -> Self {
Self($crate::ErrorBuilder::from(t).0)
}
}
};
}
cvt!(rusqlite::Error);
cvt!(rusqlite::types::FromSqlError);
cvt!(std::io::Error);
cvt!(nix::Error);
impl From<Error> for ErrorBuilder {
#[inline]
fn from(value: Error) -> Self {
Self::default()
.kind(ToErrKind::err_kind(&value))
.source(value)
}
}
/// Captures a backtrace if enabled for the given error kind.
// TODO: make this more configurable at runtime.
fn maybe_backtrace(kind: ErrorKind) -> Option<Backtrace> {
if matches!(kind, ErrorKind::Internal | ErrorKind::Unknown) {
Some(Backtrace::capture())
} else {
None
}
}
impl Error {
#[inline]
pub fn wrap<E: StdError + Sync + Send + 'static>(kind: ErrorKind, e: E) -> Self {
Self(Box::new(ErrorInner {
kind,
msg: None,
// http_status: None,
backtrace: maybe_backtrace(kind),
source: Some(Box::new(e)),
}))
}
#[inline]
pub fn map<F: FnOnce(ErrorKind) -> ErrorKind>(mut self, f: F) -> Self {
self.0.kind = f(self.0.kind);
self
}
#[inline]
pub fn kind(&self) -> ErrorKind {
self.0.kind
}
#[inline]
pub fn msg(&self) -> Option<&str> {
self.0.msg.as_deref()
}
/// Returns a borrowed value which can display not only this error but also
/// the full chain of causes and (where applicable) the stack trace.
///
/// The exact format may change. Currently, it displays the stack trace for
/// the current error but not any of the sources.
#[inline]
pub fn chain(&self) -> impl Display + '_ {
ErrorChain(self)
}
}
/// Formats this error alone (*not* its full chain).
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0.msg {
None => std::fmt::Display::fmt(self.0.kind.grpc_name(), f)?,
Some(ref msg) => write!(f, "{}: {}", self.0.kind.grpc_name(), msg)?,
}
if let Some(ref bt) = self.0.backtrace {
// TODO: only with "alternate"/# modifier?
// Shorten this, maybe by switching to `backtrace` + using
// `backtrace_ext::short_frames_strict` or similar.
write!(f, "\nBacktrace:\n{}", bt)?;
}
Ok(())
}
}
impl Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&ErrorChain(self), f)
}
}
/// Value returned by [`Error::chain`].
struct ErrorChain<'a>(&'a Error);
impl Display for ErrorChain<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(self.0, f)?;
let mut source = self.0.source();
while let Some(n) = source {
write!(f, "\ncaused by: {}", n)?;
source = n.source()
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
// https://users.rust-lang.org/t/question-about-error-source-s-static-return-type/34515/8
self.0.source.as_ref().map(|e| e.as_ref() as &_)
}
}
/// Extension methods for `Result`.
pub trait ResultExt<T, E> {
/// Annotates an error with the given kind.
/// Example:
/// ```
/// use moonfire_base::{ErrorKind, ResultExt};
/// use std::io::Read;
/// let mut buf = [0u8; 1];
/// let r = std::io::Cursor::new("").read_exact(&mut buf[..]).err_kind(ErrorKind::Internal);
/// assert_eq!(r.unwrap_err().kind(), ErrorKind::Internal);
/// ```
fn err_kind(self, k: ErrorKind) -> Result<T, Error>;
}
impl<T, E> ResultExt<T, E> for Result<T, E>
where
E: StdError + Sync + Send + 'static,
{
fn err_kind(self, k: ErrorKind) -> Result<T, Error> {
self.map_err(|e| ErrorBuilder::default().kind(k).source(e).build())
}
}
/// Wrapper around `err!` which returns the error.
///
/// Example with positional arguments:
/// ```
/// use moonfire_base::bail;
/// let e = || -> Result<(), moonfire_base::Error> {
/// bail!(Unauthenticated, msg("unknown user: {}", "slamb"));
/// }().unwrap_err();
/// assert_eq!(e.kind(), moonfire_base::ErrorKind::Unauthenticated);
/// assert_eq!(e.to_string(), "UNAUTHENTICATED: unknown user: slamb");
/// ```
///
/// Example with named arguments:
/// ```
/// use moonfire_base::bail;
/// let e = || -> Result<(), moonfire_base::Error> {
/// let user = "slamb";
/// bail!(Unauthenticated, msg("unknown user: {user}"));
/// }().unwrap_err();
/// assert_eq!(e.kind(), moonfire_base::ErrorKind::Unauthenticated);
/// assert_eq!(e.to_string(), "UNAUTHENTICATED: unknown user: slamb");
/// ```
#[macro_export]
macro_rules! bail {
($($arg:tt)+) => {
return Err($crate::err!($($arg)+).into());
};
}
/// Constructs an [`Error`], tersely.
///
/// This is a shorthand way to use [`ErrorBuilder`].
///
/// The first argument is an `Into<ErrorBuilder>`, such as the following:
///
/// * an [`ErrorKind`] enum variant name like `Unauthenticated`.
/// There's an implicit `use ::coded::ErrorKind::*` to allow the bare
/// variant names just within this restrictive scope where you're unlikely
/// to have conflicts with other identifiers.
/// * an [`std::io::Error`] as a source, which sets the new `Error`'s
/// `ErrorKind` based on the `std::io::Error`.
/// * an `Error` as a source, which similarly copies the `ErrorKind`.
/// * an existing `ErrorBuilder`, which does not create a new source link.
///
/// Following arguments may be of these forms:
///
/// * `msg(...)`, which expands to `.msg(format!(...))`. See [`ErrorBuilder::msg`].
/// * `source(...)`, which simply expands to `.source($src)`. See [`ErrorBuilder::source`].
///
/// ## Examples
///
/// Simplest:
///
/// ```rust
/// # use coded::err;
/// let e = err!(InvalidArgument);
/// let e = err!(InvalidArgument,); // trailing commas are allowed
/// assert_eq!(e.kind(), coded::ErrorKind::InvalidArgument);
/// ```
///
/// Constructing with a fixed error variant name:
///
/// ```rust
/// # use {coded::err, std::error::Error, std::num::ParseIntError};
/// let input = "a12";
/// let src = i32::from_str_radix(input, 10).unwrap_err();
///
/// let e = err!(InvalidArgument, source(src.clone()), msg("bad argument {:?}", input));
/// // The line above is equivalent to:
/// let e2 = ::coded::ErrorBuilder::from(::coded::ErrorKind::InvalidArgument)
/// .source(src.clone())
/// .msg(format!("bad argument {:?}", input))
/// .build();
///
/// assert_eq!(e.kind(), coded::ErrorKind::InvalidArgument);
/// assert_eq!(e.source().unwrap().downcast_ref::<ParseIntError>().unwrap(), &src);
/// ```
///
/// Constructing from an `std::io::Error`:
///
/// ```rust
/// # use coded::err;
/// let e = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
/// let e = err!(e, msg("path {} not found", "foo"));
/// assert_eq!(e.kind(), coded::ErrorKind::NotFound);
/// ```
#[macro_export]
macro_rules! err {
// This uses the "incremental TT munchers", "internal rules", and "push-down accumulation"
// patterns explained in the excellent "The Little Book of Rust Macros":
// <https://veykril.github.io/tlborm/decl-macros/patterns/push-down-acc.html>.
(@accum $body:tt $(,)?) => {
$body.build()
};
(@accum ($($body:tt)*), source($src:expr) $($tail:tt)*) => {
$crate::err!(@accum ($($body)*.source($src)) $($tail)*)
};
// msg(...) uses the `format!` form even when there's only the format string.
// This can catch errors (e.g. https://github.com/dtolnay/anyhow/issues/55)
// and will allow supporting implicit named parameters:
// https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html
(@accum ($($body:tt)*), msg($format:expr) $($tail:tt)*) => {
$crate::err!(@accum ($($body)*.msg(format!($format))) $($tail)*)
};
(@accum ($($body:tt)*), msg($format:expr, $($args:tt)*) $($tail:tt)*) => {
$crate::err!(@accum ($($body)*.msg(format!($format, $($args)*))) $($tail)*)
};
($builder:expr $(, $($tail:tt)*)? ) => {
$crate::err!(@accum ({
use $crate::ErrorKind::*;
$crate::ErrorBuilder::from($builder)
})
, $($($tail)*)*
)
};
}