[RN] Prevent unhandled JS errors from killing the process in Release

This commit is contained in:
Lyubomir Marinov 2017-01-05 15:06:41 -06:00
parent 29001c3ab0
commit 4fef8a3b79
5 changed files with 151 additions and 33 deletions

View File

@ -1,11 +1,8 @@
package org.jitsi.meet;
import android.os.Bundle;
import com.crashlytics.android.Crashlytics;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import io.fabric.sdk.android.Fabric;
public class MainActivity extends ReactActivity {
/**
@ -46,11 +43,4 @@ public class MainActivity extends ReactActivity {
protected String getMainComponentName() {
return "App";
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Fabric.with(this, new Crashlytics());
}
}

View File

@ -1,39 +1,60 @@
package org.jitsi.meet;
import android.app.Application;
import android.util.Log;
import com.crashlytics.android.Crashlytics;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.oney.WebRTCModule.WebRTCModulePackage;
import io.fabric.sdk.android.Fabric;
import java.util.Arrays;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
/**
* {@inheritDoc}
*/
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
/**
* {@inheritDoc}
*/
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new VectorIconsPackage(),
new WebRTCModulePackage()
new com.facebook.react.shell.MainReactPackage(),
new com.oblador.vectoricons.VectorIconsPackage(),
new com.oney.WebRTCModule.WebRTCModulePackage()
);
}
};
/**
* {@inheritDoc}
*/
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate() {
super.onCreate();
if (!getReactNativeHost()
.getReactInstanceManager()
.getDevSupportManager()
.getDevSupportEnabled()) {
Fabric.with(this, new Crashlytics());
}
}
}

View File

@ -10,10 +10,35 @@
#import "AppDelegate.h"
#import <Crashlytics/Crashlytics.h>
#import <Fabric/Fabric.h>
#import "RCTAssert.h"
#import "RCTBundleURLProvider.h"
#import "RCTLinkingManager.h"
#import "RCTRootView.h"
/**
* A <tt>RCTFatalHandler</tt> implementation which swallows JavaScript errors.
* In the Release configuration, React Native will (intentionally) raise an
* unhandled NSException for an unhandled JavaScript error. This will
* effectively kill the application. <tt>_RCTFatal</tt> is suitable to be in
* accord with the Web i.e. not kill the application.
*/
RCTFatalHandler _RCTFatal = ^(NSError *error) {
id jsStackTrace = error.userInfo[RCTJSStackTraceKey];
@try {
NSString *name
= [NSString stringWithFormat:@"%@: %@",
RCTFatalExceptionName,
error.localizedDescription];
NSString *message
= RCTFormatError(error.localizedDescription, jsStackTrace, 75);
[NSException raise:name format:@"%@", message];
} @catch (NSException *e) {
if (!jsStackTrace) {
@throw;
}
}
};
@implementation AppDelegate
// https://facebook.github.io/react-native/docs/linking.html
@ -29,8 +54,18 @@ continueUserActivity:(NSUserActivity *)userActivity
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if !DEBUG
[Fabric with:@[[Crashlytics class]]];
// In the Release configuration, React Native will (intentionally) raise an
// unhandled NSException for an unhandled JavaScript error. This will
// effectively kill the application. In accord with the Web, do not kill the
// application.
if (!RCTGetFatalHandler()) {
RCTSetFatalHandler(_RCTFatal);
}
#endif
NSURL *jsCodeLocation
= [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios"
fallbackResource:nil];

View File

@ -1,5 +1,7 @@
/* global __DEV__ */
import React from 'react';
import { Linking, Navigator } from 'react-native';
import { Linking, Navigator, Platform } from 'react-native';
import { Provider } from 'react-redux';
import { _getRouteToRender } from '../functions';
@ -30,6 +32,12 @@ export class App extends AbstractApp {
// Bind event handlers so they are only bound once for every instance.
this._navigatorRenderScene = this._navigatorRenderScene.bind(this);
this._onLinkingURL = this._onLinkingURL.bind(this);
// In the Release configuration, React Native will (intentionally) throw
// an unhandled JavascriptException for an unhandled JavaScript error.
// This will effectively kill the application. In accord with the Web,
// do not kill the application.
this._maybeDisableExceptionsManager();
}
/**
@ -101,6 +109,44 @@ export class App extends AbstractApp {
navigator && navigator.replace({ ...route });
}
/**
* Attempts to disable the use of React Native
* {@link ExceptionsManager#handleException} on platforms and in
* configurations on/in which the use of the method in questions has been
* determined to be undesirable. For example, React Native will
* (intentionally) throw an unhandled JavascriptException for an
* unhandled JavaScript error in the Release configuration. This will
* effectively kill the application. In accord with the Web, do not kill the
* application.
*
* @private
* @returns {void}
*/
_maybeDisableExceptionsManager() {
if (__DEV__) {
// As mentioned above, only the Release configuration was observed
// to suffer.
return;
}
if (Platform.OS !== 'android') {
// A solution based on RTCSetFatalHandler was implemented on iOS and
// it is preferred because it is at a later step of the
// error/exception handling and it is specific to fatal
// errors/exceptions which were observed to kill the application.
// The solution implemented bellow was tested on Android only so it
// is considered safest to use it there only.
return;
}
const oldHandler = global.ErrorUtils.getGlobalHandler();
const newHandler = _handleException;
if (!oldHandler || oldHandler !== newHandler) {
newHandler.next = oldHandler;
global.ErrorUtils.setGlobalHandler(newHandler);
}
}
/**
* Renders the scene identified by a specific route in the Navigator of this
* instance.
@ -133,3 +179,29 @@ export class App extends AbstractApp {
this._openURL(event.url);
}
}
/**
* Handles a (possibly unhandled) JavaScript error by preventing React Native
* from converting a fatal error into an unhandled native exception which will
* kill the application.
*
* @param {Error} error - The (possibly unhandled) JavaScript error to handle.
* @param {boolean} fatal - True if the specified error is fatal; otherwise,
* false.
* @private
* @returns {void}
*/
function _handleException(error, fatal) {
if (fatal) {
// In the Release configuration, React Native will (intentionally) throw
// an unhandled JavascriptException for an unhandled JavaScript error.
// This will effectively kill the application. In accord with the Web,
// do not kill the application.
console.error(error);
} else {
// Forward to the next globalHandler of ErrorUtils.
const next = _handleException.next;
typeof next === 'function' && next(error, fatal);
}
}