Improved logging and dev menu

This commit is contained in:
Nick Lockwood 2015-04-19 12:55:46 -07:00
parent 2186691812
commit 0b21df4a34
11 changed files with 306 additions and 139 deletions

View File

@ -264,7 +264,9 @@ RCT_EXPORT_METHOD(stopAnimation:(NSNumber *)animationTag)
RCTAnimationExperimentalManager *strongSelf = weakSelf;
NSNumber *reactTag = strongSelf->_animationRegistry[animationTag];
if (!reactTag) return;
if (!reactTag) {
return;
}
UIView *view = viewRegistry[reactTag];
for (NSString *animationKey in view.layer.animationKeys) {

View File

@ -9,11 +9,50 @@
#import <UIKit/UIKit.h>
@class RCTBridge;
#import "RCTBridge.h"
#import "RCTBridgeModule.h"
#import "RCTInvalidating.h"
@interface RCTDevMenu : NSObject
/**
* Developer menu, useful for exposing extra functionality when debugging.
*/
@interface RCTDevMenu : NSObject <RCTBridgeModule, RCTInvalidating>
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
/**
* Is the menu enabled. The menu is enabled by default in debug mode, but
* you may wish to disable it so that you can provide your own shake handler.
*/
@property (nonatomic, assign) BOOL shakeToShow;
/**
* Enables performance profiling.
*/
@property (nonatomic, assign) BOOL profilingEnabled;
/**
* Enables automatic polling for JS code changes. Only applicable when
* running the app from a server.
*/
@property (nonatomic, assign) BOOL liveReloadEnabled;
/**
* The time between checks for code changes. Defaults to 1 second.
*/
@property (nonatomic, assign) NSTimeInterval liveReloadPeriod;
/**
* Manually show the menu. This will.
*/
- (void)show;
@end
/**
* This category makes the developer menu instance available via the
* RCTBridge, which is useful for any class that needs to access the menu.
*/
@interface RCTBridge (RCTDevMenu)
@property (nonatomic, readonly) RCTDevMenu *devMenu;
@end

View File

@ -9,12 +9,13 @@
#import "RCTDevMenu.h"
#import "RCTRedBox.h"
#import "RCTBridge.h"
#import "RCTLog.h"
#import "RCTRootView.h"
#import "RCTSourceCode.h"
#import "RCTWebViewExecutor.h"
#import "RCTUtils.h"
@interface RCTBridge (RCTDevMenu)
@interface RCTBridge (Profiling)
@property (nonatomic, copy, readonly) NSArray *profile;
@ -23,87 +24,206 @@
@end
static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
@implementation UIWindow (RCTDevMenu)
- (void)RCT_motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (event.subtype == UIEventSubtypeMotionShake) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
}
}
@end
@interface RCTDevMenu () <UIActionSheetDelegate>
@end
@implementation RCTDevMenu
{
BOOL _liveReload;
__weak RCTBridge *_bridge;
NSTimer *_updateTimer;
UIActionSheet *_actionSheet;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
+ (void)initialize
{
if (self = [super init]) {
_bridge = bridge;
// We're swizzling here because it's poor form to override methods in a category,
// however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
// no need to call the original implementation.
RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
}
- (instancetype)init
{
if ((self = [super init])) {
_shakeToShow = YES;
_liveReloadPeriod = 1.0; // 1 second
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(showOnShake)
name:RCTShowDevMenuNotification
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)showOnShake
{
if (_shakeToShow) {
[self show];
}
}
- (void)show
{
NSString *debugTitleChrome = _bridge.executorClass != Nil && _bridge.executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Enable Chrome Debugging";
NSString *debugTitleSafari = _bridge.executorClass == [RCTWebViewExecutor class] ? @"Disable Safari Debugging" : @"Enable Safari Debugging";
NSString *liveReloadTitle = _liveReload ? @"Disable Live Reload" : @"Enable Live Reload";
if (_actionSheet) {
return;
}
NSString *debugTitleChrome = _bridge.executorClass && _bridge.executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Enable Chrome Debugging";
NSString *debugTitleSafari = _bridge.executorClass && _bridge.executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Enable Safari Debugging";
NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload";
NSString *profilingTitle = _bridge.profile ? @"Stop Profiling" : @"Start Profiling";
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development"
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:nil
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, profilingTitle, nil];
UIActionSheet *actionSheet =
[[UIActionSheet alloc] initWithTitle:@"React Native: Development"
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:nil
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, profilingTitle, nil];
actionSheet.actionSheetStyle = UIBarStyleBlack;
[actionSheet showInView:[[[[UIApplication sharedApplication] keyWindow] rootViewController] view]];
[actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view];
}
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0) {
[_bridge reload];
} else if (buttonIndex == 1) {
Class cls = NSClassFromString(@"RCTWebSocketExecutor");
_bridge.executorClass = (_bridge.executorClass != cls) ? cls : nil;
[_bridge reload];
} else if (buttonIndex == 2) {
Class cls = [RCTWebViewExecutor class];
_bridge.executorClass = (_bridge.executorClass != cls) ? cls : Nil;
[_bridge reload];
} else if (buttonIndex == 3) {
_liveReload = !_liveReload;
[self _pollAndReload];
} else if (buttonIndex == 4) {
if (_bridge.profile) {
[_bridge stopProfiling];
} else {
[_bridge startProfiling];
}
}
}
_actionSheet = nil;
- (void)_pollAndReload
{
if (_liveReload) {
RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
NSURL *url = sourceCodeModule.scriptURL;
NSURL *longPollURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:url];
[self performSelectorInBackground:@selector(_checkForUpdates:) withObject:longPollURL];
}
}
- (void)_checkForUpdates:(NSURL *)URL
{
NSMutableURLRequest *longPollRequest = [NSMutableURLRequest requestWithURL:URL];
longPollRequest.timeoutInterval = 30;
NSHTTPURLResponse *response;
[NSURLConnection sendSynchronousRequest:longPollRequest returningResponse:&response error:nil];
dispatch_async(dispatch_get_main_queue(), ^{
if (_liveReload && response.statusCode == 205) {
[[RCTRedBox sharedInstance] dismiss];
switch (buttonIndex) {
case 0: {
[_bridge reload];
break;
}
[self _pollAndReload];
});
case 1: {
Class cls = NSClassFromString(@"RCTWebSocketExecutor");
_bridge.executorClass = (_bridge.executorClass != cls) ? cls : nil;
[_bridge reload];
break;
}
case 2: {
Class cls = NSClassFromString(@"RCTWebViewExecutor");
_bridge.executorClass = (_bridge.executorClass != cls) ? cls : Nil;
[_bridge reload];
break;
}
case 3: {
self.liveReloadEnabled = !_liveReloadEnabled;
break;
}
case 4: {
self.profilingEnabled = !_profilingEnabled;
break;
}
default:
break;
}
}
- (void)setProfilingEnabled:(BOOL)enabled
{
if (_profilingEnabled == enabled) {
return;
}
_profilingEnabled = enabled;
if (_bridge.profile) {
[_bridge stopProfiling];
} else {
[_bridge startProfiling];
}
}
- (void)setLiveReloadEnabled:(BOOL)enabled
{
if (_liveReloadEnabled == enabled) {
return;
}
_liveReloadEnabled = enabled;
if (_liveReloadEnabled) {
_updateTimer = [NSTimer scheduledTimerWithTimeInterval:_liveReloadPeriod
target:self
selector:@selector(pollForUpdates)
userInfo:nil
repeats:YES];
} else {
[_updateTimer invalidate];
_updateTimer = nil;
}
}
- (void)setLiveReloadPeriod:(NSTimeInterval)liveReloadPeriod
{
_liveReloadPeriod = liveReloadPeriod;
if (_liveReloadEnabled) {
self.liveReloadEnabled = NO;
self.liveReloadEnabled = YES;
}
}
- (void)pollForUpdates
{
RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
if (!sourceCodeModule) {
RCTLogError(@"RCTSourceCode module not found");
self.liveReloadEnabled = NO;
}
NSURL *longPollURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:sourceCodeModule.scriptURL];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:longPollURL]
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
if (_liveReloadEnabled && HTTPResponse.statusCode == 205) {
[_bridge reload];
}
}];
}
- (BOOL)isValid
{
return !_liveReloadEnabled || _updateTimer != nil;
}
- (void)invalidate
{
[_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES];
[_updateTimer invalidate];
_updateTimer = nil;
}
@end
@implementation RCTBridge (RCTDevMenu)
- (RCTDevMenu *)devMenu
{
return self.modules[RCTBridgeModuleNameForClass([RCTDevMenu class])];
}
@end

