Create RCTFatal for reporting fatal React events

Summary: public

Add RCTFatal for reporting fatal runtime conditions. This centralizes failure handling to one function and allows you to customize how they should be handled. RCTFatal will be logged to the console and as a redbox and will also be triggered by fatal exceptions coming from RCTExceptionsManager.

Note that there is no RCTLogFatal, since just logging the fatal condition does not allow us to handle it consistently.

Reviewed By: nicklockwood

Differential Revision: D2615490

fb-gh-sync-id: 7d8e134419e10a8fb549297054ad955db3f6bee0
This commit is contained in:
Pieter De Baets 2015-11-05 12:19:56 -08:00 committed by facebook-github-bot-5
parent d8dd330d41
commit 31b5b0ac01
10 changed files with 140 additions and 124 deletions

View File

@ -11,25 +11,10 @@
#import "RCTDefines.h"
/**
* The default error domain to be used for React errors.
*/
RCT_EXTERN NSString *const RCTErrorDomain;
/**
* A block signature to be used for custom assertion handling.
*/
typedef void (^RCTAssertFunction)(
NSString *condition,
NSString *fileName,
NSNumber *lineNumber,
NSString *function,
NSString *message
);
/**
* This is the main assert macro that you should use. Asserts should be compiled out
* in production builds
* in production builds. You can customize the assert behaviour by setting a custom
* assert handler through `RCTSetAssertFunction`.
*/
#ifndef NS_BLOCK_ASSERTIONS
#define RCTAssert(condition, ...) do { \
@ -48,21 +33,50 @@ RCT_EXTERN void _RCTAssertFormat(
const char *, const char *, int, const char *, NSString *, ...
) NS_FORMAT_FUNCTION(5,6);
/**
* Report a fatal condition when executing. These calls will _NOT_ be compiled out
* in production, and crash the app by default. You can customize the fatal behaviour
* by setting a custom fatal handler through `RCTSetFatalHandler`.
*/
RCT_EXTERN void RCTFatal(NSError *error);
/**
* The default error domain to be used for React errors.
*/
RCT_EXTERN NSString *const RCTErrorDomain;
/**
* JS Stack trace provided as part of an NSError's userInfo
*/
RCT_EXTERN NSString *const RCTJSStackTraceKey;
/**
* A block signature to be used for custom assertion handling.
*/
typedef void (^RCTAssertFunction)(
NSString *condition,
NSString *fileName,
NSNumber *lineNumber,
NSString *function,
NSString *message
);
typedef void (^RCTFatalHandler)(NSError *error);
/**
* Convenience macro for asserting that a parameter is non-nil/non-zero.
*/
#define RCTAssertParam(name) RCTAssert(name, \
@"'%s' is a required parameter", #name)
#define RCTAssertParam(name) RCTAssert(name, @"'%s' is a required parameter", #name)
/**
* Convenience macro for asserting that we're running on main thread.
*/
#define RCTAssertMainThread() RCTAssert([NSThread isMainThread], \
@"This function must be called on the main thread")
@"This function must be called on the main thread")
/**
* These methods get and set the current assert function called by the RCTAssert
* macros. You can use these to replace the standard behavior with custom log
* macros. You can use these to replace the standard behavior with custom assert
* functionality.
*/
RCT_EXTERN void RCTSetAssertFunction(RCTAssertFunction assertFunction);
@ -82,6 +96,12 @@ RCT_EXTERN void RCTAddAssertFunction(RCTAssertFunction assertFunction);
*/
RCT_EXTERN void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction assertFunction);
/**
These methods get and set the current fatal handler called by the RCTFatal method.
*/
RCT_EXTERN void RCTSetFatalHandler(RCTFatalHandler fatalHandler);
RCT_EXTERN RCTFatalHandler RCTGetFatalHandler(void);
/**
* Get the current thread's name (or the current queue, if in debug mode)
*/
@ -106,6 +126,6 @@ _Pragma("clang diagnostic pop")
#else
#define RCTAssertThread(thread, format...)
#define RCTAssertThread(thread, format...) do { } while (0)
#endif

View File

