Files
lix/lix/libutil/error.hh
T
eldritch horrors d20c3d3643 libutil: remove ErrorInfo::programName
it's effectively unused. one use is a write and a read immediately after
the write, the other use checks whether it's not equal to itself (..wat)

Change-Id: I5f6ce26e75a6bfa500c2e9ac3fc70e8dafc9bd74
2025-07-14 17:02:30 +00:00

332 lines
8.5 KiB
C++

#pragma once
/**
* @file
*
* @brief This file defines two main structs/classes used in nix error handling.
*
* ErrorInfo provides a standard payload of error information, with conversion to string
* happening in the logger rather than at the call site.
*
* BaseError is the ancestor of nix specific exceptions (and Interrupted), and contains
* an ErrorInfo.
*
* ErrorInfo structs are sent to the logger as part of an exception, or directly with the
* logError or logWarning macros.
* See libutil/tests/logging.cc for usage examples.
*/
#include "lix/libutil/json-fwd.hh"
#include "lix/libutil/suggestions.hh"
#include "lix/libutil/fmt.hh"
#include <cstring>
#include <exception>
#include <list>
#include <memory>
#include <optional>
#include <source_location>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <system_error>
#include <type_traits>
namespace nix {
typedef enum {
lvlError = 0,
lvlWarn,
lvlNotice,
lvlInfo,
lvlTalkative,
lvlChatty,
lvlDebug,
lvlVomit
} Verbosity;
template<>
struct json::is_integral_enum<Verbosity> : std::true_type {};
Verbosity verbosityFromIntClamped(int val);
/**
* The lines of code surrounding an error.
*/
struct LinesOfCode {
std::optional<std::string> prevLineOfCode;
std::optional<std::string> errLineOfCode;
std::optional<std::string> nextLineOfCode;
};
struct Pos;
void printCodeLines(std::ostream & out,
const std::string & prefix,
const Pos & errPos,
const LinesOfCode & loc);
struct Trace {
std::shared_ptr<Pos> pos;
HintFmt hint;
};
inline bool operator<(const Trace& lhs, const Trace& rhs);
inline bool operator> (const Trace& lhs, const Trace& rhs);
inline bool operator<=(const Trace& lhs, const Trace& rhs);
inline bool operator>=(const Trace& lhs, const Trace& rhs);
struct ErrorInfo {
Verbosity level = Verbosity::lvlError;
HintFmt msg;
std::shared_ptr<Pos> pos;
std::list<Trace> traces = {};
/**
* Exit status.
*/
unsigned int status = 1;
Suggestions suggestions = {};
};
std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool showTrace);
/**
* Base class for both errors we can handle (c.f. `BaseError`) and anything
* we want to log and terminate when encountered (c.f. `ForeignException`).
*/
class BaseException : public std::exception
{
public:
struct AsyncTraceFrame
{
std::source_location location;
std::optional<std::string> description;
};
private:
/**
* Approximate list of async tasks this exception propagated through.
* It is the responsibility of each task to add itself to the back of
* this list during stack unwinding. `TRY_AWAIT` does this when used.
*/
std::shared_ptr<std::list<AsyncTraceFrame>> _asyncTrace;
public:
std::shared_ptr<const std::list<AsyncTraceFrame>> asyncTrace() const
{
return _asyncTrace;
}
void
addAsyncTrace(std::source_location loc, std::optional<std::string> description = std::nullopt)
{
if (!_asyncTrace) {
_asyncTrace = std::make_shared<std::list<AsyncTraceFrame>>();
}
_asyncTrace->push_back(AsyncTraceFrame{loc, std::move(description)});
}
};
/**
* BaseError should generally not be caught, as it has Interrupted as
* a subclass. Catch Error instead.
*/
class BaseError : public BaseException
{
protected:
mutable ErrorInfo err;
/**
* Cached formatted contents of `err.msg`.
*/
mutable std::optional<std::string> what_;
/**
* Format `err.msg` and set `what_` to the resulting value.
*/
const std::string & calcWhat() const;
public:
BaseError(const BaseError &) = default;
BaseError & operator=(BaseError const & rhs) = default;
template<typename... Args>
BaseError(unsigned int status, const Args & ... args)
: err { .level = lvlError, .msg = HintFmt(args...), .status = status }
{ }
template<typename... Args>
explicit BaseError(const std::string & fs, const Args & ... args)
: err { .level = lvlError, .msg = HintFmt(fs, args...) }
{ }
template<typename... Args>
BaseError(const Suggestions & sug, const Args & ... args)
: err { .level = lvlError, .msg = HintFmt(args...), .suggestions = sug }
{ }
BaseError(HintFmt hint)
: err { .level = lvlError, .msg = hint }
{ }
BaseError(ErrorInfo && e)
: err(std::move(e))
{ }
BaseError(const ErrorInfo & e)
: err(e)
{ }
const char * what() const noexcept override { return calcWhat().c_str(); }
const std::string & msg() const { return calcWhat(); }
const ErrorInfo & info() const { calcWhat(); return err; }
void withExitStatus(unsigned int status)
{
err.status = status;
}
void atPos(std::shared_ptr<Pos> pos) {
err.pos = pos;
}
void pushTrace(Trace trace)
{
err.traces.push_front(trace);
}
template<typename... Args>
void addTrace(std::shared_ptr<Pos> && e, std::string_view fs, const Args & ... args)
{
addTrace(std::move(e), HintFmt(std::string(fs), args...));
}
void addTrace(std::shared_ptr<Pos> && e, HintFmt hint);
bool hasTrace() const { return !err.traces.empty(); }
const ErrorInfo & info() { return err; };
};
#define MakeError(newClass, superClass) \
/* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \
class newClass : public superClass \
{ \
public: \
using superClass::superClass; \
}
MakeError(Error, BaseError);
MakeError(UsageError, Error);
MakeError(UnimplementedError, Error);
/**
* Wrap any exception as BaseException. We don't wrap as Error because we do not
* usually want to catch these exceptions and we don't use std::nested_exception
* because we need the dynamic type to preserve the original exception for that.
* This should never be used to wrap something that already is a BaseError (or a
* BaseException), but this isn't checked since this should not be thrown often.
*/
class ForeignException : public BaseException
{
std::shared_ptr<std::string> _what;
public:
const std::exception_ptr inner;
const std::type_info & innerType;
explicit ForeignException(const std::exception & inner)
: _what(std::make_shared<std::string>(inner.what()))
, inner(std::make_exception_ptr(inner))
, innerType(typeid(inner))
{
}
[[noreturn]]
void rethrow() const
{
std::rethrow_exception(inner);
}
template<typename E>
E * as() const
{
try {
rethrow();
} catch (E & e) { // NOLINT(lix-foreign-exceptions)
return &e;
} catch (...) {
return nullptr;
}
}
template<typename E>
bool is() const
{
return as<E>() != nullptr;
}
const char * what() const noexcept override
{
return _what->c_str();
}
};
class SysError : public Error
{
public:
int errNo;
template<typename... Args>
SysError(int errNo_, const Args & ... args)
: Error("")
{
errNo = errNo_;
auto hf = HintFmt(args...);
err.msg = HintFmt("%1%: %2%", Uncolored(hf.str()), strerror(errNo));
}
template<typename... Args>
SysError(std::error_code ec, const Args & ... args)
: Error("")
{
errNo = ec.value();
auto hf = HintFmt(args...);
err.msg = HintFmt("%1%: %2%", Uncolored(hf.str()), ec.message());
}
template<typename... Args>
SysError(const Args & ... args)
: SysError(errno, args ...)
{
}
};
/**
* Exception handling in destructors: print an error message, then
* ignore the exception.
*
* If you're not in a destructor, you usually want to use `ignoreExceptionExceptInterrupt()`.
*
* This function might also be used in callbacks whose caller may not handle exceptions,
* but ideally we propagate the exception using an exception_ptr in such cases.
* See e.g. `PackBuilderContext`
*/
void ignoreExceptionInDestructor(Verbosity lvl = lvlError);
/**
* Not destructor-safe.
* Print an error message, then ignore the exception.
* If the exception is an `Interrupted` exception, rethrow it.
*
* This may be used in a few places where Interrupt can't happen, but that's ok.
*/
void ignoreExceptionExceptInterrupt(Verbosity lvl = lvlError);
/** Print out details about an exception and its stack trace. */
void logException(std::string_view message_prefix, const std::exception & ex);
}