toolkit/crashreporter/client/crashreporter_osx.mm
author Benjamin Smedberg <benjamin@smedbergs.us>
Thu, 20 Mar 2008 12:42:05 -0400
changeset 13381 895712d07d4c3e0642195ba453446181ddd7a65c
parent 12907 08c95a0d7453854fda51addc1bd0b188d8ca3058
child 14003 2f0567b66496ce4838c13626a52e31aae4992b3d
permissions -rw-r--r--
Merge cvs-trunk-mirror -> mozilla-central. There's a C++ bug in js/src/jsinterp.cpp that I am going to file upstream.

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla Toolkit Crash Reporter
 *
 * The Initial Developer of the Original Code is
 *   Mozilla Corporation
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Dave Camp <dcamp@mozilla.com>
 *   Ted Mielczarek <ted.mielczarek@gmail.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#import <Cocoa/Cocoa.h>
#import <CoreFoundation/CoreFoundation.h>
#include "crashreporter.h"
#include "crashreporter_osx.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sstream>

using std::string;
using std::vector;
using std::ostringstream;

using namespace CrashReporter;

static NSAutoreleasePool* gMainPool;
static CrashReporterUI* gUI = 0;
static string gDumpFile;
static StringTable gQueryParameters;
static string gURLParameter;
static string gSendURL;
static vector<string> gRestartArgs;
static bool gDidTrySend = false;

#define NSSTR(s) [NSString stringWithUTF8String:(s).c_str()]

static NSString* Str(const char* aName)
{
  string str = gStrings[aName];
  if (str.empty()) str = "?";
  return NSSTR(str);
}

static bool RestartApplication()
{
  char** argv = reinterpret_cast<char**>(
    malloc(sizeof(char*) * (gRestartArgs.size() + 1)));

  if (!argv) return false;

  unsigned int i;
  for (i = 0; i < gRestartArgs.size(); i++) {
    argv[i] = (char*)gRestartArgs[i].c_str();
  }
  argv[i] = 0;

  pid_t pid = fork();
  if (pid == -1)
    return false;
  else if (pid == 0) {
    (void)execv(argv[0], argv);
    _exit(1);
  }

  free(argv);

  return true;
}

@implementation CrashReporterUI

-(void)awakeFromNib
{
  gUI = self;
  [mWindow center];

  [mWindow setTitle:[[NSBundle mainBundle]
                      objectForInfoDictionaryKey:@"CFBundleName"]];
}

-(void)showCrashUI:(const string&)dumpfile
   queryParameters:(const StringTable&)queryParameters
           sendURL:(const string&)sendURL
{
  gDumpFile = dumpfile;
  gQueryParameters = queryParameters;
  gSendURL = sendURL;

  [mWindow setTitle:Str(ST_CRASHREPORTERTITLE)];
  [mHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)];

  [mViewReportButton setTitle:Str(ST_VIEWREPORT)];
  [mViewReportButton sizeToFit];
  [mSubmitReportButton setTitle:Str(ST_CHECKSUBMIT)];
  [mIncludeURLButton setTitle:Str(ST_CHECKURL)];
  [mEmailMeButton setTitle:Str(ST_CHECKEMAIL)];
  [mViewReportOkButton setTitle:Str(ST_OK)];

  [mCommentText setPlaceholder:Str(ST_COMMENTGRAYTEXT)];
  [[mEmailText cell] setPlaceholderString:Str(ST_EMAILGRAYTEXT)];

  if (gQueryParameters.find("URL") != gQueryParameters.end()) {
    // save the URL value in case the checkbox gets unchecked
    gURLParameter = gQueryParameters["URL"];
  }
  else {
    // no URL specified, hide checkbox
    [mIncludeURLButton removeFromSuperview];
    // shrink window to fit
    NSRect frame = [mWindow frame];
    NSRect includeURLFrame = [mIncludeURLButton frame];
    NSRect emailFrame = [mEmailMeButton frame];
    int buttonMask = [mViewReportButton autoresizingMask];
    int checkMask = [mSubmitReportButton autoresizingMask];
    int commentScrollMask = [mCommentScrollView autoresizingMask];

    [mViewReportButton setAutoresizingMask:NSViewMinYMargin];
    [mSubmitReportButton setAutoresizingMask:NSViewMinYMargin];
    [mCommentScrollView setAutoresizingMask:NSViewMinYMargin];

    // remove all the space in between
    frame.size.height -= includeURLFrame.origin.y - emailFrame.origin.y;
    [mWindow setFrame:frame display: true animate:NO];

    [mViewReportButton setAutoresizingMask:buttonMask];
    [mSubmitReportButton setAutoresizingMask:checkMask];
    [mCommentScrollView setAutoresizingMask:commentScrollMask];
  }

  // resize some buttons horizontally and possibly some controls vertically
  [self doInitialResizing];

  [self updateSubmit];
  [self updateURL];
  [self updateEmail];

  [mWindow makeKeyAndOrderFront:nil];
}

-(void)showErrorUI:(const string&)message
{
  [self setView: mErrorView animate: NO];

  [mErrorHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)];
  [self setStringFitVertically:mErrorLabel
                        string:NSSTR(message)
                  resizeWindow:YES];
  [mErrorCloseButton setTitle:Str(ST_OK)];

  [mErrorCloseButton setKeyEquivalent:@"\r"];
  [mWindow makeFirstResponder:mErrorCloseButton];
  [mWindow makeKeyAndOrderFront:nil];
}

-(void)showReportInfo
{
  NSDictionary* boldAttr = [NSDictionary
                            dictionaryWithObject:
                            [NSFont boldSystemFontOfSize:
                             [NSFont smallSystemFontSize]]
                                          forKey:NSFontAttributeName];
  NSDictionary* normalAttr = [NSDictionary
                              dictionaryWithObject:
                              [NSFont systemFontOfSize:
                               [NSFont smallSystemFontSize]]
                                            forKey:NSFontAttributeName];

  [mViewReportTextView setString:@""];
  for (StringTable::iterator iter = gQueryParameters.begin();
       iter != gQueryParameters.end();
       iter++) {
    NSAttributedString* key = [[NSAttributedString alloc]
                               initWithString:NSSTR(iter->first + ": ")
                               attributes:boldAttr];
    NSAttributedString* value = [[NSAttributedString alloc]
                                 initWithString:NSSTR(iter->second + "\n")
                                 attributes:normalAttr];
    [[mViewReportTextView textStorage] appendAttributedString: key];
    [[mViewReportTextView textStorage] appendAttributedString: value];
    [key release];
    [value release];
  }

  NSAttributedString* extra = [[NSAttributedString alloc]
                               initWithString:NSSTR("\n" + gStrings[ST_EXTRAREPORTINFO])
                               attributes:normalAttr];
  [[mViewReportTextView textStorage] appendAttributedString: extra];
  [extra release];
}

- (void)maybeSubmitReport
{
  if ([mSubmitReportButton state] == NSOnState) {
    [self setStringFitVertically:mProgressText
                          string:Str(ST_REPORTDURINGSUBMIT)
                    resizeWindow:YES];
    // disable all the controls
    [self enableControls:NO];
    [mSubmitReportButton setEnabled:NO];
    [mRestartButton setEnabled:NO];
    [mCloseButton setEnabled:NO];
    [mProgressIndicator startAnimation:self];
    gDidTrySend = true;
    [self sendReport];
  } else {
    [NSApp terminate:self];
  }
}

- (void)closeMeDown:(id)unused
{
  [NSApp terminate:self];
}

-(IBAction)submitReportClicked:(id)sender
{
  [self updateSubmit];
}

-(IBAction)viewReportClicked:(id)sender
{
  [self showReportInfo];
  [NSApp beginSheet:mViewReportWindow modalForWindow:mWindow
   modalDelegate:nil didEndSelector:nil contextInfo:nil];
}

- (IBAction)viewReportOkClicked:(id)sender;
{
  [mViewReportWindow orderOut:nil];
  [NSApp endSheet:mViewReportWindow];
}

-(IBAction)closeClicked:(id)sender
{
  [self maybeSubmitReport];
}

-(IBAction)restartClicked:(id)sender
{
  RestartApplication();
  [self maybeSubmitReport];
}

- (IBAction)includeURLClicked:(id)sender
{
  [self updateURL];
}

-(IBAction)emailMeClicked:(id)sender
{
  [self updateEmail];
}

-(void)controlTextDidChange:(NSNotification *)note
{
  [self updateEmail];
}

- (void)textDidChange:(NSNotification *)aNotification
{
  // update comment parameter
  if ([[[mCommentText textStorage] mutableString] length] > 0)
    gQueryParameters["Comments"] = [[[mCommentText textStorage] mutableString]
                                    UTF8String];
  else
    gQueryParameters.erase("Comments");
}

// Limit the comment field to 500 bytes in UTF-8
- (BOOL)textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString
{
  // current string length + replacement text length - replaced range length
  if (([[aTextView string]
        lengthOfBytesUsingEncoding:NSUTF8StringEncoding]
	   + [replacementString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]
	   - [[[aTextView string] substringWithRange:affectedCharRange]
        lengthOfBytesUsingEncoding:NSUTF8StringEncoding])
	    > MAX_COMMENT_LENGTH) {
    return NO;
  }
  return YES;
}

- (void)doInitialResizing
{
  NSRect windowFrame = [mWindow frame];
  NSRect restartFrame = [mRestartButton frame];
  NSRect closeFrame = [mCloseButton frame];
  // resize close button to fit text
  float oldCloseWidth = closeFrame.size.width;
  [mCloseButton setTitle:Str(ST_QUIT)];
  [mCloseButton sizeToFit];
  closeFrame = [mCloseButton frame];
  // move close button left if it grew
  closeFrame.origin.x -= closeFrame.size.width - oldCloseWidth;

  if (gRestartArgs.size() == 0) {
    [mRestartButton removeFromSuperview];
    closeFrame.origin.x = restartFrame.origin.x +
      (restartFrame.size.width - closeFrame.size.width);
    [mCloseButton setFrame: closeFrame];
    [mCloseButton setKeyEquivalent:@"\r"];
  } else {
    [mRestartButton setTitle:Str(ST_RESTART)];
    // resize "restart" button
    float oldRestartWidth = restartFrame.size.width;
    [mRestartButton sizeToFit];
    restartFrame = [mRestartButton frame];
    // move left by the amount that the button grew
    restartFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
    closeFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
    [mRestartButton setFrame: restartFrame];
    [mCloseButton setFrame: closeFrame];
    // possibly resize window if both buttons no longer fit
    // leave 20 px from either side of the window, and 12 px
    // between the buttons
    float neededWidth = closeFrame.size.width + restartFrame.size.width +
                        2*20 + 12;
    
    if (neededWidth > windowFrame.size.width) {
      windowFrame.size.width = neededWidth;
      [mWindow setFrame:windowFrame display: true animate: NO];
    }
    [mRestartButton setKeyEquivalent:@"\r"];
  }

  NSButton *checkboxes[] = {
    mSubmitReportButton,
    mIncludeURLButton,
    mEmailMeButton
  };

  for (int i=0; i<3; i++) {
    [checkboxes[i] sizeToFit];
    // keep existing spacing on left side, + 20 px spare on right
    NSRect frame = [checkboxes[i] frame];
    float neededWidth = frame.origin.x + frame.size.width + 20;
    if (neededWidth > windowFrame.size.width) {
      windowFrame.size.width = neededWidth;
      [mWindow setFrame:windowFrame display: true animate: NO];
    }
  }

  // do this down here because we may have made the window wider
  // up above
  [self setStringFitVertically:mDescriptionLabel
                        string:Str(ST_CRASHREPORTERDESCRIPTION)
                  resizeWindow:YES];

  // now pin all the controls (except quit/submit) in place,
  // if we lengthen the window after this, it's just to lengthen
  // the progress text, so nothing above that text should move.
  NSView* views[] = {
    mSubmitReportButton,
    mViewReportButton,
    mCommentScrollView,
    mIncludeURLButton,
    mEmailMeButton,
    mEmailText,
    mProgressIndicator,
    mProgressText
  };
  for (unsigned int i=0; i<sizeof(views)/sizeof(views[0]); i++) {
    [views[i] setAutoresizingMask:NSViewMinYMargin];
  }
}

-(float)setStringFitVertically:(NSControl*)control
                        string:(NSString*)str
                  resizeWindow:(BOOL)resizeWindow
{
  // hack to make the text field grow vertically
  NSRect frame = [control frame];
  float oldHeight = frame.size.height;

  frame.size.height = 10000;
  NSSize oldCellSize = [[control cell] cellSizeForBounds: frame];
  [control setStringValue: str];
  NSSize newCellSize = [[control cell] cellSizeForBounds: frame];

  float delta = newCellSize.height - oldCellSize.height;
  frame.origin.y -= delta;
  frame.size.height = oldHeight + delta;
  [control setFrame: frame];

  if (resizeWindow) {
    NSRect frame = [mWindow frame];
    frame.origin.y -= delta;
    frame.size.height += delta;
    [mWindow setFrame:frame display: true animate: NO];
  }

  return delta;
}

-(void)setView: (NSView*)v animate: (BOOL)animate
{
  NSRect frame = [mWindow frame];

  NSRect oldViewFrame = [[mWindow contentView] frame];
  NSRect newViewFrame = [v frame];

  frame.origin.y += oldViewFrame.size.height - newViewFrame.size.height;
  frame.size.height -= oldViewFrame.size.height - newViewFrame.size.height;

  frame.origin.x += oldViewFrame.size.width - newViewFrame.size.width;
  frame.size.width -= oldViewFrame.size.width - newViewFrame.size.width;

  [mWindow setContentView:v];
  [mWindow setFrame:frame display:true animate:animate];
}

- (void)enableControls:(BOOL)enabled
{
  [mViewReportButton setEnabled:enabled];
  [mIncludeURLButton setEnabled:enabled];
  [mEmailMeButton setEnabled:enabled];
  [mCommentText setEnabled:enabled];
  [mCommentScrollView setHasVerticalScroller:enabled];
  [self updateEmail];
}

-(void)updateSubmit
{
  if ([mSubmitReportButton state] == NSOnState) {
    [self setStringFitVertically:mProgressText
                          string:Str(ST_REPORTPRESUBMIT)
                    resizeWindow:YES];
    [mProgressText setHidden:NO];
    // enable all the controls
    [self enableControls:YES];
  }
  else {
    // not submitting, disable all the controls under
    // the submit checkbox, and hide the status text
    [mProgressText setHidden:YES];
    [self enableControls:NO];
  }
}

-(void)updateURL
{
  if ([mIncludeURLButton state] == NSOnState && !gURLParameter.empty()) {
    gQueryParameters["URL"] = gURLParameter;
  } else {
    gQueryParameters.erase("URL");
  }
}

-(void)updateEmail
{
  if ([mEmailMeButton state] == NSOnState &&
      [mSubmitReportButton state] == NSOnState) {
    NSString* email = [mEmailText stringValue];
    gQueryParameters["Email"] = [email UTF8String];
    [mEmailText setEnabled:YES];
  } else {
    gQueryParameters.erase("Email");
    [mEmailText setEnabled:NO];
  }
}

-(void)sendReport
{
  if (![self setupPost]) {
    LogMessage("Crash report submission failed: could not set up POST data");
   [self setStringFitVertically:mProgressText
                          string:Str(ST_SUBMITFAILED)
                    resizeWindow:YES];
   // quit after 5 seconds
   [self performSelector:@selector(closeMeDown:) withObject:nil
    afterDelay:5.0];
  }

  [NSThread detachNewThreadSelector:@selector(uploadThread:)
            toTarget:self
            withObject:mPost];
}

-(bool)setupPost
{
  NSURL* url = [NSURL URLWithString:NSSTR(gSendURL)];
  if (!url) return false;

  mPost = [[HTTPMultipartUpload alloc] initWithURL: url];
  if (!mPost) return false;

  NSMutableDictionary* parameters =
    [[NSMutableDictionary alloc] initWithCapacity: gQueryParameters.size()];
  if (!parameters) return false;

  StringTable::const_iterator end = gQueryParameters.end();
  for (StringTable::const_iterator i = gQueryParameters.begin();
       i != end;
       i++) {
    NSString* key = NSSTR(i->first);
    NSString* value = NSSTR(i->second);
    [parameters setObject: value forKey: key];
  }

  [mPost addFileAtPath: NSSTR(gDumpFile) name: @"upload_file_minidump"];
  [mPost setParameters: parameters];
  [parameters release];

  return true;
}

