Bug 1409852 - Expose a hook to be informed whenever an exception is thrown;r=jandem

This hook should help us diagnose more easily typoes in our chrome code.

To avoid painting ourselves in a corner in case we need to optimize
exceptions at some later point, the API is restricted to Nightly -
which is where it will be the most useful anyway.

MozReview-Commit-ID: FvDnaALKHox

--HG--
extra : rebase_source : b3cb46b658c0638183fb80fb11f8a50d9aab28d4
This commit is contained in:
David Teller 2017-11-16 10:36:30 +01:00
Родитель 533c311f31
Коммит d96eed4450
6 изменённых файлов: 272 добавлений и 0 удалений

Просмотреть файл

@ -137,6 +137,13 @@ if CONFIG['ENABLE_STREAMS']:
'testReadableStream.cpp',
]
if CONFIG['NIGHTLY_BUILD']:
# The Error interceptor only exists on Nightly.
UNIFIED_SOURCES += [
'testErrorInterceptor.cpp',
]
if CONFIG['JS_BUILD_BINAST'] and CONFIG['JS_STANDALONE']:
# Standalone builds leave the source directory untouched,
# which lets us run tests with the data files intact.

Просмотреть файл

@ -0,0 +1,143 @@
#include "jsapi.h"
#include "jsapi-tests/tests.h"
#include "vm/StringBuffer.h"
// Tests for JS_GetErrorInterceptorCallback and JS_SetErrorInterceptorCallback.
namespace {
const double EXN_VALUE = 3.14;
static JS::PersistentRootedString gLatestMessage;
// An interceptor that stores the error in `gLatestMessage`.
struct SimpleInterceptor: JSErrorInterceptor {
virtual void interceptError(JSContext* cx, const JS::Value& val) override {
js::StringBuffer buffer(cx);
if (!ValueToStringBuffer(cx, val, buffer))
MOZ_CRASH("Could not convert to string buffer");
gLatestMessage = buffer.finishString();
if (!gLatestMessage)
MOZ_CRASH("Could not convert to string");
}
};
bool equalStrings(JSContext* cx, JSString* a, JSString* b) {
int32_t result = 0;
if (!JS_CompareStrings(cx, a, b, &result))
MOZ_CRASH("Could not compare strings");
return result == 0;
}
}
BEGIN_TEST(testErrorInterceptor)
{
// Run the following snippets.
const char* SAMPLES[] = {
"throw new Error('I am an Error')\0",
"throw new TypeError('I am a TypeError')\0",
"throw new ReferenceError('I am a ReferenceError')\0",
"throw new SyntaxError('I am a SyntaxError')\0",
"throw 5\0",
"undefined[0]\0",
"foo[0]\0",
"b[\0",
};
// With the simpleInterceptor, we should end up with the following error:
const char* TO_STRING[] = {
"Error: I am an Error\0",
"TypeError: I am a TypeError\0",
"ReferenceError: I am a ReferenceError\0",
"SyntaxError: I am a SyntaxError\0",
"5\0",
"TypeError: undefined has no properties\0",
"ReferenceError: foo is not defined\0",
"SyntaxError: expected expression, got end of script\0",
};
MOZ_ASSERT(mozilla::ArrayLength(SAMPLES) == mozilla::ArrayLength(TO_STRING));
// Save original callback.
JSErrorInterceptor* original = JS_GetErrorInterceptorCallback(cx->runtime());
gLatestMessage.init(cx);
// Test without callback.
JS_SetErrorInterceptorCallback(cx->runtime(), nullptr);
CHECK(gLatestMessage == nullptr);
for (auto sample: SAMPLES) {
if (execDontReport(sample, __FILE__, __LINE__))
MOZ_CRASH("This sample should have failed");
CHECK(JS_IsExceptionPending(cx));
CHECK(gLatestMessage == nullptr);
JS_ClearPendingException(cx);
}
// Test with callback.
SimpleInterceptor simpleInterceptor;
JS_SetErrorInterceptorCallback(cx->runtime(), &simpleInterceptor);
// Test that we return the right callback.
CHECK_EQUAL(JS_GetErrorInterceptorCallback(cx->runtime()), &simpleInterceptor);
// This shouldn't cause any error.
EXEC("function bar() {}");
CHECK(gLatestMessage == nullptr);
// Test error throwing with a callback that succeeds.
for (size_t i = 0; i < mozilla::ArrayLength(SAMPLES); ++i) {
// This should cause the appropriate error.
if (execDontReport(SAMPLES[i], __FILE__, __LINE__))
MOZ_CRASH("This sample should have failed");
CHECK(JS_IsExceptionPending(cx));
// Check result of callback.
CHECK(gLatestMessage != nullptr);
CHECK(js::StringEqualsAscii(&gLatestMessage->asLinear(), TO_STRING[i]));
// Check the final error.
JS::RootedValue exn(cx);
CHECK(JS_GetPendingException(cx, &exn));
JS_ClearPendingException(cx);
js::StringBuffer buffer(cx);
CHECK(ValueToStringBuffer(cx, exn, buffer));
CHECK(equalStrings(cx, buffer.finishString(), gLatestMessage));
// Cleanup.
gLatestMessage = nullptr;
}
// Test again without callback.
JS_SetErrorInterceptorCallback(cx->runtime(), nullptr);
for (size_t i = 0; i < mozilla::ArrayLength(SAMPLES); ++i) {
if (execDontReport(SAMPLES[i], __FILE__, __LINE__))
MOZ_CRASH("This sample should have failed");
CHECK(JS_IsExceptionPending(cx));
// Check that the callback wasn't called.
CHECK(gLatestMessage == nullptr);
// Check the final error.
JS::RootedValue exn(cx);
CHECK(JS_GetPendingException(cx, &exn));
JS_ClearPendingException(cx);
js::StringBuffer buffer(cx);
CHECK(ValueToStringBuffer(cx, exn, buffer));
CHECK(js::StringEqualsAscii(buffer.finishString(), TO_STRING[i]));
// Cleanup.
gLatestMessage = nullptr;
}
// Cleanup
JS_SetErrorInterceptorCallback(cx->runtime(), original);
gLatestMessage = nullptr;
JS_ClearPendingException(cx);
return true;
}
END_TEST(testErrorInterceptor)

