2 * Author: Andreas Linde <mail@andreaslinde.de>
5 * Copyright (c) 2011 Andreas Linde & Kent Sutherland.
8 * Permission is hereby granted, free of charge, to any person
9 * obtaining a copy of this software and associated documentation
10 * files (the "Software"), to deal in the Software without
11 * restriction, including without limitation the rights to use,
12 * copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the
14 * Software is furnished to do so, subject to the following
17 * The above copyright notice and this permission notice shall be
18 * included in all copies or substantial portions of the Software.
20 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 * OTHER DEALINGS IN THE SOFTWARE.
30 #import "BWQuincyManager.h"
31 #import "BWQuincyUI.h"
32 #import <sys/sysctl.h>
34 #define SDK_NAME @"Quincy"
35 #define SDK_VERSION @"2.1.6"
37 @interface BWQuincyManager(private)
38 - (void) startManager;
40 - (void) _postXML:(NSString*)xml toURL:(NSURL*)url;
41 - (void) searchCrashLogFile:(NSString *)path;
42 - (BOOL) hasPendingCrashReport;
43 - (void) returnToMainApplication;
47 @implementation BWQuincyManager
49 @synthesize delegate = _delegate;
50 @synthesize submissionURL = _submissionURL;
51 @synthesize companyName = _companyName;
52 @synthesize appIdentifier = _appIdentifier;
53 @synthesize autoSubmitCrashReport = _autoSubmitCrashReport;
55 + (BWQuincyManager *)sharedQuincyManager {
56 static BWQuincyManager *quincyManager = nil;
58 if (quincyManager == nil) {
59 quincyManager = [[BWQuincyManager alloc] init];
66 if ((self = [super init])) {
67 _serverResult = CrashReportStatusFailureDatabaseNotAvailable;
76 self.companyName = @"";
93 - (void) searchCrashLogFile:(NSString *)path {
94 NSFileManager* fman = [NSFileManager defaultManager];
97 NSMutableArray* filesWithModificationDate = [NSMutableArray array];
98 NSArray* crashLogFiles = [fman contentsOfDirectoryAtPath:path error:&error];
99 NSEnumerator* filesEnumerator = [crashLogFiles objectEnumerator];
101 while((crashFile = [filesEnumerator nextObject])) {
102 NSString* crashLogPath = [path stringByAppendingPathComponent:crashFile];
103 NSDate* modDate = [[[NSFileManager defaultManager] attributesOfItemAtPath:crashLogPath error:&error] fileModificationDate];
104 [filesWithModificationDate addObject:[NSDictionary dictionaryWithObjectsAndKeys:crashFile,@"name",crashLogPath,@"path",modDate,@"modDate",nil]];
107 NSSortDescriptor* dateSortDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"modDate" ascending:YES] autorelease];
108 NSArray* sortedFiles = [filesWithModificationDate sortedArrayUsingDescriptors:[NSArray arrayWithObject:dateSortDescriptor]];
110 NSPredicate* filterPredicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH %@", [self applicationName]];
111 NSArray* filteredFiles = [sortedFiles filteredArrayUsingPredicate:filterPredicate];
113 _crashFile = [[[filteredFiles valueForKeyPath:@"path"] lastObject] copy];
118 - (void)setSubmissionURL:(NSString *)anSubmissionURL {
119 if (_submissionURL != anSubmissionURL) {
120 [_submissionURL release];
121 _submissionURL = [anSubmissionURL copy];
124 [self performSelector:@selector(startManager) withObject:nil afterDelay:0.1f];
127 - (void)setAppIdentifier:(NSString *)anAppIdentifier {
128 if (_appIdentifier != anAppIdentifier) {
129 [_appIdentifier release];
130 _appIdentifier = [anAppIdentifier copy];
133 [self setSubmissionURL:@"https://rink.hockeyapp.net/"];
136 - (void)storeLastCrashDate:(NSDate *) date {
137 [[NSUserDefaults standardUserDefaults] setValue:date forKey:@"CrashReportSender.lastCrashDate"];
138 [[NSUserDefaults standardUserDefaults] synchronize];
141 - (NSDate *)loadLastCrashDate {
142 NSDate *date = [[NSUserDefaults standardUserDefaults] valueForKey:@"CrashReportSender.lastCrashDate"];
143 return date ?: [NSDate distantPast];
146 - (void)storeAppVersion:(NSString *) version {
147 [[NSUserDefaults standardUserDefaults] setValue:version forKey:@"CrashReportSender.appVersion"];
148 [[NSUserDefaults standardUserDefaults] synchronize];
151 - (NSString *)loadAppVersion {
152 NSString *appVersion = [[NSUserDefaults standardUserDefaults] valueForKey:@"CrashReportSender.appVersion"];
153 return appVersion ?: nil;
157 #pragma mark GetCrashData
159 - (BOOL) hasPendingCrashReport {
160 BOOL returnValue = NO;
162 NSString *appVersion = [self loadAppVersion];
163 NSDate *lastCrashDate = [self loadLastCrashDate];
165 if (!appVersion || ![appVersion isEqualToString:[self applicationVersion]] || [lastCrashDate isEqualToDate:[NSDate distantPast]]) {
166 [self storeAppVersion:[self applicationVersion]];
167 [self storeLastCrashDate:[NSDate date]];
171 NSArray* libraryDirectories = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, TRUE);
172 // Snow Leopard is having the log files in another location
173 [self searchCrashLogFile:[[libraryDirectories lastObject] stringByAppendingPathComponent:@"Logs/DiagnosticReports"]];
174 if (_crashFile == nil) {
175 [self searchCrashLogFile:[[libraryDirectories lastObject] stringByAppendingPathComponent:@"Logs/CrashReporter"]];
176 if (_crashFile == nil) {
177 NSString *sandboxFolder = [NSString stringWithFormat:@"/Containers/%@/Data/Library", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]];
178 if ([[libraryDirectories lastObject] rangeOfString:sandboxFolder].location != NSNotFound) {
179 NSString *libFolderName = [[libraryDirectories lastObject] stringByReplacingOccurrencesOfString:sandboxFolder withString:@""];
180 [self searchCrashLogFile:[libFolderName stringByAppendingPathComponent:@"Logs/DiagnosticReports"]];
183 // Search machine diagnostic reports directory
184 if (_crashFile == nil) {
185 NSArray* libraryDirectories = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSLocalDomainMask, TRUE);
186 [self searchCrashLogFile:[[libraryDirectories lastObject] stringByAppendingPathComponent:@"Logs/DiagnosticReports"]];
187 if (_crashFile == nil) {
188 [self searchCrashLogFile:[[libraryDirectories lastObject] stringByAppendingPathComponent:@"Logs/CrashReporter"]];
196 NSDate *crashLogModificationDate = [[[NSFileManager defaultManager] attributesOfItemAtPath:_crashFile error:&error] fileModificationDate];
197 unsigned long long crashLogFileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:_crashFile error:&error] fileSize];
198 if ([crashLogModificationDate compare: lastCrashDate] == NSOrderedDescending && crashLogFileSize > 0) {
199 [self storeLastCrashDate:crashLogModificationDate];
207 - (void) returnToMainApplication {
208 if ( self.delegate != nil && [self.delegate respondsToSelector:@selector(showMainApplicationWindow)])
209 [self.delegate showMainApplicationWindow];
212 - (void) startManager {
213 if ([self hasPendingCrashReport]) {
214 if (!self.autoSubmitCrashReport) {
215 _quincyUI = [[BWQuincyUI alloc] initWithManager:self crashFile:_crashFile companyName:_companyName applicationName:[self applicationName]];
216 [_quincyUI askCrashReportDetails];
218 NSError* error = nil;
219 NSString *crashLogs = [NSString stringWithContentsOfFile:_crashFile encoding:NSUTF8StringEncoding error:&error];
221 NSString *lastCrash = [[crashLogs componentsSeparatedByString: @"**********\n\n"] lastObject];
223 NSString* description = @"";
225 if (_delegate && [_delegate respondsToSelector:@selector(crashReportDescription)]) {
226 description = [_delegate crashReportDescription];
229 [self sendReportCrash:lastCrash description:description];
231 [self returnToMainApplication];
235 [self returnToMainApplication];
239 - (NSString*) modelVersion {
240 NSString * modelString = nil;
241 int modelInfo[2] = { CTL_HW, HW_MODEL };
244 if (sysctl(modelInfo,
249 void * modelData = malloc(modelSize);
252 if (sysctl(modelInfo,
257 modelString = [NSString stringWithUTF8String:modelData];
269 - (void) cancelReport {
270 [self returnToMainApplication];
274 - (void) sendReportCrash:(NSString*)crashContent
275 description:(NSString*)notes
277 NSString *userid = @"";
278 NSString *contact = @"";
280 SInt32 versionMajor, versionMinor, versionBugFix;
281 if (Gestalt(gestaltSystemVersionMajor, &versionMajor) != noErr) versionMajor = 0;
282 if (Gestalt(gestaltSystemVersionMinor, &versionMinor) != noErr) versionMinor= 0;
283 if (Gestalt(gestaltSystemVersionBugFix, &versionBugFix) != noErr) versionBugFix = 0;
285 NSString* xml = [NSString stringWithFormat:@"<crash><applicationname>%s</applicationname><bundleidentifier>%s</bundleidentifier><systemversion>%@</systemversion><senderversion>%@</senderversion><version>%@</version><platform>%@</platform><userid>%@</userid><contact>%@</contact><description><![CDATA[%@]]></description><log><![CDATA[%@]]></log></crash>",
286 [[self applicationName] UTF8String],
287 [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"] UTF8String],
288 [NSString stringWithFormat:@"%i.%i.%i", versionMajor, versionMinor, versionBugFix],
289 [self applicationVersion],
290 [self applicationVersion],
299 [self returnToMainApplication];
301 [self _postXML:[NSString stringWithFormat:@"<crashes>%@</crashes>", xml] toURL:[NSURL URLWithString:self.submissionURL]];
304 - (void)_postXML:(NSString*)xml toURL:(NSURL*)url {
305 NSMutableURLRequest *request = nil;
306 NSString *boundary = @"----FOO";
308 if (self.appIdentifier) {
309 request = [NSMutableURLRequest requestWithURL:
310 [NSURL URLWithString:[NSString stringWithFormat:@"%@api/2/apps/%@/crashes?sdk=%@&sdk_version=%@",
312 [self.appIdentifier stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
318 request = [NSMutableURLRequest requestWithURL:url];
321 [request setValue:@"Quincy/Mac" forHTTPHeaderField:@"User-Agent"];
322 [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
323 [request setTimeoutInterval: 15];
324 [request setHTTPMethod:@"POST"];
325 NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
326 [request setValue:contentType forHTTPHeaderField:@"Content-type"];
328 NSMutableData *postBody = [NSMutableData data];
329 [postBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
330 if (self.appIdentifier) {
331 [postBody appendData:[@"Content-Disposition: form-data; name=\"xml\"; filename=\"crash.xml\"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
332 [postBody appendData:[[NSString stringWithFormat:@"Content-Type: text/xml\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
334 [postBody appendData:[@"Content-Disposition: form-data; name=\"xmlstring\"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
336 [postBody appendData:[xml dataUsingEncoding:NSUTF8StringEncoding]];
337 [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
338 [request setHTTPBody:postBody];
340 _serverResult = CrashReportStatusUnknown;
343 NSHTTPURLResponse *response = nil;
344 NSError *error = nil;
346 NSData *responseData = nil;
347 responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
348 _statusCode = [response statusCode];
350 if (responseData != nil) {
351 if (_statusCode >= 200 && _statusCode < 400) {
352 NSXMLParser *parser = [[NSXMLParser alloc] initWithData:responseData];
353 // Set self as the delegate of the parser so that it will receive the parser delegate methods callbacks.
354 [parser setDelegate:self];
355 // Depending on the XML document you're parsing, you may want to enable these features of NSXMLParser.
356 [parser setShouldProcessNamespaces:NO];
357 [parser setShouldReportNamespacePrefixes:NO];
358 [parser setShouldResolveExternalEntities:NO];
368 #pragma mark NSXMLParser
370 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
375 if ([elementName isEqualToString:@"result"]) {
376 _contentOfProperty = [NSMutableString string];
380 - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
385 if ([elementName isEqualToString:@"result"]) {
386 if ([_contentOfProperty intValue] > _serverResult) {
387 _serverResult = [_contentOfProperty intValue];
393 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
394 if (_contentOfProperty) {
395 // If the current element is one whose content we care about, append 'string'
396 // to the property that holds the content of the current element.
398 [_contentOfProperty appendString:string];
404 #pragma mark GetterSetter
406 - (NSString *) applicationName {
407 NSString *applicationName = [[[NSBundle mainBundle] localizedInfoDictionary] valueForKey: @"CFBundleExecutable"];
409 if (!applicationName)
410 applicationName = [[[NSBundle mainBundle] infoDictionary] valueForKey: @"CFBundleExecutable"];
412 return applicationName;
416 - (NSString*) applicationVersionString {
417 NSString* string = [[[NSBundle mainBundle] localizedInfoDictionary] valueForKey: @"CFBundleShortVersionString"];
420 string = [[[NSBundle mainBundle] infoDictionary] valueForKey: @"CFBundleShortVersionString"];
425 - (NSString *) applicationVersion {
426 NSString* string = [[[NSBundle mainBundle] localizedInfoDictionary] valueForKey: @"CFBundleVersion"];
429 string = [[[NSBundle mainBundle] infoDictionary] valueForKey: @"CFBundleVersion"];