Skip to content
New issue

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

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

Already on GitHub? # to your account

Fix symbolication in Yosemite and Xcode6 #597

Merged
merged 1 commit into from
Apr 30, 2015
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 1 addition & 21 deletions Classes/Core/KWExample.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions Classes/Core/KWSymbolicator.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
//

#import <Foundation/Foundation.h>
#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
158 changes: 145 additions & 13 deletions Classes/Core/KWSymbolicator.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
// Copyright (c) 2013 Allen Ding. All rights reserved.
//

#import "KWSymbolicator.h"
#import <objc/runtime.h>
#import <libunwind.h>
#import <pthread.h>
Copy link
Contributor

Choose a reason for hiding this comment

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

This import doesn't appear to be used anywhere in this file.

#import <mach-o/dyld.h>
#import "KWSymbolicator.h"

long kwCallerAddress (void){
#if !__arm__
Expand All @@ -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
Expand All @@ -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<NSTask_KWWarningSuppressor> 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<NSTask_KWWarningSuppressor> 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<NSTask_KWWarningSuppressor> 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