Commit 55a6731a authored by Kenton Varda's avatar Kenton Varda

Make printStackTraceOnCrash() also register an std::terminate handler.

For whatever reason, the default termination handler on my machine is no longer printing the exception's `what()` string. It just aborts. That makes debugging hard.

This also means that we can now use `noexcept` in unit tests as a way to make uncaught exceptions abort the process _without_ unwinding, which is especially useful in tests that create threads since they often deadlock during unwind waiting for the thread to finish.
parent 114e20cc
......@@ -842,8 +842,10 @@ KJ_TEST("synchonous simple cross-thread events") {
Own<PromiseFulfiller<uint>> fulfiller; // accessed only from the subthread
thread_local bool isChild = false; // to assert which thread we're in
Thread thread([&]() {
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
// We use `noexcept` so that any uncaught exceptions immediately terminate the process without
// unwinding. Otherwise, the unwind would likely deadlock waiting for some synchronization with
// the other thread.
Thread thread([&]() noexcept {
isChild = true;
EventLoop loop;
......@@ -858,15 +860,9 @@ KJ_TEST("synchonous simple cross-thread events") {
// Wait until parent thread sets executor to null, as a way to tell us to quit.
executor.lockExclusive().wait([](auto& val) { return val == nullptr; });
})) {
// Log here because it's likely the parent thread will never join and we'll hang forever
// without propagating the exception.
KJ_LOG(ERROR, *exception);
kj::throwRecoverableException(kj::mv(*exception));
}
});
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
([&]() noexcept {
const Executor* exec;
{
auto lock = executor.lockExclusive();
......@@ -889,10 +885,7 @@ KJ_TEST("synchonous simple cross-thread events") {
KJ_EXPECT(i == 456);
*executor.lockExclusive() = nullptr;
})) {
// Log here because the thread join is likely to hang forever...
KJ_FAIL_EXPECT(*exception);
}
})();
}
KJ_TEST("asynchonous simple cross-thread events") {
......@@ -900,8 +893,10 @@ KJ_TEST("asynchonous simple cross-thread events") {
Own<PromiseFulfiller<uint>> fulfiller; // accessed only from the subthread
thread_local bool isChild = false; // to assert which thread we're in
Thread thread([&]() {
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
// We use `noexcept` so that any uncaught exceptions immediately terminate the process without
// unwinding. Otherwise, the unwind would likely deadlock waiting for some synchronization with
// the other thread.
Thread thread([&]() noexcept {
isChild = true;
EventLoop loop;
......@@ -916,18 +911,12 @@ KJ_TEST("asynchonous simple cross-thread events") {
// Wait until parent thread sets executor to null, as a way to tell us to quit.
executor.lockExclusive().wait([](auto& val) { return val == nullptr; });
})) {
// Log here because it's likely the parent thread will never join and we'll hang forever
// without propagating the exception.
KJ_LOG(ERROR, *exception);
kj::throwRecoverableException(kj::mv(*exception));
}
});
([&]() noexcept {
EventLoop loop;
WaitScope waitScope(loop);
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
const Executor* exec;
{
auto lock = executor.lockExclusive();
......@@ -950,10 +939,7 @@ KJ_TEST("asynchonous simple cross-thread events") {
KJ_EXPECT(promise.wait(waitScope) == 456);
*executor.lockExclusive() = nullptr;
})) {
// Log here because the thread join is likely to hang forever...
KJ_FAIL_EXPECT(*exception);
}
})();
}
KJ_TEST("synchonous promise cross-thread events") {
......@@ -962,8 +948,10 @@ KJ_TEST("synchonous promise cross-thread events") {
Promise<uint> promise = nullptr; // accessed only from the subthread
thread_local bool isChild = false; // to assert which thread we're in
Thread thread([&]() {
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
// We use `noexcept` so that any uncaught exceptions immediately terminate the process without
// unwinding. Otherwise, the unwind would likely deadlock waiting for some synchronization with
// the other thread.
Thread thread([&]() noexcept {
isChild = true;
EventLoop loop;
......@@ -986,15 +974,9 @@ KJ_TEST("synchonous promise cross-thread events") {
// Wait until parent thread sets executor to null, as a way to tell us to quit.
executor.lockExclusive().wait([](auto& val) { return val == nullptr; });
})) {
// Log here because it's likely the parent thread will never join and we'll hang forever
// without propagating the exception.
KJ_LOG(ERROR, *exception);
kj::throwRecoverableException(kj::mv(*exception));
}
});
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
([&]() noexcept {
const Executor* exec;
{
auto lock = executor.lockExclusive();
......@@ -1017,10 +999,7 @@ KJ_TEST("synchonous promise cross-thread events") {
KJ_EXPECT(i == 321);
*executor.lockExclusive() = nullptr;
})) {
// Log here because the thread join is likely to hang forever...
KJ_FAIL_EXPECT(*exception);
}
})();
}
KJ_TEST("asynchonous promise cross-thread events") {
......@@ -1029,8 +1008,10 @@ KJ_TEST("asynchonous promise cross-thread events") {
Promise<uint> promise = nullptr; // accessed only from the subthread
thread_local bool isChild = false; // to assert which thread we're in
Thread thread([&]() {
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
// We use `noexcept` so that any uncaught exceptions immediately terminate the process without
// unwinding. Otherwise, the unwind would likely deadlock waiting for some synchronization with
// the other thread.
Thread thread([&]() noexcept {
isChild = true;
EventLoop loop;
......@@ -1053,18 +1034,12 @@ KJ_TEST("asynchonous promise cross-thread events") {
// Wait until parent thread sets executor to null, as a way to tell us to quit.
executor.lockExclusive().wait([](auto& val) { return val == nullptr; });
})) {
// Log here because it's likely the parent thread will never join and we'll hang forever
// without propagating the exception.
KJ_LOG(ERROR, *exception);
kj::throwRecoverableException(kj::mv(*exception));
}
});
([&]() noexcept {
EventLoop loop;
WaitScope waitScope(loop);
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
const Executor* exec;
{
auto lock = executor.lockExclusive();
......@@ -1087,17 +1062,16 @@ KJ_TEST("asynchonous promise cross-thread events") {
KJ_EXPECT(promise2.wait(waitScope) == 321);
*executor.lockExclusive() = nullptr;
})) {
// Log here because the thread join is likely to hang forever...
KJ_FAIL_EXPECT(*exception);
}
})();
}
KJ_TEST("cancel cross-thread event before it runs") {
MutexGuarded<kj::Maybe<const Executor&>> executor; // to get the Executor from the other thread
Thread thread([&]() {
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
// We use `noexcept` so that any uncaught exceptions immediately terminate the process without
// unwinding. Otherwise, the unwind would likely deadlock waiting for some synchronization with
// the other thread.
Thread thread([&]() noexcept {
EventLoop loop;
WaitScope waitScope(loop);
......@@ -1107,18 +1081,12 @@ KJ_TEST("cancel cross-thread event before it runs") {
// Wait until parent thread sets executor to null, as a way to tell us to quit.
executor.lockExclusive().wait([](auto& val) { return val == nullptr; });
})) {
// Log here because it's likely the parent thread will never join and we'll hang forever
// without propagating the exception.
KJ_LOG(ERROR, *exception);
kj::throwRecoverableException(kj::mv(*exception));
}
});
([&]() noexcept {
EventLoop loop;
WaitScope waitScope(loop);
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
const Executor* exec;
{
auto lock = executor.lockExclusive();
......@@ -1133,18 +1101,17 @@ KJ_TEST("cancel cross-thread event before it runs") {
}
*executor.lockExclusive() = nullptr;
})) {
// Log here because the thread join is likely to hang forever...
KJ_FAIL_EXPECT(*exception);
}
})();
}
KJ_TEST("cancel cross-thread event while it runs") {
MutexGuarded<kj::Maybe<const Executor&>> executor; // to get the Executor from the other thread
Own<PromiseFulfiller<void>> fulfiller; // accessed only from the subthread
Thread thread([&]() {
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
// We use `noexcept` so that any uncaught exceptions immediately terminate the process without
// unwinding. Otherwise, the unwind would likely deadlock waiting for some synchronization with
// the other thread.
Thread thread([&]() noexcept {
EventLoop loop;
WaitScope waitScope(loop);
......@@ -1157,18 +1124,12 @@ KJ_TEST("cancel cross-thread event while it runs") {
// Wait until parent thread sets executor to null, as a way to tell us to quit.
executor.lockExclusive().wait([](auto& val) { return val == nullptr; });
})) {
// Log here because it's likely the parent thread will never join and we'll hang forever
// without propagating the exception.
KJ_LOG(ERROR, *exception);
kj::throwRecoverableException(kj::mv(*exception));
}
});
([&]() noexcept {
EventLoop loop;
WaitScope waitScope(loop);
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
const Executor* exec;
{
auto lock = executor.lockExclusive();
......@@ -1187,10 +1148,7 @@ KJ_TEST("cancel cross-thread event while it runs") {
exec->executeSync([&]() { fulfiller->fulfill(); });
*executor.lockExclusive() = nullptr;
})) {
// Log here because the thread join is likely to hang forever...
KJ_FAIL_EXPECT(*exception);
}
})();
}
} // namespace
......
......@@ -378,6 +378,43 @@ String getStackTrace() {
return kj::str(stringifyStackTraceAddresses(trace), stringifyStackTrace(trace));
}
namespace {
void terminateHandler() {
void* traceSpace[32];
// ignoreCount = 3 to ignore std::terminate entry.
auto trace = kj::getStackTrace(traceSpace, 3);
kj::String message;
auto eptr = std::current_exception();
if (eptr != nullptr) {
try {
std::rethrow_exception(eptr);
} catch (const kj::Exception& exception) {
message = kj::str("*** Fatal uncaught kj::Exception: ", exception, '\n');
} catch (const std::exception& exception) {
message = kj::str("*** Fatal uncaught std::exception: ", exception.what(),
"\nstack: ", stringifyStackTraceAddresses(trace),
stringifyStackTrace(trace), '\n');
} catch (...) {
message = kj::str("*** Fatal uncaught exception of type: ", kj::getCaughtExceptionType(),
"\nstack: ", stringifyStackTraceAddresses(trace),
stringifyStackTrace(trace), '\n');
}
} else {
message = kj::str("*** std::terminate() called with no exception"
"\nstack: ", stringifyStackTraceAddresses(trace),
stringifyStackTrace(trace), '\n');
}
kj::FdOutputStream(STDERR_FILENO).write(message.begin(), message.size());
_exit(1);
}
} // namespace
#if _WIN32 && _M_X64
namespace {
......@@ -461,6 +498,9 @@ void printStackTraceOnCrash() {
mainThreadId = GetCurrentThreadId();
KJ_WIN32(SetConsoleCtrlHandler(breakHandler, TRUE));
SetUnhandledExceptionFilter(&sehHandler);
// Also override std::terminate() handler with something nicer for KJ.
std::set_terminate(&terminateHandler);
}
#elif KJ_HAS_BACKTRACE
......@@ -523,9 +563,13 @@ void printStackTraceOnCrash() {
// because stack traces on ctrl+c can be obnoxious for, say, command-line tools.
KJ_SYSCALL(sigaction(SIGINT, &action, nullptr));
#endif
// Also override std::terminate() handler with something nicer for KJ.
std::set_terminate(&terminateHandler);
}
#else
void printStackTraceOnCrash() {
std::set_terminate(&terminateHandler);
}
#endif
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment