Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#import <Cocoa/Cocoa.h>
#import <CoreFoundation/CoreFoundation.h>
#include "crashreporter.h"
#include "crashreporter_osx.h"
#include <crt_externs.h>
#include <spawn.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sstream>
using std::ostringstream;
using std::string;
using std::vector;
using namespace CrashReporter;
static NSAutoreleasePool* gMainPool;
static CrashReporterUI* gUI = 0;
static StringTable gFiles;
static Json::Value gQueryParameters;
static string gURLParameter;
static string gSendURL;
static vector<string> gRestartArgs;
static bool gDidTrySend = false;
static bool gRTLlayout = false;
static cpu_type_t pref_cpu_types[2] = {
#if defined(__i386__)
CPU_TYPE_X86,
#elif defined(__x86_64__)
CPU_TYPE_X86_64,
#elif defined(__ppc__)
CPU_TYPE_POWERPC,
#elif defined(__aarch64__)
CPU_TYPE_ARM64,
#endif
CPU_TYPE_ANY};
#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() {
vector<char*> argv(gRestartArgs.size() + 1);
posix_spawnattr_t spawnattr;
if (posix_spawnattr_init(&spawnattr) != 0) {
return false;
}
// Set spawn attributes.
size_t attr_count = sizeof(pref_cpu_types) / sizeof(pref_cpu_types[0]);
size_t attr_ocount = 0;
if (posix_spawnattr_setbinpref_np(&spawnattr, attr_count, pref_cpu_types,
&attr_ocount) != 0 ||
attr_ocount != attr_count) {
posix_spawnattr_destroy(&spawnattr);
return false;
}
unsigned int i;
for (i = 0; i < gRestartArgs.size(); i++) {
argv[i] = (char*)gRestartArgs[i].c_str();
}
argv[i] = 0;
char** env = NULL;
char*** nsEnv = _NSGetEnviron();
if (nsEnv) env = *nsEnv;
int result = posix_spawnp(NULL, argv[0], NULL, &spawnattr, &argv[0], env);
posix_spawnattr_destroy(&spawnattr);
return result == 0;
}
@implementation CrashReporterUI
- (void)awakeFromNib {
gUI = self;
[mWindow center];
[mWindow setTitle:[[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleName"]];
[NSApp activateIgnoringOtherApps:YES];
}
- (void)showCrashUI:(const StringTable&)files
queryParameters:(const Json::Value&)queryParameters
sendURL:(const string&)sendURL {
gFiles = files;
gQueryParameters = queryParameters;
gSendURL = sendURL;
if (gAutoSubmit) {
gDidTrySend = true;
[self sendReport];
return;
}
[mWindow setTitle:Str(ST_CRASHREPORTERTITLE)];
[mHeaderLabel setStringValue:Str(ST_CRASHREPORTERHEADER)];
NSRect viewReportFrame = [mViewReportButton frame];
[mViewReportButton setTitle:Str(ST_VIEWREPORT)];
[mViewReportButton sizeToFit];
if (gRTLlayout) {
// sizeToFit will keep the left side fixed, so realign
float oldWidth = viewReportFrame.size.width;
viewReportFrame = [mViewReportButton frame];
viewReportFrame.origin.x += oldWidth - viewReportFrame.size.width;
[mViewReportButton setFrame:viewReportFrame];
}
[mSubmitReportButton setTitle:Str(ST_CHECKSUBMIT)];
[mIncludeURLButton setTitle:Str(ST_CHECKURL)];
[mViewReportOkButton setTitle:Str(ST_OK)];
[mCommentText setPlaceholder:Str(ST_COMMENTGRAYTEXT)];
if (gRTLlayout) [mCommentText toggleBaseWritingDirection:self];
if (gQueryParameters.isMember("URL")) {
// save the URL value in case the checkbox gets unchecked
gURLParameter = gQueryParameters["URL"].asString();
} 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];
// load default state of submit checkbox
// we don't just do this via IB because we want the default to be
// off a certain percentage of the time
BOOL submitChecked = YES;
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
if (nil != [userDefaults objectForKey:@"submitReport"]) {
submitChecked = [userDefaults boolForKey:@"submitReport"];
} else {
[userDefaults setBool:submitChecked forKey:@"submitReport"];
}
[mSubmitReportButton setState:(submitChecked ? NSOnState : NSOffState)];
// load default state of include URL checkbox
BOOL includeChecked = YES;
if (nil != [userDefaults objectForKey:@"IncludeURL"]) {
includeChecked = [userDefaults boolForKey:@"IncludeURL"];
} else {
[userDefaults setBool:includeChecked forKey:@"IncludeURL"];
}
[mIncludeURLButton setState:(includeChecked ? NSOnState : NSOffState)];
[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 = @{
NSFontAttributeName :
[NSFont boldSystemFontOfSize:[NSFont smallSystemFontSize]],
NSForegroundColorAttributeName : NSColor.textColor,
};
NSDictionary* normalAttr = @{
NSFontAttributeName :
[NSFont systemFontOfSize:[NSFont smallSystemFontSize]],
NSForegroundColorAttributeName : NSColor.textColor,
};
[mViewReportTextView setString:@""];
for (Json::ValueConstIterator iter = gQueryParameters.begin();
iter != gQueryParameters.end(); ++iter) {
NSAttributedString* key =
[[NSAttributedString alloc] initWithString:NSSTR(iter.name() + ": ")
attributes:boldAttr];
string str;
if (iter->isString()) {
str = iter->asString();
} else {
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
str = writeString(builder, *iter);
}
NSAttributedString* value =
[[NSAttributedString alloc] initWithString:NSSTR(str + "\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];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setBool:([mSubmitReportButton state] == NSOnState)
forKey:@"submitReport"];
[userDefaults synchronize];
}
- (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];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setBool:([mIncludeURLButton state] == NSOnState)
forKey:@"IncludeURL"];
[userDefaults synchronize];
}
- (void)textDidChange:(NSNotification*)aNotification {
// update comment parameter
if ([[[mCommentText textStorage] mutableString] length] > 0)
gQueryParameters["Comments"] =
[[[mCommentText textStorage] mutableString] UTF8String];
else
gQueryParameters.removeMember("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
if (!gRTLlayout) {
closeFrame.origin.x -= closeFrame.size.width - oldCloseWidth;
}
if (gRestartArgs.size() == 0) {
[mRestartButton removeFromSuperview];
if (!gRTLlayout) {
closeFrame.origin.x = restartFrame.origin.x +
(restartFrame.size.width - closeFrame.size.width);
} else {
closeFrame.origin.x = restartFrame.origin.x;
}
[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];
if (!gRTLlayout) {
// move left by the amount that the button grew
restartFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
closeFrame.origin.x -= restartFrame.size.width - oldRestartWidth;
} else {
// shift the close button right in RTL
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};
for (auto checkbox : checkboxes) {
NSRect frame = [checkbox frame];
[checkbox sizeToFit];
if (gRTLlayout) {
// sizeToFit will keep the left side fixed, so realign
float oldWidth = frame.size.width;
frame = [checkbox frame];
frame.origin.x += oldWidth - frame.size.width;
[checkbox setFrame:frame];
}
// keep existing spacing on left side, + 20 px spare on right
float neededWidth =
frame.origin.x + checkbox.intrinsicContentSize.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,
mProgressIndicator, mProgressText};
for (auto view : views) {
[view 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];
[mCommentText setEnabled:enabled];
[mCommentScrollView setHasVerticalScroller:enabled];
}
- (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.removeMember("URL");
}
}
- (void)updateEmail {
// In order to remove the email fields, we have to edit the .nib files which
// we can't do with current xcode so we make them hidden; updating the
// crashreporter interface for mac is covered in bug #1696164
[mEmailMeButton setHidden:YES];
[mEmailText setHidden:YES];
}
- (void)sendReport {
if (![self setupPost]) {
LogMessage("Crash report submission failed: could not set up POST data");
if (gAutoSubmit) {
[NSApp terminate:self];
}
[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) stringByAddingPercentEscapesUsingEncoding:
NSUTF8StringEncoding]];
if (!url) return false;
mPost = [[HTTPMultipartUpload alloc] initWithURL:url];
if (!mPost) return false;
for (StringTable::const_iterator i = gFiles.begin(); i != gFiles.end(); i++) {
[mPost addFileAtPath:NSSTR(i->second) name:NSSTR(i->first)];
}
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
string output = writeString(builder, gQueryParameters).append("\r\n");
NSMutableString* parameters =
[[NSMutableString alloc] initWithUTF8String:output.c_str()];
[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);
if (gAutoSubmit) {
[NSApp terminate:self];
}
[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 drawInRect:[self frame]];
}
- (BOOL)resignFirstResponder {
[self setNeedsDisplay:YES];
return [super resignFirstResponder];
}
- (void)setPlaceholder:(NSString*)placeholder {
NSColor* txtColor = [NSColor disabledControlTextColor];
NSDictionary* txtDict = [NSDictionary
dictionaryWithObjectsAndKeys:txtColor, NSForegroundColorAttributeName,
nil];
mPlaceHolderString =
[[NSMutableAttributedString alloc] initWithString:placeholder
attributes:txtDict];
if (gRTLlayout)
[mPlaceHolderString setAlignment:NSTextAlignmentRight
range:NSMakeRange(0, [placeholder length])];
}
- (void)insertTab:(id)sender {
// don't actually want to insert tabs, just tab to next control
[[self window] selectNextKeyView:sender];
}
- (void)insertBacktab:(id)sender {
[[self window] selectPreviousKeyView: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];
if (gStrings.find("isRTL") != gStrings.end() && gStrings["isRTL"] == "yes")
gRTLlayout = true;
if (gAutoSubmit) {
gUI = [[CrashReporterUI alloc] init];
} else {
[[NSBundle mainBundle]
loadNibNamed:(gRTLlayout ? @"MainMenuRTL" : @"MainMenu")
owner:NSApp
topLevelObjects:nil];
}
return true;
}
void UIShutdown() { [gMainPool release]; }
void UIShowDefaultUI() {
[gUI showErrorUI:gStrings[ST_CRASHREPORTERDEFAULT]];
[NSApp run];
}
bool UIShowCrashUI(const StringTable& files, const Json::Value& queryParameters,
const string& sendURL, const vector<string>& restartArgs) {
gRestartArgs = restartArgs;
[gUI showCrashUI:files 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) {
NSString* tmpPath = [NSString stringWithUTF8String:gArgv[0]];
NSString* iniName = [tmpPath lastPathComponent];
iniName = [iniName stringByAppendingPathExtension:@"ini"];
tmpPath = [tmpPath stringByDeletingLastPathComponent];
tmpPath = [tmpPath stringByDeletingLastPathComponent];
tmpPath = [tmpPath stringByAppendingPathComponent:@"Resources"];
tmpPath = [tmpPath stringByAppendingPathComponent:iniName];
path = [tmpPath UTF8String];
return true;
}
bool UIGetSettingsPath(const string& vendor, const string& product,
string& settingsPath) {
NSArray* paths = NSSearchPathForDirectoriesInDomains(
NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString* destPath = [paths firstObject];
// 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 UIMoveFile(const string& file, const string& newfile) {
if (!rename(file.c_str(), newfile.c_str())) return true;
if (errno != EXDEV) return false;
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* source =
[fileManager stringWithFileSystemRepresentation:file.c_str()
length:file.length()];
NSString* dest =
[fileManager stringWithFileSystemRepresentation:newfile.c_str()
length:newfile.length()];
if (!source || !dest) return false;
[fileManager moveItemAtPath:source toPath:dest error:NULL];
return UIFileExists(newfile);
}