5 // Created by Caleb Davenport on 12/15/10.
6 // Copyright 2010 GUI Cocoa, LLC. All rights reserved.
12 #import <sys/sysctl.h>
13 #import <TargetConditionals.h>
15 #import "HTFunctions.h"
16 #import "HTNotifier.h"
19 static NSString * const HTNotifierDirectoryName = @"Hoptoad Notices";
22 int ht_signals_count = 6;
32 // internal function prototypes
33 void ht_handle_signal(int, siginfo_t *, void *);
34 void ht_handle_exception(NSException *);
35 int ht_open_file(int);
37 #pragma mark crash time methods
38 void ht_handle_signal(int signal, siginfo_t *info, void *context) {
39 HTStopSignalHandler();
40 int fd = ht_open_file(HTSignalNoticeType);
44 write(fd, &signal, sizeof(int));
49 count = backtrace(frames, count);
50 backtrace_symbols_fd(frames, count, fd);
59 void ht_handle_exception(NSException *exception) {
61 int fd = ht_open_file(HTExceptionNoticeType);
65 NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:5];
68 NSArray *addresses = [exception callStackReturnAddresses];
69 NSArray *symbols = HTCallStackSymbolsFromReturnAddresses(addresses);
70 [dictionary setObject:symbols forKey:@"call stack"];
72 // exception name and reason
73 [dictionary setObject:[exception name] forKey:@"exception name"];
74 [dictionary setObject:[exception reason] forKey:@"exception reason"];
79 NSString *viewController = HTCurrentViewController();
80 if (viewController != nil) {
81 [dictionary setObject:viewController forKey:@"view controller"];
87 NSDictionary *environmentInfo = [[HTNotifier sharedNotifier] environmentInfo];
88 [dictionary setObject:environmentInfo forKey:@"environment info"];
91 NSData *data = [NSKeyedArchiver archivedDataWithRootObject:dictionary];
92 NSUInteger length = [data length];
93 write(fd, &length, sizeof(unsigned long));
94 write(fd, [data bytes], length);
99 id<HTNotifierDelegate> delegate = [[HTNotifier sharedNotifier] delegate];
100 if ([delegate respondsToSelector:@selector(notifierDidHandleException:)]) {
101 [delegate notifierDidHandleException:exception];
104 int ht_open_file(int type) {
105 int fd = open(ht_notice_info.notice_path, O_WRONLY | O_CREAT, S_IREAD | S_IWRITE);
107 write(fd, &HTNoticeFileVersion, sizeof(int));
108 write(fd, &type, sizeof(int));
109 write(fd, &ht_notice_info.os_version_len, sizeof(unsigned long));
110 if (ht_notice_info.os_version_len > 0) {
111 write(fd, ht_notice_info.os_version, ht_notice_info.os_version_len);
113 write(fd, &ht_notice_info.platform_len, sizeof(unsigned long));
114 if (ht_notice_info.platform_len > 0) {
115 write(fd, ht_notice_info.platform, ht_notice_info.platform_len);
117 write(fd, &ht_notice_info.app_version_len, sizeof(unsigned long));
118 if (ht_notice_info.app_version_len > 0) {
119 write(fd, ht_notice_info.app_version, ht_notice_info.app_version_len);
121 write(fd, &ht_notice_info.env_name_len, sizeof(unsigned long));
122 if (ht_notice_info.env_name_len > 0) {
123 write(fd, ht_notice_info.env_name, ht_notice_info.env_name_len);
125 write(fd, &ht_notice_info.git_hash_len, sizeof(unsigned long));
126 if (ht_notice_info.git_hash_len > 0) {
127 write(fd, ht_notice_info.git_hash, ht_notice_info.git_hash_len);
133 #pragma mark - modify handler state
134 void HTStartHandlers() {
135 HTStartExceptionHandler();
136 HTStartSignalHandler();
138 void HTStartExceptionHandler() {
139 NSSetUncaughtExceptionHandler(&ht_handle_exception);
141 void HTStartSignalHandler() {
142 for (NSUInteger i = 0; i < ht_signals_count; i++) {
143 int signal = ht_signals[i];
144 struct sigaction action;
145 sigemptyset(&action.sa_mask);
146 action.sa_flags = SA_SIGINFO;
147 action.sa_sigaction = ht_handle_signal;
148 if (sigaction(signal, &action, NULL) != 0) {
149 HTLog(@"unable to register signal handler for %s", strsignal(signal));
153 void HTStopHandlers() {
154 HTStopExceptionHandler();
155 HTStopSignalHandler();
157 void HTStopExceptionHandler() {
158 NSSetUncaughtExceptionHandler(NULL);
160 void HTStopSignalHandler() {
161 for (NSUInteger i = 0; i < ht_signals_count; i++) {
162 int signal = ht_signals[i];
163 struct sigaction action;
164 sigemptyset(&action.sa_mask);
165 action.sa_handler = SIG_DFL;
166 sigaction(signal, &action, NULL);
170 #pragma mark - Info.plist accessors
171 id HTInfoPlistValueForKey(NSString *key) {
172 return [[[NSBundle mainBundle] infoDictionary] objectForKey:key];
174 NSString * HTExecutableName() {
175 return HTInfoPlistValueForKey(@"CFBundleExecutable");
177 NSString * HTApplicationVersion() {
178 NSString *bundleVersion = HTInfoPlistValueForKey(@"CFBundleVersion");
179 NSString *versionString = HTInfoPlistValueForKey(@"CFBundleShortVersionString");
180 if (bundleVersion != nil && versionString != nil) {
181 return [NSString stringWithFormat:@"%@ (%@)", versionString, bundleVersion];
183 else if (bundleVersion != nil) { return bundleVersion; }
184 else if (versionString != nil) { return versionString; }
187 NSString * HTApplicationName() {
188 NSString *displayName = HTInfoPlistValueForKey(@"CFBundleDisplayName");
189 NSString *bundleName = HTInfoPlistValueForKey(@"CFBundleName");
190 NSString *identifier = HTInfoPlistValueForKey(@"CFBundleIdentifier");
191 if (displayName != nil) { return displayName; }
192 else if (bundleName != nil) { return bundleName; }
193 else if (identifier != nil) { return identifier; }
197 #pragma mark - platform accessors
198 NSString * HTOperatingSystemVersion() {
199 #if TARGET_IPHONE_SIMULATOR
200 return [[UIDevice currentDevice] systemVersion];
202 return [[NSProcessInfo processInfo] operatingSystemVersionString];
205 NSString * HTMachine() {
206 #if TARGET_IPHONE_SIMULATOR
207 return @"iPhone Simulator";
211 char *machine = malloc(size);
213 sysctlbyname("hw.machine", machine, &size, NULL, 0);
215 sysctlbyname("hw.model", machine, &size, NULL, 0);
217 NSString *platform = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding];
223 NSString * HTPlatform() {
224 #if TARGET_IPHONE_SIMULATOR
225 return @"iPhone Simulator";
228 NSString *machine = HTMachine();
231 if ([machine isEqualToString:@"iPhone1,1"]) { return @"iPhone"; }
232 else if ([machine isEqualToString:@"iPhone1,2"]) { return @"iPhone 3G"; }
233 else if ([machine isEqualToString:@"iPhone2,1"]) { return @"iPhone 3GS"; }
234 else if ([machine isEqualToString:@"iPhone3,1"]) { return @"iPhone 4 (GSM)"; }
235 else if ([machine isEqualToString:@"iPhone3,3"]) { return @"iPhone 4 (CDMA)"; }
237 else if ([machine isEqualToString:@"iPad1,1"]) { return @"iPad"; }
238 else if ([machine isEqualToString:@"iPad2,1"]) { return @"iPad 2 (WiFi)"; }
239 else if ([machine isEqualToString:@"iPad2,2"]) { return @"iPad 2 (GSM)"; }
240 else if ([machine isEqualToString:@"iPad2,3"]) { return @"iPad 2 (CDMA)"; }
242 else if ([machine isEqualToString:@"iPod1,1"]) { return @"iPod Touch"; }
243 else if ([machine isEqualToString:@"iPod2,1"]) { return @"iPod Touch 2nd Gen"; }
244 else if ([machine isEqualToString:@"iPod3,1"]) { return @"iPod Touch 3rd Gen"; }
245 else if ([machine isEqualToString:@"iPod4,1"]) { return @"iPod Touch 4th Gen"; }
247 else { return machine; }
255 #pragma mark - init notice info
256 void HTInitNoticeInfo() {
259 const char *value_str;
262 // exception file name
263 NSString *directory = HTNoticesDirectory();
264 NSString *fileName = [NSString stringWithFormat:@"%d", time(NULL)];
265 value = [directory stringByAppendingPathComponent:fileName];
266 value = [value stringByAppendingPathExtension:HTNoticePathExtension];
267 value_str = [value UTF8String];
268 length = (strlen(value_str) + 1);
269 ht_notice_info.notice_path = malloc(length);
270 memcpy((void *)ht_notice_info.notice_path, value_str, length);
273 value = HTOperatingSystemVersion();
274 if (value == nil) { HTLog(@"unable to cache operating system version"); }
276 value_str = [value UTF8String];
277 length = (strlen(value_str) + 1);
278 ht_notice_info.os_version = malloc(length);
279 ht_notice_info.os_version_len = length;
280 memcpy((void *)ht_notice_info.os_version, value_str, length);
284 value = HTApplicationVersion();
285 if (value == nil) { HTLog(@"unable to cache app version"); }
287 value_str = [value UTF8String];
288 length = (strlen(value_str) + 1);
289 ht_notice_info.app_version = malloc(length);
290 ht_notice_info.app_version_len = length;
291 memcpy((void *)ht_notice_info.app_version, value_str, length);
295 value = HTPlatform();
296 if (value == nil) { HTLog(@"unable to cache platform"); }
298 value_str = [value UTF8String];
299 length = (strlen(value_str) + 1);
300 ht_notice_info.platform = malloc(length);
301 ht_notice_info.platform_len = length;
302 memcpy((void *)ht_notice_info.platform, value_str, length);
306 value = [[HTNotifier sharedNotifier] environmentName];
307 if (value == nil) { HTLog(@"unable to cache environment name"); }
309 value_str = [value UTF8String];
310 length = (strlen(value_str) + 1);
311 ht_notice_info.env_name = malloc(length);
312 ht_notice_info.env_name_len = length;
313 memcpy((void *)ht_notice_info.env_name, value_str, length);
317 value = HTInfoPlistValueForKey(@"GCGitCommitHash");
318 if (value == nil) { HTLog(@"unable to cache git commit hash"); }
320 value_str = [value UTF8String];
321 length = (strlen(value_str) + 1);
322 ht_notice_info.git_hash = malloc(length);
323 ht_notice_info.git_hash_len = length;
324 memcpy((void *)ht_notice_info.git_hash, value_str, length);
328 void HTReleaseNoticeInfo() {
329 free((void *)ht_notice_info.notice_path);
330 ht_notice_info.notice_path = NULL;
331 free((void *)ht_notice_info.os_version);
332 ht_notice_info.os_version = NULL;
333 ht_notice_info.os_version_len = 0;
334 free((void *)ht_notice_info.app_version);
335 ht_notice_info.app_version = NULL;
336 ht_notice_info.app_version_len = 0;
337 free((void *)ht_notice_info.platform);
338 ht_notice_info.platform = NULL;
339 ht_notice_info.platform_len = 0;
340 free((void *)ht_notice_info.env_name);
341 ht_notice_info.env_name = NULL;
342 ht_notice_info.env_name_len = 0;
343 free((void *)ht_notice_info.git_hash);
344 ht_notice_info.git_hash = NULL;
345 ht_notice_info.git_hash_len = 0;
348 #pragma mark - notice information on disk
349 NSString * HTNoticesDirectory() {
351 NSArray *folders = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
352 NSString *path = [folders objectAtIndex:0];
353 if ([folders count] == 0) { path = NSTemporaryDirectory(); }
354 return [path stringByAppendingPathComponent:HTNotifierDirectoryName];
356 NSArray *folders = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
357 NSString *path = [folders objectAtIndex:0];
358 if ([folders count] == 0) { path = NSTemporaryDirectory(); }
359 path = [path stringByAppendingPathComponent:HTApplicationName()];
360 return [path stringByAppendingPathComponent:HTNotifierDirectoryName];
363 NSArray * HTNotices() {
364 NSString *directory = HTNoticesDirectory();
365 NSArray *directoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:directory error:nil];
366 NSMutableArray *crashes = [NSMutableArray arrayWithCapacity:[directoryContents count]];
367 for (NSString *file in directoryContents) {
368 NSString *ext = [file pathExtension];
369 if ([ext isEqualToString:HTNoticePathExtension]) {
370 NSString *crashPath = [directory stringByAppendingPathComponent:file];
371 [crashes addObject:crashPath];
377 #pragma mark - callstack functions
378 NSArray * HTCallStackSymbolsFromReturnAddresses(NSArray *addresses) {
379 int frames = [addresses count];
381 for (NSInteger i = 0; i < frames; i++) {
382 stack[i] = (void *)[[addresses objectAtIndex:i] unsignedIntegerValue];
384 char **strs = backtrace_symbols(stack, frames);
385 NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
386 for (NSInteger i = 0; i < frames; i++) {
387 NSString *entry = [NSString stringWithUTF8String:strs[i]];
388 [backtrace addObject:entry];
393 NSArray * HTParseCallstack(NSArray *symbols) {
394 NSCharacterSet *whiteSpace = [NSCharacterSet whitespaceAndNewlineCharacterSet];
395 NSCharacterSet *nonWhiteSpace = [whiteSpace invertedSet];
396 NSMutableArray *parsed = [NSMutableArray arrayWithCapacity:[symbols count]];
397 for (NSString *line in symbols) {
400 NSScanner *scanner = [NSScanner scannerWithString:line];
404 [scanner scanInteger:&number];
408 [scanner scanCharactersFromSet:nonWhiteSpace intoString:&binary];
411 NSUInteger location = [scanner scanLocation];
412 NSString *method = [line substringFromIndex:location];
413 method = [method stringByTrimmingCharactersInSet:whiteSpace];
417 [NSDictionary dictionaryWithObjectsAndKeys:
418 [NSNumber numberWithInteger:number], @"number",
426 NSString * HTActionFromCallstack(NSArray *callStack) {
427 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"file matches %@", HTExecutableName()];
428 NSArray *matching = [[callStack filteredArrayUsingPredicate:predicate] valueForKey:@"method"];
429 for (NSString *file in matching) {
430 if ([file rangeOfString:@"ht_handle_signal"].location == NSNotFound) {
440 #pragma mark - string substitution
441 NSString * HTStringByReplacingHoptoadVariablesInString(NSString *string) {
442 NSString *toReturn = string;
445 stringByReplacingOccurrencesOfString:HTNotifierBundleName
446 withString:HTApplicationName()];
448 stringByReplacingOccurrencesOfString:HTNotifierBundleVersion
449 withString:HTApplicationVersion()];
454 #pragma mark - get view controller
456 NSString * HTCurrentViewController() {
457 // view controller to inspect
458 UIViewController *rootController = nil;
460 // try getting view controller from notifier delegate
461 id<HTNotifierDelegate> notifierDelegate = [[HTNotifier sharedNotifier] delegate];
462 if ([notifierDelegate respondsToSelector:@selector(rootViewControllerForNotice)]) {
463 rootController = [notifierDelegate rootViewControllerForNotice];
466 // try getting view controller from window
467 UIApplication *app = [UIApplication sharedApplication];
468 UIWindow *keyWindow = [app keyWindow];
469 if (rootController == nil && [keyWindow respondsToSelector:@selector(rootViewController)]) {
470 rootController = [keyWindow rootViewController];
473 // if we don't have a controller yet, give up
474 if (rootController == nil) {
478 // call method to get class name
479 return HTVisibleViewControllerWithViewController(rootController);
482 NSString * HTVisibleViewControllerWithViewController(UIViewController *controller) {
484 // tab bar controller
485 if ([controller isKindOfClass:[UITabBarController class]]) {
486 UIViewController *visibleController = [(UITabBarController *)controller selectedViewController];
487 return HTVisibleViewControllerWithViewController(visibleController);
489 // navigation controller
490 else if ([controller isKindOfClass:[UINavigationController class]]) {
491 UIViewController *visibleController = [(UINavigationController *)controller visibleViewController];
492 return HTVisibleViewControllerWithViewController(visibleController);
496 return NSStringFromClass([controller class]);