Bug 1940287 - [devtools] Make callFunctionAndLogException re-throw the original exception. r=mccr8

Moving this existing helper from Console (idl) to ChromeUtils (WebIdl)
in order to be able to use aRv.MightThrowJSException and aRv.ThrowJSException
which are key ingredients to be able to re-throw the intercepted exception.

Differential Revision: https://phabricator.services.mozilla.com/D233548
This commit is contained in:
Alexandre Poirot
2025-04-29 07:44:28 +00:00
parent 7ae452b85e
commit 48541ca810
8 changed files with 117 additions and 77 deletions

View File

@@ -2526,6 +2526,67 @@ bool ChromeUtils::ShouldResistFingerprinting(
overriddenFingerprintingSettings); overriddenFingerprintingSettings);
} }
/* static */
void ChromeUtils::CallFunctionAndLogException(
GlobalObject& aGlobal, JS::Handle<JS::Value> aTargetGlobal,
JS::Handle<JS::Value> aFunction, JS::MutableHandle<JS::Value> aRetVal,
ErrorResult& aRv) {
JSContext* cx = aGlobal.Context();
if (!aTargetGlobal.isObject() || !aFunction.isObject()) {
aRv.Throw(NS_ERROR_INVALID_ARG);
return;
}
JS::Rooted<JS::Realm*> contextRealm(cx, JS::GetCurrentRealmOrNull(cx));
if (!contextRealm) {
aRv.Throw(NS_ERROR_INVALID_ARG);
return;
}
JS::Rooted<JSObject*> global(
cx, js::CheckedUnwrapDynamic(&aTargetGlobal.toObject(), cx));
if (!global) {
aRv.Throw(NS_ERROR_INVALID_ARG);
return;
}
// Use AutoJSAPI in order to trigger AutoJSAPI::ReportException
// which will do most of the work required for this function.
//
// We only have to pick the right global for which we want to flag
// the exception against.
dom::AutoJSAPI jsapi;
if (!jsapi.Init(global)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
JSContext* ccx = jsapi.cx();
// AutoJSAPI picks `aTargetGlobal` as execution compartment
// whereas we expect to run `aFunction` from the callsites compartment.
JSAutoRealm ar(ccx, JS::GetRealmGlobalOrNull(contextRealm));
JS::Rooted<JS::Value> funVal(ccx, aFunction);
if (!JS_WrapValue(ccx, &funVal)) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
if (!JS_CallFunctionValue(ccx, nullptr, funVal, JS::HandleValueArray::empty(),
aRetVal)) {
// Ensure re-throwing the exception which may have been thrown by
// `aFunction`
if (JS_IsExceptionPending(ccx)) {
JS::Rooted<JS::Value> exception(cx);
if (JS_GetPendingException(ccx, &exception)) {
if (JS_WrapValue(cx, &exception)) {
aRv.MightThrowJSException();
aRv.ThrowJSException(cx, exception);
}
}
}
}
}
std::atomic<uint32_t> ChromeUtils::sDevToolsOpenedCount = 0; std::atomic<uint32_t> ChromeUtils::sDevToolsOpenedCount = 0;
/* static */ /* static */

View File

@@ -321,6 +321,12 @@ class ChromeUtils {
nsIRFPTargetSetIDL* aOverriddenFingerprintingSettings, nsIRFPTargetSetIDL* aOverriddenFingerprintingSettings,
const Optional<bool>& aIsPBM); const Optional<bool>& aIsPBM);
static void CallFunctionAndLogException(GlobalObject& aGlobal,
JS::Handle<JS::Value> aTargetGlobal,
JS::Handle<JS::Value> aFunction,
JS::MutableHandle<JS::Value> aRetval,
ErrorResult& aRv);
#ifdef MOZ_WMF_CDM #ifdef MOZ_WMF_CDM
static already_AddRefed<Promise> GetWMFContentDecryptionModuleInformation( static already_AddRefed<Promise> GetWMFContentDecryptionModuleInformation(
GlobalObject& aGlobal, ErrorResult& aRv); GlobalObject& aGlobal, ErrorResult& aRv);

View File