@ -8,12 +8,15 @@
*/
#import "RCTAssert.h"
#import "RCTLog.h"
NSString *const RCTErrorDomain = @"RCTErrorDomain";
NSString *const RCTJSStackTraceKey = @"RCTJSStackTraceKey";
static NSString *const RCTAssertFunctionStack = @"RCTAssertFunctionStack";
RCTAssertFunction RCTCurrentAssertFunction = nil;
RCTFatalHandler RCTCurrentFatalHandler = nil;
NSException *_RCTNotImplementedException(SEL, Class);
NSException *_RCTNotImplementedException(SEL cmd, Class cls)
@ -112,3 +115,44 @@ void _RCTAssertFormat(
assertFunction(@(condition), @(fileName), @(lineNumber), @(function), message);
}
}
void RCTFatal(NSError *error)
{
_RCTLogInternal(RCTLogLevelFatal, NULL, 0, @"%@", [error localizedDescription]);
RCTFatalHandler fatalHandler = RCTGetFatalHandler();
if (fatalHandler) {
fatalHandler(error);
} else {
const NSUInteger maxMessageLength = 75;
NSString *message = [error localizedDescription];
if (message.length > maxMessageLength) {
message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."];
}
NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"];
if ([error.userInfo[RCTJSStackTraceKey] isKindOfClass:[NSArray class]]) {
for (NSDictionary *frame in error.userInfo[RCTJSStackTraceKey]) {
[prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]];
}
}
#if DEBUG
@try {
#endif
[NSException raise:@"RCTFatalException" format:@"%@", message];
#if DEBUG
} @catch (NSException *e) {}
#endif
}
}
void RCTSetFatalHandler(RCTFatalHandler fatalhandler)
{
RCTCurrentFatalHandler = fatalhandler;
}
RCTFatalHandler RCTGetFatalHandler(void)
{
return RCTCurrentFatalHandler;
}

View File

