95f0246b65c61d213db7576aa639011a1a2595e7
[pithos-ios] / Classes / hoptoadnotifier / HTFunctions.m
1 //
2 //  HTHandler.m
3 //  CrashPhone
4 //
5 //  Created by Caleb Davenport on 12/15/10.
6 //  Copyright 2010 GUI Cocoa, LLC. All rights reserved.
7 //
8
9 #import <execinfo.h>
10 #import <fcntl.h>
11 #import <unistd.h>
12 #import <sys/sysctl.h>
13 #import <TargetConditionals.h>
14
15 #import "HTFunctions.h"
16 #import "HTNotifier.h"
17 #import "HTNotice.h"
18
19 static NSString * const HTNotifierDirectoryName = @"Hoptoad Notices";
20
21 // handled signals
22 int ht_signals_count = 6;
23 int ht_signals[] = {
24         SIGABRT,
25         SIGBUS,
26         SIGFPE,
27         SIGILL,
28         SIGSEGV,
29         SIGTRAP
30 };
31
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);
36
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);
41     if (fd > -1) {
42                 
43                 // signal
44         write(fd, &signal, sizeof(int));
45                 
46                 // backtraces
47                 int count = 128;
48                 void *frames[count];
49                 count = backtrace(frames, count);
50                 backtrace_symbols_fd(frames, count, fd);
51                 
52                 // close
53         close(fd);
54     }
55         
56         // re raise
57         raise(signal);
58 }
59 void ht_handle_exception(NSException *exception) {
60     HTStopHandlers();
61     int fd = ht_open_file(HTExceptionNoticeType);
62     if (fd > -1) {
63         
64                 // crash info
65                 NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:5];
66                 
67                 // addresses
68         NSArray *addresses = [exception callStackReturnAddresses];
69                 NSArray *symbols = HTCallStackSymbolsFromReturnAddresses(addresses);
70                 [dictionary setObject:symbols forKey:@"call stack"];
71                 
72                 // exception name and reason
73                 [dictionary setObject:[exception name] forKey:@"exception name"];
74                 [dictionary setObject:[exception reason] forKey:@"exception reason"];
75                 
76 #if TARGET_OS_IPHONE
77                 
78                 // view controller
79                 NSString *viewController = HTCurrentViewController();
80                 if (viewController != nil) {
81                         [dictionary setObject:viewController forKey:@"view controller"];
82                 }
83                 
84 #endif
85                 
86                 // environment info
87                 NSDictionary *environmentInfo = [[HTNotifier sharedNotifier] environmentInfo];
88                 [dictionary setObject:environmentInfo forKey:@"environment info"];
89                 
90         // write data
91         NSData *data = [NSKeyedArchiver archivedDataWithRootObject:dictionary];
92         NSUInteger length = [data length];
93         write(fd, &length, sizeof(unsigned long));
94         write(fd, [data bytes], length);
95         
96         // close file
97         close(fd);
98     }
99         id<HTNotifierDelegate> delegate = [[HTNotifier sharedNotifier] delegate];
100         if ([delegate respondsToSelector:@selector(notifierDidHandleException:)]) {
101                 [delegate notifierDidHandleException:exception];
102         }
103 }
104 int ht_open_file(int type) {
105     int fd = open(ht_notice_info.notice_path, O_WRONLY | O_CREAT, S_IREAD | S_IWRITE);
106     if (fd > -1) {
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);
112         }
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);
116         }
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);
120         }
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);
124         }
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);
128         }
129     }
130     return fd;
131 }
132
133 #pragma mark - modify handler state
134 void HTStartHandlers() {
135     HTStartExceptionHandler();
136     HTStartSignalHandler();
137 }
138 void HTStartExceptionHandler() {
139     NSSetUncaughtExceptionHandler(&ht_handle_exception);
140 }
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));
150                 }
151         }
152 }
153 void HTStopHandlers() {
154     HTStopExceptionHandler();
155     HTStopSignalHandler();
156 }
157 void HTStopExceptionHandler() {
158     NSSetUncaughtExceptionHandler(NULL);
159 }
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);
167         }
168 }
169
170 #pragma mark - Info.plist accessors
171 id HTInfoPlistValueForKey(NSString *key) {
172         return [[[NSBundle mainBundle] infoDictionary] objectForKey:key];
173 }
174 NSString * HTExecutableName() {
175         return HTInfoPlistValueForKey(@"CFBundleExecutable");
176 }
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];
182         }
183         else if (bundleVersion != nil) { return bundleVersion; }
184         else if (versionString != nil) { return versionString; }
185         else { return nil; }
186 }
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; }
194         else { return nil; }
195 }
196
197 #pragma mark - platform accessors
198 NSString * HTOperatingSystemVersion() {
199 #if TARGET_IPHONE_SIMULATOR
200         return [[UIDevice currentDevice] systemVersion];
201 #else
202         return [[NSProcessInfo processInfo] operatingSystemVersionString];
203 #endif
204 }
205 NSString * HTMachine() {
206 #if TARGET_IPHONE_SIMULATOR
207         return @"iPhone Simulator";
208 #else
209     
210     size_t size = 256;
211         char *machine = malloc(size);
212 #if TARGET_OS_IPHONE
213     sysctlbyname("hw.machine", machine, &size, NULL, 0);
214 #else
215     sysctlbyname("hw.model", machine, &size, NULL, 0);
216 #endif
217     NSString *platform = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding];
218     free(machine);
219     return platform;
220     
221 #endif
222 }
223 NSString * HTPlatform() {
224 #if TARGET_IPHONE_SIMULATOR
225         return @"iPhone Simulator";
226 #else
227     
228     NSString *machine = HTMachine();
229 #if TARGET_OS_IPHONE
230     // iphone
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)"; }
236         // ipad
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)"; }
241         // ipod
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"; }
246         // unknown
247         else { return machine; }
248 #else
249     return machine;
250 #endif
251     
252 #endif
253 }
254
255 #pragma mark - init notice info
256 void HTInitNoticeInfo() {
257     
258     NSString *value;
259     const char *value_str;
260     NSUInteger length;
261     
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);
271     
272     // os version
273     value = HTOperatingSystemVersion();
274     if (value == nil) { HTLog(@"unable to cache operating system version"); }
275     else {
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);
281     }
282     
283     // app version
284     value = HTApplicationVersion();
285     if (value == nil) { HTLog(@"unable to cache app version"); }
286     else {
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);
292     }
293     
294     // platform
295     value = HTPlatform();
296     if (value == nil) { HTLog(@"unable to cache platform"); }
297     else {
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);
303     }
304     
305     // environment
306     value = [[HTNotifier sharedNotifier] environmentName];
307     if (value == nil) { HTLog(@"unable to cache environment name"); }
308     else {
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);
314     }
315     
316     // git hash
317     value = HTInfoPlistValueForKey(@"GCGitCommitHash");
318     if (value == nil) { HTLog(@"unable to cache git commit hash"); }
319     else {
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);
325     }
326     
327 }
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;
346 }
347
348 #pragma mark - notice information on disk
349 NSString * HTNoticesDirectory() {
350 #if TARGET_OS_IPHONE
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];
355 #else
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];
361 #endif
362 }
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];
372                 }
373         }
374         return crashes;
375 }
376
377 #pragma mark - callstack functions
378 NSArray * HTCallStackSymbolsFromReturnAddresses(NSArray *addresses) {
379         int frames = [addresses count];
380         void *stack[frames];
381         for (NSInteger i = 0; i < frames; i++) {
382                 stack[i] = (void *)[[addresses objectAtIndex:i] unsignedIntegerValue];
383         }
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];
389         }
390         free(strs);
391         return backtrace;
392 }
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) {
398                 
399                 // create scanner
400                 NSScanner *scanner = [NSScanner scannerWithString:line];
401                 
402                 // line number
403                 NSInteger number;
404                 [scanner scanInteger:&number];
405                 
406                 // binary name
407                 NSString *binary;
408                 [scanner scanCharactersFromSet:nonWhiteSpace intoString:&binary];
409                 
410                 // method
411         NSUInteger location = [scanner scanLocation];
412                 NSString *method = [line substringFromIndex:location];
413                 method = [method stringByTrimmingCharactersInSet:whiteSpace];
414                 
415                 // add line
416                 [parsed addObject:
417                  [NSDictionary dictionaryWithObjectsAndKeys:
418                   [NSNumber numberWithInteger:number], @"number",
419                   binary, @"file",
420                   method, @"method",
421                   nil]];
422                 
423         }
424         return parsed;
425 }
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) {
431                         return file;
432                 }
433                 else {
434                         continue;
435                 }
436         }
437         return @"";
438 }
439
440 #pragma mark - string substitution
441 NSString * HTStringByReplacingHoptoadVariablesInString(NSString *string) {
442         NSString *toReturn = string;
443         
444         toReturn = [toReturn
445                                 stringByReplacingOccurrencesOfString:HTNotifierBundleName
446                                 withString:HTApplicationName()];
447         toReturn = [toReturn
448                                 stringByReplacingOccurrencesOfString:HTNotifierBundleVersion
449                                 withString:HTApplicationVersion()];
450         
451         return toReturn;
452 }
453
454 #pragma mark - get view controller
455 #if TARGET_OS_IPHONE
456 NSString * HTCurrentViewController() {
457         // view controller to inspect
458         UIViewController *rootController = nil;
459         
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];
464         }
465         
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];
471         }
472         
473         // if we don't have a controller yet, give up
474         if (rootController == nil) {
475                 return nil;
476         }
477         
478         // call method to get class name
479         return HTVisibleViewControllerWithViewController(rootController);
480 }
481
482 NSString * HTVisibleViewControllerWithViewController(UIViewController *controller) {
483         
484         // tab bar controller
485         if ([controller isKindOfClass:[UITabBarController class]]) {
486                 UIViewController *visibleController = [(UITabBarController *)controller selectedViewController];
487                 return HTVisibleViewControllerWithViewController(visibleController);
488         }
489         // navigation controller
490         else if ([controller isKindOfClass:[UINavigationController class]]) {
491                 UIViewController *visibleController = [(UINavigationController *)controller visibleViewController];
492                 return HTVisibleViewControllerWithViewController(visibleController);
493         }
494         // other type
495         else {
496                 return NSStringFromClass([controller class]);
497         }
498         
499 }
500 #endif