diff --git a/Classes/Core/KWExample.m b/Classes/Core/KWExample.m index 95a18798..e7885154 100644 --- a/Classes/Core/KWExample.m +++ b/Classes/Core/KWExample.m @@ -308,27 +308,7 @@ - (NSString *)generateDescriptionForAnonymousItNode { KWCallSite *callSiteAtAddressIfNecessary(long address){ BOOL shouldLookup = [[KWExampleSuiteBuilder sharedExampleSuiteBuilder] isFocused] && ![[KWExampleSuiteBuilder sharedExampleSuiteBuilder] foundFocus]; - return shouldLookup ? callSiteWithAddress(address) : nil; -} - -KWCallSite *callSiteWithAddress(long address){ - NSArray *args = @[@"-p", @(getpid()).stringValue, [NSString stringWithFormat:@"%lx", address]]; - NSString *callSite = [NSString stringWithShellCommand:@"/usr/bin/atos" arguments:args]; - - NSString *pattern = @".+\\((.+):([0-9]+)\\)"; - NSError *e; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&e]; - NSArray *res = [regex matchesInString:callSite options:0 range:NSMakeRange(0, callSite.length)]; - - NSString *fileName = nil; - NSInteger lineNumber = 0; - - for (NSTextCheckingResult *ntcr in res) { - fileName = [callSite substringWithRange:[ntcr rangeAtIndex:1]]; - NSString *lineNumberMatch = [callSite substringWithRange:[ntcr rangeAtIndex:2]]; - lineNumber = lineNumberMatch.integerValue; - } - return [KWCallSite callSiteWithFilename:fileName lineNumber:lineNumber]; + return shouldLookup ? [KWCallSite callSiteWithCallerAddress:address] : nil; } #pragma mark - Building Example Groups diff --git a/Classes/Core/KWSymbolicator.h b/Classes/Core/KWSymbolicator.h index 452852fd..9164b49c 100644 --- a/Classes/Core/KWSymbolicator.h +++ b/Classes/Core/KWSymbolicator.h @@ -7,11 +7,13 @@ // #import +#import "KWSymbolicator.h" +#import "KWCallSite.h" long kwCallerAddress(void); -@interface NSString (KWShellCommand) +@interface KWCallSite (KWSymbolication) -+ (NSString *)stringWithShellCommand:(NSString *)command arguments:(NSArray *)arguments; ++ (KWCallSite *)callSiteWithCallerAddress:(long)address; @end diff --git a/Classes/Core/KWSymbolicator.m b/Classes/Core/KWSymbolicator.m index 8128622b..af53969e 100644 --- a/Classes/Core/KWSymbolicator.m +++ b/Classes/Core/KWSymbolicator.m @@ -6,9 +6,11 @@ // Copyright (c) 2013 Allen Ding. All rights reserved. // -#import "KWSymbolicator.h" #import #import +#import +#import +#import "KWSymbolicator.h" long kwCallerAddress (void){ #if !__arm__ @@ -27,6 +29,8 @@ long kwCallerAddress (void){ return 0; } +NSString *const NSTaskDidTerminateNotification; + // Used to suppress compiler warnings by // casting receivers to this protocol @protocol NSTask_KWWarningSuppressor @@ -35,28 +39,156 @@ - (void)setLaunchPath:(NSString *)path; - (void)setArguments:(NSArray *)arguments; - (void)setEnvironment:(NSDictionary *)dict; - (void)setStandardOutput:(id)output; +- (void)setStandardError:(id)output; - (void)launch; - (void)waitUntilExit; +@property (readonly) int terminationStatus; + @end -@implementation NSString (KWShellCommand) +static NSString *const KWTaskDidTerminateNotification = @"KWTaskDidTerminateNotification"; + +@interface KWBackgroundTask : NSObject + +@property (nonatomic, readonly) id task; +@property (nonatomic, readonly) NSPipe *standardOutput; +@property (nonatomic, readonly) NSPipe *standardError; +@property (nonatomic, readonly) NSString *command; +@property (nonatomic, readonly) NSArray *arguments; +@property (nonatomic, readonly) NSData *output; + +- (void)launchAndWaitForExit; + +@end + +static NSString *const KWBackgroundTaskException = @"KWBackgroundTaskException"; + +@implementation KWBackgroundTask -+ (NSString *)stringWithShellCommand:(NSString *)command arguments:(NSArray *)arguments { - id task = [[NSClassFromString(@"NSTask") alloc] init]; +- (instancetype)initWithCommand:(NSString *)command arguments:(NSArray *)arguments { + if (self = [super init]) { + _command = command; + _arguments = arguments; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSTaskDidTerminateNotification object:nil]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ `%@ %@`", [super description], self.command, [self.arguments componentsJoinedByString:@" "]]; +} + +// Run this task for 10 seconds +// if it times out raise an exception +- (void)launchAndWaitForExit { + CFRunLoopRef runLoop = [NSRunLoop currentRunLoop].getCFRunLoop; + __weak KWBackgroundTask *weakSelf = self; + CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 10.0, 0, 0, 0, ^(CFRunLoopTimerRef timer) { + [NSException raise:KWBackgroundTaskException format:@"Task %@ timed out", weakSelf]; + CFRunLoopStop(runLoop); + }); + CFRunLoopAddTimer(runLoop, timer, kCFRunLoopDefaultMode); + + id taskObserver = [[NSNotificationCenter defaultCenter] addObserverForName:KWTaskDidTerminateNotification object:self queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + CFRunLoopStop(runLoop); + }]; + + [NSThread detachNewThreadSelector:@selector(launch) toTarget:self withObject:nil]; + CFRunLoopRun(); + CFRunLoopRemoveTimer(runLoop, timer, kCFRunLoopDefaultMode); + + [[NSNotificationCenter defaultCenter] removeObserver:taskObserver]; +} + +#pragma mark - Private + +- (void)launch { + __block id task = [[NSClassFromString(@"NSTask") alloc] init]; [task setEnvironment:[NSDictionary dictionary]]; - [task setLaunchPath:command]; - [task setArguments:arguments]; + [task setLaunchPath:_command]; + [task setArguments:_arguments]; + + NSPipe *standardOutput = [NSPipe pipe]; + [task setStandardOutput:standardOutput]; + + // Consume standard error but don't use it + NSPipe *standardError = [NSPipe pipe]; + [task setStandardError:standardError]; + + _task = task; + _standardError = standardError; + _standardOutput = standardOutput; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidTerminate:) name:NSTaskDidTerminateNotification object:task]; + + @try { + [_task launch]; + } @catch (NSException *exception) { + [NSException raise:KWBackgroundTaskException format:@"Task %@ failed to launch", self]; + } + CFRunLoopRun(); +} + +- (void)taskDidTerminate:(NSNotification *)note { + if ([_task terminationStatus] != 0) { + [NSException raise:KWBackgroundTaskException format:@"Task %@ terminated with non 0 exit code", self]; + } else { + _output = [[_standardOutput fileHandleForReading] readDataToEndOfFile]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:KWTaskDidTerminateNotification object:self]; + [NSThread exit]; +} + +@end + +@implementation KWCallSite (KWSymbolication) + +static void GetTestBundleExecutablePathSlide(NSString **executablePath, long *slide) { + for (int i = 0; i < _dyld_image_count(); i++) { + if (strstr(_dyld_get_image_name(i), ".octest/") || strstr(_dyld_get_image_name(i), ".xctest/")) { + *executablePath = [NSString stringWithUTF8String:_dyld_get_image_name(i)]; + *slide = _dyld_get_image_vmaddr_slide(i); + break; + } + } +} + ++ (KWCallSite *)callSiteWithCallerAddress:(long)address { + // Symbolicate the address with atos to get the line number & filename. + // If the command raises, no specs will run so don't bother catching + // In the case of a non 0 exit code, failure to launch, or timeout, the + // user will atleast have an idea of why the task failed. + + long slide; + NSString *executablePath; + GetTestBundleExecutablePathSlide(&executablePath, &slide); + NSArray *arguments = @[@"-o", executablePath, @"-s", [NSString stringWithFormat:@"%lx", slide], [NSString stringWithFormat:@"%lx", address]]; + + // See atos man page for more information on arguments. + KWBackgroundTask *symbolicationTask = [[KWBackgroundTask alloc] initWithCommand:@"/usr/bin/atos" arguments:arguments]; + [symbolicationTask launchAndWaitForExit]; + + NSString *symbolicatedCallerAddress = [[NSString alloc] initWithData:symbolicationTask.output encoding:NSUTF8StringEncoding]; - NSPipe *pipe = [NSPipe pipe]; - [task setStandardOutput:pipe]; - [task launch]; + NSString *pattern = @".+\\((.+):([0-9]+)\\)"; + NSError *error; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error]; + NSArray *matches = [regex matchesInString:symbolicatedCallerAddress options:0 range:NSMakeRange(0, symbolicatedCallerAddress.length)]; - [task waitUntilExit]; + NSString *fileName; + NSInteger lineNumber = 0; - NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile]; - NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - return string; + for (NSTextCheckingResult *ntcr in matches) { + fileName = [symbolicatedCallerAddress substringWithRange:[ntcr rangeAtIndex:1]]; + NSString *lineNumberMatch = [symbolicatedCallerAddress substringWithRange:[ntcr rangeAtIndex:2]]; + lineNumber = lineNumberMatch.integerValue; + } + return [KWCallSite callSiteWithFilename:fileName lineNumber:lineNumber]; } @end