1
// Copyright 2008, Google Inc.
3
// Redistribution and use in source and binary forms, with or without
4
// modification, are permitted provided that the following conditions are met:
6
// 1. Redistributions of source code must retain the above copyright notice,
7
// this list of conditions and the following disclaimer.
8
// 2. Redistributions in binary form must reproduce the above copyright notice,
9
// this list of conditions and the following disclaimer in the documentation
10
// and/or other materials provided with the distribution.
11
// 3. Neither the name of Google Inc. nor the names of its contributors may be
12
// used to endorse or promote products derived from this software without
13
// specific prior written permission.
15
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
16
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
17
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
18
// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21
// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
#import <SystemConfiguration/SystemConfiguration.h>
32
#import "third_party/breakpad_osx/src/common/mac/HTTPMultipartUpload.h"
33
#import "gears/base/common/exception_handler_osx/nshost_macaddress.h"
35
#import "gears/crash_sender/crash_sender_osx.h"
37
#define kLastSubmission @"LastSubmission"
39
// Gears: Increase default crash limit of 200K -> 800K.
40
const int kMinidumpFileLengthLimit = 800000;
42
#define kApplePrefsSyncExcludeAllKey @"com.apple.PreferenceSync.ExcludeAllSyncKeys"
44
@interface Reporter(PrivateMethods)
47
- (id)initWithConfigurationFD:(int)fd;
49
- (NSString *)readString;
50
- (NSData *)readData:(size_t)length;
52
- (BOOL)readConfigurationData;
53
- (BOOL)readMinidumpData;
55
- (BOOL)askUserPermissionToSend:(BOOL)shouldSubmitReport;
56
- (BOOL)shouldSubmitReport;
59
@implementation Reporter
60
//=============================================================================
62
SCDynamicStoreRef store =
63
SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("Reporter"), NULL, NULL);
64
uid_t uid = -2; // Default to "nobody"
66
CFStringRef user = SCDynamicStoreCopyConsoleUser(store, &uid, NULL);
79
//=============================================================================
80
- (id)initWithConfigurationFD:(int)fd {
81
if ((self = [super init])) {
85
// Because the reporter is embedded in the framework (and many copies
86
// of the framework may exist) its not completely certain that the OS
87
// will obey the com.apple.PreferenceSync.ExcludeAllSyncKeys in our
88
// Info.plist. To make sure, also set the key directly if needed.
89
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
90
if (![ud boolForKey:kApplePrefsSyncExcludeAllKey]) {
91
[ud setBool:YES forKey:kApplePrefsSyncExcludeAllKey];
97
//=============================================================================
98
- (NSString *)readString {
99
NSMutableString *str = [NSMutableString stringWithCapacity:32];
102
while (read(configFile_, &ch[0], 1) == 1) {
104
// Break if this is the first newline after reading some other string
109
[str appendString:[NSString stringWithUTF8String:ch]];
116
//=============================================================================
117
- (NSData *)readData:(size_t)length {
118
NSMutableData *data = [NSMutableData dataWithLength:length];
119
char *bytes = (char *)[data bytes];
121
if (read(configFile_, bytes, length) != length)
127
//=============================================================================
128
- (BOOL)readConfigurationData {
129
parameters_ = [[NSMutableDictionary alloc] init];
132
NSString *key = [self readString];
137
// Read the data. Try to convert to a UTF-8 string, or just save
139
NSString *lenStr = [self readString];
140
size_t len = [lenStr intValue];
141
NSData *data = [self readData:len];
142
id value = [[NSString alloc] initWithData:data
143
encoding:NSUTF8StringEncoding];
145
[parameters_ setObject:value ? value : data forKey:key];
150
// !!@ JUST FOR TESTING
151
[parameters_ setObject:@"******** THIS IS NOT A REAL CRASH - JUST TESTING!! *******************"
152
forKey:@"TestingMessage1"];
153
[parameters_ setObject:@"******** THIS IS NOT A REAL CRASH - IGNORE!! *******************"
154
forKey:@"TestingMessage2"];
155
[parameters_ setObject:@"******** THIS IS NOT A REAL CRASH - IGNORE!! *******************"
156
forKey:@"TestingMessage3"];
157
[parameters_ setObject:@"******** THIS IS NOT A REAL CRASH - IGNORE!! *******************"
158
forKey:@"TestingMessage4"];
161
// generate a unique client ID based on this host's MAC address
162
// then add a key/value pair for it
163
NSString *clientID = [[NSHost currentHost] obfuscatedMACAddress];
164
[parameters_ setObject:clientID forKey:@"guid"];
172
//=============================================================================
173
- (BOOL)readMinidumpData {
174
NSString *minidumpDir = [parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
175
NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];
177
if (![minidumpID length])
180
NSString *path = [minidumpDir stringByAppendingPathComponent:minidumpID];
181
path = [path stringByAppendingPathExtension:@"dmp"];
183
// check the size of the minidump and limit it to a reasonable size
184
// before attempting to load into memory and upload
185
const char *fileName = [path fileSystemRepresentation];
186
struct stat fileStatus;
190
if (!stat(fileName, &fileStatus)) {
191
if (fileStatus.st_size > kMinidumpFileLengthLimit) {
192
fprintf(stderr, "GoogleBreakpad Reporter: minidump file too large to upload : %d\n",
193
(int)fileStatus.st_size);
197
fprintf(stderr, "GoogleBreakpad Reporter: unable to determine minidump file length\n");
202
minidumpContents_ = [[NSData alloc] initWithContentsOfFile:path];
203
success = ([minidumpContents_ length] ? YES : NO);
207
// something wrong with the minidump file -- delete it
214
//=============================================================================
215
- (BOOL)askUserPermissionToSend:(BOOL)shouldSubmitReport {
216
// Send without confirmation
217
if ([[parameters_ objectForKey:@GOOGLE_BREAKPAD_SKIP_CONFIRM] isEqualToString:@"YES"])
220
NSString *display = [parameters_ objectForKey:@GOOGLE_BREAKPAD_PRODUCT_DISPLAY];
222
if (![display length])
223
display = [parameters_ objectForKey:@GOOGLE_BREAKPAD_PRODUCT];
225
NSBundle *bundle = [NSBundle mainBundle];
226
NSString *header = [NSString stringWithFormat:
227
NSLocalizedStringFromTableInBundle(@"headerFmt", nil, bundle, @""), display];
229
// Setup the localized dialog
230
NSMutableDictionary *noteDict = [NSMutableDictionary dictionary];
231
[noteDict setObject:header
232
forKey:(NSString *)kCFUserNotificationAlertHeaderKey];
234
// If we're going to submit a report, prompt the user if this is okay.
235
// Otherwise, just let them know that the app crashed.
236
float timeout = 60.0; // timeout value for the user notification
238
if (shouldSubmitReport) {
239
[noteDict setObject:NSLocalizedStringFromTableInBundle(@"msg", nil, bundle, @"")
240
forKey:(NSString *)kCFUserNotificationAlertMessageKey];
241
[noteDict setObject:NSLocalizedStringFromTableInBundle(@"sendReportButton", nil, bundle, @"")
242
forKey:(NSString *)kCFUserNotificationDefaultButtonTitleKey];
243
[noteDict setObject:NSLocalizedStringFromTableInBundle(@"cancelButton", nil, bundle, @"")
244
forKey:(NSString *)kCFUserNotificationAlternateButtonTitleKey];
246
// Nominally use the report interval
247
timeout = [[parameters_ objectForKey:@GOOGLE_BREAKPAD_REPORT_INTERVAL]
250
[noteDict setObject:NSLocalizedStringFromTableInBundle(@"msgNoSend", nil, bundle, @"")
251
forKey:(NSString *)kCFUserNotificationAlertMessageKey];
252
[noteDict setObject:NSLocalizedStringFromTableInBundle(@"noSendButton", nil, bundle, @"")
253
forKey:(NSString *)kCFUserNotificationDefaultButtonTitleKey];
257
// show the notification for at least one minute
258
if (timeout < 60.0) {
262
// Show the notification
263
CFOptionFlags flags = kCFUserNotificationCautionAlertLevel;
265
CFUserNotificationRef note =
266
CFUserNotificationCreate(NULL, timeout, flags, &error, (CFDictionaryRef)noteDict);
267
CFUserNotificationReceiveResponse(note, timeout, &flags);
272
//=============================================================================
273
- (BOOL)shouldSubmitReport {
274
float interval = [[parameters_ objectForKey:@GOOGLE_BREAKPAD_REPORT_INTERVAL]
276
NSString *program = [parameters_ objectForKey:@GOOGLE_BREAKPAD_PRODUCT];
277
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
278
NSMutableDictionary *programDict =
279
[NSMutableDictionary dictionaryWithDictionary:[ud dictionaryForKey:program]];
280
NSNumber *lastTimeNum = [programDict objectForKey:kLastSubmission];
281
NSTimeInterval lastTime = lastTimeNum ? [lastTimeNum floatValue] : 0;
282
NSTimeInterval now = CFAbsoluteTimeGetCurrent();
283
NSTimeInterval spanSeconds = (now - lastTime);
285
[programDict setObject:[NSNumber numberWithFloat:now] forKey:kLastSubmission];
286
[ud setObject:programDict forKey:program];
289
// If we've specified an interval and we're within that time, don't ask the
290
// user if we should report
291
if (interval > spanSeconds)
297
//=============================================================================
299
NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@GOOGLE_BREAKPAD_URL]];
300
HTTPMultipartUpload *upload = [[HTTPMultipartUpload alloc] initWithURL:url];
301
NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary];
303
// Set the known parameters. This should be kept up to date with the
304
// parameters defined in the GoogleBreakpad.h list of parameters. The intent
305
// is so that if there's a parameter that's not in this list, we consider
306
// it to be a "user-defined" parameter and we'll upload it to the server.
307
NSSet *knownParameters =
308
[NSSet setWithObjects:@kReporterMinidumpDirectoryKey,
309
@kReporterMinidumpIDKey, @GOOGLE_BREAKPAD_PRODUCT_DISPLAY,
310
@GOOGLE_BREAKPAD_PRODUCT, @GOOGLE_BREAKPAD_VERSION, @GOOGLE_BREAKPAD_URL,
311
@GOOGLE_BREAKPAD_REPORT_INTERVAL, @GOOGLE_BREAKPAD_SKIP_CONFIRM,
312
@GOOGLE_BREAKPAD_SEND_AND_EXIT, nil];
315
[uploadParameters setObject:[parameters_ objectForKey:@GOOGLE_BREAKPAD_PRODUCT]
317
[uploadParameters setObject:[parameters_ objectForKey:@GOOGLE_BREAKPAD_VERSION]
320
// Add any user parameters
321
NSArray *parameterKeys = [parameters_ allKeys];
322
int keyCount = [parameterKeys count];
324
for (i = 0; i < keyCount; ++i) {
325
NSString *key = [parameterKeys objectAtIndex:i];
326
if (![knownParameters containsObject:key])
327
[uploadParameters setObject:[parameters_ objectForKey:key] forKey:key];
330
[upload setParameters:uploadParameters];
333
[upload addFileContents:minidumpContents_ name:@"upload_file_minidump"];
336
NSError *error = nil;
337
NSData *data = [upload send:&error];
338
NSString *result = [[NSString alloc] initWithData:data
339
encoding:NSUTF8StringEncoding];
340
const char *reportID = "ERR";
343
fprintf(stderr, "GoogleBreakpad Reporter: Send Error: %s\n",
344
[[error description] UTF8String]);
346
reportID = [result UTF8String];
348
// rename the minidump file according to the id returned from the server
349
NSString *minidumpDir = [parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
350
NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];
352
NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp",
353
minidumpDir, minidumpID];
354
NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp",
355
minidumpDir, reportID];
357
const char *src = [srcString fileSystemRepresentation];
358
const char *dest = [destString fileSystemRepresentation];
360
if (rename(src, dest) == 0) {
361
fprintf(stderr, "GoogleBreakpad Reporter: Renamed %s to %s after successful upload\n",
365
// can't rename - don't worry - it's not important for users
366
fprintf(stderr, "GoogleBreakpad Reporter: successful upload report ID = %s\n",
374
//=============================================================================
376
[parameters_ release];
377
[minidumpContents_ release];
382
//=============================================================================
383
int main(int argc, const char *argv[]) {
384
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
385
NSLog(@"CrashSender launched.");
387
// The expectation is that there will be one argument which is the path
388
// to the configuration file
390
fprintf(stderr, "Missing configuration file\n");
394
// Open the file before (potentially) switching to console user
395
int configFile = open(argv[1], O_RDONLY, 0600);
397
// we want to avoid a build-up of old config files even if they
398
// have been incorrectly written by the framework
401
if (configFile == -1) {
402
fprintf(stderr, "Unable to open configuration file: %s\n", argv[1]);
406
Reporter *reporter = [[Reporter alloc] initWithConfigurationFD:configFile];
408
// Gather the configuration data
409
if (![reporter readConfigurationData]) {
410
fprintf(stderr, "Unable to load configuration data from %s\n", argv[1]);
414
// Read the minidump into memory before we (potentially) switch from the
416
if (![reporter readMinidumpData]) {
417
fprintf(stderr, "Unable to read minidump data\n");
421
// only submit a report if we have not recently crashed in the past
422
BOOL shouldSubmitReport = [reporter shouldSubmitReport];
423
BOOL okayToSend = NO;
425
// ask user if we should send
426
if (shouldSubmitReport) {
427
okayToSend = [reporter askUserPermissionToSend:shouldSubmitReport];
430
// TODO(waylonis): What is the behavior when there is no console user?
431
// Also, what about the global "Send statistics / metrics"?
433
// If we're running as root, switch over to nobody
434
if (getuid() == 0 || geteuid() == 0) {
435
struct passwd *pw = getpwnam("nobody");
437
// If we can't get a non-root uid, don't send the report
441
if (setgid(pw->pw_gid) == -1)
444
if (setuid(pw->pw_uid) == -1)
448
if (okayToSend && shouldSubmitReport) {