Skip to content

Commit

Permalink
Add exception and signal handling for iOS
Browse files Browse the repository at this point in the history
First half of #161
  • Loading branch information
LeoNatan committed Dec 6, 2017
1 parent 7f6d55e commit 1be1e5a
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 40 deletions.
4 changes: 4 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.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 */; };
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.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetoxCrashHandler.m; 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.m */,
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.m in Sources */,
46A6A63D1EF697BB00E3AA79 /* GREYConfiguration+Detox.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
110 changes: 110 additions & 0 deletions detox/ios/Detox/DetoxCrashHandler.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// DetoxCrashHandler.m
// Detox
//
// Created by Leo Natan (Wix) on 12/5/17.
// Copyright © 2017 Wix. All rights reserved.
//

#include <fishhook.h>
@import Darwin;
@import Foundation;
#import "DetoxManager.h"
#import <Detox/Detox-Swift.h>

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<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));

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

@interface DetoxManager : NSObject<WebSocketDelegate, TestRunnerDelegate>

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

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

@end
57 changes: 30 additions & 27 deletions detox/ios/Detox/DetoxManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -74,49 +72,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 +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"])
{
Expand All @@ -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];
}
}

Expand All @@ -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];
}];
}

Expand All @@ -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
4 changes: 2 additions & 2 deletions detox/ios/Detox/TestRunner.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

@property (nonatomic, assign) id<TestRunnerDelegate> delegate;

- (void) invoke:(NSDictionary*)params withMessageId:(NSNumber*) messageId;
- (void) cleanup;
- (void)invoke:(NSDictionary*)params withMessageId:(NSNumber*) messageId;
- (void)cleanup;

@end
8 changes: 4 additions & 4 deletions detox/ios/Detox/TestRunner.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -48,7 +48,7 @@ - (instancetype)init
return self;
}

- (void) cleanup
- (void)cleanup
{
[self cleanupEarlGrey];
}
Expand All @@ -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
4 changes: 2 additions & 2 deletions detox/ios/Detox/WebSocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
@interface WebSocket : NSObject<SRWebSocketDelegate>

@property (nonatomic, assign) id<WebSocketDelegate> 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
6 changes: 3 additions & 3 deletions detox/ios/Detox/WebSocket.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand All @@ -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];
Expand Down

0 comments on commit 1be1e5a

Please sign in to comment.