@ -21,7 +21,6 @@
#import "RCTModuleMap.h"
#import "RCTPerformanceLogger.h"
#import "RCTProfile.h"
#import "RCTRedBox.h"
#import "RCTSourceCode.h"
#import "RCTSparseArray.h"
#import "RCTUtils.h"
@ -416,18 +415,10 @@ RCT_EXTERN NSArray<Class> *RCTGetModuleClasses(void);
_loading = NO;
NSArray<NSDictionary *> *stack = error.userInfo[@"stack"];
if (stack) {
[self.redBox showErrorMessage:error.localizedDescription withStack:stack];
} else {
[self.redBox showError:error];
}
RCTLogError(@"Error while loading: %@", error.localizedDescription);
NSDictionary *userInfo = @{@"bridge": self, @"error": error};
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification
object:_parentBridge
userInfo:userInfo];
userInfo:@{@"bridge": self, @"error": error}];
RCTFatal(error);
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL
@ -661,7 +652,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
RCTJavaScriptCallback processResponse = ^(id json, NSError *error) {
if (error) {
[self.redBox showError:error];
RCTFatal(error);
}
if (!self.isValid) {
@ -810,17 +801,23 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
[method invokeWithBridge:self module:moduleData.instance arguments:params];
}
@catch (NSException *exception) {
RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, moduleData.name, params, exception);
if (!RCT_DEBUG && [exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) {
// Pass on JS exceptions
if ([exception.name rangeOfString:@"Unhandled JS Exception"].location == 0) {
@throw exception;
}
NSString *message = [NSString stringWithFormat:
@"Exception thrown while invoking %@ on target %@ with params %@: %@",
method.JSMethodName, moduleData.name, params, exception];
RCTFatal(RCTErrorWithMessage(message));
}
NSMutableDictionary *args = [method.profileArgs mutableCopy];
[args setValue:method.JSMethodName forKey:@"method"];
[args setValue:RCTJSONStringify(RCTNullIfNil(params), NULL) forKey:@"args"];
RCTProfileEndEvent(0, @"objc_call", args);
if (RCTProfileIsProfiling()) {
NSMutableDictionary *args = [method.profileArgs mutableCopy];
args[@"method"] = method.JSMethodName;
args[@"args"] = RCTJSONStringify(RCTNullIfNil(params), NULL);
RCTProfileEndEvent(0, @"objc_call", args);
}
return YES;
}

View File

@ -284,8 +284,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
#define RCT_INNER_BRIDGE_ONLY(...) \
- (void)__VA_ARGS__ \
{ \
RCTLogMustFix(@"Called method \"%@\" on top level bridge. This method should \
only be called from bridge instance in a bridge module", @(__func__)); \
NSString *errorMessage = [NSString stringWithFormat:@"Called method \"%@\" on top level bridge. \
This method should oly be called from bridge instance in a bridge module", @(__func__)]; \
RCTFatal(RCTErrorWithMessage(errorMessage)); \
}
- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args

View File

@ -17,14 +17,9 @@
#endif
/**
* Thresholds for logs to raise an assertion, or display redbox, respectively.
* You can override these values when debugging in order to tweak the default
* logging behavior.
* Thresholds for logs to display a redbox. You can override these values when debugging
* in order to tweak the default logging behavior.
*/
#ifndef RCTLOG_FATAL_LEVEL
#define RCTLOG_FATAL_LEVEL RCTLogLevelMustFix
#endif
#ifndef RCTLOG_REDBOX_LEVEL
#define RCTLOG_REDBOX_LEVEL RCTLogLevelError
#endif
@ -37,7 +32,6 @@
#define RCTLogInfo(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__)
#define RCTLogWarn(...) _RCTLog(RCTLogLevelWarning, __VA_ARGS__)
#define RCTLogError(...) _RCTLog(RCTLogLevelError, __VA_ARGS__)
#define RCTLogMustFix(...) _RCTLog(RCTLogLevelMustFix, __VA_ARGS__)
/**
* An enum representing the severity of the log message.
@ -46,7 +40,7 @@ typedef NS_ENUM(NSInteger, RCTLogLevel) {
RCTLogLevelInfo = 1,
RCTLogLevelWarning = 2,
RCTLogLevelError = 3,
RCTLogLevelMustFix = 4
RCTLogLevelFatal = 4
};
/**
@ -119,9 +113,7 @@ RCT_EXTERN void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *pref
* Private logging function - ignore this.
*/
#if RCTLOG_ENABLED
#define _RCTLog(lvl, ...) do { \
if (lvl >= RCTLOG_FATAL_LEVEL) { RCTAssert(NO, __VA_ARGS__); } \
_RCTLogInternal(lvl, __FILE__, __LINE__, __VA_ARGS__); } while (0)
#define _RCTLog(lvl, ...) _RCTLogInternal(lvl, __FILE__, __LINE__, __VA_ARGS__);
#else
#define _RCTLog(lvl, ...) do { } while (0)
#endif

View File

