diff --git a/src/client/mac/Breakpad.xcodeproj/project.pbxproj b/src/client/mac/Breakpad.xcodeproj/project.pbxproj index 2d396542..fc3e462f 100644 --- a/src/client/mac/Breakpad.xcodeproj/project.pbxproj +++ b/src/client/mac/Breakpad.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 163201D61443019E00C4DBF5 /* ConfigFile.h in Headers */ = {isa = PBXBuildFile; fileRef = 163201D41443019E00C4DBF5 /* ConfigFile.h */; }; 163201D71443019E00C4DBF5 /* ConfigFile.mm in Sources */ = {isa = PBXBuildFile; fileRef = 163201D51443019E00C4DBF5 /* ConfigFile.mm */; }; 163201E31443029300C4DBF5 /* ConfigFile.mm in Sources */ = {isa = PBXBuildFile; fileRef = 163201D51443019E00C4DBF5 /* ConfigFile.mm */; }; + 163202451443201300C4DBF5 /* uploader.m in Sources */ = {isa = PBXBuildFile; fileRef = 163202441443201300C4DBF5 /* uploader.m */; }; 3329D4ED0FA16D820007BBC5 /* Breakpad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3329D4EC0FA16D820007BBC5 /* Breakpad.xib */; }; 33880C800F9E097100817F82 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 33880C7E0F9E097100817F82 /* InfoPlist.strings */; }; 4084699D0F5D9CF900FDCA37 /* crash_report_sender.icns in Resources */ = {isa = PBXBuildFile; fileRef = 4084699C0F5D9CF900FDCA37 /* crash_report_sender.icns */; }; @@ -549,6 +550,8 @@ 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 163201D41443019E00C4DBF5 /* ConfigFile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ConfigFile.h; path = crash_generation/ConfigFile.h; sourceTree = ""; }; 163201D51443019E00C4DBF5 /* ConfigFile.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; fileEncoding = 4; name = ConfigFile.mm; path = crash_generation/ConfigFile.mm; sourceTree = ""; }; + 163202431443201300C4DBF5 /* uploader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = uploader.h; path = sender/uploader.h; sourceTree = ""; }; + 163202441443201300C4DBF5 /* uploader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = uploader.m; path = sender/uploader.m; sourceTree = ""; }; 32DBCF5E0370ADEE00C91783 /* Breakpad_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Breakpad_Prefix.pch; path = Framework/Breakpad_Prefix.pch; sourceTree = ""; }; 3329D4EC0FA16D820007BBC5 /* Breakpad.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Breakpad.xib; path = sender/Breakpad.xib; sourceTree = ""; }; 33880C7F0F9E097100817F82 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = sender/English.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -992,6 +995,8 @@ F92C56A60ECE04B6009BE4BA /* sender */ = { isa = PBXGroup; children = ( + 163202431443201300C4DBF5 /* uploader.h */, + 163202441443201300C4DBF5 /* uploader.m */, F9B6309F100FF96B00D0F4AC /* goArrow.png */, F92C56A70ECE04C5009BE4BA /* crash_report_sender.h */, F92C56A80ECE04C5009BE4BA /* crash_report_sender.m */, @@ -1732,6 +1737,7 @@ F9C44EA20EF09F93003AEBAA /* HTTPMultipartUpload.m in Sources */, F92C56A90ECE04C5009BE4BA /* crash_report_sender.m in Sources */, F9C44EE90EF0A3C1003AEBAA /* GTMLogger.m in Sources */, + 163202451443201300C4DBF5 /* uploader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/client/mac/sender/crash_report_sender.h b/src/client/mac/sender/crash_report_sender.h index c0728ca8..cff83fb4 100644 --- a/src/client/mac/sender/crash_report_sender.h +++ b/src/client/mac/sender/crash_report_sender.h @@ -34,15 +34,9 @@ #include -#include "client/mac/Framework/Breakpad.h" +#include "client/mac/sender/uploader.h" #import "GTMDefines.h" -#define kClientIdPreferenceKey @"clientid" - -extern NSString *const kGoogleServerType; -extern NSString *const kSocorroServerType; -extern NSString *const kDefaultServerType; - // We're sublcassing NSTextField in order to override a particular // method (see the implementation) that lets us reject changes if they // are longer than a particular length. Bindings would normally solve @@ -87,29 +81,12 @@ extern NSString *const kDefaultServerType; NSString *countdownMessage_; // Message indicating time // left for input. @private - int configFile_; // File descriptor for config file - NSMutableDictionary *parameters_; // Key value pairs of data (STRONG) - NSData *minidumpContents_; // The data in the minidump (STRONG) - NSData *logFileData_; // An NSdata for the tar, - // bz2'd log file. NSTimeInterval remainingDialogTime_; // Keeps track of how long // we have until we cancel // the dialog NSTimer *messageTimer_; // Timer we use to update // the dialog - NSMutableDictionary *serverDictionary_; // The dictionary mapping a - // server type name to a - // dictionary of server - // parameter names. - NSMutableDictionary *socorroDictionary_; // The dictionary for - // Socorro. - NSMutableDictionary *googleDictionary_; // The dictionary for - // Google. - NSMutableDictionary *extraServerVars_; // A dictionary containing - // extra key/value pairs - // that are uploaded to the - // crash server with the - // minidump. + Uploader* uploader_; // Uploader we use to send the data. } // Stops the modal panel with an NSAlertDefaultReturn value. This is the action diff --git a/src/client/mac/sender/crash_report_sender.m b/src/client/mac/sender/crash_report_sender.m index e3889761..155a9bf4 100644 --- a/src/client/mac/sender/crash_report_sender.m +++ b/src/client/mac/sender/crash_report_sender.m @@ -41,17 +41,12 @@ #define kLastSubmission @"LastSubmission" -const int kMinidumpFileLengthLimit = 800000; const int kUserCommentsMaxLength = 1500; const int kEmailMaxLength = 64; #define kApplePrefsSyncExcludeAllKey \ @"com.apple.PreferenceSync.ExcludeAllSyncKeys" -NSString *const kGoogleServerType = @"google"; -NSString *const kSocorroServerType = @"socorro"; -NSString *const kDefaultServerType = @"google"; - #pragma mark - @interface NSView (ResizabilityExtentions) @@ -160,18 +155,8 @@ NSString *const kDefaultServerType = @"google"; #pragma mark - - @interface Reporter(PrivateMethods) -+ (uid_t)consoleUID; - -- (id)initWithConfigurationFD:(int)fd; - -- (NSString *)readString; -- (NSData *)readData:(ssize_t)length; - -- (BOOL)readConfigurationData; -- (BOOL)readMinidumpData; -- (BOOL)readLogFileData; +- (id)initWithConfigFile:(const char *)configFile; // Returns YES if it has been long enough since the last report that we should // submit a report for this crash. @@ -221,30 +206,6 @@ NSString *const kDefaultServerType = @"google"; - (NSInteger)runModalWindow:(NSWindow*)window withTimeout:(NSTimeInterval)timeout; -// Returns a unique client id (user-specific), creating a persistent -// one in the user defaults, if necessary. -- (NSString*)clientID; - -// Returns a dictionary that can be used to map Breakpad parameter names to -// URL parameter names. -- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType; - -// Helper method to set HTTP parameters based on server type. This is -// called right before the upload - crashParameters will contain, on exit, -// URL parameters that should be sent with the minidump. -- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters; - -// Initialization helper to create dictionaries mapping Breakpad -// parameters to URL parameters -- (void)createServerParameterDictionaries; - -// Accessor method for the URL parameter dictionary -- (NSMutableDictionary *)urlParameterDictionary; - -// This method adds a key/value pair to the dictionary that -// will be uploaded to the crash server. -- (void)addServerParameter:(id)value forKey:(NSString *)key; - // This method is used to periodically update the UI with how many // seconds are left in the dialog display. - (void)updateSecondsLeftInDialogDisplay:(NSTimer*)theTimer; @@ -255,291 +216,24 @@ NSString *const kDefaultServerType = @"google"; // in their comments/email. - (void)controlTextDidBeginEditing:(NSNotification *)aNotification; -// Records the uploaded crash ID to the log file. -- (void)logUploadWithID:(const char *)uploadID; +- (void)report; @end @implementation Reporter //============================================================================= -+ (uid_t)consoleUID { - SCDynamicStoreRef store = - SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("Reporter"), NULL, NULL); - uid_t uid = -2; // Default to "nobody" - if (store) { - CFStringRef user = SCDynamicStoreCopyConsoleUser(store, &uid, NULL); - - if (user) - CFRelease(user); - else - uid = -2; - - CFRelease(store); - } - - return uid; -} - -//============================================================================= -- (id)initWithConfigurationFD:(int)fd { +- (id)initWithConfigFile:(const char *)configFile { if ((self = [super init])) { - configFile_ = fd; remainingDialogTime_ = 0; + uploader_ = [[Uploader alloc] initWithConfigFile:configFile]; + if (!uploader_) { + [self release]; + return nil; + } } - - // Because the reporter is embedded in the framework (and many copies - // of the framework may exist) its not completely certain that the OS - // will obey the com.apple.PreferenceSync.ExcludeAllSyncKeys in our - // Info.plist. To make sure, also set the key directly if needed. - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - if (![ud boolForKey:kApplePrefsSyncExcludeAllKey]) { - [ud setBool:YES forKey:kApplePrefsSyncExcludeAllKey]; - } - - [self createServerParameterDictionaries]; - return self; } -//============================================================================= -- (NSString *)readString { - NSMutableString *str = [NSMutableString stringWithCapacity:32]; - char ch[2] = { 0 }; - - while (read(configFile_, &ch[0], 1) == 1) { - if (ch[0] == '\n') { - // Break if this is the first newline after reading some other string - // data. - if ([str length]) - break; - } else { - [str appendString:[NSString stringWithUTF8String:ch]]; - } - } - - return str; -} - -//============================================================================= -- (NSData *)readData:(ssize_t)length { - NSMutableData *data = [NSMutableData dataWithLength:length]; - char *bytes = (char *)[data bytes]; - - if (read(configFile_, bytes, length) != length) - return nil; - - return data; -} - -//============================================================================= -- (BOOL)readConfigurationData { - parameters_ = [[NSMutableDictionary alloc] init]; - - while (1) { - NSString *key = [self readString]; - - if (![key length]) - break; - - // Read the data. Try to convert to a UTF-8 string, or just save - // the data - NSString *lenStr = [self readString]; - ssize_t len = [lenStr intValue]; - NSData *data = [self readData:len]; - id value = [[NSString alloc] initWithData:data - encoding:NSUTF8StringEncoding]; - - // If the keyname is prefixed by BREAKPAD_SERVER_PARAMETER_PREFIX - // that indicates that it should be uploaded to the server along - // with the minidump, so we treat it specially. - if ([key hasPrefix:@BREAKPAD_SERVER_PARAMETER_PREFIX]) { - NSString *urlParameterKey = - [key substringFromIndex:[@BREAKPAD_SERVER_PARAMETER_PREFIX length]]; - if ([urlParameterKey length]) { - if (value) { - [self addServerParameter:value - forKey:urlParameterKey]; - } else { - [self addServerParameter:data - forKey:urlParameterKey]; - } - } - } else { - [parameters_ setObject:(value ? value : data) forKey:key]; - } - [value release]; - } - - // generate a unique client ID based on this host's MAC address - // then add a key/value pair for it - NSString *clientID = [self clientID]; - [parameters_ setObject:clientID forKey:@"guid"]; - - close(configFile_); - configFile_ = -1; - - return YES; -} - -// Per user per machine -- (NSString *)clientID { - NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; - NSString *crashClientID = [ud stringForKey:kClientIdPreferenceKey]; - if (crashClientID) { - return crashClientID; - } - - // Otherwise, if we have no client id, generate one! - srandom((int)[[NSDate date] timeIntervalSince1970]); - long clientId1 = random(); - long clientId2 = random(); - long clientId3 = random(); - crashClientID = [NSString stringWithFormat:@"%x%x%x", - clientId1, clientId2, clientId3]; - - [ud setObject:crashClientID forKey:kClientIdPreferenceKey]; - [ud synchronize]; - return crashClientID; -} - -//============================================================================= -- (BOOL)readLogFileData { - unsigned int logFileCounter = 0; - - NSString *logPath; - size_t logFileTailSize = - [[parameters_ objectForKey:@BREAKPAD_LOGFILE_UPLOAD_SIZE] intValue]; - - NSMutableArray *logFilenames; // An array of NSString, one per log file - logFilenames = [[NSMutableArray alloc] init]; - - char tmpDirTemplate[80] = "/tmp/CrashUpload-XXXXX"; - char *tmpDir = mkdtemp(tmpDirTemplate); - - // Construct key names for the keys we expect to contain log file paths - for(logFileCounter = 0;; logFileCounter++) { - NSString *logFileKey = [NSString stringWithFormat:@"%@%d", - @BREAKPAD_LOGFILE_KEY_PREFIX, - logFileCounter]; - - logPath = [parameters_ objectForKey:logFileKey]; - - // They should all be consecutive, so if we don't find one, assume - // we're done - - if (!logPath) { - break; - } - - NSData *entireLogFile = [[NSData alloc] initWithContentsOfFile:logPath]; - - if (entireLogFile == nil) { - continue; - } - - NSRange fileRange; - - // Truncate the log file, only if necessary - - if ([entireLogFile length] <= logFileTailSize) { - fileRange = NSMakeRange(0, [entireLogFile length]); - } else { - fileRange = NSMakeRange([entireLogFile length] - logFileTailSize, - logFileTailSize); - } - - char tmpFilenameTemplate[100]; - - // Generate a template based on the log filename - sprintf(tmpFilenameTemplate,"%s/%s-XXXX", tmpDir, - [[logPath lastPathComponent] fileSystemRepresentation]); - - char *tmpFile = mktemp(tmpFilenameTemplate); - - NSData *logSubdata = [entireLogFile subdataWithRange:fileRange]; - NSString *tmpFileString = [NSString stringWithUTF8String:tmpFile]; - [logSubdata writeToFile:tmpFileString atomically:NO]; - - [logFilenames addObject:[tmpFileString lastPathComponent]]; - [entireLogFile release]; - } - - if ([logFilenames count] == 0) { - [logFilenames release]; - logFileData_ = nil; - return NO; - } - - // now, bzip all files into one - NSTask *tarTask = [[NSTask alloc] init]; - - [tarTask setCurrentDirectoryPath:[NSString stringWithUTF8String:tmpDir]]; - [tarTask setLaunchPath:@"/usr/bin/tar"]; - - NSMutableArray *bzipArgs = [NSMutableArray arrayWithObjects:@"-cjvf", - @"log.tar.bz2",nil]; - [bzipArgs addObjectsFromArray:logFilenames]; - - [logFilenames release]; - - [tarTask setArguments:bzipArgs]; - [tarTask launch]; - [tarTask waitUntilExit]; - [tarTask release]; - - NSString *logTarFile = [NSString stringWithFormat:@"%s/log.tar.bz2",tmpDir]; - logFileData_ = [[NSData alloc] initWithContentsOfFile:logTarFile]; - if (logFileData_ == nil) { - GTMLoggerDebug(@"Cannot find temp tar log file: %@", logTarFile); - return NO; - } - return YES; - -} - -//============================================================================= -- (BOOL)readMinidumpData { - NSString *minidumpDir = [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; - NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey]; - - if (![minidumpID length]) - return NO; - - NSString *path = [minidumpDir stringByAppendingPathComponent:minidumpID]; - path = [path stringByAppendingPathExtension:@"dmp"]; - - // check the size of the minidump and limit it to a reasonable size - // before attempting to load into memory and upload - const char *fileName = [path fileSystemRepresentation]; - struct stat fileStatus; - - BOOL success = YES; - - if (!stat(fileName, &fileStatus)) { - if (fileStatus.st_size > kMinidumpFileLengthLimit) { - fprintf(stderr, "Breakpad Reporter: minidump file too large " \ - "to upload : %d\n", (int)fileStatus.st_size); - success = NO; - } - } else { - fprintf(stderr, "Breakpad Reporter: unable to determine minidump " \ - "file length\n"); - success = NO; - } - - if (success) { - minidumpContents_ = [[NSData alloc] initWithContentsOfFile:path]; - success = ([minidumpContents_ length] ? YES : NO); - } - - if (!success) { - // something wrong with the minidump file -- delete it - unlink(fileName); - } - - return success; -} - //============================================================================= - (BOOL)askUserPermissionToSend { // Initialize Cocoa, needed to display the alert @@ -560,12 +254,14 @@ NSString *const kDefaultServerType = @"google"; buttonPressed = [self runModalWindow:alertWindow_ withTimeout:timeout]; - // Extract info from the user into the parameters_ dictionary + // Extract info from the user into the uploader_. if ([self commentsValue]) { - [parameters_ setObject:[self commentsValue] forKey:@BREAKPAD_COMMENTS]; + [[uploader_ parameters] setObject:[self commentsValue] + forKey:@BREAKPAD_COMMENTS]; } if ([self emailValue]) { - [parameters_ setObject:[self emailValue] forKey:@BREAKPAD_EMAIL]; + [[uploader_ parameters] setObject:[self emailValue] + forKey:@BREAKPAD_EMAIL]; } } else { // Create an alert panel to tell the user something happened @@ -804,9 +500,9 @@ doCommandBySelector:(SEL)commandSelector { #pragma mark - //============================================================================= - (BOOL)reportIntervalElapsed { - float interval = [[parameters_ objectForKey:@BREAKPAD_REPORT_INTERVAL] - floatValue]; - NSString *program = [parameters_ objectForKey:@BREAKPAD_PRODUCT]; + float interval = [[[uploader_ parameters] + objectForKey:@BREAKPAD_REPORT_INTERVAL] floatValue]; + NSString *program = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT]; NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; NSMutableDictionary *programDict = [NSMutableDictionary dictionaryWithDictionary:[ud dictionaryForKey:program]]; @@ -831,29 +527,30 @@ doCommandBySelector:(SEL)commandSelector { } - (BOOL)isOnDemand { - return [[parameters_ objectForKey:@BREAKPAD_ON_DEMAND] + return [[[uploader_ parameters] objectForKey:@BREAKPAD_ON_DEMAND] isEqualToString:@"YES"]; } - (BOOL)shouldSubmitSilently { - return [[parameters_ objectForKey:@BREAKPAD_SKIP_CONFIRM] + return [[[uploader_ parameters] objectForKey:@BREAKPAD_SKIP_CONFIRM] isEqualToString:@"YES"]; } - (BOOL)shouldRequestComments { - return [[parameters_ objectForKey:@BREAKPAD_REQUEST_COMMENTS] + return [[[uploader_ parameters] objectForKey:@BREAKPAD_REQUEST_COMMENTS] isEqualToString:@"YES"]; } - (BOOL)shouldRequestEmail { - return [[parameters_ objectForKey:@BREAKPAD_REQUEST_EMAIL] + return [[[uploader_ parameters] objectForKey:@BREAKPAD_REQUEST_EMAIL] isEqualToString:@"YES"]; } - (NSString*)shortDialogMessage { - NSString *displayName = [parameters_ objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; + NSString *displayName = + [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; if (![displayName length]) - displayName = [parameters_ objectForKey:@BREAKPAD_PRODUCT]; + displayName = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT]; if ([self isOnDemand]) { return [NSString @@ -867,11 +564,12 @@ doCommandBySelector:(SEL)commandSelector { } - (NSString*)explanatoryDialogText { - NSString *displayName = [parameters_ objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; + NSString *displayName = + [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; if (![displayName length]) - displayName = [parameters_ objectForKey:@BREAKPAD_PRODUCT]; + displayName = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT]; - NSString *vendor = [parameters_ objectForKey:@BREAKPAD_VENDOR]; + NSString *vendor = [[uploader_ parameters] objectForKey:@BREAKPAD_VENDOR]; if (![vendor length]) vendor = @"unknown vendor"; @@ -888,8 +586,8 @@ doCommandBySelector:(SEL)commandSelector { - (NSTimeInterval)messageTimeout { // Get the timeout value for the notification. - NSTimeInterval timeout = [[parameters_ objectForKey:@BREAKPAD_CONFIRM_TIMEOUT] - floatValue]; + NSTimeInterval timeout = [[[uploader_ parameters] + objectForKey:@BREAKPAD_CONFIRM_TIMEOUT] floatValue]; // Require a timeout of at least a minute (except 0, which means no timeout). if (timeout > 0.001 && timeout < 60.0) { timeout = 60.0; @@ -897,194 +595,13 @@ doCommandBySelector:(SEL)commandSelector { return timeout; } -- (void)createServerParameterDictionaries { - serverDictionary_ = [[NSMutableDictionary alloc] init]; - socorroDictionary_ = [[NSMutableDictionary alloc] init]; - googleDictionary_ = [[NSMutableDictionary alloc] init]; - extraServerVars_ = [[NSMutableDictionary alloc] init]; - - [serverDictionary_ setObject:socorroDictionary_ forKey:kSocorroServerType]; - [serverDictionary_ setObject:googleDictionary_ forKey:kGoogleServerType]; - - [googleDictionary_ setObject:@"ptime" forKey:@BREAKPAD_PROCESS_UP_TIME]; - [googleDictionary_ setObject:@"email" forKey:@BREAKPAD_EMAIL]; - [googleDictionary_ setObject:@"comments" forKey:@BREAKPAD_COMMENTS]; - [googleDictionary_ setObject:@"prod" forKey:@BREAKPAD_PRODUCT]; - [googleDictionary_ setObject:@"ver" forKey:@BREAKPAD_VERSION]; - - [socorroDictionary_ setObject:@"Comments" forKey:@BREAKPAD_COMMENTS]; - [socorroDictionary_ setObject:@"CrashTime" - forKey:@BREAKPAD_PROCESS_CRASH_TIME]; - [socorroDictionary_ setObject:@"StartupTime" - forKey:@BREAKPAD_PROCESS_START_TIME]; - [socorroDictionary_ setObject:@"Version" - forKey:@BREAKPAD_VERSION]; - [socorroDictionary_ setObject:@"ProductName" - forKey:@BREAKPAD_PRODUCT]; - [socorroDictionary_ setObject:@"Email" - forKey:@BREAKPAD_EMAIL]; -} - -- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType { - if (serverType == nil || [serverType length] == 0) { - return [serverDictionary_ objectForKey:kDefaultServerType]; - } - return [serverDictionary_ objectForKey:serverType]; -} - -- (NSMutableDictionary *)urlParameterDictionary { - NSString *serverType = [parameters_ objectForKey:@BREAKPAD_SERVER_TYPE]; - return [self dictionaryForServerType:serverType]; - -} - -- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters { - NSDictionary *urlParameterNames = [self urlParameterDictionary]; - - id key; - NSEnumerator *enumerator = [parameters_ keyEnumerator]; - - while ((key = [enumerator nextObject])) { - // The key from parameters_ corresponds to a key in - // urlParameterNames. The value in parameters_ gets stored in - // crashParameters with a key that is the value in - // urlParameterNames. - - // For instance, if parameters_ has [PRODUCT_NAME => "FOOBAR"] and - // urlParameterNames has [PRODUCT_NAME => "pname"] the final HTTP - // URL parameter becomes [pname => "FOOBAR"]. - NSString *breakpadParameterName = (NSString *)key; - NSString *urlParameter = [urlParameterNames - objectForKey:breakpadParameterName]; - if (urlParameter) { - [crashParameters setObject:[parameters_ objectForKey:key] - forKey:urlParameter]; - } - } - - // Now, add the parameters that were added by the application. - enumerator = [extraServerVars_ keyEnumerator]; - - while ((key = [enumerator nextObject])) { - NSString *urlParameterName = (NSString *)key; - NSString *urlParameterValue = - [extraServerVars_ objectForKey:urlParameterName]; - [crashParameters setObject:urlParameterValue - forKey:urlParameterName]; - } - return YES; -} - -- (void)addServerParameter:(id)value forKey:(NSString *)key { - [extraServerVars_ setObject:value forKey:key]; -} - -//============================================================================= - (void)report { - NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]]; - HTTPMultipartUpload *upload = [[HTTPMultipartUpload alloc] initWithURL:url]; - NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary]; - - if (![self populateServerDictionary:uploadParameters]) { - return; - } - - [upload setParameters:uploadParameters]; - - // Add minidump file - if (minidumpContents_) { - [upload addFileContents:minidumpContents_ name:@"upload_file_minidump"]; - - // Send it - NSError *error = nil; - NSData *data = [upload send:&error]; - NSString *result = [[NSString alloc] initWithData:data - encoding:NSUTF8StringEncoding]; - const char *reportID = "ERR"; - - if (error) { - fprintf(stderr, "Breakpad Reporter: Send Error: %s\n", - [[error description] UTF8String]); - } else { - NSCharacterSet *trimSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; - reportID = [[result stringByTrimmingCharactersInSet:trimSet] UTF8String]; - [self logUploadWithID:reportID]; - } - - // rename the minidump file according to the id returned from the server - NSString *minidumpDir = [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; - NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey]; - - NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp", - minidumpDir, minidumpID]; - NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp", - minidumpDir, reportID]; - - const char *src = [srcString fileSystemRepresentation]; - const char *dest = [destString fileSystemRepresentation]; - - if (rename(src, dest) == 0) { - GTMLoggerInfo(@"Breakpad Reporter: Renamed %s to %s after successful " \ - "upload",src, dest); - } - else { - // can't rename - don't worry - it's not important for users - GTMLoggerDebug(@"Breakpad Reporter: successful upload report ID = %s\n", - reportID ); - } - [result release]; - } - - if (logFileData_) { - HTTPMultipartUpload *logUpload = [[HTTPMultipartUpload alloc] initWithURL:url]; - - [uploadParameters setObject:@"log" forKey:@"type"]; - [logUpload setParameters:uploadParameters]; - [logUpload addFileContents:logFileData_ name:@"log"]; - - NSError *error = nil; - NSData *data = [logUpload send:&error]; - NSString *result = [[NSString alloc] initWithData:data - encoding:NSUTF8StringEncoding]; - [result release]; - [logUpload release]; - } - - [upload release]; -} - -- (void)logUploadWithID:(const char *)uploadID { - NSString *minidumpDir = - [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; - NSString *logFilePath = [NSString stringWithFormat:@"%@/%s", - minidumpDir, kReporterLogFilename]; - NSString *logLine = [NSString stringWithFormat:@"%0.f,%s\n", - [[NSDate date] timeIntervalSince1970], uploadID]; - NSData *logData = [logLine dataUsingEncoding:kCFStringEncodingUTF8]; - - NSFileManager *fileManager = [NSFileManager defaultManager]; - if ([fileManager fileExistsAtPath:logFilePath]) { - NSFileHandle *logFileHandle = - [NSFileHandle fileHandleForWritingAtPath:logFilePath]; - [logFileHandle seekToEndOfFile]; - [logFileHandle writeData:logData]; - [logFileHandle closeFile]; - } else { - [fileManager createFileAtPath:logFilePath - contents:logData - attributes:nil]; - } + [uploader_ report]; } //============================================================================= - (void)dealloc { - [parameters_ release]; - [minidumpContents_ release]; - [logFileData_ release]; - [googleDictionary_ release]; - [socorroDictionary_ release]; - [serverDictionary_ release]; - [extraServerVars_ release]; + [uploader_ release]; [super dealloc]; } @@ -1175,40 +692,12 @@ int main(int argc, const char *argv[]) { exit(1); } - // Open the file before (potentially) switching to console user - int configFile = open(argv[1], O_RDONLY, 0600); - - if (configFile == -1) { - GTMLoggerDebug(@"Couldn't open config file %s - %s", - argv[1], - strerror(errno)); - } - - // we want to avoid a build-up of old config files even if they - // have been incorrectly written by the framework - unlink(argv[1]); - - if (configFile == -1) { - GTMLoggerDebug(@"Couldn't unlink config file %s - %s", - argv[1], - strerror(errno)); + Reporter *reporter = [[Reporter alloc] initWithConfigFile:argv[1]]; + if (!reporter) { + GTMLoggerDebug(@"reporter initialization failed"); exit(1); } - Reporter *reporter = [[Reporter alloc] initWithConfigurationFD:configFile]; - - // Gather the configuration data - if (![reporter readConfigurationData]) { - GTMLoggerDebug(@"reporter readConfigurationData failed"); - exit(1); - } - - // Read the minidump into memory before we (potentially) switch from the - // root user - [reporter readMinidumpData]; - - [reporter readLogFileData]; - // only submit a report if we have not recently crashed in the past BOOL shouldSubmitReport = [reporter reportIntervalElapsed]; BOOL okayToSend = NO; diff --git a/src/client/mac/sender/uploader.h b/src/client/mac/sender/uploader.h new file mode 100644 index 00000000..653bfacd --- /dev/null +++ b/src/client/mac/sender/uploader.h @@ -0,0 +1,74 @@ +// Copyright (c) 2011, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// This component uses the HTTPMultipartUpload of the breakpad project to send +// the minidump and associated data to the crash reporting servers. +// It will perform throttling based on the parameters passed to it and will +// prompt the user to send the minidump. + +#include + +#include "client/mac/Framework/Breakpad.h" +#import "common/mac/GTMDefines.h" + +#define kClientIdPreferenceKey @"clientid" + +extern NSString *const kGoogleServerType; +extern NSString *const kSocorroServerType; +extern NSString *const kDefaultServerType; + +@interface Uploader : NSObject { + @private + int configFile_; // File descriptor for config file + NSMutableDictionary *parameters_; // Key value pairs of data (STRONG) + NSData *minidumpContents_; // The data in the minidump (STRONG) + NSData *logFileData_; // An NSdata for the tar, + // bz2'd log file. + NSMutableDictionary *serverDictionary_; // The dictionary mapping a + // server type name to a + // dictionary of server + // parameter names. + NSMutableDictionary *socorroDictionary_; // The dictionary for + // Socorro. + NSMutableDictionary *googleDictionary_; // The dictionary for + // Google. + NSMutableDictionary *extraServerVars_; // A dictionary containing + // extra key/value pairs + // that are uploaded to the + // crash server with the + // minidump. +} + +- (id)initWithConfigFile:(const char *)configFile; + +- (NSMutableDictionary *)parameters; + +- (void)report; + +@end diff --git a/src/client/mac/sender/uploader.m b/src/client/mac/sender/uploader.m new file mode 100644 index 00000000..45d98a54 --- /dev/null +++ b/src/client/mac/sender/uploader.m @@ -0,0 +1,579 @@ +// Copyright (c) 2011, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#import +#import +#import + +#import + +#import "common/mac/HTTPMultipartUpload.h" + +#import "client/mac/sender/uploader.h" +#import "common/mac/GTMLogger.h" + + +const int kMinidumpFileLengthLimit = 800000; + +#define kApplePrefsSyncExcludeAllKey \ + @"com.apple.PreferenceSync.ExcludeAllSyncKeys" + +NSString *const kGoogleServerType = @"google"; +NSString *const kSocorroServerType = @"socorro"; +NSString *const kDefaultServerType = @"google"; + +#define GTMLoggerDebug NSLog + +#pragma mark - + +@interface Uploader(PrivateMethods) +- (NSString *)readString; +- (NSData *)readData:(ssize_t)length; + +- (BOOL)readConfigurationData; +- (BOOL)readMinidumpData; +- (BOOL)readLogFileData; + +// Returns a unique client id (user-specific), creating a persistent +// one in the user defaults, if necessary. +- (NSString*)clientID; + +// Returns a dictionary that can be used to map Breakpad parameter names to +// URL parameter names. +- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType; + +// Helper method to set HTTP parameters based on server type. This is +// called right before the upload - crashParameters will contain, on exit, +// URL parameters that should be sent with the minidump. +- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters; + +// Initialization helper to create dictionaries mapping Breakpad +// parameters to URL parameters +- (void)createServerParameterDictionaries; + +// Accessor method for the URL parameter dictionary +- (NSMutableDictionary *)urlParameterDictionary; + +// This method adds a key/value pair to the dictionary that +// will be uploaded to the crash server. +- (void)addServerParameter:(id)value forKey:(NSString *)key; + +// Records the uploaded crash ID to the log file. +- (void)logUploadWithID:(const char *)uploadID; + +@end + +@implementation Uploader + +//============================================================================= +- (id)initWithConfigFile:(const char *)configFile { + if ((self = [super init])) { + + configFile_ = open(configFile, O_RDONLY, 0600); + if (configFile_ == -1) { + GTMLoggerDebug(@"Couldn't open config file %s - %s", + configFile, + strerror(errno)); + } + + // we want to avoid a build-up of old config files even if they + // have been incorrectly written by the framework + if (unlink(configFile)) { + GTMLoggerDebug(@"Couldn't unlink config file %s - %s", + configFile, + strerror(errno)); + } + + if (configFile_ == -1) { + [self release]; + return nil; + } + + // Because the reporter is embedded in the framework (and many copies + // of the framework may exist) its not completely certain that the OS + // will obey the com.apple.PreferenceSync.ExcludeAllSyncKeys in our + // Info.plist. To make sure, also set the key directly if needed. + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + if (![ud boolForKey:kApplePrefsSyncExcludeAllKey]) { + [ud setBool:YES forKey:kApplePrefsSyncExcludeAllKey]; + } + + [self createServerParameterDictionaries]; + + if (![self readConfigurationData]) { + GTMLoggerDebug(@"uploader readConfigurationData failed"); + [self release]; + return nil; + } + + // Read the minidump into memory. + [self readMinidumpData]; + [self readLogFileData]; + + } + return self; +} + +//============================================================================= +- (NSString *)readString { + NSMutableString *str = [NSMutableString stringWithCapacity:32]; + char ch[2] = { 0 }; + + while (read(configFile_, &ch[0], 1) == 1) { + if (ch[0] == '\n') { + // Break if this is the first newline after reading some other string + // data. + if ([str length]) + break; + } else { + [str appendString:[NSString stringWithUTF8String:ch]]; + } + } + + return str; +} + +//============================================================================= +- (NSData *)readData:(ssize_t)length { + NSMutableData *data = [NSMutableData dataWithLength:length]; + char *bytes = (char *)[data bytes]; + + if (read(configFile_, bytes, length) != length) + return nil; + + return data; +} + +//============================================================================= +- (BOOL)readConfigurationData { + parameters_ = [[NSMutableDictionary alloc] init]; + + while (1) { + NSString *key = [self readString]; + + if (![key length]) + break; + + // Read the data. Try to convert to a UTF-8 string, or just save + // the data + NSString *lenStr = [self readString]; + ssize_t len = [lenStr intValue]; + NSData *data = [self readData:len]; + id value = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + + // If the keyname is prefixed by BREAKPAD_SERVER_PARAMETER_PREFIX + // that indicates that it should be uploaded to the server along + // with the minidump, so we treat it specially. + if ([key hasPrefix:@BREAKPAD_SERVER_PARAMETER_PREFIX]) { + NSString *urlParameterKey = + [key substringFromIndex:[@BREAKPAD_SERVER_PARAMETER_PREFIX length]]; + if ([urlParameterKey length]) { + if (value) { + [self addServerParameter:value + forKey:urlParameterKey]; + } else { + [self addServerParameter:data + forKey:urlParameterKey]; + } + } + } else { + [parameters_ setObject:(value ? value : data) forKey:key]; + } + [value release]; + } + + // generate a unique client ID based on this host's MAC address + // then add a key/value pair for it + NSString *clientID = [self clientID]; + [parameters_ setObject:clientID forKey:@"guid"]; + + close(configFile_); + configFile_ = -1; + + return YES; +} + +// Per user per machine +- (NSString *)clientID { + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + NSString *crashClientID = [ud stringForKey:kClientIdPreferenceKey]; + if (crashClientID) { + return crashClientID; + } + + // Otherwise, if we have no client id, generate one! + srandom((int)[[NSDate date] timeIntervalSince1970]); + long clientId1 = random(); + long clientId2 = random(); + long clientId3 = random(); + crashClientID = [NSString stringWithFormat:@"%x%x%x", + clientId1, clientId2, clientId3]; + + [ud setObject:crashClientID forKey:kClientIdPreferenceKey]; + [ud synchronize]; + return crashClientID; +} + +//============================================================================= +- (BOOL)readLogFileData { + unsigned int logFileCounter = 0; + + NSString *logPath; + size_t logFileTailSize = + [[parameters_ objectForKey:@BREAKPAD_LOGFILE_UPLOAD_SIZE] intValue]; + + NSMutableArray *logFilenames; // An array of NSString, one per log file + logFilenames = [[NSMutableArray alloc] init]; + + char tmpDirTemplate[80] = "/tmp/CrashUpload-XXXXX"; + char *tmpDir = mkdtemp(tmpDirTemplate); + + // Construct key names for the keys we expect to contain log file paths + for(logFileCounter = 0;; logFileCounter++) { + NSString *logFileKey = [NSString stringWithFormat:@"%@%d", + @BREAKPAD_LOGFILE_KEY_PREFIX, + logFileCounter]; + + logPath = [parameters_ objectForKey:logFileKey]; + + // They should all be consecutive, so if we don't find one, assume + // we're done + + if (!logPath) { + break; + } + + NSData *entireLogFile = [[NSData alloc] initWithContentsOfFile:logPath]; + + if (entireLogFile == nil) { + continue; + } + + NSRange fileRange; + + // Truncate the log file, only if necessary + + if ([entireLogFile length] <= logFileTailSize) { + fileRange = NSMakeRange(0, [entireLogFile length]); + } else { + fileRange = NSMakeRange([entireLogFile length] - logFileTailSize, + logFileTailSize); + } + + char tmpFilenameTemplate[100]; + + // Generate a template based on the log filename + sprintf(tmpFilenameTemplate,"%s/%s-XXXX", tmpDir, + [[logPath lastPathComponent] fileSystemRepresentation]); + + char *tmpFile = mktemp(tmpFilenameTemplate); + + NSData *logSubdata = [entireLogFile subdataWithRange:fileRange]; + NSString *tmpFileString = [NSString stringWithUTF8String:tmpFile]; + [logSubdata writeToFile:tmpFileString atomically:NO]; + + [logFilenames addObject:[tmpFileString lastPathComponent]]; + [entireLogFile release]; + } + + if ([logFilenames count] == 0) { + [logFilenames release]; + logFileData_ = nil; + return NO; + } + + // now, bzip all files into one + NSTask *tarTask = [[NSTask alloc] init]; + + [tarTask setCurrentDirectoryPath:[NSString stringWithUTF8String:tmpDir]]; + [tarTask setLaunchPath:@"/usr/bin/tar"]; + + NSMutableArray *bzipArgs = [NSMutableArray arrayWithObjects:@"-cjvf", + @"log.tar.bz2",nil]; + [bzipArgs addObjectsFromArray:logFilenames]; + + [logFilenames release]; + + [tarTask setArguments:bzipArgs]; + [tarTask launch]; + [tarTask waitUntilExit]; + [tarTask release]; + + NSString *logTarFile = [NSString stringWithFormat:@"%s/log.tar.bz2",tmpDir]; + logFileData_ = [[NSData alloc] initWithContentsOfFile:logTarFile]; + if (logFileData_ == nil) { + GTMLoggerDebug(@"Cannot find temp tar log file: %@", logTarFile); + return NO; + } + return YES; +} + +//============================================================================= +- (BOOL)readMinidumpData { + NSString *minidumpDir = + [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; + NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey]; + + if (![minidumpID length]) + return NO; + + NSString *path = [minidumpDir stringByAppendingPathComponent:minidumpID]; + path = [path stringByAppendingPathExtension:@"dmp"]; + + // check the size of the minidump and limit it to a reasonable size + // before attempting to load into memory and upload + const char *fileName = [path fileSystemRepresentation]; + struct stat fileStatus; + + BOOL success = YES; + + if (!stat(fileName, &fileStatus)) { + if (fileStatus.st_size > kMinidumpFileLengthLimit) { + fprintf(stderr, "Breakpad Uploader: minidump file too large " \ + "to upload : %d\n", (int)fileStatus.st_size); + success = NO; + } + } else { + fprintf(stderr, "Breakpad Uploader: unable to determine minidump " \ + "file length\n"); + success = NO; + } + + if (success) { + minidumpContents_ = [[NSData alloc] initWithContentsOfFile:path]; + success = ([minidumpContents_ length] ? YES : NO); + } + + if (!success) { + // something wrong with the minidump file -- delete it + unlink(fileName); + } + + return success; +} + +#pragma mark - +//============================================================================= + +- (void)createServerParameterDictionaries { + serverDictionary_ = [[NSMutableDictionary alloc] init]; + socorroDictionary_ = [[NSMutableDictionary alloc] init]; + googleDictionary_ = [[NSMutableDictionary alloc] init]; + extraServerVars_ = [[NSMutableDictionary alloc] init]; + + [serverDictionary_ setObject:socorroDictionary_ forKey:kSocorroServerType]; + [serverDictionary_ setObject:googleDictionary_ forKey:kGoogleServerType]; + + [googleDictionary_ setObject:@"ptime" forKey:@BREAKPAD_PROCESS_UP_TIME]; + [googleDictionary_ setObject:@"email" forKey:@BREAKPAD_EMAIL]; + [googleDictionary_ setObject:@"comments" forKey:@BREAKPAD_COMMENTS]; + [googleDictionary_ setObject:@"prod" forKey:@BREAKPAD_PRODUCT]; + [googleDictionary_ setObject:@"ver" forKey:@BREAKPAD_VERSION]; + + [socorroDictionary_ setObject:@"Comments" forKey:@BREAKPAD_COMMENTS]; + [socorroDictionary_ setObject:@"CrashTime" + forKey:@BREAKPAD_PROCESS_CRASH_TIME]; + [socorroDictionary_ setObject:@"StartupTime" + forKey:@BREAKPAD_PROCESS_START_TIME]; + [socorroDictionary_ setObject:@"Version" + forKey:@BREAKPAD_VERSION]; + [socorroDictionary_ setObject:@"ProductName" + forKey:@BREAKPAD_PRODUCT]; + [socorroDictionary_ setObject:@"Email" + forKey:@BREAKPAD_EMAIL]; +} + +- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType { + if (serverType == nil || [serverType length] == 0) { + return [serverDictionary_ objectForKey:kDefaultServerType]; + } + return [serverDictionary_ objectForKey:serverType]; +} + +- (NSMutableDictionary *)urlParameterDictionary { + NSString *serverType = [parameters_ objectForKey:@BREAKPAD_SERVER_TYPE]; + return [self dictionaryForServerType:serverType]; + +} + +- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters { + NSDictionary *urlParameterNames = [self urlParameterDictionary]; + + id key; + NSEnumerator *enumerator = [parameters_ keyEnumerator]; + + while ((key = [enumerator nextObject])) { + // The key from parameters_ corresponds to a key in + // urlParameterNames. The value in parameters_ gets stored in + // crashParameters with a key that is the value in + // urlParameterNames. + + // For instance, if parameters_ has [PRODUCT_NAME => "FOOBAR"] and + // urlParameterNames has [PRODUCT_NAME => "pname"] the final HTTP + // URL parameter becomes [pname => "FOOBAR"]. + NSString *breakpadParameterName = (NSString *)key; + NSString *urlParameter = [urlParameterNames + objectForKey:breakpadParameterName]; + if (urlParameter) { + [crashParameters setObject:[parameters_ objectForKey:key] + forKey:urlParameter]; + } + } + + // Now, add the parameters that were added by the application. + enumerator = [extraServerVars_ keyEnumerator]; + + while ((key = [enumerator nextObject])) { + NSString *urlParameterName = (NSString *)key; + NSString *urlParameterValue = + [extraServerVars_ objectForKey:urlParameterName]; + [crashParameters setObject:urlParameterValue + forKey:urlParameterName]; + } + return YES; +} + +- (void)addServerParameter:(id)value forKey:(NSString *)key { + [extraServerVars_ setObject:value forKey:key]; +} + +//============================================================================= +- (void)report { + NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]]; + HTTPMultipartUpload *upload = [[HTTPMultipartUpload alloc] initWithURL:url]; + NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary]; + + if (![self populateServerDictionary:uploadParameters]) { + return; + } + + [upload setParameters:uploadParameters]; + + // Add minidump file + if (minidumpContents_) { + [upload addFileContents:minidumpContents_ name:@"upload_file_minidump"]; + + // Send it + NSError *error = nil; + NSData *data = [upload send:&error]; + NSString *result = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + const char *reportID = "ERR"; + + if (error) { + fprintf(stderr, "Breakpad Uploader: Send Error: %s\n", + [[error description] UTF8String]); + } else { + NSCharacterSet *trimSet = + [NSCharacterSet whitespaceAndNewlineCharacterSet]; + reportID = [[result stringByTrimmingCharactersInSet:trimSet] UTF8String]; + [self logUploadWithID:reportID]; + } + + // rename the minidump file according to the id returned from the server + NSString *minidumpDir = + [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; + NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey]; + + NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp", + minidumpDir, minidumpID]; + NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp", + minidumpDir, reportID]; + + const char *src = [srcString fileSystemRepresentation]; + const char *dest = [destString fileSystemRepresentation]; + + if (rename(src, dest) == 0) { + GTMLoggerInfo(@"Breakpad Uploader: Renamed %s to %s after successful " \ + "upload",src, dest); + } + else { + // can't rename - don't worry - it's not important for users + GTMLoggerDebug(@"Breakpad Uploader: successful upload report ID = %s\n", + reportID ); + } + [result release]; + } + + if (logFileData_) { + HTTPMultipartUpload *logUpload = + [[HTTPMultipartUpload alloc] initWithURL:url]; + + [uploadParameters setObject:@"log" forKey:@"type"]; + [logUpload setParameters:uploadParameters]; + [logUpload addFileContents:logFileData_ name:@"log"]; + + NSError *error = nil; + NSData *data = [logUpload send:&error]; + NSString *result = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + [result release]; + [logUpload release]; + } + + [upload release]; +} + +- (void)logUploadWithID:(const char *)uploadID { + NSString *minidumpDir = + [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; + NSString *logFilePath = [NSString stringWithFormat:@"%@/%s", + minidumpDir, kReporterLogFilename]; + NSString *logLine = [NSString stringWithFormat:@"%0.f,%s\n", + [[NSDate date] timeIntervalSince1970], uploadID]; + NSData *logData = [logLine dataUsingEncoding:NSUTF8StringEncoding]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:logFilePath]) { + NSFileHandle *logFileHandle = + [NSFileHandle fileHandleForWritingAtPath:logFilePath]; + [logFileHandle seekToEndOfFile]; + [logFileHandle writeData:logData]; + [logFileHandle closeFile]; + } else { + [fileManager createFileAtPath:logFilePath + contents:logData + attributes:nil]; + } +} + +//============================================================================= +- (void)dealloc { + [parameters_ release]; + [minidumpContents_ release]; + [logFileData_ release]; + [googleDictionary_ release]; + [socorroDictionary_ release]; + [serverDictionary_ release]; + [extraServerVars_ release]; + [super dealloc]; +} + +@end