Просмотреть файл

@ -653,6 +653,40 @@ JS_SetCompartmentNameCallback(JSContext* cx, JSCompartmentNameCallback callback)
cx->runtime()->compartmentNameCallback = callback;
}
#if defined(NIGHTLY_BUILD)
JS_PUBLIC_API(void)
JS_SetErrorInterceptorCallback(JSRuntime* rt, JSErrorInterceptor* callback)
{
rt->errorInterception.interceptor = callback;
}
JS_PUBLIC_API(JSErrorInterceptor*)
JS_GetErrorInterceptorCallback(JSRuntime* rt)
{
return rt->errorInterception.interceptor;
}
JS_PUBLIC_API(Maybe<JSExnType>)
JS_GetErrorType(const JS::Value& val)
{
// All errors are objects.
if (!val.isObject())
return mozilla::Nothing();
const JSObject& obj = val.toObject();
// All errors are `ErrorObject`.
if (!obj.is<js::ErrorObject>()) {
// Not one of the primitive errors.
return mozilla::Nothing();
}
const js::ErrorObject& err = obj.as<js::ErrorObject>();
return mozilla::Some(err.type());
}
#endif // defined(NIGHTLY_BUILD)
JS_PUBLIC_API(void)
JS_SetWrapObjectCallbacks(JSContext* cx, const JSWrapObjectCallbacks* callbacks)
{

Просмотреть файл

@ -680,6 +680,18 @@ typedef void
using JSExternalStringSizeofCallback =
size_t (*)(JSString* str, mozilla::MallocSizeOf mallocSizeOf);
/**
* Callback used to intercept JavaScript errors.
*/
struct JSErrorInterceptor {
/**
* This method is called whenever an error has been raised from JS code.
*
* This method MUST be infallible.
*/
virtual void interceptError(JSContext* cx, const JS::Value& error) = 0;
};
/************************************************************************/
static MOZ_ALWAYS_INLINE JS::Value
@ -1327,6 +1339,33 @@ JS_SetWrapObjectCallbacks(JSContext* cx, const JSWrapObjectCallbacks* callbacks)
extern JS_PUBLIC_API(void)
JS_SetExternalStringSizeofCallback(JSContext* cx, JSExternalStringSizeofCallback callback);
#if defined(NIGHTLY_BUILD)
// Set a callback that will be called whenever an error
// is thrown in this runtime. This is designed as a mechanism
// for logging errors. Note that the VM makes no attempt to sanitize
// the contents of the error (so it may contain private data)
// or to sort out among errors (so it may not be the error you
// are interested in or for the component in which you are
// interested).
//
// If the callback sets a new error, this new error
// will replace the original error.
//
// May be `nullptr`.
extern JS_PUBLIC_API(void)
JS_SetErrorInterceptorCallback(JSRuntime*, JSErrorInterceptor* callback);
extern JS_PUBLIC_API(JSErrorInterceptor*)
JS_GetErrorInterceptorCallback(JSRuntime*);
// Examine a value to determine if it is one of the built-in Error types.
// If so, return the error type.
extern JS_PUBLIC_API(mozilla::Maybe<JSExnType>)
JS_GetErrorType(const JS::Value& val);
#endif // defined(NIGHTLY_BUILD)
extern JS_PUBLIC_API(void)
JS_SetCompartmentPrivate(JSCompartment* compartment, void* data);

Просмотреть файл

@ -434,6 +434,31 @@ JSContext::minorGC(JS::gcreason::Reason reason)
inline void
JSContext::setPendingException(const js::Value& v)
{
#if defined(NIGHTLY_BUILD)
do {
// Do not intercept exceptions if we are already
// in the exception interceptor. That would lead
// to infinite recursion.
if (this->runtime()->errorInterception.isExecuting)
break;
// Check whether we have an interceptor at all.
if (!this->runtime()->errorInterception.interceptor)
break;
// Make sure that we do not call the interceptor from within
// the interceptor.
this->runtime()->errorInterception.isExecuting = true;
// The interceptor must be infallible.
const mozilla::DebugOnly<bool> wasExceptionPending = this->isExceptionPending();
this->runtime()->errorInterception.interceptor->interceptError(this, v);
MOZ_ASSERT(wasExceptionPending == this->isExceptionPending());
this->runtime()->errorInterception.isExecuting = false;
} while (false);
#endif // defined(NIGHTLY_BUILD)
// overRecursed_ is set after the fact by ReportOverRecursed.
this->overRecursed_ = false;
this->throwing = true;

Просмотреть файл

@ -1081,6 +1081,30 @@ struct JSRuntime : public js::MallocProvider<JSRuntime>
void* wasmUnwindPC() const {
return wasmUnwindPC_;
}
public:
#if defined(NIGHTLY_BUILD)
// Support for informing the embedding of any error thrown.
// This mechanism is designed to let the embedding
// log/report/fail in case certain errors are thrown
// (e.g. SyntaxError, ReferenceError or TypeError
// in critical code).
struct ErrorInterceptionSupport {
ErrorInterceptionSupport()
: isExecuting(false)
, interceptor(nullptr)
{ }
// true if the error interceptor is currently executing,
// false otherwise. Used to avoid infinite loops.
bool isExecuting;
// if non-null, any call to `setPendingException`
// in this runtime will trigger the call to `interceptor`
JSErrorInterceptor* interceptor;
};
ErrorInterceptionSupport errorInterception;
#endif // defined(NIGHTLY_BUILD)
};
namespace js {