pub type Result<T> = std::result::Result<T, Error>;
#[cfg(feature = "backtrace")]
pub use std::backtrace::Backtrace;
#[cfg(feature = "backtrace")]
#[derive(Debug)]
pub struct BacktraceError {
message: String,
backtrace: Backtrace,
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
}
#[cfg(feature = "backtrace")]
impl BacktraceError {
#[allow(dead_code)]
pub fn new<S: Into<String>>(message: S) -> Self {
Self {
message: message.into(),
backtrace: Backtrace::capture(),
source: None,
}
}
#[allow(dead_code)]
pub fn with_source<S: Into<String>, E: std::error::Error + Send + Sync + 'static>(
message: S,
source: E,
) -> Self {
Self {
message: message.into(),
backtrace: Backtrace::capture(),
source: Some(Box::new(source)),
}
}
pub const fn backtrace(&self) -> &Backtrace {
&self.backtrace
}
}
#[cfg(feature = "backtrace")]
impl std::fmt::Display for BacktraceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
#[cfg(feature = "backtrace")]
impl std::error::Error for BacktraceError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|s| s.as_ref() as &(dyn std::error::Error + 'static))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum ErrorCode {
ConfigInvalid = 1000,
ConfigParse = 1001,
ConfigMissing = 1002,
ConfigTypeMismatch = 1003,
SignalRegisterFailed = 2000,
SignalSendFailed = 2001,
SignalInvalid = 2002,
ShutdownTimeout = 3000,
ShutdownAlreadyInProgress = 3001,
ShutdownFailed = 3002,
SubsystemStartFailed = 4000,
SubsystemStopFailed = 4001,
SubsystemNotFound = 4002,
SubsystemAlreadyRegistered = 4003,
SubsystemStateInvalid = 4004,
IoError = 5000,
FileNotFound = 5001,
FilePermissionDenied = 5002,
RuntimePanic = 6000,
RuntimeAsyncError = 6001,
LockFailed = 6002,
RuntimeSpawnError = 6003,
MissingRuntime = 6004,
ResourceExhaustedMemory = 7000,
ResourceExhaustedCpu = 7001,
ResourceExhaustedFileDescriptors = 7002,
TimeoutOperation = 8000,
TimeoutConnection = 8001,
InvalidStateTransition = 9000,
InvalidStateValue = 9001,
PlatformNotSupported = 10000,
PlatformFeatureNotAvailable = 10001,
Unknown = 99999,
}
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({})", self.as_str(), *self as i32)
}
}
impl ErrorCode {
pub const fn as_str(self) -> &'static str {
match self {
Self::ConfigInvalid => "CONFIG_INVALID",
Self::ConfigParse => "CONFIG_PARSE",
Self::ConfigMissing => "CONFIG_MISSING",
Self::ConfigTypeMismatch => "CONFIG_TYPE_MISMATCH",
Self::SignalRegisterFailed => "SIGNAL_REGISTER_FAILED",
Self::SignalSendFailed => "SIGNAL_SEND_FAILED",
Self::SignalInvalid => "SIGNAL_INVALID",
Self::ShutdownTimeout => "SHUTDOWN_TIMEOUT",
Self::ShutdownAlreadyInProgress => "SHUTDOWN_ALREADY_IN_PROGRESS",
Self::ShutdownFailed => "SHUTDOWN_FAILED",
Self::SubsystemStartFailed => "SUBSYSTEM_START_FAILED",
Self::SubsystemStopFailed => "SUBSYSTEM_STOP_FAILED",
Self::SubsystemNotFound => "SUBSYSTEM_NOT_FOUND",
Self::SubsystemAlreadyRegistered => "SUBSYSTEM_ALREADY_REGISTERED",
Self::SubsystemStateInvalid => "SUBSYSTEM_STATE_INVALID",
Self::IoError => "IO_ERROR",
Self::FileNotFound => "FILE_NOT_FOUND",
Self::FilePermissionDenied => "FILE_PERMISSION_DENIED",
Self::RuntimePanic => "RUNTIME_PANIC",
Self::RuntimeAsyncError => "RUNTIME_ASYNC_ERROR",
Self::LockFailed => "LOCK_FAILED",
Self::RuntimeSpawnError => "RUNTIME_SPAWN_ERROR",
Self::MissingRuntime => "MISSING_RUNTIME",
Self::ResourceExhaustedMemory => "RESOURCE_EXHAUSTED_MEMORY",
Self::ResourceExhaustedCpu => "RESOURCE_EXHAUSTED_CPU",
Self::ResourceExhaustedFileDescriptors => "RESOURCE_EXHAUSTED_FILE_DESCRIPTORS",
Self::TimeoutOperation => "TIMEOUT_OPERATION",
Self::TimeoutConnection => "TIMEOUT_CONNECTION",
Self::InvalidStateTransition => "INVALID_STATE_TRANSITION",
Self::InvalidStateValue => "INVALID_STATE_VALUE",
Self::PlatformNotSupported => "PLATFORM_NOT_SUPPORTED",
Self::PlatformFeatureNotAvailable => "PLATFORM_FEATURE_NOT_AVAILABLE",
Self::Unknown => "UNKNOWN_ERROR",
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Configuration error [{code}]: {message}")]
Config {
code: ErrorCode,
message: String,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[error("Signal handling error [{code}]: {message}{signal:?}")]
Signal {
code: ErrorCode,
message: String,
signal: Option<i32>,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[error("Shutdown error [{code}]: {message}{timeout_ms:?}")]
Shutdown {
code: ErrorCode,
message: String,
timeout_ms: Option<u64>,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[error("Subsystem '{name}' error [{code}]: {message}")]
Subsystem {
code: ErrorCode,
name: String,
message: String,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[error("I/O error [{code}]: {message}")]
Io {
code: ErrorCode,
message: String,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[error("Resource exhausted [{code}]: {resource} - {message}")]
ResourceExhausted {
code: ErrorCode,
resource: String,
message: String,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[error("Operation timed out [{code}] after {timeout_ms}ms: {operation}")]
Timeout {
code: ErrorCode,
operation: String,
timeout_ms: u64,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[error("Invalid state [{code}]: {message}{current_state:?}")]
InvalidState {
code: ErrorCode,
message: String,
current_state: Option<String>,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[error("Platform error [{code}]: {message} (platform: {platform})")]
Platform {
code: ErrorCode,
message: String,
platform: String,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[error("Runtime error [{code}]: {message}")]
Runtime {
code: ErrorCode,
message: String,
#[source]
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
}
impl Error {
#[cfg(feature = "backtrace")]
#[must_use]
pub fn backtrace(&self) -> Option<&Backtrace> {
match self {
Self::Config {
source: Some(err), ..
}
| Self::Subsystem {
source: Some(err), ..
}
| Self::Io {
source: Some(err), ..
}
| Self::Runtime {
source: Some(err), ..
}
| Self::ResourceExhausted {
source: Some(err), ..
}
| Self::Timeout {
source: Some(err), ..
}
| Self::InvalidState {
source: Some(err), ..
}
| Self::Platform {
source: Some(err), ..
}
| Self::Signal {
source: Some(err), ..
}
| Self::Shutdown {
source: Some(err), ..
} => err
.downcast_ref::<BacktraceError>()
.map(BacktraceError::backtrace),
_ => None,
}
}
pub fn config<S: Into<String>>(message: S) -> Self {
Self::Config {
code: ErrorCode::ConfigInvalid,
message: message.into(),
source: None,
}
}
pub fn signal<S: Into<String>>(message: S) -> Self {
Self::Signal {
code: ErrorCode::SignalInvalid,
message: message.into(),
signal: None,
source: None,
}
}
pub fn signal_with_number<S: Into<String>>(message: S, signal: i32) -> Self {
Self::Signal {
code: ErrorCode::SignalInvalid,
message: message.into(),
signal: Some(signal),
source: None,
}
}
pub fn signal_with_code<S: Into<String>>(code: ErrorCode, message: S) -> Self {
Self::Signal {
code,
message: message.into(),
signal: None,
source: None,
}
}
pub fn shutdown<S: Into<String>>(message: S) -> Self {
Self::Shutdown {
code: ErrorCode::ShutdownFailed,
message: message.into(),
timeout_ms: None,
source: None,
}
}
pub fn shutdown_timeout<S: Into<String>>(message: S, timeout_ms: u64) -> Self {
Self::Shutdown {
code: ErrorCode::ShutdownTimeout,
message: message.into(),
timeout_ms: Some(timeout_ms),
source: None,
}
}
pub fn shutdown_with_code<S: Into<String>>(code: ErrorCode, message: S) -> Self {
Self::Shutdown {
code,
message: message.into(),
timeout_ms: None,
source: None,
}
}
pub fn subsystem<S: Into<String>, M: Into<String>>(name: S, message: M) -> Self {
Self::Subsystem {
code: ErrorCode::SubsystemNotFound,
name: name.into(),
message: message.into(),
source: None,
}
}
pub fn subsystem_with_code<S: Into<String>, M: Into<String>>(
code: ErrorCode,
name: S,
message: M,
) -> Self {
Self::Subsystem {
code,
name: name.into(),
message: message.into(),
source: None,
}
}
pub fn io<S: Into<String>>(message: S) -> Self {
Self::Io {
code: ErrorCode::IoError,
message: message.into(),
source: None,
}
}
pub fn io_with_source<S: Into<String>, E: std::error::Error + Send + Sync + 'static>(
message: S,
source: E,
) -> Self {
Self::Io {
code: ErrorCode::IoError,
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn runtime<S: Into<String>>(message: S) -> Self {
Self::Runtime {
code: ErrorCode::RuntimePanic,
message: message.into(),
source: None,
}
}
pub fn runtime_with_code<S: Into<String>>(code: ErrorCode, message: S) -> Self {
Self::Runtime {
code,
message: message.into(),
source: None,
}
}
pub fn runtime_with_source<S: Into<String>, E: std::error::Error + Send + Sync + 'static>(
message: S,
source: E,
) -> Self {
Self::Runtime {
code: ErrorCode::RuntimePanic,
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn resource_exhausted<S: Into<String>, M: Into<String>>(resource: S, message: M) -> Self {
Self::ResourceExhausted {
code: ErrorCode::ResourceExhaustedMemory,
resource: resource.into(),
message: message.into(),
source: None,
}
}
pub fn resource_exhausted_with_code<S: Into<String>, M: Into<String>>(
code: ErrorCode,
resource: S,
message: M,
) -> Self {
Self::ResourceExhausted {
code,
resource: resource.into(),
message: message.into(),
source: None,
}
}
pub fn timeout<S: Into<String>>(operation: S, timeout_ms: u64) -> Self {
Self::Timeout {
code: ErrorCode::TimeoutOperation,
operation: operation.into(),
timeout_ms,
source: None,
}
}
pub fn timeout_with_source<S: Into<String>, E: std::error::Error + Send + Sync + 'static>(
operation: S,
timeout_ms: u64,
source: E,
) -> Self {
Self::Timeout {
code: ErrorCode::TimeoutOperation,
operation: operation.into(),
timeout_ms,
source: Some(Box::new(source)),
}
}
pub fn invalid_state<S: Into<String>>(message: S) -> Self {
Self::InvalidState {
code: ErrorCode::InvalidStateValue,
message: message.into(),
current_state: None,
source: None,
}
}
pub fn invalid_state_with_current<S: Into<String>, C: Into<String>>(
message: S,
current_state: C,
) -> Self {
Self::InvalidState {
code: ErrorCode::InvalidStateTransition,
message: message.into(),
current_state: Some(current_state.into()),
source: None,
}
}
pub fn invalid_state_with_code<S: Into<String>>(code: ErrorCode, message: S) -> Self {
Self::InvalidState {
code,
message: message.into(),
current_state: None,
source: None,
}
}
pub fn platform<S: Into<String>, P: Into<String>>(message: S, platform: P) -> Self {
Self::Platform {
code: ErrorCode::PlatformNotSupported,
message: message.into(),
platform: platform.into(),
source: None,
}
}
pub fn platform_with_code<S: Into<String>, P: Into<String>>(
code: ErrorCode,
message: S,
platform: P,
) -> Self {
Self::Platform {
code,
message: message.into(),
platform: platform.into(),
source: None,
}
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(
self,
Self::Io { .. } | Self::Runtime { .. } | Self::ResourceExhausted { .. }
)
}
#[must_use]
pub const fn is_timeout(&self) -> bool {
matches!(self, Self::Timeout { .. })
}
#[must_use]
pub const fn is_config_error(&self) -> bool {
matches!(self, Self::Config { .. })
}
#[must_use]
pub const fn category(&self) -> &'static str {
match self {
Self::Config { .. } => "config",
Self::Signal { .. } => "signal",
Self::Shutdown { .. } => "shutdown",
Self::Subsystem { .. } => "subsystem",
Self::Io { .. } => "io",
Self::Runtime { .. } => "runtime",
Self::ResourceExhausted { .. } => "resource",
Self::Timeout { .. } => "timeout",
Self::InvalidState { .. } => "state",
Self::Platform { .. } => "platform",
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self::io(format!("I/O operation failed: {err}"))
}
}
impl From<ctrlc::Error> for Error {
fn from(err: ctrlc::Error) -> Self {
Self::signal(format!("Signal handler error: {err}"))
}
}
impl From<figment::Error> for Error {
fn from(err: figment::Error) -> Self {
Self::config(format!("Configuration loading failed: {err}"))
}
}
#[cfg(feature = "toml")]
impl From<toml::de::Error> for Error {
fn from(err: toml::de::Error) -> Self {
Self::config(format!("TOML parsing failed: {err}"))
}
}
#[cfg(feature = "serde_json")]
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::config(format!("JSON parsing failed: {err}"))
}
}
#[macro_export]
macro_rules! daemon_error {
($kind:ident, $($arg:tt)*) => {
$crate::Error::$kind(format!($($arg)*))
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = Error::config("test message");
assert!(err.is_config_error());
assert_eq!(err.category(), "config");
}
#[test]
fn test_retryable_errors() {
let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
let err = Error::io(format!("operation failed: {io_err}"));
assert!(err.is_retryable());
}
#[test]
fn test_timeout_error() {
let err = Error::timeout("test operation", 5000);
assert!(err.is_timeout());
assert_eq!(err.category(), "timeout");
}
}