View File

@ -1,4 +1,11 @@
// Copyright 2004-present Facebook. All Rights Reserved.
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <UIKit/UIKit.h>

View File

@ -1,4 +1,11 @@
// Copyright 2004-present Facebook. All Rights Reserved.
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTJavaScriptLoader.h"

View File

@ -31,8 +31,8 @@ const char *RCTLogLevels[] = {
static RCTLogFunction RCTCurrentLogFunction;
static RCTLogLevel RCTCurrentLogThreshold;
void RCTLogSetup(void) __attribute__((constructor));
void RCTLogSetup()
__attribute__((constructor))
static void RCTLogSetup()
{
RCTCurrentLogFunction = RCTDefaultLogFunction;

View File

@ -57,12 +57,6 @@
*/
@property (nonatomic, strong) Class executorClass;
/**
* If YES will watch for shake gestures and show development menu
* with options like "Reload", "Enable Debugging", etc.
*/
@property (nonatomic, assign) BOOL enableDevMenu;
/**
* The backing view controller of the root view.
*/

View File

@ -13,7 +13,6 @@
#import "RCTBridge.h"
#import "RCTContextExecutor.h"
#import "RCTDevMenu.h"
#import "RCTEventDispatcher.h"
#import "RCTKeyCommands.h"
#import "RCTLog.h"
@ -42,7 +41,6 @@
@implementation RCTRootView
{
RCTDevMenu *_devMenu;
RCTBridge *_bridge;
RCTTouchHandler *_touchHandler;
NSString *_moduleName;
@ -60,12 +58,6 @@
self.backgroundColor = [UIColor whiteColor];
#ifdef DEBUG
_enableDevMenu = YES;
#endif
_bridge = bridge;
_moduleName = moduleName;
@ -120,18 +112,6 @@
return YES;
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake && self.enableDevMenu) {
if (!_devMenu) {
_devMenu = [[RCTDevMenu alloc] initWithBridge:_bridge];
}
[_devMenu show];
} else {
[super motionEnded:motion withEvent:event];
}
}
RCT_IMPORT_METHOD(AppRegistry, runApplication)
RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer)

