зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
10402d0590
Коммит
c42032ad72
|
@ -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 {
|
||||
|
|
Загрузка…
Ссылка в новой задаче