Skip to content

Commit

Permalink
Add exception and signal handling for iOS (#453)
Browse files Browse the repository at this point in the history
* Add exception and signal handling for iOS

First half of #161

* Move exception handling to lower level C++ API to catch C++ exceptions as well as ObjC exceptions

* Update DetoxCrashHandler.mm

* Handling native crashes gracefully

* fixing tests

* Fixed e2e
  • Loading branch information
LeoNatan authored and rotemmiz committed Feb 8, 2018
1 parent 44589fa commit a2bf0f0
Show file tree
Hide file tree
Showing 17 changed files with 366 additions and 44 deletions.
6 changes: 6 additions & 0 deletions detox/ios/Detox.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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.mm in Sources */ = {isa = PBXBuildFile; fileRef = 39FFD9451FD730A600C97030 /* DetoxCrashHandler.mm */; };
39F642281FDD5EB100468FED /* DTXLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 39F642201FDD5EB000468FED /* DTXLogging.h */; };
39F642291FDD5EB100468FED /* DTXLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = 39F642271FDD5EB000468FED /* DTXLogging.m */; };
468731A51E6C6D0500F151BE /* EarlGrey+Detox.h in Headers */ = {isa = PBXBuildFile; fileRef = 468731A31E6C6D0500F151BE /* EarlGrey+Detox.h */; };
Expand Down Expand Up @@ -239,6 +240,7 @@
39A34C6F1E30F10D00BEBB59 /* DetoxAppDelegateProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DetoxAppDelegateProxy.h; sourceTree = "<group>"; };
39A34C701E30F10D00BEBB59 /* DetoxAppDelegateProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DetoxAppDelegateProxy.m; sourceTree = "<group>"; };
39CEFCDA1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetoxUserNotificationDispatcher.swift; sourceTree = "<group>"; };
39FFD9451FD730A600C97030 /* DetoxCrashHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = DetoxCrashHandler.mm; sourceTree = "<group>"; };
39F642201FDD5EB000468FED /* DTXLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DTXLogging.h; path = DTXLoggingInfra/DTXLogging.h; sourceTree = SOURCE_ROOT; };
39F642271FDD5EB000468FED /* DTXLogging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DTXLogging.m; path = DTXLoggingInfra/DTXLogging.m; sourceTree = SOURCE_ROOT; };
39F6422A1FDD5EEC00468FED /* Detox.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Detox.pch; sourceTree = "<group>"; };
Expand Down Expand Up @@ -358,6 +360,7 @@
3947679A1DBF985400D72256 /* Detox.h */,
394767A41DBF987E00D72256 /* DetoxManager.h */,
394767A51DBF987E00D72256 /* DetoxManager.m */,
39FFD9451FD730A600C97030 /* DetoxCrashHandler.mm */,
39A34C6F1E30F10D00BEBB59 /* DetoxAppDelegateProxy.h */,
39A34C701E30F10D00BEBB59 /* DetoxAppDelegateProxy.m */,
39CEFCDA1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift */,
Expand Down Expand Up @@ -712,6 +715,7 @@
39CEFCDB1E34E91B00A09124 /* DetoxUserNotificationDispatcher.swift in Sources */,
394767C21DBF98A700D72256 /* GREYMatchers+Detox.m in Sources */,
394767AF1DBF987E00D72256 /* DetoxManager.m in Sources */,
39FFD9471FD730A600C97030 /* DetoxCrashHandler.mm in Sources */,
46A6A63D1EF697BB00E3AA79 /* GREYConfiguration+Detox.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -924,6 +928,7 @@
OTHER_LDFLAGS = (
"-ObjC",
"-all_load",
"-lstdc++",
);
PRODUCT_BUNDLE_IDENTIFIER = com.wix.Detox;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -954,6 +959,7 @@
OTHER_LDFLAGS = (
"-ObjC",
"-all_load",
"-lstdc++",
);
PRODUCT_BUNDLE_IDENTIFIER = com.wix.Detox;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
153 changes: 153 additions & 0 deletions detox/ios/Detox/DetoxCrashHandler.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// DetoxCrashHandler.mm
// Detox
//
// Created by Leo Natan (Wix) on 12/5/17.
// Copyright © 2017 Wix. All rights reserved.
//

#include <fishhook.h>
#import <dlfcn.h>
#import <Foundation/Foundation.h>
#import "DetoxManager.h"
#import <Detox/Detox-Swift.h>

#include <cstdlib>
#include <exception>
#include <typeinfo>
#include <cxxabi.h>

static void __DTXHandleCrash(NSException* exception, NSNumber* signal, NSString* other)
{
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[@"errorDetails"] = exception.debugDescription;
}
else if(signal)
{
report[@"errorDetails"] = [NSString stringWithFormat:@"Signal %@ was raised\n%@", signal, [NSThread callStackSymbols]];
}
else if(other)
{
report[@"errorDetails"] = other;
}

[DetoxManager.sharedManager notifyOnCrashWithDetails:report];

[NSThread sleepForTimeInterval:5];
}

static NSSet<NSNumber*>* __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), nil);

exit(1);
}

OBJC_EXTERN std::type_info *__cxa_current_exception_type(void);
OBJC_EXTERN void __cxa_rethrow(void);

static void (*__old_terminate)(void) = nil;
static void __dtx_terminate(void)
{
std::type_info* exceptionType = __cxa_current_exception_type();
if (exceptionType == nullptr)
{
// No current exception.
__DTXHandleCrash(nil, nil, @"Unknown error");
(*__old_terminate)();
}
else
{
// There is a current exception. Check if it's an objc exception.
@try
{
__cxa_rethrow();
}
@catch (id e)
{
__DTXHandleCrash(e, nil, nil);
// It's an objc object. Call Foundation's handler, if any.
void (*handler)(NSException*) = NSGetUncaughtExceptionHandler();
if(handler != nullptr)
{
handler(e);
}
}
@catch (...)
{
const char* exceptionTypeMangledName = exceptionType->name();

int status = -1;
const char* demangled = abi::__cxa_demangle(exceptionTypeMangledName, NULL, NULL, &status);
NSString* exceptionTypeName = nil;
if(demangled)
{
exceptionTypeName = [NSString stringWithUTF8String:demangled];
free((void*)demangled);
}
else
{
exceptionTypeName = [NSString stringWithUTF8String:exceptionTypeMangledName];
}

__DTXHandleCrash(nil, nil, [NSString stringWithFormat:@"C++ exception of type \"%@\" was thrown", exceptionTypeName]);
// It's not an objc object. Continue to C++ terminate.
(*__old_terminate)();
}
}
}

__attribute__((constructor))
static void __DTXInstallCrashHandlers()
{
__old_terminate = std::set_terminate(__dtx_terminate);

__supportedSignals = [NSSet setWithArray:@[@(SIGQUIT), @(SIGILL), @(SIGTRAP), @(SIGABRT), @(SIGFPE), @(SIGBUS), @(SIGSEGV), @(SIGSYS)]];

__orig_sigaction = (int (*)(int, const struct sigaction * __restrict, struct sigaction * __restrict))dlsym(RTLD_DEFAULT, "sigaction");

{
struct rebinding rebindings[] = {
{"sigaction", (void*)__dtx_sigaction, nullptr}
};

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, nullptr);
}];
}
9 changes: 4 additions & 5 deletions detox/ios/Detox/DetoxManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
//

#import <Foundation/Foundation.h>
#import "WebSocket.h"
#import "TestRunner.h"
#import "ReactNativeSupport.h"

@interface DetoxManager : NSObject<WebSocketDelegate, TestRunnerDelegate>
@interface DetoxManager : NSObject

+ (instancetype)sharedInstance;
+ (instancetype)sharedManager;
- (void)connectToServer:(NSString*)url withSessionId:(NSString*)sessionId;

- (void)notifyOnCrashWithDetails:(NSDictionary*)details;

@end
55 changes: 31 additions & 24 deletions detox/ios/Detox/DetoxManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@
//

#import "DetoxManager.h"

#import "WebSocket.h"
#import "TestRunner.h"
#import "ReactNativeSupport.h"

#import <Detox/Detox-Swift.h>
#import "DetoxAppDelegateProxy.h"
#import "EarlGreyExtensions.h"
#import "EarlGreyStatistics.h"

DTX_CREATE_LOG(DetoxManager)

@interface DetoxManager()
@interface DetoxManager() <WebSocketDelegate, TestRunnerDelegate>

@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

Expand All @@ -41,14 +46,13 @@ 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;
Expand All @@ -63,8 +67,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;

Expand All @@ -78,15 +82,15 @@ - (instancetype)init

- (void)connectToServer:(NSString*)url withSessionId:(NSString*)sessionId
{
[self.websocket connectToServer:url withSessionId:sessionId];
[self.webSocket connectToServer:url withSessionId:sessionId];
}

- (void)websocketDidConnect
{
if (![ReactNativeSupport isReactNativeApp])
{
_isReady = YES;
[self.websocket sendAction:@"ready" withParams:@{} withMessageId: @-1000];
[self.webSocket sendAction:@"ready" withParams:@{} withMessageId:@-1000];
}
}

Expand All @@ -96,21 +100,21 @@ - (void)websocketDidReceiveAction:(NSString *)type withParams:(NSDictionary *)pa

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"])
Expand All @@ -119,7 +123,7 @@ - (void)websocketDidReceiveAction:(NSString *)type withParams:(NSDictionary *)pa
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"])
Expand All @@ -142,13 +146,11 @@ - (void)websocketDidReceiveAction:(NSString *)type withParams:(NSDictionary *)pa
[[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:@"shakeDevice"])
{

}
{ }
else if([type isEqualToString:@"reactNativeReload"])
{
_isReady = NO;
Expand All @@ -165,7 +167,7 @@ - (void)websocketDidReceiveAction:(NSString *)type withParams:(NSDictionary *)pa
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];
}
}

Expand All @@ -174,7 +176,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];
}];
}

Expand All @@ -185,19 +187,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
2 changes: 1 addition & 1 deletion detox/ios/Detox/TestRunner.m
Original file line number Diff line number Diff line change
Expand Up @@ -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(^{
Expand Down
Loading

0 comments on commit a2bf0f0

Please sign in to comment.