diff --git a/React/Base/RCTAssert.h b/React/Base/RCTAssert.h index 7914664ad..92cf1dae6 100644 --- a/React/Base/RCTAssert.h +++ b/React/Base/RCTAssert.h @@ -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 diff --git a/React/Base/RCTAssert.m b/React/Base/RCTAssert.m index 7d242ba9a..f28773ae5 100644 --- a/React/Base/RCTAssert.m +++ b/React/Base/RCTAssert.m @@ -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; +} diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index edf436e10..2a8fcc36c 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -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 *RCTGetModuleClasses(void); _loading = NO; - NSArray *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; } diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 189cdb096..74f8b9a94 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -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 diff --git a/React/Base/RCTLog.h b/React/Base/RCTLog.h index 13bb150be..3d7670933 100644 --- a/React/Base/RCTLog.h +++ b/React/Base/RCTLog.h @@ -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 diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index bc095afaf..b939bf8ad 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -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); }; diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index afc865de4..732240aec 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -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) diff --git a/React/Modules/RCTExceptionsManager.h b/React/Modules/RCTExceptionsManager.h index dac22b220..20ca8d26e 100644 --- a/React/Modules/RCTExceptionsManager.h +++ b/React/Modules/RCTExceptionsManager.h @@ -13,16 +13,11 @@ @protocol RCTExceptionsManagerDelegate -// 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 *)stack; -- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack; -- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack; - -- (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId; -- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId; -- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId; +- (void)updateJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId; @end diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index 17a713e8e..9cc5d5722 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -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 diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 6c7e500a4..44ac329e3 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -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; }