From 80cd687e95226683c0494c35dbd624560de22074 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 7 Apr 2015 07:36:26 -0700 Subject: [PATCH] Refactored RCTLog and added facility to prepend extra data to the log message --- .../story-background.imageset/Contents.json | 4 + .../RCTAnimationExperimentalManager.m | 2 +- .../RCTWebSocketExecutor.m | 4 +- React/Base/RCTBridge.h | 13 +- React/Base/RCTBridge.m | 37 +-- React/Base/RCTConvert.h | 5 +- React/Base/RCTConvert.m | 15 +- React/Base/RCTLog.h | 170 ++++++++--- React/Base/RCTLog.m | 265 +++++++++++++----- React/Base/RCTRootView.h | 7 + React/Base/RCTRootView.m | 1 - React/Executors/RCTContextExecutor.m | 21 +- React/Modules/RCTAlertManager.m | 1 + React/Modules/RCTUIManager.m | 43 ++- React/Views/RCTScrollViewManager.m | 2 +- 15 files changed, 411 insertions(+), 179 deletions(-) diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json index 9c8120dff..e1e9cd56b 100644 --- a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json +++ b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json @@ -1,5 +1,9 @@ { "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, { "idiom" : "universal", "scale" : "2x", diff --git a/Libraries/Animation/RCTAnimationExperimentalManager.m b/Libraries/Animation/RCTAnimationExperimentalManager.m index 88bb5fe28..b7c76c9f5 100644 --- a/Libraries/Animation/RCTAnimationExperimentalManager.m +++ b/Libraries/Animation/RCTAnimationExperimentalManager.m @@ -71,7 +71,7 @@ UIView *view = viewRegistry[reactTag]; if (!view) { - RCTLogWarn(@"React tag %@ is not registered with the view registry", reactTag); + RCTLogWarn(@"React tag #%@ is not registered with the view registry", reactTag); return; } diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m index ddf973b24..4d6aba5cc 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m @@ -119,7 +119,9 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); [_jsQueue addOperationWithBlock:^{ if (!self.valid) { - NSError *error = [NSError errorWithDomain:@"WS" code:1 userInfo:@{NSLocalizedDescriptionKey:@"socket closed"}]; + NSError *error = [NSError errorWithDomain:@"WS" code:1 userInfo:@{ + NSLocalizedDescriptionKey: @"socket closed" + }]; callback(error, nil); return; } diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 05767533d..3f0ad735e 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -28,6 +28,11 @@ typedef NSArray *(^RCTBridgeModuleProviderBlock)(void); extern NSString *const RCTReloadBridge; +/** + * This function returns the module name for a given class. + */ +extern NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); + /** * Async batched bridge used to communicate with the JavaScript application. */ @@ -81,14 +86,6 @@ extern NSString *const RCTReloadBridge; */ @property (nonatomic, readonly) dispatch_queue_t shadowQueue; -/** - * Global logging function that will print to both xcode and JS debugger consoles. - * - * NOTE: Use via RCTLog* macros defined in RCTLog.h - * TODO (#5906496): should log function be exposed here, or could it be a module? - */ -+ (void)log:(NSArray *)objects level:(NSString *)level; - @property (nonatomic, copy, readonly) NSDictionary *launchOptions; diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 1ad3d58b7..3bf23a5d3 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -22,6 +22,7 @@ #import "RCTJavaScriptLoader.h" #import "RCTKeyCommands.h" #import "RCTLog.h" +#import "RCTRedBox.h" #import "RCTRootView.h" #import "RCTSparseArray.h" #import "RCTUtils.h" @@ -41,10 +42,7 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { NSString *const RCTReloadBridge = @"RCTReloadBridge"; -/** - * This function returns the module name for a given class. - */ -static NSString *RCTModuleNameForClass(Class cls) +NSString *RCTBridgeModuleNameForClass(Class cls) { return [cls respondsToSelector:@selector(moduleName)] ? [cls moduleName] : NSStringFromClass(cls); } @@ -92,7 +90,7 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) [(NSMutableArray *)modules addObject:cls]; // Add module name - NSString *moduleName = RCTModuleNameForClass(cls); + NSString *moduleName = RCTBridgeModuleNameForClass(cls); [(NSMutableArray *)RCTModuleNamesByID addObject:moduleName]; } }); @@ -187,7 +185,7 @@ static Class _globalExecutorClass; RCT_ARG_BLOCK( \ if (json && ![json isKindOfClass:[_class class]]) { \ RCTLogError(@"Argument %tu (%@) of %@.%@ should be of type %@", index, \ - json, RCTModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \ + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \ return; \ } \ _logic \ @@ -203,7 +201,7 @@ static Class _globalExecutorClass; RCT_ARG_BLOCK( \ if (json && ![json respondsToSelector:@selector(_selector)]) { \ RCTLogError(@"Argument %tu (%@) of %@.%@ does not respond to selector: %@", \ - index, json, RCTModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \ + index, json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \ return; \ } \ _type value = [json _selector]; \ @@ -231,7 +229,7 @@ static Class _globalExecutorClass; RCT_ARG_BLOCK( if (json && ![json isKindOfClass:[NSNumber class]]) { RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index, - json, RCTModuleNameForClass(_moduleClass), _JSMethodName); + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); return; } // Marked as autoreleasing, because NSInvocation doesn't retain arguments @@ -268,7 +266,7 @@ static Class _globalExecutorClass; // Safety check if (arguments.count != _argumentBlocks.count) { RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", - RCTModuleNameForClass(_moduleClass), _JSMethodName, + RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, arguments.count, _argumentBlocks.count); return; } @@ -544,7 +542,7 @@ static id _latestJSExecutor; // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; for (id module in _moduleProvider ? _moduleProvider() : nil) { - preregisteredModules[RCTModuleNameForClass([module class])] = module; + preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module; } // Instantiate modules @@ -895,27 +893,18 @@ static id _latestJSExecutor; return (_latestJSExecutor != nil && [_latestJSExecutor isValid]); } -+ (void)log:(NSArray *)objects level:(NSString *)level ++ (void)logMessage:(NSString *)message level:(NSString *)level { if (!_latestJSExecutor || ![_latestJSExecutor isValid]) { - RCTLogError(@"ERROR: No valid JS executor to log %@.", objects); + RCTLogError(@"ERROR: No valid JS executor to log '%@'.", message); return; } - NSMutableArray *args = [NSMutableArray arrayWithObject:level]; - // TODO (#5906496): Find out and document why we skip the first object - for (id ob in [objects subarrayWithRange:(NSRange){1, [objects count] - 1}]) { - if ([NSJSONSerialization isValidJSONObject:@[ob]]) { - [args addObject:ob]; - } else { - [args addObject:[ob description]]; - } - } - - // Note: the js executor could get invalidated while we're trying to call this...need to watch out for that. + // Note: the js executor could get invalidated while we're trying to call + // this...need to watch out for that. [_latestJSExecutor executeJSCall:@"RCTLog" method:@"logIfNoNativeHook" - arguments:args + arguments:@[level, message] callback:^(id json, NSError *error) {}]; } diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 666ce013e..7e573370e 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -145,7 +145,7 @@ RCT_CUSTOM_CONVERTER(type, name, [json getter]) } \ @catch (__unused NSException *e) { \ RCTLogError(@"JSON value '%@' of type '%@' cannot be converted to '%s'", \ - json, [json class], #type); \ + json, [json classForCoder], #type); \ json = nil; \ return code; \ } \ @@ -181,7 +181,8 @@ RCT_CUSTOM_CONVERTER(type, type, [[self NSNumber:json] getter]) return default; \ } \ if (![json isKindOfClass:[NSString class]]) { \ - RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", #type, [json class], json); \ + RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", \ + #type, [json classForCoder], json); \ } \ id value = mapping[json]; \ if(!value && [json description].length > 0) { \ diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index abb96b398..1ddc9884c 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -45,7 +45,7 @@ RCT_CONVERTER(NSString *, NSString, description) } return number; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json class]); + RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json classForCoder]); } return nil; } @@ -53,7 +53,7 @@ RCT_CONVERTER(NSString *, NSString, description) + (NSURL *)NSURL:(id)json { if (![json isKindOfClass:[NSString class]]) { - RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json class], json); + RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json); return nil; } @@ -98,7 +98,7 @@ RCT_CONVERTER(NSString *, NSString, description) } return date; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json class]); + RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json classForCoder]); } return nil; } @@ -234,7 +234,8 @@ RCT_ENUM_CONVERTER(UIBarStyle, (@{ ((CGFloat *)&result)[i] = [self CGFloat:json[fields[i]]]; \ } \ } else if (json && json != [NSNull null]) { \ - RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \ + RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", \ + #type, [json classForCoder], json); \ } \ return result; \ } \ @@ -511,8 +512,8 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ } else if (json && ![json isKindOfClass:[NSNull class]]) { - RCTLogError(@"Expected NSArray, NSDictionary or NSString for UIColor, \ - received %@: %@", [json class], json); + RCTLogError(@"Expected NSArray, NSDictionary or NSString for UIColor, received %@: %@", + [json classForCoder], json); } // Default color @@ -538,7 +539,7 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ // image itself) so as to reduce overhead on subsequent checks of the same input if (![json isKindOfClass:[NSString class]]) { - RCTLogError(@"Expected NSString for UIImage, received %@: %@", [json class], json); + RCTLogError(@"Expected NSString for UIImage, received %@: %@", [json classForCoder], json); return nil; } diff --git a/React/Base/RCTLog.h b/React/Base/RCTLog.h index 9698e6476..c30da141b 100644 --- a/React/Base/RCTLog.h +++ b/React/Base/RCTLog.h @@ -7,58 +7,138 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import "RCTAssert.h" -#import "RCTRedBox.h" - -#define RCTLOG_INFO 1 -#define RCTLOG_WARN 2 -#define RCTLOG_ERROR 3 -#define RCTLOG_MUSTFIX 4 - -// If set to e.g. `RCTLOG_ERROR`, will assert after logging the first error. -#if DEBUG -#define RCTLOG_FATAL_LEVEL RCTLOG_MUSTFIX -#define RCTLOG_REDBOX_LEVEL RCTLOG_ERROR -#else -#define RCTLOG_FATAL_LEVEL (RCTLOG_MUSTFIX + 1) -#define RCTLOG_REDBOX_LEVEL (RCTLOG_MUSTFIX + 1) -#endif - -// If defined, only log messages that match this regex will fatal -#define RCTLOG_FATAL_REGEX nil - -extern __unsafe_unretained NSString *RCTLogLevels[]; - -#define _RCTLog(_level, ...) do { \ - NSString *__RCTLog__levelStr = RCTLogLevels[_level - 1]; \ - NSString *__RCTLog__msg = RCTLogObjects(RCTLogFormat(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__), __RCTLog__levelStr); \ - if (_level >= RCTLOG_FATAL_LEVEL) { \ - BOOL __RCTLog__fail = YES; \ - if (RCTLOG_FATAL_REGEX) { \ - NSRegularExpression *__RCTLog__regex = [NSRegularExpression regularExpressionWithPattern:RCTLOG_FATAL_REGEX options:0 error:NULL]; \ - __RCTLog__fail = [__RCTLog__regex numberOfMatchesInString:__RCTLog__msg options:0 range:NSMakeRange(0, [__RCTLog__msg length])] > 0; \ - } \ - RCTCAssert(!__RCTLog__fail, @"RCTLOG_FATAL_LEVEL %@: %@", __RCTLog__levelStr, __RCTLog__msg); \ - } \ - if (_level >= RCTLOG_REDBOX_LEVEL) { \ - [[RCTRedBox sharedInstance] showErrorMessage:__RCTLog__msg]; \ - } \ -} while (0) - -#define RCTLog(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__) -#define RCTLogInfo(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__) -#define RCTLogWarn(...) _RCTLog(RCTLOG_WARN, __VA_ARGS__) -#define RCTLogError(...) _RCTLog(RCTLOG_ERROR, __VA_ARGS__) -#define RCTLogMustFix(...) _RCTLog(RCTLOG_MUSTFIX, __VA_ARGS__) #ifdef __cplusplus extern "C" { #endif -NSString *RCTLogObjects(NSArray *objects, NSString *level); -NSArray *RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) NS_FORMAT_FUNCTION(4,5); +/** + * 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. + */ +#define RCTLOG_FATAL_LEVEL RCTLogLevelMustFix +#define RCTLOG_REDBOX_LEVEL RCTLogLevelError -void RCTInjectLogFunction(void (^logFunction)(NSString *msg)); +/** + * A regular expression that can be used to selectively limit the throwing of + * a exception to specific log contents. + */ +#define RCTLOG_FATAL_REGEX nil + +/** + * An enum representing the severity of the log message. + */ +typedef NS_ENUM(NSInteger, RCTLogLevel) { + RCTLogLevelInfo = 1, + RCTLogLevelWarning = 2, + RCTLogLevelError = 3, + RCTLogLevelMustFix = 4 +}; + +/** + * A block signature to be used for custom logging functions. In most cases you + * will want to pass these arguments to the RCTFormatLog function in order to + * generate a string, or use the RCTSimpleLogFunction() constructor to register + * a simple function that does not use all of the arguments. + */ +typedef void (^RCTLogFunction)( + RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message +); + +/** + * A method to generate a string from a collection of log data. To omit any + * particular data from the log, just pass nil or zero for the argument. + */ +NSString *RCTFormatLog( + NSDate *timestamp, + NSThread *thread, + RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message +); + +/** + * A method to generate a log function from a block with a much simpler + * template. The message passed to the simpler block is equivalent to the + * output of the RCTFormatLog() function. + */ +RCTLogFunction RCTSimpleLogFunction(void (^logFunction)(RCTLogLevel level, NSString *message)); + +/** + * The default logging function used by RCTLogXX. + */ +extern RCTLogFunction RCTDefaultLogFunction; + +/** + * These methods get and set the current logging threshold. This is the level + * below which logs will be ignored. Default is RCTLogLevelInfo for debug and + * RCTLogLevelError for production. + */ +void RCTSetLogThreshold(RCTLogLevel threshold); +RCTLogLevel RCTGetLogThreshold(void); + +/** + * These methods get and set the current logging function called by the RCTLogXX + * macros. You can use these to replace the standard behavior with custom log + * functionality. + */ +void RCTSetLogFunction(RCTLogFunction logFunction); +RCTLogFunction RCTGetLogFunction(void); + +/** + * This appends additional code to the existing log function, without replacing + * the existing functionality. Useful if you just want to forward logs to an + * extra service without changing the default behavior. + */ +void RCTAddLogFunction(RCTLogFunction logFunction); + +/** + * This method adds a conditional prefix to any messages logged within the scope + * of the passed block. This is useful for adding additional context to log + * messages. The block will be performed synchronously on the current thread. + */ +void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix); + +/** + * Private logging functions - ignore these. + */ +void _RCTLogFormat(RCTLogLevel, const char *, int, NSString *, ...) NS_FORMAT_FUNCTION(4,5); +#define _RCTLog(lvl, ...) do { \ + NSString *msg = [NSString stringWithFormat:__VA_ARGS__]; \ + if (lvl >= RCTLOG_FATAL_LEVEL) { \ + BOOL fail = YES; \ + if (RCTLOG_FATAL_REGEX) { \ + if ([msg rangeOfString:RCTLOG_FATAL_REGEX options:NSRegularExpressionSearch].length) { \ + fail = NO; \ + } \ + } \ + RCTCAssert(!fail, @"FATAL ERROR: %@", msg); \ + }\ + _RCTLogFormat(lvl, __FILE__, __LINE__, __VA_ARGS__); \ +} while (0) + +/** + * Legacy injection function - don't use this. + */ +void RCTInjectLogFunction(void (^)(NSString *msg)); + +/** + * Logging macros. Use these to log information, warnings and errors in your + * own code. + */ +#define RCTLog(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__) +#define RCTLogInfo(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__) +#define RCTLogWarn(...) _RCTLog(RCTLogLevelWarning, __VA_ARGS__) +#define RCTLogError(...) _RCTLog(RCTLogLevelError, __VA_ARGS__) +#define RCTLogMustFix(...) _RCTLog(RCTLogLevelMustFix, __VA_ARGS__) #ifdef __cplusplus } diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index cd035eda2..d2de897f1 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -9,85 +9,218 @@ #import "RCTLog.h" +#import "RCTAssert.h" #import "RCTBridge.h" +#import "RCTRedBox.h" -__unsafe_unretained NSString *RCTLogLevels[] = { - @"info", - @"warn", - @"error", - @"mustfix" +@interface RCTBridge (Logging) + ++ (void)logMessage:(NSString *)message level:(NSString *)level; + +@end + +static NSString *const RCTLogPrefixStack = @"RCTLogPrefixStack"; + +const char *RCTLogLevels[] = { + "info", + "warn", + "error", + "mustfix" }; -static void (^RCTInjectedLogFunction)(NSString *msg); +static RCTLogFunction RCTCurrentLogFunction; +static RCTLogLevel RCTCurrentLogThreshold; -void RCTInjectLogFunction(void (^logFunction)(NSString *msg)) { - RCTInjectedLogFunction = logFunction; -} - -static inline NSString *_RCTLogPreamble(const char *file, int lineNumber, const char *funcName) +void RCTLogSetup(void) __attribute__((constructor)); +void RCTLogSetup() { - NSString *threadName = [[NSThread currentThread] name]; - NSString *fileName=[[NSString stringWithUTF8String:file] lastPathComponent]; - if (!threadName || threadName.length <= 0) { - threadName = [NSString stringWithFormat:@"%p", [NSThread currentThread]]; - } - return [NSString stringWithFormat:@"[RCTLog][tid:%@][%@:%d]>", threadName, fileName, lineNumber]; -} + RCTCurrentLogFunction = RCTDefaultLogFunction; -// TODO (#5906496): Does this need to be tied to RCTBridge? -NSString *RCTLogObjects(NSArray *objects, NSString *level) -{ - NSString *str = objects[0]; -#if TARGET_IPHONE_SIMULATOR - if ([RCTBridge hasValidJSExecutor]) { - fprintf(stderr, "%s\n", [str UTF8String]); // don't print timestamps and other junk - [RCTBridge log:objects level:level]; - } else +#if DEBUG + RCTCurrentLogThreshold = RCTLogLevelInfo - 1; +#else + RCTCurrentLogThreshold = RCTLogLevelError; #endif - { - // Print normal errors with timestamps when not in simulator. - // Non errors are already compiled out above, so log as error here. - if (RCTInjectedLogFunction) { - RCTInjectedLogFunction(str); - } else { - NSLog(@">\n %@", str); - } - } - return str; + } -// Returns array of objects. First arg is a simple string to print, remaining args -// are objects to pass through to the debugger so they are inspectable in the console. -NSArray *RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) +RCTLogFunction RCTDefaultLogFunction = ^( + RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message +) { - va_list args; - va_start(args, format); - NSString *preamble = _RCTLogPreamble(file, lineNumber, funcName); + NSString *log = RCTFormatLog( + [NSDate date], [NSThread currentThread], level, fileName, lineNumber, message + ); + fprintf(stderr, "%s\n", log.UTF8String); +}; - // Pull out NSObjects so we can pass them through as inspectable objects to the js debugger - NSArray *formatParts = [format componentsSeparatedByString:@"%"]; - NSMutableArray *objects = [NSMutableArray arrayWithObject:preamble]; - BOOL valid = YES; - for (int i = 0; i < formatParts.count; i++) { - if (i == 0) { // first part is always a string - [objects addObject:formatParts[i]]; +void RCTSetLogFunction(RCTLogFunction logFunction) +{ + RCTCurrentLogFunction = logFunction; +} + +RCTLogFunction RCTGetLogFunction() +{ + return RCTCurrentLogFunction; +} + +void RCTAddLogFunction(RCTLogFunction logFunction) +{ + RCTLogFunction existing = RCTCurrentLogFunction; + if (existing) { + RCTCurrentLogFunction = ^(RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message) { + + existing(level, fileName, lineNumber, message); + logFunction(level, fileName, lineNumber, message); + }; + } else { + RCTCurrentLogFunction = logFunction; + } +} + +void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix) +{ + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + NSMutableArray *prefixStack = threadDictionary[RCTLogPrefixStack]; + if (!prefixStack) { + prefixStack = [[NSMutableArray alloc] init]; + threadDictionary[RCTLogPrefixStack] = prefixStack; + } + [prefixStack addObject:prefix]; + block(); + [prefixStack removeLastObject]; +} + +NSString *RCTFormatLog( + NSDate *timestamp, + NSThread *thread, + RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message +) +{ + NSMutableString *log = [[NSMutableString alloc] init]; + if (timestamp) { + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS "; + }); + [log appendString:[formatter stringFromDate:timestamp]]; + } + [log appendString:@"[react]"]; + if (level) { + [log appendFormat:@"[%s]", RCTLogLevels[level - 1]]; + } + if (thread) { + NSString *threadName = thread.name; + if (threadName.length == 0) { +#if DEBUG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + threadName = @(dispatch_queue_get_label(dispatch_get_current_queue())); +#pragma clang diagnostic pop +#else + threadName = [NSString stringWithFormat:@"%p", thread]; +#endif + } + [log appendFormat:@"[tid:%@]", threadName]; + } + if (fileName) { + fileName = [fileName lastPathComponent]; + if (lineNumber) { + [log appendFormat:@"[%@:%@]", fileName, lineNumber]; } else { - if (valid && [formatParts[i] length] && [formatParts[i] characterAtIndex:0] == '@') { - id obj = va_arg(args, id); - [objects addObject:obj ?: @"null"]; - [objects addObject:[formatParts[i] substringFromIndex:1]]; // remove formatting char - } else { - // We could determine the type (double, int?) of the va_arg by parsing the formatPart, but for now we just bail. - valid = NO; - [objects addObject:[NSString stringWithFormat:@"unknown object for %%%@", formatParts[i]]]; - } + [log appendFormat:@"[%@]", fileName]; } } - va_end(args); - va_start(args, format); - NSString *strOut = [preamble stringByAppendingString:[[NSString alloc] initWithFormat:format arguments:args]]; - va_end(args); - NSMutableArray *objectsOut = [NSMutableArray arrayWithObject:strOut]; - [objectsOut addObjectsFromArray:objects]; - return objectsOut; + if (message) { + [log appendString:@" "]; + [log appendString:message]; + } + return log; +} + +RCTLogFunction RCTSimpleLogFunction(void (^logFunction)(RCTLogLevel level, NSString *message)) +{ + return ^(RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message) { + + logFunction(level, RCTFormatLog( + [NSDate date], [NSThread currentThread], level, fileName, lineNumber, message + )); + }; +} + +void _RCTLogFormat(RCTLogLevel level, const char *fileName, int lineNumber, NSString *format, ...) +{ + if (RCTCurrentLogFunction && level >= RCTCurrentLogThreshold) { + + // Get message + va_list args; + va_start(args, format); + __block NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + + // Add prefix + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + NSArray *prefixStack = threadDictionary[RCTLogPrefixStack]; + NSString *prefix = [prefixStack lastObject]; + if (prefix) { + message = [prefix stringByAppendingString:message]; + } + + // Call log function + RCTCurrentLogFunction( + level, fileName ? @(fileName) : nil, (lineNumber >= 0) ? @(lineNumber) : nil, message + ); + +#if DEBUG + + // Log to red box + if (level >= RCTLOG_REDBOX_LEVEL) { + [[RCTRedBox sharedInstance] showErrorMessage:message]; + } + + // Log to JS executor + if ([RCTBridge hasValidJSExecutor]) { + [RCTBridge logMessage:message level:level ? @(RCTLogLevels[level - 1]) : @"info"]; + } + +#endif + + } +} + +#pragma mark - Deprecated + +void RCTInjectLogFunction(void (^logFunction)(NSString *msg)) +{ + RCTSetLogFunction(^(RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message) { + + if (level > RCTLogLevelError) { + + // Use custom log function + NSString *loc = fileName ? [NSString stringWithFormat:@"[%@:%@] ", fileName, lineNumber] : @""; + logFunction([loc stringByAppendingString:message]); + + } else if (RCTDefaultLogFunction && level >= RCTCurrentLogThreshold) { + + // Use default logger + RCTDefaultLogFunction(level, fileName, lineNumber, message); + } + }); } diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index b9e91b7a0..f85bb7ecb 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -17,6 +17,9 @@ extern NSString *const RCTReloadViewsNotification; @interface RCTRootView : UIView +/** + * - Designated initializer - + */ - (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName NS_DESIGNATED_INITIALIZER; @@ -39,6 +42,10 @@ extern NSString *const RCTReloadViewsNotification; */ @property (nonatomic, copy, readonly) NSString *moduleName; +/** + * The bridge used by the root view. Bridges can be shared between multiple + * root views, so you can use this property to initialize another RCTRootView. + */ @property (nonatomic, strong, readonly) RCTBridge *bridge; /** diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 9d913c048..6c15e509c 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -17,7 +17,6 @@ #import "RCTEventDispatcher.h" #import "RCTKeyCommands.h" #import "RCTLog.h" -#import "RCTRedBox.h" #import "RCTSourceCode.h" #import "RCTTouchHandler.h" #import "RCTUIManager.h" diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index e2b289f80..6c424ec7f 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -38,23 +38,18 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object, if (!string) { return JSValueMakeUndefined(context); } - - NSString *str = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string); - NSError *error = nil; + NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string); + JSStringRelease(string); NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: @"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).includeRequire.runModule.bundle(:[0-9]+:[0-9]+)" options:NSRegularExpressionCaseInsensitive - error:&error]; - NSString *modifiedString = [regex stringByReplacingMatchesInString:str options:0 range:NSMakeRange(0, [str length]) withTemplate:@"[$4$5] \t$2"]; + error:NULL]; + message = [regex stringByReplacingMatchesInString:message + options:0 + range:(NSRange){0, message.length} + withTemplate:@"[$4$5] \t$2"]; - modifiedString = [@"RCTJSLog> " stringByAppendingString:modifiedString]; -#if TARGET_IPHONE_SIMULATOR - fprintf(stderr, "%s\n", [modifiedString UTF8String]); // don't print timestamps and other junk -#else - // Print normal errors with timestamps to files when not in simulator. - RCTLogObjects(@[modifiedString], @"log"); -#endif - JSStringRelease(string); + _RCTLogFormat(0, NULL, -1, @"%@", message); } return JSValueMakeUndefined(context); diff --git a/React/Modules/RCTAlertManager.m b/React/Modules/RCTAlertManager.m index 11da8e7e7..bda7c357e 100644 --- a/React/Modules/RCTAlertManager.m +++ b/React/Modules/RCTAlertManager.m @@ -9,6 +9,7 @@ #import "RCTAlertManager.h" +#import "RCTAssert.h" #import "RCTLog.h" @interface RCTAlertManager() diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 185982fdc..81770f7de 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -197,6 +197,11 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio @synthesize bridge = _bridge; +/** + * Declared in RCTBridge. + */ +extern NSString *RCTBridgeModuleNameForClass(Class cls); + /** * This function derives the view name automatically * from the module name. @@ -334,7 +339,7 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName) dispatch_async(_bridge.shadowQueue, ^{ RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag]; - RCTAssert(rootShadowView != nil, @"Could not locate root view with tag %@", reactTag); + RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag); rootShadowView.frame = frame; [rootShadowView updateLayout]; @@ -672,7 +677,7 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName) } } -static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, RCTViewManager *manager) +static BOOL RCTCallPropertySetter(NSString *key, SEL setter, id value, id view, id defaultView, RCTViewManager *manager) { // TODO: cache respondsToSelector tests if ([manager respondsToSelector:setter]) { @@ -681,7 +686,25 @@ static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, value = nil; } - ((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView); + void (^block)() = ^{ + ((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView); + }; + +#if DEBUG + + NSString *viewName = RCTViewNameForModuleName(RCTBridgeModuleNameForClass([manager class])); + NSString *logPrefix = [NSString stringWithFormat: + @"Error setting property '%@' of %@ with tag #%@: ", + key, viewName, [view reactTag]]; + + RCTPerformBlockWithLogPrefix(block, logPrefix); + +#else + + block(); + +#endif + return YES; } return NO; @@ -693,7 +716,7 @@ static void RCTSetViewProps(NSDictionary *props, UIView *view, [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forView:withDefaultView:", key]); - RCTCallPropertySetter(setter, obj, view, defaultView, manager); + RCTCallPropertySetter(key, setter, obj, view, defaultView, manager); }]; } @@ -704,7 +727,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forShadowView:withDefaultView:", key]); - RCTCallPropertySetter(setter, obj, shadowView, defaultView, manager); + RCTCallPropertySetter(key, setter, obj, shadowView, defaultView, manager); }]; @@ -875,7 +898,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *view = viewRegistry[reactTag]; if (!view) { - RCTLogError(@"measure cannot find view with tag %@", reactTag); + RCTLogError(@"measure cannot find view with tag #%@", reactTag); return; } CGRect frame = view.frame; @@ -1039,7 +1062,7 @@ static void RCTMeasureLayout(RCTShadowView *view, uiManager.mainScrollView = (id)rkObject; ((id)rkObject).nativeMainScrollDelegate = uiManager.nativeMainScrollDelegate; } else { - RCTCAssert(NO, @"Tag %@ does not conform to RCTScrollableProtocol", reactTag); + RCTCAssert(NO, @"Tag #%@ does not conform to RCTScrollableProtocol", reactTag); } } else { uiManager.mainScrollView = nil; @@ -1056,7 +1079,7 @@ static void RCTMeasureLayout(RCTShadowView *view, if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue]) animated:YES]; } else { - RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag); + RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); } }]; } @@ -1070,7 +1093,7 @@ static void RCTMeasureLayout(RCTShadowView *view, if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue]) animated:NO]; } else { - RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag); + RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); } }]; } @@ -1084,7 +1107,7 @@ static void RCTMeasureLayout(RCTShadowView *view, if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view zoomToRect:[RCTConvert CGRect:rectDict] animated:YES]; } else { - RCTLogError(@"tried to zoomToRect: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag); + RCTLogError(@"tried to zoomToRect: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); } }]; } diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index 9c0c56f04..066d28adc 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -72,7 +72,7 @@ RCT_DEPRECATED_VIEW_PROPERTY(throttleScrollCallbackMS, scrollEventThrottle) UIView *view = viewRegistry[reactTag]; if (!view) { - RCTLogError(@"Cannot find view with tag %@", reactTag); + RCTLogError(@"Cannot find view with tag #%@", reactTag); return; }