View File

@ -85,7 +85,14 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object,
range:(NSRange){0, message.length}
withTemplate:@"[$4$5] \t$2"];
_RCTLogFormat(RCTLogLevelInfo, NULL, -1, @"%@", message);
// TODO: it would be good if log level was sent as a param, instead of this hack
RCTLogLevel level = RCTLogLevelInfo;
if ([message rangeOfString:@"error" options:NSCaseInsensitiveSearch].length) {
level = RCTLogLevelError;
} else if ([message rangeOfString:@"warning" options:NSCaseInsensitiveSearch].length) {
level = RCTLogLevelWarning;
}
_RCTLogFormat(level, NULL, -1, @"%@", message);
}
return JSValueMakeUndefined(context);
@ -126,8 +133,6 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
+ (void)runRunLoopThread
{
// TODO (#5906496): Investigate exactly what this does and why
@autoreleasepool {
// copy thread name to pthread name
pthread_setname_np([[[NSThread currentThread] name] UTF8String]);
@ -273,11 +278,11 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
}
- (void)executeApplicationScript:(NSString *)script
sourceURL:(NSURL *)url
sourceURL:(NSURL *)sourceURL
onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
RCTAssert(url != nil, @"url should not be nil");
RCTAssert(onComplete != nil, @"onComplete block should not be nil");
RCTAssert(sourceURL != nil, @"url should not be nil");
__weak RCTContextExecutor *weakSelf = self;
[self executeBlockOnJavaScriptQueue:^{
RCTContextExecutor *strongSelf = weakSelf;
@ -286,17 +291,18 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
}
JSValueRef jsError = NULL;
JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
JSStringRef sourceURL = JSStringCreateWithCFString((__bridge CFStringRef)url.absoluteString);
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, sourceURL, 0, &jsError);
JSStringRelease(sourceURL);
JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString);
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError);
JSStringRelease(jsURL);
JSStringRelease(execJSString);
NSError *error;
if (!result) {
error = RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError);
if (onComplete) {
NSError *error;
if (!result) {
error = RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError);
}
onComplete(error);
}
onComplete(error);
}];
}
@ -314,7 +320,7 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
asGlobalObjectNamed:(NSString *)objectName
callback:(RCTJavaScriptCompleteBlock)onComplete
{
RCTAssert(onComplete != nil, @"onComplete block should not be nil");
#if DEBUG
RCTAssert(RCTJSONParse(script, NULL) != nil, @"%@ wasn't valid JSON!", script);
#endif
@ -333,19 +339,21 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
NSString *errorDesc = [NSString stringWithFormat:@"Can't make JSON value from script '%@'", script];
RCTLogError(@"%@", errorDesc);
NSError *error = [NSError errorWithDomain:@"JS" code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}];
onComplete(error);
if (onComplete) {
NSError *error = [NSError errorWithDomain:@"JS" code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}];
onComplete(error);
}
return;
}
JSObjectRef globalObject = JSContextGetGlobalObject(strongSelf->_context.ctx);
JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)objectName);
JSObjectSetProperty(strongSelf->_context.ctx, globalObject, JSName, valueToInject, kJSPropertyAttributeNone, NULL);
JSStringRelease(JSName);
onComplete(nil);
if (onComplete) {
onComplete(nil);
}
}];
}
@end

