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:
parent
d8dd330d41
commit
31b5b0ac01
|
@ -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,11 +33,40 @@ 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.
|
||||
|
@ -62,7 +76,7 @@ RCT_EXTERN void _RCTAssertFormat(
|
|||
|
||||
/**
|
||||
* 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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
if (RCTProfileIsProfiling()) {
|
||||
NSMutableDictionary *args = [method.profileArgs mutableCopy];
|
||||
[args setValue:method.JSMethodName forKey:@"method"];
|
||||
[args setValue:RCTJSONStringify(RCTNullIfNil(params), NULL) forKey:@"args"];
|
||||
|
||||
args[@"method"] = method.JSMethodName;
|
||||
args[@"args"] = RCTJSONStringify(RCTNullIfNil(params), NULL);
|
||||
RCTProfileEndEvent(0, @"objc_call", args);
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
||||
if (!RCT_DEBUG && reloadRetries < _maxReloadAttempts) {
|
||||
reloadRetries++;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification
|
||||
object:nil];
|
||||
|
||||
[[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];
|
||||
}
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue