diff --git a/detox/ios/Detox.xcodeproj/project.pbxproj b/detox/ios/Detox.xcodeproj/project.pbxproj index bde781926f..15f99d461a 100644 --- a/detox/ios/Detox.xcodeproj/project.pbxproj +++ b/detox/ios/Detox.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ 39C3C3511DBF9A13008177E1 /* EarlGrey.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 394767DC1DBF991E00D72256 /* EarlGrey.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 39C3C3531DBF9A19008177E1 /* SocketRocket.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 394767E91DBF992400D72256 /* SocketRocket.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 39CEFCDB1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CEFCDA1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift */; }; + 39FFD9471FD730A600C97030 /* DetoxCrashHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 39FFD9451FD730A600C97030 /* DetoxCrashHandler.m */; }; 468731A51E6C6D0500F151BE /* EarlGrey+Detox.h in Headers */ = {isa = PBXBuildFile; fileRef = 468731A31E6C6D0500F151BE /* EarlGrey+Detox.h */; }; 468731A61E6C6D0500F151BE /* EarlGrey+Detox.m in Sources */ = {isa = PBXBuildFile; fileRef = 468731A41E6C6D0500F151BE /* EarlGrey+Detox.m */; }; 46A6A63D1EF697BB00E3AA79 /* GREYConfiguration+Detox.m in Sources */ = {isa = PBXBuildFile; fileRef = 46A6A63B1EF697BB00E3AA79 /* GREYConfiguration+Detox.m */; }; @@ -236,6 +237,7 @@ 39A34C6F1E30F10D00BEBB59 /* DetoxAppDelegateProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DetoxAppDelegateProxy.h; sourceTree = ""; }; 39A34C701E30F10D00BEBB59 /* DetoxAppDelegateProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DetoxAppDelegateProxy.m; sourceTree = ""; }; 39CEFCDA1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetoxUserNotificationDispatcher.swift; sourceTree = ""; }; + 39FFD9451FD730A600C97030 /* DetoxCrashHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetoxCrashHandler.m; sourceTree = ""; }; 468731A31E6C6D0500F151BE /* EarlGrey+Detox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "EarlGrey+Detox.h"; sourceTree = ""; }; 468731A41E6C6D0500F151BE /* EarlGrey+Detox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "EarlGrey+Detox.m"; sourceTree = ""; }; 46A6A6341EF696B600E3AA79 /* GREYConfiguration+Detox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GREYConfiguration+Detox.h"; sourceTree = ""; }; @@ -339,6 +341,7 @@ 3947679A1DBF985400D72256 /* Detox.h */, 394767A41DBF987E00D72256 /* DetoxManager.h */, 394767A51DBF987E00D72256 /* DetoxManager.m */, + 39FFD9451FD730A600C97030 /* DetoxCrashHandler.m */, 39A34C6F1E30F10D00BEBB59 /* DetoxAppDelegateProxy.h */, 39A34C701E30F10D00BEBB59 /* DetoxAppDelegateProxy.m */, 39CEFCDA1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift */, @@ -690,6 +693,7 @@ 39CEFCDB1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift in Sources */, 394767C21DBF98A700D72256 /* GREYMatchers+Detox.m in Sources */, 394767AF1DBF987E00D72256 /* DetoxManager.m in Sources */, + 39FFD9471FD730A600C97030 /* DetoxCrashHandler.m in Sources */, 46A6A63D1EF697BB00E3AA79 /* GREYConfiguration+Detox.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/detox/ios/Detox/DetoxCrashHandler.m b/detox/ios/Detox/DetoxCrashHandler.m new file mode 100644 index 0000000000..dc445f6e4f --- /dev/null +++ b/detox/ios/Detox/DetoxCrashHandler.m @@ -0,0 +1,110 @@ +// +// DetoxCrashHandler.m +// Detox +// +// Created by Leo Natan (Wix) on 12/5/17. +// Copyright © 2017 Wix. All rights reserved. +// + +#include +@import Darwin; +@import Foundation; +#import "DetoxManager.h" +#import + +static void __DTXHandleCrash(NSException* exception, NSNumber* signal) +{ + NSNumber* threadNumber = [[NSThread currentThread] valueForKeyPath:@"private.seqNum"]; + NSString* queueName = @""; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + dispatch_queue_t currentQueue = dispatch_get_current_queue(); +#pragma clang diagnostic pop + if(currentQueue) + { + queueName = [NSString stringWithUTF8String:dispatch_queue_get_label(currentQueue)]; + } + + NSMutableDictionary* report = [@{@"threadNumber": threadNumber, @"queueName": queueName} mutableCopy]; + if(exception) + { + report[@"exceptionDetails"] = exception.debugDescription; + } + + if(signal) + { + report[@"exceptionDetails"] = [NSString stringWithFormat:@"Signal %@ was raised\n%@", signal, [NSThread callStackSymbols]]; + } + + [DetoxManager.sharedManager notifyOnCrashWithDetails:report]; + + [NSThread sleepForTimeInterval:5]; +} + +static void (*__orig_NSSetUncaughtExceptionHandler)(NSUncaughtExceptionHandler * _Nullable); +static void __dtx_NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable handler) +{} + +static void __DTXUncaughtExceptionHandler(NSException* exception) +{ + __DTXHandleCrash(exception, nil); +} + +static NSSet* __supportedSignals; + +static int (*__orig_sigaction)(int, const struct sigaction * __restrict, struct sigaction * __restrict); +static int __dtx_sigaction(int signal, const struct sigaction * __restrict newaction, struct sigaction * __restrict oldaction) +{ + if([__supportedSignals containsObject:@(signal)] == NO) + { + return __orig_sigaction(signal, newaction, oldaction); + } + + return 0; +} + +static void __DTXHandleSignal(int signal) +{ + __DTXHandleCrash(nil, @(signal)); + + exit(1); +} + +__attribute__((constructor)) +static void __DTXInstallCrashHandlers() +{ + __orig_NSSetUncaughtExceptionHandler = dlsym(RTLD_DEFAULT, "NSSetUncaughtExceptionHandler"); + + { + struct rebinding rebindings[] = { + {"NSSetUncaughtExceptionHandler", __dtx_NSSetUncaughtExceptionHandler, NULL} + }; + + rebind_symbols(rebindings, 1); + } + + __orig_NSSetUncaughtExceptionHandler(__DTXUncaughtExceptionHandler); + + __supportedSignals = [NSSet setWithArray:@[@(SIGQUIT), @(SIGILL), @(SIGTRAP), @(SIGABRT), @(SIGFPE), @(SIGBUS), @(SIGSEGV), @(SIGSYS)]]; + + __orig_sigaction = dlsym(RTLD_DEFAULT, "sigaction"); + + { + struct rebinding rebindings[] = { + {"sigaction", __dtx_sigaction, NULL} + }; + + rebind_symbols(rebindings, 1); + } + + struct sigaction signalAction; + memset(&signalAction, 0, sizeof(signalAction)); + sigemptyset(&signalAction.sa_mask); + signalAction.sa_handler = &__DTXHandleSignal; + + [__supportedSignals enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, BOOL * _Nonnull stop) { + int signum = obj.intValue; + + __orig_sigaction(signum, &signalAction, NULL); + }]; +} diff --git a/detox/ios/Detox/DetoxManager.h b/detox/ios/Detox/DetoxManager.h index ecb23608c0..0437b48307 100644 --- a/detox/ios/Detox/DetoxManager.h +++ b/detox/ios/Detox/DetoxManager.h @@ -13,7 +13,9 @@ @interface DetoxManager : NSObject -+ (instancetype)sharedInstance; -- (void) connectToServer:(NSString*)url withSessionId:(NSString*)sessionId; ++ (instancetype)sharedManager; +- (void)connectToServer:(NSString*)url withSessionId:(NSString*)sessionId; + +- (void)notifyOnCrashWithDetails:(NSDictionary*)details; @end diff --git a/detox/ios/Detox/DetoxManager.m b/detox/ios/Detox/DetoxManager.m index 1305dcaaaf..fd8b70112e 100644 --- a/detox/ios/Detox/DetoxManager.m +++ b/detox/ios/Detox/DetoxManager.m @@ -15,13 +15,14 @@ @interface DetoxManager() @property (nonatomic) BOOL isReady; -@property (nonatomic, retain) WebSocket *websocket; -@property (nonatomic, retain) TestRunner *testRunner; +@property (nonatomic, strong) WebSocket *webSocket; +@property (nonatomic, strong) TestRunner *testRunner; @end -__attribute__((constructor)) -static void detoxConditionalInit() +@implementation DetoxManager + ++ (void)load { //This forces accessibility support in the application. [[[NSUserDefaults alloc] initWithSuiteName:@"com.apple.Accessibility"] setBool:YES forKey:@"ApplicationAccessibilityEnabled"]; @@ -39,14 +40,11 @@ static void detoxConditionalInit() // if these args were not provided as part of options, don't start Detox at all! return; } - - [[DetoxManager sharedInstance] connectToServer:detoxServer withSessionId:detoxSessionId]; + + [[DetoxManager sharedManager] connectToServer:detoxServer withSessionId:detoxSessionId]; } - -@implementation DetoxManager - -+ (instancetype)sharedInstance ++ (instancetype)sharedManager { static DetoxManager *sharedInstance = nil; static dispatch_once_t onceToken; @@ -61,8 +59,8 @@ - (instancetype)init self = [super init]; if (self == nil) return nil; - self.websocket = [[WebSocket alloc] init]; - self.websocket.delegate = self; + self.webSocket = [[WebSocket alloc] init]; + self.webSocket.delegate = self; self.testRunner = [[TestRunner alloc] init]; self.testRunner.delegate = self; @@ -74,41 +72,41 @@ - (instancetype)init return self; } -- (void) connectToServer:(NSString*)url withSessionId:(NSString*)sessionId +- (void)connectToServer:(NSString*)url withSessionId:(NSString*)sessionId { - [self.websocket connectToServer:url withSessionId:sessionId]; + [self.webSocket connectToServer:url withSessionId:sessionId]; } -- (void) websocketDidConnect +- (void)websocketDidConnect { if (![ReactNativeSupport isReactNativeApp]) { _isReady = YES; - [self.websocket sendAction:@"ready" withParams:@{} withMessageId: @-1000]; + [self.webSocket sendAction:@"ready" withParams:@{} withMessageId:@-1000]; } } -- (void) websocketDidReceiveAction:(NSString *)type withParams:(NSDictionary *)params withMessageId:(NSNumber *)messageId +- (void)websocketDidReceiveAction:(NSString *)type withParams:(NSDictionary *)params withMessageId:(NSNumber *)messageId { NSAssert(messageId != nil, @"Got action with a null messageId"); if([type isEqualToString:@"invoke"]) { - [self.testRunner invoke:params withMessageId: messageId]; + [self.testRunner invoke:params withMessageId:messageId]; return; } else if([type isEqualToString:@"isReady"]) { if(_isReady) { - [self.websocket sendAction:@"ready" withParams:@{} withMessageId: @-1000]; + [self.webSocket sendAction:@"ready" withParams:@{} withMessageId:@-1000]; } return; } else if([type isEqualToString:@"cleanup"]) { [self.testRunner cleanup]; - [self.websocket sendAction:@"cleanupDone" withParams:@{} withMessageId: messageId]; + [self.webSocket sendAction:@"cleanupDone" withParams:@{} withMessageId:messageId]; return; } else if([type isEqualToString:@"userNotification"]) @@ -116,7 +114,7 @@ - (void) websocketDidReceiveAction:(NSString *)type withParams:(NSDictionary *)p NSURL* userNotificationDataURL = [NSURL fileURLWithPath:params[@"detoxUserNotificationDataURL"]]; DetoxUserNotificationDispatcher* dispatcher = [[DetoxUserNotificationDispatcher alloc] initWithUserNotificationDataURL:userNotificationDataURL]; [dispatcher dispatchOnAppDelegate:DetoxAppDelegateProxy.currentAppDelegateProxy simulateDuringLaunch:NO]; - [self.websocket sendAction:@"userNotificationDone" withParams:@{} withMessageId: messageId]; + [self.webSocket sendAction:@"userNotificationDone" withParams:@{} withMessageId:messageId]; } else if([type isEqualToString:@"openURL"]) { @@ -137,7 +135,7 @@ - (void) websocketDidReceiveAction:(NSString *)type withParams:(NSDictionary *)p [[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] openURL:URLToOpen options:options]; } - [self.websocket sendAction:@"openURLDone" withParams:@{} withMessageId: messageId]; + [self.webSocket sendAction:@"openURLDone" withParams:@{} withMessageId:messageId]; } else if([type isEqualToString:@"reactNativeReload"]) { @@ -153,7 +151,7 @@ - (void) websocketDidReceiveAction:(NSString *)type withParams:(NSDictionary *)p NSMutableDictionary* statsStatus = [[[EarlGreyStatistics sharedInstance] currentStatus] mutableCopy]; statsStatus[@"messageId"] = messageId; - [self.websocket sendAction:@"currentStatusResult" withParams:statsStatus withMessageId: messageId]; + [self.webSocket sendAction:@"currentStatusResult" withParams:statsStatus withMessageId:messageId]; } } @@ -162,7 +160,7 @@ - (void)_waitForRNLoadWithId:(id)messageId __weak __typeof(self) weakSelf = self; [ReactNativeSupport waitForReactNativeLoadWithCompletionHandler:^{ weakSelf.isReady = YES; - [weakSelf.websocket sendAction:@"ready" withParams:@{} withMessageId: @-1000]; + [weakSelf.webSocket sendAction:@"ready" withParams:@{} withMessageId:@-1000]; }]; } @@ -173,19 +171,24 @@ - (void)testRunnerOnInvokeResult:(id)res withMessageId:(NSNumber *)messageId { res = [NSString stringWithFormat:@"(%@)", NSStringFromClass([res class])]; } - [self.websocket sendAction:@"invokeResult" withParams:@{@"result": res} withMessageId: messageId]; + [self.webSocket sendAction:@"invokeResult" withParams:@{@"result": res} withMessageId:messageId]; } - (void)testRunnerOnTestFailed:(NSString *)details withMessageId:(NSNumber *) messageId { if (details == nil) details = @""; - [self.websocket sendAction:@"testFailed" withParams:@{@"details": details} withMessageId: messageId]; + [self.webSocket sendAction:@"testFailed" withParams:@{@"details": details} withMessageId:messageId]; } - (void)testRunnerOnError:(NSString *)error withMessageId:(NSNumber *) messageId { if (error == nil) error = @""; - [self.websocket sendAction:@"error" withParams:@{@"error": error} withMessageId: messageId]; + [self.webSocket sendAction:@"error" withParams:@{@"error": error} withMessageId:messageId]; +} + +- (void)notifyOnCrashWithDetails:(NSDictionary*)details +{ + [self.webSocket sendAction:@"AppWillTerminateWithError" withParams:details withMessageId:@-10000]; } @end diff --git a/detox/ios/Detox/TestRunner.h b/detox/ios/Detox/TestRunner.h index 9c0397668e..2c159bea42 100644 --- a/detox/ios/Detox/TestRunner.h +++ b/detox/ios/Detox/TestRunner.h @@ -21,7 +21,7 @@ @property (nonatomic, assign) id delegate; -- (void) invoke:(NSDictionary*)params withMessageId:(NSNumber*) messageId; -- (void) cleanup; +- (void)invoke:(NSDictionary*)params withMessageId:(NSNumber*) messageId; +- (void)cleanup; @end diff --git a/detox/ios/Detox/TestRunner.m b/detox/ios/Detox/TestRunner.m index 484cc242de..8dd21e1a48 100644 --- a/detox/ios/Detox/TestRunner.m +++ b/detox/ios/Detox/TestRunner.m @@ -20,14 +20,14 @@ @interface TestRunner() @implementation TestRunner -- (void) initEarlGrey +- (void)initEarlGrey { [EarlGrey setFailureHandler:self.failureHandler]; [[GREYConfiguration sharedInstance] setValue:@(NO) forConfigKey:kGREYConfigKeyAnalyticsEnabled]; //[[GREYConfiguration sharedInstance] setValue:@".*localhost.*" forConfigKey:kGREYConfigKeyURLBlacklistRegex]; } -- (void) cleanupEarlGrey +- (void)cleanupEarlGrey { // this triggers grey_tearDown in GREYAutomationSetup [[NSNotificationCenter defaultCenter] postNotificationName:@"GREYXCTestCaseInstanceDidFinish" @@ -48,7 +48,7 @@ - (instancetype)init return self; } -- (void) cleanup +- (void)cleanup { [self cleanupEarlGrey]; } @@ -61,7 +61,7 @@ - (void)onTestFailed:(NSString *)details { } } -- (void) invoke:(NSDictionary*)params withMessageId: (NSNumber *)messageId +- (void)invoke:(NSDictionary*)params withMessageId:(NSNumber *)messageId { self.currentMessageId = messageId; grey_execute_async(^{ diff --git a/detox/ios/Detox/WebSocket.h b/detox/ios/Detox/WebSocket.h index 4a074161c6..779bd4cd03 100644 --- a/detox/ios/Detox/WebSocket.h +++ b/detox/ios/Detox/WebSocket.h @@ -21,7 +21,7 @@ @interface WebSocket : NSObject @property (nonatomic, assign) id delegate; -- (void) connectToServer:(NSString*)url withSessionId:(NSString*)sessionId; -- (void) sendAction:(NSString*)type withParams:(NSDictionary*)params withMessageId:(NSNumber*)messageId; +- (void)connectToServer:(NSString*)url withSessionId:(NSString*)sessionId; +- (void)sendAction:(NSString*)type withParams:(NSDictionary*)params withMessageId:(NSNumber*)messageId; @end diff --git a/detox/ios/Detox/WebSocket.m b/detox/ios/Detox/WebSocket.m index 72afed27d3..379f34e405 100644 --- a/detox/ios/Detox/WebSocket.m +++ b/detox/ios/Detox/WebSocket.m @@ -18,7 +18,7 @@ @interface WebSocket() @implementation WebSocket -- (void) connectToServer:(NSString*)url withSessionId:(NSString*)sessionId +- (void)connectToServer:(NSString*)url withSessionId:(NSString*)sessionId { if (self.websocket) { @@ -31,7 +31,7 @@ - (void) connectToServer:(NSString*)url withSessionId:(NSString*)sessionId [self.websocket open]; } -- (void) sendAction:(NSString*)type withParams:(NSDictionary*)params withMessageId:(NSNumber*)messageId +- (void)sendAction:(NSString*)type withParams:(NSDictionary*)params withMessageId:(NSNumber*)messageId { NSDictionary *data = @{@"type": type, @"params": params, @"messageId": messageId}; NSError *error; @@ -46,7 +46,7 @@ - (void) sendAction:(NSString*)type withParams:(NSDictionary*)params withMessage [self.websocket sendString:json error:NULL]; } -- (void) receiveAction:(NSString*)json +- (void)receiveAction:(NSString*)json { NSError *error; NSData *jsonData = [json dataUsingEncoding:NSUTF8StringEncoding];