View File

@ -19,10 +19,6 @@
NSUInteger _reloadRetries;
}
#ifndef DEBUG
static NSUInteger RCTReloadRetries = 0;
#endif
RCT_EXPORT_MODULE()
- (instancetype)initWithDelegate:(id<RCTExceptionsManagerDelegate>)delegate
@ -47,27 +43,32 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message
return;
}
#ifdef DEBUG
#if DEBUG
[[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack];
#else
if (RCTReloadRetries < _maxReloadAttempts) {
RCTReloadRetries++;
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil];
static NSUInteger reloadRetries = 0;
const NSUInteger maxMessageLength = 75;
if (reloadRetries < _maxReloadAttempts) {
reloadRetries++;
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification
object:nil];
} else {
NSError *error;
const NSUInteger MAX_SANITIZED_LENGTH = 75;
// Filter out numbers so the same base errors are mapped to the same categories independent of incorrect values.
NSString *pattern = @"[+-]?\\d+[,.]?\\d*";
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error];
RCTAssert(error == nil, @"Bad regex pattern: %@", pattern);
NSString *sanitizedMessage = [regex stringByReplacingMatchesInString:message
options:0
range:NSMakeRange(0, message.length)
withTemplate:@"<num>"];
if (sanitizedMessage.length > MAX_SANITIZED_LENGTH) {
sanitizedMessage = [[sanitizedMessage substringToIndex:MAX_SANITIZED_LENGTH] stringByAppendingString:@"..."];
NSString *sanitizedMessage = [message stringByReplacingOccurrencesOfString:pattern withString:@"<num>" options:NSRegularExpressionSearch range:(NSRange){0, message.length}];
if (sanitizedMessage.length > maxMessageLength) {
sanitizedMessage = [[sanitizedMessage substringToIndex:maxMessageLength] stringByAppendingString:@"..."];
}
NSMutableString *prettyStack = [@"\n" mutableCopy];
NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"];
for (NSDictionary *frame in stack) {
[prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]];
}
@ -75,13 +76,21 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message
NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:sanitizedMessage];
[NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack];
}
#endif
}
RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message
stack:(NSArray *)stack)
{
#if DEBUG
[[RCTRedBox sharedInstance] updateErrorMessage:message withStack:stack];
#endif
}
@end

View File

@ -999,7 +999,7 @@ RCT_EXPORT_METHOD(measureLayoutRelativeToParent:(NSNumber *)reactTag
* Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the
* passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts.
*/
RCT_EXPORT_METHOD(measureViewsInRect:(NSDictionary *)rect
RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect
parentView:(NSNumber *)reactTag
errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback)
@ -1011,7 +1011,7 @@ RCT_EXPORT_METHOD(measureViewsInRect:(NSDictionary *)rect
}
NSArray *childShadowViews = [shadowView reactSubviews];
NSMutableArray *results = [[NSMutableArray alloc] initWithCapacity:[childShadowViews count]];
CGRect layoutRect = [RCTConvert CGRect:rect];
[childShadowViews enumerateObjectsUsingBlock:^(RCTShadowView *childShadowView, NSUInteger idx, BOOL *stop) {
CGRect childLayout = [childShadowView measureLayoutRelativeToAncestor:shadowView];
@ -1026,10 +1026,11 @@ RCT_EXPORT_METHOD(measureViewsInRect:(NSDictionary *)rect
CGFloat width = childLayout.size.width;
CGFloat height = childLayout.size.height;
if (leftOffset <= layoutRect.origin.x + layoutRect.size.width &&
leftOffset + width >= layoutRect.origin.x &&
topOffset <= layoutRect.origin.y + layoutRect.size.height &&
topOffset + height >= layoutRect.origin.y) {
if (leftOffset <= rect.origin.x + rect.size.width &&
leftOffset + width >= rect.origin.x &&
topOffset <= rect.origin.y + rect.size.height &&
topOffset + height >= rect.origin.y) {
// This view is within the layout rect
NSDictionary *result = @{@"index": @(idx),
@"left": @(leftOffset),