Commit 6632dc8d authored by Kenton Varda's avatar Kenton Varda

Refactor exception handling a bunch.

parent cd02679f
......@@ -70,8 +70,9 @@ public:
throw MockException();
}
void logMessage(StringPtr text) override {
void logMessage(const char* file, int line, int contextDepth, String&& text) override {
this->text += "log message: ";
text = str(file, ":", line, ":+", contextDepth, ": ", mv(text));
this->text.append(text.begin(), text.end());
}
};
......@@ -86,11 +87,10 @@ std::string fileLine(std::string file, int line) {
TEST(Logging, Log) {
MockExceptionCallback mockCallback;
MockExceptionCallback::ScopedRegistration reg(mockCallback);
int line;
KJ_LOG(WARNING, "Hello world!"); line = __LINE__;
EXPECT_EQ("log message: warning: " + fileLine(__FILE__, line) + ": Hello world!\n",
EXPECT_EQ("log message: " + fileLine(__FILE__, line) + ":+0: warning: Hello world!\n",
mockCallback.text);
mockCallback.text.clear();
......@@ -98,12 +98,12 @@ TEST(Logging, Log) {
const char* str = "foo";
KJ_LOG(ERROR, i, str); line = __LINE__;
EXPECT_EQ("log message: error: " + fileLine(__FILE__, line) + ": i = 123; str = foo\n",
EXPECT_EQ("log message: " + fileLine(__FILE__, line) + ":+0: error: i = 123; str = foo\n",
mockCallback.text);
mockCallback.text.clear();
KJ_DBG("Some debug text."); line = __LINE__;
EXPECT_EQ("log message: debug: " + fileLine(__FILE__, line) + ": Some debug text.\n",
EXPECT_EQ("log message: " + fileLine(__FILE__, line) + ":+0: debug: Some debug text.\n",
mockCallback.text);
mockCallback.text.clear();
......@@ -115,7 +115,7 @@ TEST(Logging, Log) {
// Enable it.
Log::setLogLevel(Log::Severity::INFO);
KJ_LOG(INFO, "Some text."); line = __LINE__;
EXPECT_EQ("log message: info: " + fileLine(__FILE__, line) + ": Some text.\n",
EXPECT_EQ("log message: " + fileLine(__FILE__, line) + ":+0: info: Some text.\n",
mockCallback.text);
mockCallback.text.clear();
......@@ -182,7 +182,6 @@ TEST(Logging, Catch) {
TEST(Logging, Syscall) {
MockExceptionCallback mockCallback;
MockExceptionCallback::ScopedRegistration reg(mockCallback);
int line;
int i = 123;
......@@ -207,12 +206,17 @@ TEST(Logging, Syscall) {
TEST(Logging, Context) {
MockExceptionCallback mockCallback;
MockExceptionCallback::ScopedRegistration reg(mockCallback);
{
KJ_CONTEXT("foo"); int cline = __LINE__;
EXPECT_THROW(KJ_FAIL_ASSERT("bar"), MockException); int line = __LINE__;
KJ_LOG(WARNING, "blah"); int line = __LINE__;
EXPECT_EQ("log message: " + fileLine(__FILE__, cline) + ":+0: context: foo\n"
"log message: " + fileLine(__FILE__, line) + ":+1: warning: blah\n",
mockCallback.text);
mockCallback.text.clear();
EXPECT_THROW(KJ_FAIL_ASSERT("bar"), MockException); line = __LINE__;
EXPECT_EQ("fatal exception: " + fileLine(__FILE__, cline) + ": context: foo\n"
+ fileLine(__FILE__, line) + ": bug in code: bar\n",
mockCallback.text);
......@@ -244,6 +248,11 @@ TEST(Logging, Context) {
}
}
TEST(Logging, ExceptionCallbackMustBeOnStack) {
// TODO(cleanup): Put in exception-test.c++, when it exists.
EXPECT_ANY_THROW(new ExceptionCallback);
}
} // namespace
} // namespace internal
} // namespace kj
......@@ -93,8 +93,8 @@ static String makeDescription(DescriptionStyle style, const char* code, int erro
++index;
if (index != argValues.size()) {
getExceptionCallback().logMessage(
str(__FILE__, ":", __LINE__, ": Failed to parse logging macro args into ",
getExceptionCallback().logMessage(__FILE__, __LINE__, 0,
str("Failed to parse logging macro args into ",
argValues.size(), " names: ", macroArgs, '\n'));
}
}
......@@ -189,9 +189,8 @@ static String makeDescription(DescriptionStyle style, const char* code, int erro
void Log::logInternal(const char* file, int line, Severity severity, const char* macroArgs,
ArrayPtr<String> argValues) {
getExceptionCallback().logMessage(
str(severity, ": ", file, ":", line, ": ",
makeDescription(LOG, nullptr, 0, macroArgs, argValues), '\n'));
getExceptionCallback().logMessage(file, line, 0,
str(severity, ": ", makeDescription(LOG, nullptr, 0, macroArgs, argValues), '\n'));
}
Log::Fault::~Fault() noexcept(false) {
......@@ -218,9 +217,8 @@ void Log::Fault::init(
condition, errorNumber, macroArgs, argValues));
}
void Log::addContextToInternal(Exception& exception, const char* file, int line,
const char* macroArgs, ArrayPtr<String> argValues) {
exception.wrapContext(file, line, makeDescription(LOG, nullptr, 0, macroArgs, argValues));
String Log::makeContextDescriptionInternal(const char* macroArgs, ArrayPtr<String> argValues) {
return makeDescription(LOG, nullptr, 0, macroArgs, argValues);
}
int Log::getOsErrorNumber() {
......@@ -228,21 +226,37 @@ int Log::getOsErrorNumber() {
return result == EINTR ? -1 : result;
}
Log::Context::Context(): next(getExceptionCallback()), registration(*this) {}
Log::Context::Context(): logged(false) {}
Log::Context::~Context() {}
Log::Context::Value Log::Context::ensureInitialized() {
KJ_IF_MAYBE(v, value) {
return Value(v->file, v->line, heapString(v->description));
} else {
Value result = evaluate();
value = Value(result.file, result.line, heapString(result.description));
return result;
}
}
void Log::Context::onRecoverableException(Exception&& exception) {
addTo(exception);
Value v = ensureInitialized();
exception.wrapContext(v.file, v.line, mv(v.description));
next.onRecoverableException(kj::mv(exception));
}
void Log::Context::onFatalException(Exception&& exception) {
addTo(exception);
Value v = ensureInitialized();
exception.wrapContext(v.file, v.line, mv(v.description));
next.onFatalException(kj::mv(exception));
}
void Log::Context::logMessage(StringPtr text) {
// TODO(someday): We could do something like log the context and then indent all log messages
// written until the end of the context.
next.logMessage(text);
void Log::Context::logMessage(const char* file, int line, int contextDepth, String&& text) {
if (!logged) {
Value v = ensureInitialized();
next.logMessage(v.file, v.line, 0, str("context: ", mv(v.description), '\n'));
logged = true;
}
next.logMessage(file, line, contextDepth + 1, mv(text));
}
} // namespace kj
......@@ -167,15 +167,27 @@ public:
Context();
KJ_DISALLOW_COPY(Context);
virtual ~Context();
virtual void addTo(Exception& exception) = 0;
struct Value {
const char* file;
int line;
String description;
inline Value(const char* file, int line, String&& description)
: file(file), line(line), description(mv(description)) {}
};
virtual Value evaluate() = 0;
virtual void onRecoverableException(Exception&& exception) override;
virtual void onFatalException(Exception&& exception) override;
virtual void logMessage(StringPtr text) override;
virtual void logMessage(const char* file, int line, int contextDepth, String&& text) override;
private:
ExceptionCallback& next;
ScopedRegistration registration;
bool logged;
Maybe<Value> value;
Value ensureInitialized();
};
template <typename Func>
......@@ -184,24 +196,22 @@ public:
inline ContextImpl(Func& func): func(func) {}
KJ_DISALLOW_COPY(ContextImpl);
void addTo(Exception& exception) override {
func(exception);
Value evaluate() override {
return func();
}
private:
Func& func;
};
template <typename... Params>
static void addContextTo(Exception& exception, const char* file,
int line, const char* macroArgs, Params&&... params);
static String makeContextDescription(const char* macroArgs, Params&&... params);
private:
static Severity minSeverity;
static void logInternal(const char* file, int line, Severity severity, const char* macroArgs,
ArrayPtr<String> argValues);
static void addContextToInternal(Exception& exception, const char* file, int line,
const char* macroArgs, ArrayPtr<String> argValues);
static String makeContextDescriptionInternal(const char* macroArgs, ArrayPtr<String> argValues);
static int getOsErrorNumber();
// Get the error code of the last error (e.g. from errno). Returns -1 on EINTR.
......@@ -243,9 +253,9 @@ ArrayPtr<const char> KJ_STRINGIFY(Log::Severity severity);
errorNumber, code, #__VA_ARGS__, ##__VA_ARGS__);; f.fatal())
#define KJ_CONTEXT(...) \
auto _kjContextFunc = [&](::kj::Exception& exception) { \
return ::kj::Log::addContextTo(exception, \
__FILE__, __LINE__, #__VA_ARGS__, ##__VA_ARGS__); \
auto _kjContextFunc = [&]() -> ::kj::Log::Context::Value { \
return ::kj::Log::Context::Value( \
__FILE__, __LINE__, ::kj::Log::makeContextDescription(#__VA_ARGS__, ##__VA_ARGS__)); \
}; \
::kj::Log::ContextImpl<decltype(_kjContextFunc)> _kjContext(_kjContextFunc)
......@@ -288,10 +298,9 @@ Log::SyscallResult Log::syscall(Call&& call) {
}
template <typename... Params>
void Log::addContextTo(Exception& exception, const char* file, int line,
const char* macroArgs, Params&&... params) {
String Log::makeContextDescription(const char* macroArgs, Params&&... params) {
String argValues[sizeof...(Params)] = {str(params)...};
addContextToInternal(exception, file, line, macroArgs, arrayPtr(argValues, sizeof...(Params)));
return makeContextDescriptionInternal(macroArgs, arrayPtr(argValues, sizeof...(Params)));
}
} // namespace kj
......
......@@ -146,72 +146,135 @@ namespace {
#define thread_local __thread
#endif
thread_local ExceptionCallback::ScopedRegistration* threadLocalCallback = nullptr;
ExceptionCallback* globalCallback = nullptr;
thread_local ExceptionCallback* threadLocalCallback = nullptr;
class RepeatChar {
// A pseudo-sequence of characters that is actually just one character repeated.
//
// TODO(cleanup): Put this somewhere reusable. Maybe templatize it too.
public:
inline RepeatChar(char c, uint size): c(c), size_(size) {}
class Iterator {
public:
Iterator() = default;
inline Iterator(char c, uint index): c(c), index(index) {}
inline Iterator& operator++() { ++index; return *this; }
inline Iterator operator++(int) { ++index; return Iterator(c, index - 1); }
inline char operator*() const { return c; }
inline bool operator==(const Iterator& other) const { return index == other.index; }
inline bool operator!=(const Iterator& other) const { return index != other.index; }
private:
char c;
uint index;
};
inline uint size() const { return size_; }
inline Iterator begin() const { return Iterator(c, 0); }
inline Iterator end() const { return Iterator(c, size_); }
private:
char c;
uint size_;
};
inline RepeatChar KJ_STRINGIFY(RepeatChar value) { return value; }
} // namespace
ExceptionCallback::ExceptionCallback() {}
ExceptionCallback::ExceptionCallback(): next(getExceptionCallback()) {
char stackVar;
ptrdiff_t offset = reinterpret_cast<char*>(this) - &stackVar;
KJ_ASSERT(offset < 4096 && offset > -4096,
"ExceptionCallback must be allocated on the stack.");
threadLocalCallback = this;
}
ExceptionCallback::ExceptionCallback(ExceptionCallback& next): next(next) {}
ExceptionCallback::~ExceptionCallback() {
if (globalCallback == this) {
globalCallback = nullptr;
if (&next != this) {
threadLocalCallback = &next;
}
}
void ExceptionCallback::onRecoverableException(Exception&& exception) {
next.onRecoverableException(mv(exception));
}
void ExceptionCallback::onFatalException(Exception&& exception) {
next.onFatalException(mv(exception));
}
void ExceptionCallback::logMessage(const char* file, int line, int contextDepth, String&& text) {
next.logMessage(file, line, contextDepth, mv(text));
}
class ExceptionCallback::RootExceptionCallback: public ExceptionCallback {
public:
RootExceptionCallback(): ExceptionCallback(*this) {}
void onRecoverableException(Exception&& exception) override {
#if KJ_NO_EXCEPTIONS
logMessage(str(exception.what(), '\n'));
logException(mv(exception));
#else
if (std::uncaught_exception()) {
logMessage(str("unwind: ", ExceptionImpl(mv(exception)).what(), '\n'));
} else {
throw ExceptionImpl(mv(exception));
}
if (std::uncaught_exception()) {
// Throwing is probably dangerous. Log instead.
logException(mv(exception));
} else {
throw ExceptionImpl(mv(exception));
}
#endif
}
}
void ExceptionCallback::onFatalException(Exception&& exception) {
void onFatalException(Exception&& exception) override {
#if KJ_NO_EXCEPTIONS
logMessage(str(exception.what(), '\n'));
logException(mv(exception));
#else
throw ExceptionImpl(mv(exception));
throw ExceptionImpl(mv(exception));
#endif
}
}
void ExceptionCallback::logMessage(StringPtr text) {
while (text != nullptr) {
ssize_t n = write(STDERR_FILENO, text.begin(), text.size());
if (n <= 0) {
// stderr is broken. Give up.
return;
void logMessage(const char* file, int line, int contextDepth, String&& text) override {
if (contextDepth > 0) {
text = str(RepeatChar('_', contextDepth), mv(text));
}
text = text.slice(n);
}
}
void ExceptionCallback::useProcessWide() {
KJ_REQUIRE(globalCallback == nullptr,
"Can't register multiple global ExceptionCallbacks at once.") {
return;
}
globalCallback = this;
}
StringPtr textPtr = text;
ExceptionCallback::ScopedRegistration::ScopedRegistration(ExceptionCallback& callback)
: callback(callback) {
old = threadLocalCallback;
threadLocalCallback = this;
}
while (text != nullptr) {
ssize_t n = write(STDERR_FILENO, textPtr.begin(), textPtr.size());
if (n <= 0) {
// stderr is broken. Give up.
return;
}
textPtr = textPtr.slice(n);
}
}
ExceptionCallback::ScopedRegistration::~ScopedRegistration() {
threadLocalCallback = old;
}
private:
void logException(Exception&& e) {
// We intentionally go back to the top exception callback on the stack because we don't want to
// bypass whatever log processing is in effect.
//
// We intentionally don't log the context since it should get re-added by the exception callback
// anyway.
getExceptionCallback().logMessage(e.getFile(), e.getLine(), 0, str(
e.getNature(), e.getDurability() == Exception::Durability::TEMPORARY ? " (temporary)" : "",
e.getDescription() == nullptr ? "" : ": ", e.getDescription(),
"\nstack: ", strArray(e.getStackTrace(), " ")));
}
};
ExceptionCallback& getExceptionCallback() {
static ExceptionCallback defaultCallback;
ExceptionCallback::ScopedRegistration* scoped = threadLocalCallback;
return scoped != nullptr ? scoped->getCallback() :
globalCallback != nullptr ? *globalCallback : defaultCallback;
static ExceptionCallback::RootExceptionCallback defaultCallback;
ExceptionCallback* scoped = threadLocalCallback;
return scoped != nullptr ? *scoped : defaultCallback;
}
} // namespace kj
......@@ -125,10 +125,16 @@ String KJ_STRINGIFY(const Exception& e);
class ExceptionCallback {
// If you don't like C++ exceptions, you may implement and register an ExceptionCallback in order
// to perform your own exception handling.
// to perform your own exception handling. For example, a reasonable thing to do is to have
// onRecoverableException() set a flag indicating that an error occurred, and then check for that
// flag just before writing to storage and/or returning results to the user. If the flag is set,
// discard whatever you have and return an error instead.
//
// For example, a reasonable thing to do is to have onRecoverableException() set a flag
// indicating that an error occurred, and then check for that flag further up the stack.
// ExceptionCallbacks must always be allocated on the stack. When an exception is thrown, the
// newest ExceptionCallback on the calling thread's stack is called. The default implementation
// of each method calls the next-oldest ExceptionCallback for that thread. Thus the callbacks
// behave a lot like try/catch blocks, except that they are called before any stack unwinding
// occurs.
public:
ExceptionCallback();
......@@ -138,45 +144,34 @@ public:
virtual void onRecoverableException(Exception&& exception);
// Called when an exception has been raised, but the calling code has the ability to continue by
// producing garbage output. This method _should_ throw the exception, but is allowed to simply
// return if garbage output is acceptable. The default implementation throws an exception unless
// the library was compiled with -fno-exceptions, in which case it logs an error and returns.
// return if garbage output is acceptable.
//
// The global default implementation throws an exception unless the library was compiled with
// -fno-exceptions, in which case it logs an error and returns.
virtual void onFatalException(Exception&& exception);
// Called when an exception has been raised and the calling code cannot continue. If this method
// returns normally, abort() will be called. The method must throw the exception to avoid
// aborting. The default implementation throws an exception unless the library was compiled with
// aborting.
//
// The global default implementation throws an exception unless the library was compiled with
// -fno-exceptions, in which case it logs an error and returns.
virtual void logMessage(StringPtr text);
virtual void logMessage(const char* file, int line, int contextDepth, String&& text);
// Called when something wants to log some debug text. The text always ends in a newline if
// it is non-empty. The default implementation writes the text to stderr.
void useProcessWide();
// Use this ExceptionCallback for all exceptions thrown from any thread in the process which
// doesn't otherwise have a thread-local callback. When the ExceptionCallback is destroyed,
// the default behavior will be restored. It is an error to set multiple process-wide callbacks
// at the same time.
// it is non-empty. `contextDepth` indicates how many levels of context the message passed
// through; it may make sense to indent the message accordingly.
//
// Note that to delete (and thus unregister) a global ExceptionCallback, you must ensure that
// no other threads might running code that could throw at that time. It is probably best to
// leave the callback registered permanently.
// The global default implementation writes the text to stderr.
class ScopedRegistration {
// Allocate a ScopedRegistration on the stack to register you ExceptionCallback just within
// the current thread. When the ScopedRegistration is destroyed, the previous thread-local
// callback will be restored.
protected:
ExceptionCallback& next;
public:
ScopedRegistration(ExceptionCallback& callback);
KJ_DISALLOW_COPY(ScopedRegistration);
~ScopedRegistration();
inline ExceptionCallback& getCallback() { return callback; }
private:
ExceptionCallback(ExceptionCallback& next);
private:
ExceptionCallback& callback;
ScopedRegistration* old;
};
class RootExceptionCallback;
friend ExceptionCallback& getExceptionCallback();
};
ExceptionCallback& getExceptionCallback();
......
......@@ -138,6 +138,7 @@ String heapString(size_t size);
String heapString(const char* value);
String heapString(const char* value, size_t size);
String heapString(StringPtr value);
String heapString(const String& value);
String heapString(ArrayPtr<const char> value);
// Allocates a copy of the given value on the heap.
......@@ -374,6 +375,9 @@ inline String heapString(const char* value) {
inline String heapString(StringPtr value) {
return heapString(value.begin(), value.size());
}
inline String heapString(const String& value) {
return heapString(value.begin(), value.size());
}
inline String heapString(ArrayPtr<const char> value) {
return heapString(value.begin(), value.size());
}
......
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