-(void)uploadComplete:(NSData*)data
{
  NSHTTPURLResponse* response = [mPost response];
  [mPost release];

  bool success;
  string reply;
  if (!data || !response || [response statusCode] != 200) {
    success = false;
    reply = "";

    // if data is nil, we probably logged an error in uploadThread
    if (data != nil && response != nil) {
      ostringstream message;
      message << "Crash report submission failed: server returned status "
              << [response statusCode];
      LogMessage(message.str());
    }
  } else {
    success = true;
    LogMessage("Crash report submitted successfully");

    NSString* encodingName = [response textEncodingName];
    NSStringEncoding encoding;
    if (encodingName) {
      encoding = CFStringConvertEncodingToNSStringEncoding(
        CFStringConvertIANACharSetNameToEncoding((CFStringRef)encodingName));
    } else {
      encoding = NSISOLatin1StringEncoding;
    }
    NSString* r = [[NSString alloc] initWithData: data encoding: encoding];
    reply = [r UTF8String];
    [r release];
  }

  SendCompleted(success, reply);

  [mProgressIndicator stopAnimation:self];
  if (success) {
   [self setStringFitVertically:mProgressText
                          string:Str(ST_REPORTSUBMITSUCCESS)
                    resizeWindow:YES];
  } else {
   [self setStringFitVertically:mProgressText
                          string:Str(ST_SUBMITFAILED)
                    resizeWindow:YES];
  }
  // quit after 5 seconds
  [self performSelector:@selector(closeMeDown:) withObject:nil
   afterDelay:5.0];
}

-(void)uploadThread:(HTTPMultipartUpload*)post
{
  NSAutoreleasePool* autoreleasepool = [[NSAutoreleasePool alloc] init];
  NSError* error = nil;
  NSData* data = [post send: &error];
  if (error) {
    data = nil;
    NSString* errorDesc = [error localizedDescription];
    string message = [errorDesc UTF8String];
    LogMessage("Crash report submission failed: " + message);
  }

  [self performSelectorOnMainThread: @selector(uploadComplete:)
        withObject: data
        waitUntilDone: YES];

  [autoreleasepool release];
}

// to get auto-quit when we close the window
-(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)theApplication
{
  return YES;
}

-(void)applicationWillTerminate:(NSNotification *)aNotification
{
  // since we use [NSApp terminate:] we never return to main,
  // so do our cleanup here
  if (!gDidTrySend)
    DeleteDump();
}

@end

@implementation TextViewWithPlaceHolder

- (BOOL)becomeFirstResponder
{
  [self setNeedsDisplay:YES];
  return [super becomeFirstResponder];
}

- (void)drawRect:(NSRect)rect
{
  [super drawRect:rect];
  if (mPlaceHolderString && [[self string] isEqualToString:@""] &&
      self != [[self window] firstResponder])
    [mPlaceHolderString drawAtPoint:NSMakePoint(0,0)];
}

- (BOOL)resignFirstResponder
{
  [self setNeedsDisplay:YES];
  return [super resignFirstResponder];
}

- (void)setPlaceholder:(NSString*)placeholder
{
	NSColor* txtColor = [NSColor disabledControlTextColor];
  NSDictionary* txtDict = [NSDictionary
                           dictionaryWithObjectsAndKeys:txtColor,
                           NSForegroundColorAttributeName, nil];
  mPlaceHolderString = [[NSAttributedString alloc]
                       initWithString:placeholder attributes:txtDict];
}

- (void)insertTab:(id)sender
{
  // don't actually want to insert tabs, just tab to next control
  [[self window] selectNextKeyView:sender];
}

