diff --git a/Libraries/BatchedBridge/BatchedBridge.js b/Libraries/BatchedBridge/BatchedBridge.js index 5d108f52abbbdd..f3e735f48749a0 100644 --- a/Libraries/BatchedBridge/BatchedBridge.js +++ b/Libraries/BatchedBridge/BatchedBridge.js @@ -12,7 +12,21 @@ 'use strict'; const MessageQueue = require('MessageQueue'); -const BatchedBridge = new MessageQueue(); + +// MessageQueue can install a global handler to catch all exceptions where JS users can register their own behavior +// This handler makes all exceptions to be handled inside MessageQueue rather than by the VM at its origin +// This makes stacktraces to be placed at MessageQueue rather than at where they were launched +// The parameter __fbUninstallRNGlobalErrorHandler is passed to MessageQueue to prevent the handler from being installed +// +// __fbUninstallRNGlobalErrorHandler is conditionally set by the Inspector while the VM is paused for intialization +// If the Inspector isn't present it defaults to undefined and the global error handler is installed +// The Inspector can still call MessageQueue#uninstallGlobalErrorHandler to uninstalled on attach + +const BatchedBridge = new MessageQueue( + // $FlowFixMe + typeof __fbUninstallRNGlobalErrorHandler !== 'undefined' && + __fbUninstallRNGlobalErrorHandler === true, // eslint-disable-line no-undef +); // Wire up the batched bridge on the global object so that we can call into it. // Ideally, this would be the inverse relationship. I.e. the native environment diff --git a/Libraries/BatchedBridge/MessageQueue.js b/Libraries/BatchedBridge/MessageQueue.js index 0f017bfb481981..9d3b8c6d66914a 100644 --- a/Libraries/BatchedBridge/MessageQueue.js +++ b/Libraries/BatchedBridge/MessageQueue.js @@ -59,7 +59,9 @@ class MessageQueue { __spy: ?(data: SpyData) => void; - constructor() { + __guard: (() => void) => void; + + constructor(shouldUninstallGlobalErrorHandler: boolean = false) { this._lazyCallableModules = {}; this._queue = [[], [], [], 0]; this._successCallbacks = []; @@ -67,6 +69,11 @@ class MessageQueue { this._callID = 0; this._lastFlush = 0; this._eventLoopStartTime = new Date().getTime(); + if (shouldUninstallGlobalErrorHandler) { + this.uninstallGlobalErrorHandler(); + } else { + this.installGlobalErrorHandler(); + } if (__DEV__) { this._debugInfo = {}; @@ -252,11 +259,26 @@ class MessageQueue { } } + uninstallGlobalErrorHandler() { + this.__guard = this.__guardUnsafe; + } + + installGlobalErrorHandler() { + this.__guard = this.__guardSafe; + } + /** * Private methods */ - __guard(fn: () => void) { + // Lets exceptions propagate to be handled by the VM at the origin + __guardUnsafe(fn: () => void) { + this._inCall++; + fn(); + this._inCall--; + } + + __guardSafe(fn: () => void) { this._inCall++; try { fn(); diff --git a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js index 670cc3b5b2e88b..2dd760bf399f35 100644 --- a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js +++ b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @emails oncall+react_native + * @format */ 'use strict'; @@ -38,10 +39,12 @@ describe('MessageQueue', function() { queue = new MessageQueue(); queue.registerCallableModule( 'MessageQueueTestModule', - MessageQueueTestModule + MessageQueueTestModule, ); - queue.createDebugLookup(0, 'MessageQueueTestModule', - ['testHook1', 'testHook2']); + queue.createDebugLookup(0, 'MessageQueueTestModule', [ + 'testHook1', + 'testHook2', + ]); }); it('should enqueue native calls', () => { @@ -65,7 +68,15 @@ describe('MessageQueue', function() { it('should call the stored callback', () => { let done = false; - queue.enqueueNativeCall(0, 1, [], () => {}, () => { done = true; }); + queue.enqueueNativeCall( + 0, + 1, + [], + () => {}, + () => { + done = true; + }, + ); queue.__invokeCallback(1, []); expect(done).toEqual(true); }); @@ -83,32 +94,92 @@ describe('MessageQueue', function() { }); it('should throw when calling with unknown module', () => { - const unknownModule = 'UnknownModule', unknownMethod = 'UnknownMethod'; + const unknownModule = 'UnknownModule', + unknownMethod = 'UnknownMethod'; expect(() => queue.__callFunction(unknownModule, unknownMethod)).toThrow( `Module ${unknownModule} is not a registered callable module (calling ${unknownMethod})`, ); }); it('should return lazily registered module', () => { - const dummyModule = {}, name = 'modulesName'; + const dummyModule = {}, + name = 'modulesName'; queue.registerLazyCallableModule(name, () => dummyModule); expect(queue.getCallableModule(name)).toEqual(dummyModule); }); it('should not initialize lazily registered module before it was used for the first time', () => { - const dummyModule = {}, name = 'modulesName'; + const dummyModule = {}, + name = 'modulesName'; const factory = jest.fn(() => dummyModule); queue.registerLazyCallableModule(name, factory); expect(factory).not.toHaveBeenCalled(); }); it('should initialize lazily registered module only once', () => { - const dummyModule = {}, name = 'modulesName'; + const dummyModule = {}, + name = 'modulesName'; const factory = jest.fn(() => dummyModule); queue.registerLazyCallableModule(name, factory); queue.getCallableModule(name); queue.getCallableModule(name); expect(factory).toHaveBeenCalledTimes(1); }); + + it('should catch all exceptions if the global error handler is installed', () => { + const errorMessage = 'intentional error'; + const errorModule = { + explode: function() { + throw new Error(errorMessage); + }, + }; + const name = 'errorModuleName'; + const factory = jest.fn(() => errorModule); + queue.__guardSafe = jest.fn(() => {}); + queue.__guardUnsafe = jest.fn(() => {}); + queue.installGlobalErrorHandler(); + queue.registerLazyCallableModule(name, factory); + queue.callFunctionReturnFlushedQueue(name, 'explode', []); + expect(queue.__guardUnsafe).toHaveBeenCalledTimes(0); + expect(queue.__guardSafe).toHaveBeenCalledTimes(2); + }); + + it('should propagate exceptions if the global error handler is uninstalled', () => { + queue.uninstallGlobalErrorHandler(); + const errorMessage = 'intentional error'; + const errorModule = { + explode: function() { + throw new Error(errorMessage); + }, + }; + const name = 'errorModuleName'; + const factory = jest.fn(() => errorModule); + queue.__guardUnsafe = jest.fn(() => {}); + queue.__guardSafe = jest.fn(() => {}); + queue.registerLazyCallableModule(name, factory); + queue.uninstallGlobalErrorHandler(); + queue.callFunctionReturnFlushedQueue(name, 'explode'); + expect(queue.__guardUnsafe).toHaveBeenCalledTimes(2); + expect(queue.__guardSafe).toHaveBeenCalledTimes(0); + }); + + it('should catch all exceptions if the global error handler is re-installed', () => { + const errorMessage = 'intentional error'; + const errorModule = { + explode: function() { + throw new Error(errorMessage); + }, + }; + const name = 'errorModuleName'; + const factory = jest.fn(() => errorModule); + queue.__guardUnsafe = jest.fn(() => {}); + queue.__guardSafe = jest.fn(() => {}); + queue.registerLazyCallableModule(name, factory); + queue.uninstallGlobalErrorHandler(); + queue.installGlobalErrorHandler(); + queue.callFunctionReturnFlushedQueue(name, 'explode'); + expect(queue.__guardUnsafe).toHaveBeenCalledTimes(0); + expect(queue.__guardSafe).toHaveBeenCalledTimes(2); + }); });