Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exception and signal handling for iOS #453

Merged
merged 8 commits into from
Feb 8, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 */; };
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 */; };
Expand Down Expand Up @@ -236,6 +237,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>"; };
468731A31E6C6D0500F151BE /* EarlGrey+Detox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "EarlGrey+Detox.h"; sourceTree = "<group>"; };
468731A41E6C6D0500F151BE /* EarlGrey+Detox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "EarlGrey+Detox.m"; sourceTree = "<group>"; };
46A6A6341EF696B600E3AA79 /* GREYConfiguration+Detox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GREYConfiguration+Detox.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -339,6 +341,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 @@ -690,6 +693,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 @@ -901,6 +905,7 @@
OTHER_LDFLAGS = (
"-ObjC",
"-all_load",
"-lstdc++",
);
PRODUCT_BUNDLE_IDENTIFIER = com.wix.Detox;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -930,6 +935,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.m
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably You forgot about one m in extension 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was an Objective C file before, but now Objective C++.

// 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);
}];
}
11 changes: 5 additions & 6 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;
- (void) connectToServer:(NSString*)url withSessionId:(NSString*)sessionId;
+ (instancetype)sharedManager;
- (void)connectToServer:(NSString*)url withSessionId:(NSString*)sessionId;

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

@end
64 changes: 36 additions & 28 deletions detox/ios/Detox/DetoxManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,27 @@
//

#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"

@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

__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"];
Expand All @@ -39,14 +45,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;
Expand All @@ -61,8 +64,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 @@ -74,49 +77,49 @@ - (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"])
{
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 @@ -137,7 +140,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"])
{
Expand All @@ -153,7 +156,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];
}
}

Expand All @@ -162,7 +165,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 @@ -173,19 +176,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
Loading