libutil: expose the functionality of Lix's exception handler

This introduces three new things:

* `handleException` which prints out exception details and its stack
  trace.
* `handleExceptionWithAsyncTrace` which does the same, but also prints
  the async trace if any.
* `LIX_BLOCK_ON` which is awaits a promise and adds an exception trace
  if an exception got thrown, similar to `LIX_TRY_AWAIT`. However, this
  is not supposed to be used in async functions, but on callsites of
  `aio.blockOn()` which is especially useful for Hydra[1].

For `LIX_BLOCK_ON` I had to introduce another function because there's
apparently no way to implement all of it in a macro: on macros with
compound statements the return value must be a trivial expression at the
bottom, i.e. no `try`/`catch`. Now, returning the value from the
`try`-block requires the variable to be defined up-front, but for that
we'd need to know the type-name. Hence the construction with a
template-function being invoked by a macro that injects the current
source-location.

[1] https://git.lix.systems/lix-project/hydra/pulls/52

Change-Id: I56cc92c94f7e8f0be5d4dc5a7d8cb21a92e776ef
This commit is contained in:
Maximilian Bosch
2025-06-20 18:14:38 +02:00
parent 35c3bfdacb
commit e23bed5e64
4 changed files with 47 additions and 31 deletions
+2 -28
View File
@@ -1,11 +1,9 @@
#include "lix/libmain/crash-handler.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/fmt.hh"
#include "lix/libutil/logging.hh"
#include <boost/core/demangle.hpp>
#include <exception>
#include <source_location>
namespace nix {
@@ -23,38 +21,14 @@ void onTerminate()
} else {
logFatal("std::terminate() called without exception");
}
} catch (const ForeignException & ex) {
asyncTrace = ex.asyncTrace();
logFatal(fmt("Exception: %s: %s", boost::core::demangle(ex.innerType.name()), ex.what()));
} catch (const BaseException & ex) {
asyncTrace = ex.asyncTrace();
logFatal(fmt("Exception: %s: %s", boost::core::demangle(typeid(ex).name()), ex.what()));
logException("Exception", ex);
} catch (const std::exception & ex) { // NOLINT(lix-foreign-exceptions)
logFatal(fmt("Exception: %s: %s", boost::core::demangle(typeid(ex).name()), ex.what()));
logException("Exception", ex);
} catch (...) {
logFatal("Unknown exception! Spooky.");
}
logFatal("Stack trace:");
logFatal(getStackTrace());
if (asyncTrace && !asyncTrace->empty()) {
logFatal("Async task trace (probably incomplete):");
for (auto [i, frame] : enumerate(*asyncTrace)) {
logFatal(
fmt("#%i: %s (%s:%i:%i)",
i,
frame.location.function_name(),
frame.location.file_name(),
frame.location.line(),
frame.location.column())
);
if (frame.description) {
logFatal(fmt("\t%s", *frame.description));
}
}
}
std::abort();
}
}
+12 -3
View File
@@ -44,7 +44,9 @@ struct AsyncIoRoot
KJ_DISALLOW_COPY_AND_MOVE(AsyncIoRoot);
template<typename T>
auto blockOn(kj::Promise<T> && promise);
auto blockOn(
kj::Promise<T> && promise, std::source_location call_site = std::source_location::current()
);
};
inline AsyncContext & AIO()
@@ -137,7 +139,14 @@ static constexpr std::optional<std::string> lixAsyncTaskContext()
#endif
template<typename T>
inline auto nix::AsyncIoRoot::blockOn(kj::Promise<T> && promise)
{
inline auto nix::AsyncIoRoot::blockOn(kj::Promise<T> && promise, std::source_location call_site)
try {
return detail::runAsyncUnwrap(promise.wait(kj.waitScope));
} catch (BaseException & e) {
e.addAsyncTrace(call_site);
throw;
} catch (std::exception & e) { /* NOLINT(lix-foreign-exceptions) */
ForeignException fe(e);
fe.addAsyncTrace(call_site);
throw fe;
}
+31
View File
@@ -10,6 +10,8 @@
#include <optional>
#include <sstream>
#include <boost/core/demangle.hpp>
namespace nix {
void BaseError::addTrace(std::shared_ptr<Pos> && e, HintFmt hint)
@@ -441,4 +443,33 @@ void ignoreExceptionExceptInterrupt(Verbosity lvl)
}
}
void logException(std::string_view message_prefix, const std::exception & ex)
{
const ForeignException * cast_exc = dynamic_cast<const ForeignException *>(&ex);
auto typeName = cast_exc ? cast_exc->innerType.name() : typeid(ex).name();
logFatal(fmt("%s: %s: %s", message_prefix, boost::core::demangle(typeName), ex.what()));
logFatal("Stack trace:");
logFatal(getStackTrace());
const BaseException * cast_exc_with_async_trace = dynamic_cast<const BaseException *>(&ex);
if (cast_exc_with_async_trace) {
auto asyncTrace = cast_exc_with_async_trace->asyncTrace();
if (asyncTrace && !asyncTrace->empty()) {
logFatal("Async task trace (probably incomplete):");
for (auto [i, frame] : enumerate(*asyncTrace)) {
logFatal(
fmt("#%i: %s (%s:%i:%i)",
i,
frame.location.function_name(),
frame.location.file_name(),
frame.location.line(),
frame.location.column())
);
if (frame.description) {
logFatal(fmt("\t%s", *frame.description));
}
}
}
}
}
}
+2
View File
@@ -328,4 +328,6 @@ void ignoreExceptionInDestructor(Verbosity lvl = lvlError);
*/
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);
}