@ -63,7 +63,7 @@ RCTLogFunction RCTDefaultLogFunction = ^(
fprintf(stderr, "%s\n", log.UTF8String);
fflush(stderr);
int aslLevel = ASL_LEVEL_ERR;
int aslLevel;
switch(level) {
case RCTLogLevelInfo:
aslLevel = ASL_LEVEL_NOTICE;
@ -74,11 +74,9 @@ RCTLogFunction RCTDefaultLogFunction = ^(
case RCTLogLevelError:
aslLevel = ASL_LEVEL_ERR;
break;
case RCTLogLevelMustFix:
aslLevel = ASL_LEVEL_EMERG;
case RCTLogLevelFatal:
aslLevel = ASL_LEVEL_CRIT;
break;
default:
aslLevel = ASL_LEVEL_DEBUG;
}
asl_log(NULL, NULL, aslLevel, "%s", message.UTF8String);
};

View File

@ -400,8 +400,7 @@ BOOL RCTImageHasAlpha(CGImageRef image)
NSError *RCTErrorWithMessage(NSString *message)
{
NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message};
NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
return error;
return [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
}
id RCTNullIfNil(id value)

View File

@ -13,16 +13,11 @@
@protocol RCTExceptionsManagerDelegate <NSObject>
// NOTE: Remove these three methods and the @optional directive after updating the codebase to use only the three below
- (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId;
- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId;
@optional
- (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack;
- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack;
- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack;
- (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack exceptionId:(NSNumber *)exceptionId;
- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack exceptionId:(NSNumber *)exceptionId;
- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray<NSDictionary *> *)stack exceptionId:(NSNumber *)exceptionId;
- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId;
@end

View File

@ -43,58 +43,31 @@ RCT_EXPORT_METHOD(reportSoftException:(NSString *)message
stack:(NSDictionaryArray *)stack
exceptionId:(nonnull NSNumber *)exceptionId)
{
// TODO(#7070533): report a soft error to the server
if (_delegate) {
if ([_delegate respondsToSelector:@selector(handleSoftJSExceptionWithMessage:stack:exceptionId:)]) {
[_delegate handleSoftJSExceptionWithMessage:message stack:stack exceptionId:exceptionId];
} else {
[_delegate handleSoftJSExceptionWithMessage:message stack:stack];
}
return;
}
[_bridge.redBox showErrorMessage:message withStack:stack];
if (_delegate) {
[_delegate handleSoftJSExceptionWithMessage:message stack:stack exceptionId:exceptionId];
}
}
RCT_EXPORT_METHOD(reportFatalException:(NSString *)message
stack:(NSDictionaryArray *)stack
exceptionId:(nonnull NSNumber *)exceptionId)
{
if (_delegate) {
if ([_delegate respondsToSelector:@selector(handleFatalJSExceptionWithMessage:stack:exceptionId:)]) {
[_delegate handleFatalJSExceptionWithMessage:message stack:stack exceptionId:exceptionId];
} else {
[_delegate handleFatalJSExceptionWithMessage:message stack:stack];
}
return;
}
[_bridge.redBox showErrorMessage:message withStack:stack];
if (!RCT_DEBUG) {
if (_delegate) {
[_delegate handleFatalJSExceptionWithMessage:message stack:stack exceptionId:exceptionId];
}
static NSUInteger reloadRetries = 0;
const NSUInteger maxMessageLength = 75;
if (reloadRetries < _maxReloadAttempts) {
reloadRetries++;
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification
object:nil];
} else {
if (message.length > maxMessageLength) {
message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."];
}
NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"];
for (NSDictionary *frame in stack) {
[prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]];
}
NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:message];
[NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack];
}
static NSUInteger reloadRetries = 0;
if (!RCT_DEBUG && reloadRetries < _maxReloadAttempts) {
reloadRetries++;
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil];
} else {
NSString *description = [@"Unhandled JS Exception: " stringByAppendingString:message];
NSDictionary *errorInfo = @{ NSLocalizedDescriptionKey: description, RCTJSStackTraceKey: stack };
RCTFatal([NSError errorWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]);
}
}
@ -102,16 +75,11 @@ RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message
stack:(NSDictionaryArray *)stack
exceptionId:(nonnull NSNumber *)exceptionId)
{
if (_delegate) {
if ([_delegate respondsToSelector:@selector(updateJSExceptionWithMessage:stack:exceptionId:)]) {
[_delegate updateJSExceptionWithMessage:message stack:stack exceptionId:exceptionId];
} else {
[_delegate updateJSExceptionWithMessage:message stack:stack];
}
return;
}
[_bridge.redBox updateErrorMessage:message withStack:stack];
if (_delegate && [_delegate respondsToSelector:@selector(updateJSExceptionWithMessage:stack:exceptionId:)]) {
[_delegate updateJSExceptionWithMessage:message stack:stack exceptionId:exceptionId];
}
}
// Deprecated. Use reportFatalException directly instead.
@ -120,4 +88,5 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message
{
[self reportFatalException:message stack:stack exceptionId:@-1];
}
@end

View File

@ -647,8 +647,9 @@ RCT_EXPORT_METHOD(removeSubviewsFromContainerWithID:(nonnull NSNumber *)containe
}
}
if (removedChildren.count != atIndices.count) {
RCTLogMustFix(@"removedChildren count (%tu) was not what we expected (%tu)",
removedChildren.count, atIndices.count);
NSString *message = [NSString stringWithFormat:@"removedChildren count (%tu) was not what we expected (%tu)",
removedChildren.count, atIndices.count];
RCTFatal(RCTErrorWithMessage(message));
}
return removedChildren;
}