@@ -51,15 +51,20 @@ add_task(async function customScriptError() {
add_task(async function callFunctionAndLogExceptionWithChromeGlobal() { add_task(async function callFunctionAndLogExceptionWithChromeGlobal() {
try { try {
Services.console.callFunctionAndLogException(globalThis, function () { ChromeUtils.callFunctionAndLogException(globalThis, function () {
throw new Error("custom exception"); throw new Error("custom exception");
}); });
Assert.fail("callFunctionAndLogException should throw"); Assert.ok(false, "callFunctionAndLogException should throw");
} catch (e) { } catch (e) {
Assert.equal( Assert.equal(
e.name, e.name,
"NS_ERROR_XPC_JAVASCRIPT_ERROR", "Error",
"callFunctionAndLogException thrown" "callFunctionAndLogException thrown with the expected exception name"
);
Assert.equal(
e.message,
"custom exception",
"callFunctionAndLogException thrown with the expected message"
); );
} }
@@ -92,15 +97,20 @@ add_task(async function callFunctionAndLogExceptionWithChromeGlobal() {
add_task(async function callFunctionAndLogExceptionWithContentGlobal() { add_task(async function callFunctionAndLogExceptionWithContentGlobal() {
const window = createContentWindow(); const window = createContentWindow();
try { try {
Services.console.callFunctionAndLogException(window, function () { ChromeUtils.callFunctionAndLogException(window, function () {
throw new Error("another custom exception"); throw new Error("another custom exception");
}); });
Assert.fail("callFunctionAndLogException should throw"); Assert.ok(false, "callFunctionAndLogException should throw");
} catch (e) { } catch (e) {
Assert.equal( Assert.equal(
e.name, e.name,
"NS_ERROR_XPC_JAVASCRIPT_ERROR", "Error",
"callFunctionAndLogException thrown" "callFunctionAndLogException thrown with the expected exception name"
);
Assert.equal(
e.message,
"another custom exception",
"callFunctionAndLogException thrown with the expected message"
); );
} }
@@ -114,13 +124,13 @@ add_task(async function callFunctionAndLogExceptionWithContentGlobal() {
Assert.equal(lastMessage.errorMessage, "Error: another custom exception"); Assert.equal(lastMessage.errorMessage, "Error: another custom exception");
Assert.equal(lastMessage.sourceName, _TEST_FILE); Assert.equal(lastMessage.sourceName, _TEST_FILE);
Assert.equal(lastMessage.lineNumber, 96); Assert.equal(lastMessage.lineNumber, 101);
Assert.equal(lastMessage.columnNumber, 13); Assert.equal(lastMessage.columnNumber, 13);
Assert.equal(lastMessage.flags, Ci.nsIScriptError.errorFlag); Assert.equal(lastMessage.flags, Ci.nsIScriptError.errorFlag);
Assert.equal(lastMessage.category, "content javascript"); Assert.equal(lastMessage.category, "content javascript");
Assert.ok(lastMessage.stack, "It has a stack"); Assert.ok(lastMessage.stack, "It has a stack");
Assert.equal(lastMessage.stack.source, _TEST_FILE); Assert.equal(lastMessage.stack.source, _TEST_FILE);
Assert.equal(lastMessage.stack.line, 96); Assert.equal(lastMessage.stack.line, 101);
Assert.equal(lastMessage.stack.column, 13); Assert.equal(lastMessage.stack.column, 13);
Assert.ok(!!lastMessage.stack.parent, "stack has a parent frame"); Assert.ok(!!lastMessage.stack.parent, "stack has a parent frame");
Assert.ok( Assert.ok(
@@ -145,13 +155,18 @@ add_task(async function callFunctionAndLogExceptionForContentScriptSandboxes() {
0 0
); );
try { try {
Services.console.callFunctionAndLogException(window, sandbox.foo); ChromeUtils.callFunctionAndLogException(window, sandbox.foo);
Assert.fail("callFunctionAndLogException should throw"); Assert.fail("callFunctionAndLogException should throw");
} catch (e) { } catch (e) {
Assert.equal( Assert.equal(
e.name, e.name,
"NS_ERROR_XPC_JAVASCRIPT_ERROR", "Error",
"callFunctionAndLogException thrown" "callFunctionAndLogException thrown with the expected exception name"
);
Assert.equal(
e.message,
"sandbox exception",
"callFunctionAndLogException thrown with the expected message"
); );
} }
@@ -198,15 +213,20 @@ add_task(
0 0
); );
try { try {
Services.console.callFunctionAndLogException(window, function () { ChromeUtils.callFunctionAndLogException(window, function () {
sandbox.foo(); sandbox.foo();
}); });
Assert.fail("callFunctionAndLogException should throw"); Assert.fail("callFunctionAndLogException should throw");
} catch (e) { } catch (e) {
Assert.equal( Assert.equal(
e.name, e.name,
"NS_ERROR_XPC_JAVASCRIPT_ERROR", "Error",
"callFunctionAndLogException thrown" "callFunctionAndLogException thrown with the expected exception name"
);
Assert.equal(
e.message,
"sandbox exception",
"callFunctionAndLogException thrown with the expected message"
); );
} }

View File

@@ -38,6 +38,8 @@ prefs = ["network.xhr.block_sync_system_requests=false"] # Bug 721336
["test_chromeutils_base64.js"] ["test_chromeutils_base64.js"]
["test_chromeutils_callFunctionAndLogException.js"]
["test_chromeutils_defineLazyGetter.js"] ["test_chromeutils_defineLazyGetter.js"]
["test_chromeutils_isJSIdentifier.js"] ["test_chromeutils_isJSIdentifier.js"]

View File

@@ -796,6 +796,18 @@ partial namespace ChromeUtils {
[Throws] [Throws]
ContentSecurityPolicy createCSPFromHeader(DOMString header, URI selfURI, Principal loadingPrincipal); ContentSecurityPolicy createCSPFromHeader(DOMString header, URI selfURI, Principal loadingPrincipal);
// This helper function executes `func` and redirects any exception
// that may be thrown while running it to the DevTools Console currently
// debugging `targetGlobal`.
//
// This helps flag the nsIScriptError with a particular innerWindowID
// which is especially useful for WebExtension content scripts
// where script are running in a Sandbox whose prototype is the content window.
// We expect content script exception to be flagged with the content window
// innerWindowID in order to appear in the tab's DevTools.
[ChromeOnly, Throws]
any callFunctionAndLogException(any targetGlobal, any func);
}; };
/* /*

View File

@@ -425,53 +425,6 @@ nsresult nsConsoleService::LogMessageWithMode(
return NS_OK; return NS_OK;
} }
// See nsIConsoleService.idl for more info about this method
NS_IMETHODIMP
nsConsoleService::CallFunctionAndLogException(
JS::Handle<JS::Value> targetGlobal, JS::HandleValue function, JSContext* cx,
JS::MutableHandleValue retval) {
if (!targetGlobal.isObject() || !function.isObject()) {
return NS_ERROR_INVALID_ARG;
}
JS::Rooted<JS::Realm*> contextRealm(cx, JS::GetCurrentRealmOrNull(cx));
if (!contextRealm) {
return NS_ERROR_INVALID_ARG;
}
JS::Rooted<JSObject*> global(
cx, js::CheckedUnwrapDynamic(&targetGlobal.toObject(), cx));
if (!global) {
return NS_ERROR_INVALID_ARG;
}
// Use AutoJSAPI in order to trigger AutoJSAPI::ReportException
// which will do most of the work required for this function.
//
// We only have to pick the right global for which we want to flag
// the exception against.
dom::AutoJSAPI jsapi;
if (!jsapi.Init(global)) {
return NS_ERROR_UNEXPECTED;
}
JSContext* ccx = jsapi.cx();
// AutoJSAPI picks `targetGlobal` as execution compartment
// whereas we expect to run `function` from the callsites compartment.
JSAutoRealm ar(ccx, JS::GetRealmGlobalOrNull(contextRealm));
JS::RootedValue funVal(ccx, function);
if (!JS_WrapValue(ccx, &funVal)) {
return NS_ERROR_FAILURE;
}
if (!JS_CallFunctionValue(ccx, nullptr, funVal, JS::HandleValueArray::empty(),
retval)) {
return NS_ERROR_XPC_JAVASCRIPT_ERROR;
}
return NS_OK;
}
void nsConsoleService::CollectCurrentListeners( void nsConsoleService::CollectCurrentListeners(
nsCOMArray<nsIConsoleListener>& aListeners) { nsCOMArray<nsIConsoleListener>& aListeners) {
MutexAutoLock lock(mLock); MutexAutoLock lock(mLock);

View File

@@ -13,18 +13,6 @@ interface nsIConsoleService : nsISupports
{ {
void logMessage(in nsIConsoleMessage message); void logMessage(in nsIConsoleMessage message);
// This helper function executes `func` and redirects any exception
// that may be thrown while running it to the DevTools Console currently
// debugging `targetGlobal`.
//
// This helps flag the nsIScriptError with a particular innerWindowID
// which is especially useful for WebExtension content scripts
// where script are running in a Sandbox whose prototype is the content window.
// We expect content script exception to be flaged with the content window
// innerWindowID in order to appear in the tab's DevTools.
[implicit_jscontext]
jsval callFunctionAndLogException(in jsval targetGlobal, in jsval func);
// This is a variant of LogMessage which allows the caller to determine // This is a variant of LogMessage which allows the caller to determine
// if the message should be output to an OS-specific log. This is used on // if the message should be output to an OS-specific log. This is used on
// B2G to control whether the message is logged to the android log or not. // B2G to control whether the message is logged to the android log or not.

View File

@@ -33,8 +33,6 @@ fail-if = ["os == 'android'"]
["test_bug1434856.js"] ["test_bug1434856.js"]
["test_console_service_callFunctionAndLogException.js"]
["test_debugger_malloc_size_of.js"] ["test_debugger_malloc_size_of.js"]
["test_error_iserror.js"] ["test_error_iserror.js"]