- (void)setEnabled:(BOOL)enabled
{
  [self setSelectable:enabled];
  [self setEditable:enabled];
  if (![[self string] isEqualToString:@""]) {
    NSAttributedString* colorString;
    NSColor* txtColor;
    if (enabled)
      txtColor = [NSColor textColor];
    else
      txtColor = [NSColor disabledControlTextColor];
    NSDictionary *txtDict = [NSDictionary
                             dictionaryWithObjectsAndKeys:txtColor,
                             NSForegroundColorAttributeName, nil];
    colorString = [[NSAttributedString alloc]
                   initWithString:[self string]
                   attributes:txtDict];
    [[self textStorage] setAttributedString: colorString];
    [self setInsertionPointColor:txtColor];
    [colorString release];
  }
}

- (void)dealloc
{
  [mPlaceHolderString release];
  [super dealloc];
}

@end

/* === Crashreporter UI Functions === */

bool UIInit()
{
  gMainPool = [[NSAutoreleasePool alloc] init];
  [NSApplication sharedApplication];
  [NSBundle loadNibNamed:@"MainMenu" owner:NSApp];

  return true;
}

void UIShutdown()
{
  [gMainPool release];
}

void UIShowDefaultUI()
{
  [gUI showErrorUI: gStrings[ST_CRASHREPORTERDEFAULT]];
  [NSApp run];
}

bool UIShowCrashUI(const string& dumpfile,
                   const StringTable& queryParameters,
                   const string& sendURL,
                   const vector<string>& restartArgs)
{
  gRestartArgs = restartArgs;

  [gUI showCrashUI: dumpfile
       queryParameters: queryParameters
       sendURL: sendURL];
  [NSApp run];

  return gDidTrySend;
}

void UIError_impl(const string& message)
{
  if (!gUI) {
    // UI failed to initialize, printing is the best we can do
    printf("Error: %s\n", message.c_str());
    return;
  }

  [gUI showErrorUI: message];
  [NSApp run];
}

bool UIGetIniPath(string& path)
{
  path = gArgv[0];
  path.append(".ini");

  return true;
}

bool UIGetSettingsPath(const string& vendor,
                       const string& product,
                       string& settingsPath)
{
  FSRef foundRef;
  OSErr err = FSFindFolder(kUserDomain, kApplicationSupportFolderType,
                           kCreateFolder, &foundRef);
  if (err != noErr)
    return false;

  unsigned char path[PATH_MAX];
  FSRefMakePath(&foundRef, path, sizeof(path));
  NSString* destPath = [NSString stringWithUTF8String:reinterpret_cast<char*>(path)];

  // Note that MacOS ignores the vendor when creating the profile hierarchy -
  // all application preferences directories live alongside one another in
  // ~/Library/Application Support/
  destPath = [destPath stringByAppendingPathComponent: NSSTR(product)];
  // Thunderbird stores its profile in ~/Library/Thunderbird,
  // but we're going to put stuff in ~/Library/Application Support/Thunderbird
  // anyway, so we have to ensure that path exists.
  string tempPath = [destPath UTF8String];
  if (!UIEnsurePathExists(tempPath))
    return false;

  destPath = [destPath stringByAppendingPathComponent: @"Crash Reports"];

  settingsPath = [destPath UTF8String];

  return true;
}

bool UIEnsurePathExists(const string& path)
{
  int ret = mkdir(path.c_str(), S_IRWXU);
  int e = errno;
  if (ret == -1 && e != EEXIST)
    return false;

  return true;
}

bool UIFileExists(const string& path)
{
  struct stat sb;
  int ret = stat(path.c_str(), &sb);
  if (ret == -1 || !(sb.st_mode & S_IFREG))
    return false;

  return true;
}

bool UIMoveFile(const string& file, const string& newfile)
{
  return (rename(file.c_str(), newfile.c_str()) != -1);
}

bool UIDeleteFile(const string& file)
{
  return (unlink(file.c_str()) != -1);
}

std::ifstream* UIOpenRead(const string& filename)
{
  return new std::ifstream(filename.c_str(), std::ios::in);
}

std::ofstream* UIOpenWrite(const string& filename, bool append) // append=false
{
  return new std::ofstream(filename.c_str(),
                           append ? std::ios::out | std::ios::app
                                  : std::ios::out);
}