blob: 88d26fb0383ad8ee09d22de8688a177ae22b8b23 [file] [log] [blame]
// Copyright (c) 2006, 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 "client/mac/sender/crash_report_sender.h"
#import <Cocoa/Cocoa.h>
#import <pwd.h>
#import <sys/stat.h>
#import <SystemConfiguration/SystemConfiguration.h>
#import <unistd.h>
#import "client/apple/Framework/BreakpadDefines.h"
#import "common/mac/GTMLogger.h"
#import "common/mac/HTTPMultipartUpload.h"
#define kLastSubmission @"LastSubmission"
const int kUserCommentsMaxLength = 1500;
const int kEmailMaxLength = 64;
#define kApplePrefsSyncExcludeAllKey \
@"com.apple.PreferenceSync.ExcludeAllSyncKeys"
#pragma mark -
@interface NSView (ResizabilityExtentions)
// Shifts the view vertically by the given amount.
- (void)breakpad_shiftVertically:(CGFloat)offset;
// Shifts the view horizontally by the given amount.
- (void)breakpad_shiftHorizontally:(CGFloat)offset;
@end
@implementation NSView (ResizabilityExtentions)
- (void)breakpad_shiftVertically:(CGFloat)offset {
NSPoint origin = [self frame].origin;
origin.y += offset;
[self setFrameOrigin:origin];
}
- (void)breakpad_shiftHorizontally:(CGFloat)offset {
NSPoint origin = [self frame].origin;
origin.x += offset;
[self setFrameOrigin:origin];
}
@end
@interface NSWindow (ResizabilityExtentions)
// Adjusts the window height by heightDelta relative to its current height,
// keeping all the content at the same size.
- (void)breakpad_adjustHeight:(CGFloat)heightDelta;
@end
@implementation NSWindow (ResizabilityExtentions)
- (void)breakpad_adjustHeight:(CGFloat)heightDelta {
[[self contentView] setAutoresizesSubviews:NO];
NSRect windowFrame = [self frame];
windowFrame.size.height += heightDelta;
[self setFrame:windowFrame display:YES];
// For some reason the content view is resizing, but not adjusting its origin,
// so correct it manually.
[[self contentView] setFrameOrigin:NSMakePoint(0, 0)];
[[self contentView] setAutoresizesSubviews:YES];
}
@end
@interface NSTextField (ResizabilityExtentions)
// Grows or shrinks the height of the field to the minimum required to show the
// current text, preserving the existing width and origin.
// Returns the change in height.
- (CGFloat)breakpad_adjustHeightToFit;
// Grows or shrinks the width of the field to the minimum required to show the
// current text, preserving the existing height and origin.
// Returns the change in width.
- (CGFloat)breakpad_adjustWidthToFit;
@end
@implementation NSTextField (ResizabilityExtentions)
- (CGFloat)breakpad_adjustHeightToFit {
NSRect oldFrame = [self frame];
// Starting with the 10.5 SDK, height won't grow, so make it huge to start.
NSRect presizeFrame = oldFrame;
presizeFrame.size.height = MAXFLOAT;
// sizeToFit will blow out the width rather than making the field taller, so
// we do it manually.
NSSize newSize = [[self cell] cellSizeForBounds:presizeFrame];
NSRect newFrame = NSMakeRect(oldFrame.origin.x, oldFrame.origin.y,
NSWidth(oldFrame), newSize.height);
[self setFrame:newFrame];
return newSize.height - NSHeight(oldFrame);
}
- (CGFloat)breakpad_adjustWidthToFit {
NSRect oldFrame = [self frame];
[self sizeToFit];
return NSWidth([self frame]) - NSWidth(oldFrame);
}
@end
@interface NSButton (ResizabilityExtentions)
// Resizes to fit the label using IB-style size-to-fit metrics and enforcing a
// minimum width of 70, while preserving the right edge location.
// Returns the change in width.
- (CGFloat)breakpad_smartSizeToFit;
@end
@implementation NSButton (ResizabilityExtentions)
- (CGFloat)breakpad_smartSizeToFit {
NSRect oldFrame = [self frame];
[self sizeToFit];
NSRect newFrame = [self frame];
// sizeToFit gives much worse results that IB's Size to Fit option. This is
// the amount of padding IB adds over a sizeToFit, empirically determined.
const float kExtraPaddingAmount = 12;
const float kMinButtonWidth = 70; // The default button size in IB.
newFrame.size.width = NSWidth(newFrame) + kExtraPaddingAmount;
if (NSWidth(newFrame) < kMinButtonWidth)
newFrame.size.width = kMinButtonWidth;
// Preserve the right edge location.
newFrame.origin.x = NSMaxX(oldFrame) - NSWidth(newFrame);
[self setFrame:newFrame];
return NSWidth(newFrame) - NSWidth(oldFrame);
}
@end
#pragma mark -
@interface Reporter(PrivateMethods)
- (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.
- (BOOL)reportIntervalElapsed;
// Returns YES if we should send the report without asking the user first.
- (BOOL)shouldSubmitSilently;
// Returns YES if the minidump was generated on demand.
- (BOOL)isOnDemand;
// Returns YES if we should ask the user to provide comments.
- (BOOL)shouldRequestComments;
// Returns YES if we should ask the user to provide an email address.
- (BOOL)shouldRequestEmail;
// Shows UI to the user to ask for permission to send and any extra information
// we've been instructed to request. Returns YES if the user allows the report
// to be sent.
- (BOOL)askUserPermissionToSend;
// Returns the short description of the crash, suitable for use as a dialog
// title (e.g., "The application Foo has quit unexpectedly").
- (NSString*)shortDialogMessage;
// Return explanatory text about the crash and the reporter, suitable for the
// body text of a dialog.
- (NSString*)explanatoryDialogText;
// Returns the amount of time the UI should be shown before timing out.
- (NSTimeInterval)messageTimeout;
// Preps the comment-prompting alert window for display:
// * localizes all the elements
// * resizes and adjusts layout as necessary for localization
// * removes the email section if includeEmail is NO
- (void)configureAlertWindowIncludingEmail:(BOOL)includeEmail;
// Rmevoes the email section of the dialog, adjusting the rest of the window
// as necessary.
- (void)removeEmailPrompt;
// Run an alert window with the given timeout. Returns
// NSRunStoppedResponse if the timeout is exceeded. A timeout of 0
// queues the message immediately in the modal run loop.
- (NSInteger)runModalWindow:(NSWindow*)window
withTimeout:(NSTimeInterval)timeout;
// This method is used to periodically update the UI with how many
// seconds are left in the dialog display.
- (void)updateSecondsLeftInDialogDisplay:(NSTimer*)theTimer;
// When we receive this notification, it means that the user has
// begun editing the email address or comments field, and we disable
// the timers so that the user has as long as they want to type
// in their comments/email.
- (void)controlTextDidBeginEditing:(NSNotification *)aNotification;
- (void)report;
@end
@implementation Reporter
//=============================================================================
- (id)initWithConfigFile:(const char *)configFile {
if ((self = [super init])) {
remainingDialogTime_ = 0;
uploader_ = [[Uploader alloc] initWithConfigFile:configFile];
if (!uploader_) {
[self release];
return nil;
}
}
return self;
}
//=============================================================================
- (BOOL)askUserPermissionToSend {
// Initialize Cocoa, needed to display the alert
NSApplicationLoad();
// Get the timeout value for the notification.
NSTimeInterval timeout = [self messageTimeout];
NSInteger buttonPressed = NSAlertAlternateReturn;
// Determine whether we should create a text box for user feedback.
if ([self shouldRequestComments]) {
BOOL didLoadNib = [NSBundle loadNibNamed:@"Breakpad" owner:self];
if (!didLoadNib) {
return NO;
}
[self configureAlertWindowIncludingEmail:[self shouldRequestEmail]];
buttonPressed = [self runModalWindow:alertWindow_ withTimeout:timeout];
// Extract info from the user into the uploader_.
if ([self commentsValue]) {
[[uploader_ parameters] setObject:[self commentsValue]
forKey:@BREAKPAD_COMMENTS];
}
if ([self emailValue]) {
[[uploader_ parameters] setObject:[self emailValue]
forKey:@BREAKPAD_EMAIL];
}
} else {
// Create an alert panel to tell the user something happened
NSPanel* alert =
NSGetAlertPanel([self shortDialogMessage],
@"%@",
NSLocalizedString(@"sendReportButton", @""),
NSLocalizedString(@"cancelButton", @""),
nil,
[self explanatoryDialogText]);
// Pop the alert with an automatic timeout, and wait for the response
buttonPressed = [self runModalWindow:alert withTimeout:timeout];
// Release the panel memory
NSReleaseAlertPanel(alert);
}
return buttonPressed == NSAlertDefaultReturn;
}
- (void)configureAlertWindowIncludingEmail:(BOOL)includeEmail {
// Swap in localized values, making size adjustments to impacted elements as
// we go. Remember that the origin is in the bottom left, so elements above
// "fall" as text areas are shrunk from their overly-large IB sizes.
// Localize the header. No resizing needed, as it has plenty of room.
[dialogTitle_ setStringValue:[self shortDialogMessage]];
// Localize the explanatory text field.
[commentMessage_ setStringValue:[NSString stringWithFormat:@"%@\n\n%@",
[self explanatoryDialogText],
NSLocalizedString(@"commentsMsg", @"")]];
CGFloat commentHeightDelta = [commentMessage_ breakpad_adjustHeightToFit];
[headerBox_ breakpad_shiftVertically:commentHeightDelta];
[alertWindow_ breakpad_adjustHeight:commentHeightDelta];
// Either localize the email explanation field or remove the whole email
// section depending on whether or not we are asking for email.
if (includeEmail) {
[emailMessage_ setStringValue:NSLocalizedString(@"emailMsg", @"")];
CGFloat emailHeightDelta = [emailMessage_ breakpad_adjustHeightToFit];
[preEmailBox_ breakpad_shiftVertically:emailHeightDelta];
[alertWindow_ breakpad_adjustHeight:emailHeightDelta];
} else {
[self removeEmailPrompt]; // Handles necessary resizing.
}
// Localize the email label, and shift the associated text field.
[emailLabel_ setStringValue:NSLocalizedString(@"emailLabel", @"")];
CGFloat emailLabelWidthDelta = [emailLabel_ breakpad_adjustWidthToFit];
[emailEntryField_ breakpad_shiftHorizontally:emailLabelWidthDelta];
// Localize the privacy policy label, and keep it right-aligned to the arrow.
[privacyLinkLabel_ setStringValue:NSLocalizedString(@"privacyLabel", @"")];
CGFloat privacyLabelWidthDelta =
[privacyLinkLabel_ breakpad_adjustWidthToFit];
[privacyLinkLabel_ breakpad_shiftHorizontally:(-privacyLabelWidthDelta)];
// Ensure that the email field and the privacy policy link don't overlap.
CGFloat kMinControlPadding = 8;
CGFloat maxEmailFieldWidth = NSMinX([privacyLinkLabel_ frame]) -
NSMinX([emailEntryField_ frame]) -
kMinControlPadding;
if (NSWidth([emailEntryField_ bounds]) > maxEmailFieldWidth &&
maxEmailFieldWidth > 0) {
NSSize emailSize = [emailEntryField_ frame].size;
emailSize.width = maxEmailFieldWidth;
[emailEntryField_ setFrameSize:emailSize];
}
// Localize the placeholder text.
[[commentsEntryField_ cell]
setPlaceholderString:NSLocalizedString(@"commentsPlaceholder", @"")];
[[emailEntryField_ cell]
setPlaceholderString:NSLocalizedString(@"emailPlaceholder", @"")];
// Localize the buttons, and keep the cancel button at the right distance.
[sendButton_ setTitle:NSLocalizedString(@"sendReportButton", @"")];
CGFloat sendButtonWidthDelta = [sendButton_ breakpad_smartSizeToFit];
[cancelButton_ breakpad_shiftHorizontally:(-sendButtonWidthDelta)];
[cancelButton_ setTitle:NSLocalizedString(@"cancelButton", @"")];
[cancelButton_ breakpad_smartSizeToFit];
}
- (void)removeEmailPrompt {
[emailSectionBox_ setHidden:YES];
CGFloat emailSectionHeight = NSHeight([emailSectionBox_ frame]);
[preEmailBox_ breakpad_shiftVertically:(-emailSectionHeight)];
[alertWindow_ breakpad_adjustHeight:(-emailSectionHeight)];
}
- (NSInteger)runModalWindow:(NSWindow*)window
withTimeout:(NSTimeInterval)timeout {
// Queue a |stopModal| message to be performed in |timeout| seconds.
if (timeout > 0.001) {
remainingDialogTime_ = timeout;
SEL updateSelector = @selector(updateSecondsLeftInDialogDisplay:);
messageTimer_ = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:updateSelector
userInfo:nil
repeats:YES];
}
// Run the window modally and wait for either a |stopModal| message or a
// button click.
[NSApp activateIgnoringOtherApps:YES];
NSInteger returnMethod = [NSApp runModalForWindow:window];
return returnMethod;
}
- (IBAction)sendReport:(id)sender {
// Force the text fields to end editing so text for the currently focused
// field will be commited.
[alertWindow_ makeFirstResponder:alertWindow_];
[alertWindow_ orderOut:self];
// Use NSAlertDefaultReturn so that the return value of |runModalWithWindow|
// matches the AppKit function NSRunAlertPanel()
[NSApp stopModalWithCode:NSAlertDefaultReturn];
}
// UI Button Actions
//=============================================================================
- (IBAction)cancel:(id)sender {
[alertWindow_ orderOut:self];
// Use NSAlertDefaultReturn so that the return value of |runModalWithWindow|
// matches the AppKit function NSRunAlertPanel()
[NSApp stopModalWithCode:NSAlertAlternateReturn];
}
- (IBAction)showPrivacyPolicy:(id)sender {
// Get the localized privacy policy URL and open it in the default browser.
NSURL* privacyPolicyURL =
[NSURL URLWithString:NSLocalizedString(@"privacyPolicyURL", @"")];
[[NSWorkspace sharedWorkspace] openURL:privacyPolicyURL];
}
// Text Field Delegate Methods
//=============================================================================
- (BOOL) control:(NSControl*)control
textView:(NSTextView*)textView
doCommandBySelector:(SEL)commandSelector {
BOOL result = NO;
// If the user has entered text on the comment field, don't end
// editing on "return".
if (control == commentsEntryField_ &&
commandSelector == @selector(insertNewline:)
&& [[textView string] length] > 0) {
[textView insertNewlineIgnoringFieldEditor:self];
result = YES;
}
return result;
}
- (void)controlTextDidBeginEditing:(NSNotification *)aNotification {
[messageTimer_ invalidate];
[self setCountdownMessage:@""];
}
- (void)updateSecondsLeftInDialogDisplay:(NSTimer*)theTimer {
remainingDialogTime_ -= 1;
NSString *countdownMessage;
NSString *formatString;
int displayedTimeLeft; // This can be either minutes or seconds.
if (remainingDialogTime_ > 59) {
// calculate minutes remaining for UI purposes
displayedTimeLeft = (int)(remainingDialogTime_ / 60);
if (displayedTimeLeft == 1) {
formatString = NSLocalizedString(@"countdownMsgMinuteSingular", @"");
} else {
formatString = NSLocalizedString(@"countdownMsgMinutesPlural", @"");
}
} else {
displayedTimeLeft = (int)remainingDialogTime_;
if (displayedTimeLeft == 1) {
formatString = NSLocalizedString(@"countdownMsgSecondSingular", @"");
} else {
formatString = NSLocalizedString(@"countdownMsgSecondsPlural", @"");
}
}
countdownMessage = [NSString stringWithFormat:formatString,
displayedTimeLeft];
if (remainingDialogTime_ <= 30) {
[countdownLabel_ setTextColor:[NSColor redColor]];
}
[self setCountdownMessage:countdownMessage];
if (remainingDialogTime_ <= 0) {
[messageTimer_ invalidate];
[NSApp stopModal];
}
}
#pragma mark Accessors
#pragma mark -
//=============================================================================
- (NSString *)commentsValue {
return [[commentsValue_ retain] autorelease];
}
- (void)setCommentsValue:(NSString *)value {
if (commentsValue_ != value) {
[commentsValue_ release];
commentsValue_ = [value copy];
}
}
- (NSString *)emailValue {
return [[emailValue_ retain] autorelease];
}
- (void)setEmailValue:(NSString *)value {
if (emailValue_ != value) {
[emailValue_ release];
emailValue_ = [value copy];
}
}
- (NSString *)countdownMessage {
return [[countdownMessage_ retain] autorelease];
}
- (void)setCountdownMessage:(NSString *)value {
if (countdownMessage_ != value) {
[countdownMessage_ release];
countdownMessage_ = [value copy];
}
}
#pragma mark -
//=============================================================================
- (BOOL)reportIntervalElapsed {
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]];
NSNumber *lastTimeNum = [programDict objectForKey:kLastSubmission];
NSTimeInterval lastTime = lastTimeNum ? [lastTimeNum floatValue] : 0;
NSTimeInterval now = CFAbsoluteTimeGetCurrent();
NSTimeInterval spanSeconds = (now - lastTime);
[programDict setObject:[NSNumber numberWithDouble:now]
forKey:kLastSubmission];
[ud setObject:programDict forKey:program];
[ud synchronize];
// If we've specified an interval and we're within that time, don't ask the
// user if we should report
GTMLoggerDebug(@"Reporter Interval: %f", interval);
if (interval > spanSeconds) {
GTMLoggerDebug(@"Within throttling interval, not sending report");
return NO;
}
return YES;
}
- (BOOL)isOnDemand {
return [[[uploader_ parameters] objectForKey:@BREAKPAD_ON_DEMAND]
isEqualToString:@"YES"];
}
- (BOOL)shouldSubmitSilently {
return [[[uploader_ parameters] objectForKey:@BREAKPAD_SKIP_CONFIRM]
isEqualToString:@"YES"];
}
- (BOOL)shouldRequestComments {
return [[[uploader_ parameters] objectForKey:@BREAKPAD_REQUEST_COMMENTS]
isEqualToString:@"YES"];
}
- (BOOL)shouldRequestEmail {
return [[[uploader_ parameters] objectForKey:@BREAKPAD_REQUEST_EMAIL]
isEqualToString:@"YES"];
}
- (NSString*)shortDialogMessage {
NSString *displayName =
[[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT_DISPLAY];
if (![displayName length])
displayName = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT];
if ([self isOnDemand]) {
// Local variable to pacify clang's -Wformat-extra-args.
NSString* format = NSLocalizedString(@"noCrashDialogHeader", @"");
return [NSString stringWithFormat:format, displayName];
} else {
// Local variable to pacify clang's -Wformat-extra-args.
NSString* format = NSLocalizedString(@"crashDialogHeader", @"");
return [NSString stringWithFormat:format, displayName];
}
}
- (NSString*)explanatoryDialogText {
NSString *displayName =
[[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT_DISPLAY];
if (![displayName length])
displayName = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT];
NSString *vendor = [[uploader_ parameters] objectForKey:@BREAKPAD_VENDOR];
if (![vendor length])
vendor = @"unknown vendor";
if ([self isOnDemand]) {
// Local variable to pacify clang's -Wformat-extra-args.
NSString* format = NSLocalizedString(@"noCrashDialogMsg", @"");
return [NSString stringWithFormat:format, vendor, displayName];
} else {
// Local variable to pacify clang's -Wformat-extra-args.
NSString* format = NSLocalizedString(@"crashDialogMsg", @"");
return [NSString stringWithFormat:format, vendor];
}
}
- (NSTimeInterval)messageTimeout {
// Get the timeout value for the notification.
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;
}
return timeout;
}
- (void)report {
[uploader_ report];
}
//=============================================================================
- (void)dealloc {
[uploader_ release];
[super dealloc];
}
- (void)awakeFromNib {
[emailEntryField_ setMaximumLength:kEmailMaxLength];
[commentsEntryField_ setMaximumLength:kUserCommentsMaxLength];
}
@end
//=============================================================================
@implementation LengthLimitingTextField
- (void)setMaximumLength:(NSUInteger)maxLength {
maximumLength_ = maxLength;
}
// This is the method we're overriding in NSTextField, which lets us
// limit the user's input if it makes the string too long.
- (BOOL) textView:(NSTextView *)textView
shouldChangeTextInRange:(NSRange)affectedCharRange
replacementString:(NSString *)replacementString {
// Sometimes the range comes in invalid, so reject if we can't
// figure out if the replacement text is too long.
if (affectedCharRange.location == NSNotFound) {
return NO;
}
// Figure out what the new string length would be, taking into
// account user selections.
NSUInteger newStringLength =
[[textView string] length] - affectedCharRange.length +
[replacementString length];
if (newStringLength > maximumLength_) {
return NO;
} else {
return YES;
}
}
// Cut, copy, and paste have to be caught specifically since there is no menu.
- (BOOL)performKeyEquivalent:(NSEvent*)event {
// Only handle the key equivalent if |self| is the text field with focus.
NSText* fieldEditor = [self currentEditor];
if (fieldEditor != nil) {
// Check for a single "Command" modifier
NSUInteger modifiers = [event modifierFlags];
modifiers &= NSDeviceIndependentModifierFlagsMask;
if (modifiers == NSCommandKeyMask) {
// Now, check for Select All, Cut, Copy, or Paste key equivalents.
NSString* characters = [event characters];
// Select All is Command-A.
if ([characters isEqualToString:@"a"]) {
[fieldEditor selectAll:self];
return YES;
// Cut is Command-X.
} else if ([characters isEqualToString:@"x"]) {
[fieldEditor cut:self];
return YES;
// Copy is Command-C.
} else if ([characters isEqualToString:@"c"]) {
[fieldEditor copy:self];
return YES;
// Paste is Command-V.
} else if ([characters isEqualToString:@"v"]) {
[fieldEditor paste:self];
return YES;
}
}
}
// Let the super class handle the rest (e.g. Command-Period will cancel).
return [super performKeyEquivalent:event];
}
@end
//=============================================================================
int main(int argc, const char *argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
#if DEBUG
// Log to stderr in debug builds.
[GTMLogger setSharedLogger:[GTMLogger standardLoggerWithStderr]];
#endif
GTMLoggerDebug(@"Reporter Launched, argc=%d", argc);
// The expectation is that there will be one argument which is the path
// to the configuration file
if (argc != 2) {
exit(1);
}
Reporter *reporter = [[Reporter alloc] initWithConfigFile:argv[1]];
if (!reporter) {
GTMLoggerDebug(@"reporter initialization failed");
exit(1);
}
// only submit a report if we have not recently crashed in the past
BOOL shouldSubmitReport = [reporter reportIntervalElapsed];
BOOL okayToSend = NO;
// ask user if we should send
if (shouldSubmitReport) {
if ([reporter shouldSubmitSilently]) {
GTMLoggerDebug(@"Skipping confirmation and sending report");
okayToSend = YES;
} else {
okayToSend = [reporter askUserPermissionToSend];
}
}
// If we're running as root, switch over to nobody
if (getuid() == 0 || geteuid() == 0) {
struct passwd *pw = getpwnam("nobody");
// If we can't get a non-root uid, don't send the report
if (!pw) {
GTMLoggerDebug(@"!pw - %s", strerror(errno));
exit(0);
}
if (setgid(pw->pw_gid) == -1) {
GTMLoggerDebug(@"setgid(pw->pw_gid) == -1 - %s", strerror(errno));
exit(0);
}
if (setuid(pw->pw_uid) == -1) {
GTMLoggerDebug(@"setuid(pw->pw_uid) == -1 - %s", strerror(errno));
exit(0);
}
}
else {
GTMLoggerDebug(@"getuid() !=0 || geteuid() != 0");
}
if (okayToSend && shouldSubmitReport) {
GTMLoggerDebug(@"Sending Report");
[reporter report];
GTMLoggerDebug(@"Report Sent!");
} else {
GTMLoggerDebug(@"Not sending crash report okayToSend=%d, "\
"shouldSubmitReport=%d", okayToSend, shouldSubmitReport);
}
GTMLoggerDebug(@"Exiting with no errors");
// Cleanup
[reporter release];
[pool release];
return 0;
}