blob: 01fb5f13a85c30944238e6594e15782a3471459e [file] [log] [blame]
// Copyright (c) 2012, 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 "BreakpadController.h"
#import <UIKit/UIKit.h>
#include <asl.h>
#include <execinfo.h>
#include <signal.h>
#include <unistd.h>
#include <sys/sysctl.h>
#include <common/scoped_ptr.h>
#pragma mark -
#pragma mark Private Methods
@interface BreakpadController ()
// Init the singleton instance.
- (id)initSingleton;
// Load a crash report and send it to the server.
- (void)sendStoredCrashReports;
// Returns when a report can be sent. |-1| means never, |0| means that a report
// can be sent immediately, a positive number is the number of seconds to wait
// before being allowed to upload a report.
- (int)sendDelay;
// Notifies that a report will be sent, and update the last sending time
// accordingly.
- (void)reportWillBeSent;
@end
#pragma mark -
#pragma mark Anonymous namespace
namespace {
// The name of the user defaults key for the last submission to the crash
// server.
NSString* const kLastSubmission = @"com.google.Breakpad.LastSubmission";
// Returns a NSString describing the current platform.
NSString* GetPlatform() {
// Name of the system call for getting the platform.
static const char kHwMachineSysctlName[] = "hw.machine";
NSString* result = nil;
size_t size = 0;
if (sysctlbyname(kHwMachineSysctlName, NULL, &size, NULL, 0) || size == 0)
return nil;
google_breakpad::scoped_array<char> machine(new char[size]);
if (sysctlbyname(kHwMachineSysctlName, machine.get(), &size, NULL, 0) == 0)
result = [NSString stringWithUTF8String:machine.get()];
return result;
}
} // namespace
#pragma mark -
#pragma mark BreakpadController Implementation
@implementation BreakpadController
+ (BreakpadController*)sharedInstance {
static dispatch_once_t onceToken;
static BreakpadController* sharedInstance ;
dispatch_once(&onceToken, ^{
sharedInstance = [[BreakpadController alloc] initSingleton];
});
return sharedInstance;
}
- (id)init {
return nil;
}
- (id)initSingleton {
self = [super init];
if (self) {
queue_ = dispatch_queue_create("com.google.BreakpadQueue", NULL);
enableUploads_ = NO;
started_ = NO;
[self resetConfiguration];
}
return self;
}
// Since this class is a singleton, this method is not expected to be called.
- (void)dealloc {
assert(!breakpadRef_);
dispatch_release(queue_);
[configuration_ release];
[uploadTimeParameters_ release];
[super dealloc];
}
#pragma mark -
- (void)start:(BOOL)onCurrentThread {
if (started_)
return;
started_ = YES;
void(^startBlock)() = ^{
assert(!breakpadRef_);
breakpadRef_ = BreakpadCreate(configuration_);
if (breakpadRef_) {
BreakpadAddUploadParameter(breakpadRef_, @"platform", GetPlatform());
}
};
if (onCurrentThread)
startBlock();
else
dispatch_async(queue_, startBlock);
}
- (void)stop {
if (!started_)
return;
started_ = NO;
dispatch_sync(queue_, ^{
if (breakpadRef_) {
BreakpadRelease(breakpadRef_);
breakpadRef_ = NULL;
}
});
}
- (BOOL)isStarted {
return started_;
}
// This method must be called from the breakpad queue.
- (void)threadUnsafeSendReportWithConfiguration:(NSDictionary*)configuration
withBreakpadRef:(BreakpadRef)ref {
NSAssert(started_, @"The controller must be started before "
"threadUnsafeSendReportWithConfiguration is called");
if (breakpadRef_) {
BreakpadUploadReportWithParametersAndConfiguration(
breakpadRef_, uploadTimeParameters_, configuration,
uploadCompleteCallback_);
}
}
- (void)setUploadingEnabled:(BOOL)enabled {
NSAssert(started_,
@"The controller must be started before setUploadingEnabled is called");
dispatch_async(queue_, ^{
if (enabled == enableUploads_)
return;
if (enabled) {
// Set this before calling doSendStoredCrashReport, because that
// calls sendDelay, which in turn checks this flag.
enableUploads_ = YES;
[self sendStoredCrashReports];
} else {
// disable the enableUpload_ flag.
// sendDelay checks this flag and disables the upload of logs by sendStoredCrashReports
enableUploads_ = NO;
}
});
}
- (void)updateConfiguration:(NSDictionary*)configuration {
NSAssert(!started_,
@"The controller must not be started when updateConfiguration is called");
[configuration_ addEntriesFromDictionary:configuration];
NSString *uploadInterval =
[configuration_ valueForKey:@BREAKPAD_REPORT_INTERVAL];
if (uploadInterval)
[self setUploadInterval:[uploadInterval intValue]];
}
- (void)resetConfiguration {
NSAssert(!started_,
@"The controller must not be started when resetConfiguration is called");
[configuration_ autorelease];
configuration_ = [[[NSBundle mainBundle] infoDictionary] mutableCopy];
NSString *uploadInterval =
[configuration_ valueForKey:@BREAKPAD_REPORT_INTERVAL];
[self setUploadInterval:[uploadInterval intValue]];
[self setParametersToAddAtUploadTime:nil];
}
- (void)setUploadingURL:(NSString*)url {
NSAssert(!started_,
@"The controller must not be started when setUploadingURL is called");
[configuration_ setValue:url forKey:@BREAKPAD_URL];
}
- (void)setUploadInterval:(int)intervalInSeconds {
NSAssert(!started_,
@"The controller must not be started when setUploadInterval is called");
[configuration_ removeObjectForKey:@BREAKPAD_REPORT_INTERVAL];
uploadIntervalInSeconds_ = intervalInSeconds;
if (uploadIntervalInSeconds_ < 0)
uploadIntervalInSeconds_ = 0;
}
- (void)setParametersToAddAtUploadTime:(NSDictionary*)uploadTimeParameters {
NSAssert(!started_, @"The controller must not be started when "
"setParametersToAddAtUploadTime is called");
[uploadTimeParameters_ autorelease];
uploadTimeParameters_ = [uploadTimeParameters copy];
}
- (void)addUploadParameter:(NSString*)value forKey:(NSString*)key {
NSAssert(started_,
@"The controller must be started before addUploadParameter is called");
dispatch_async(queue_, ^{
if (breakpadRef_)
BreakpadAddUploadParameter(breakpadRef_, key, value);
});
}
- (void)setUploadCallback:(BreakpadUploadCompletionCallback)callback {
NSAssert(started_,
@"The controller must not be started before setUploadCallback is "
"called");
dispatch_async(queue_, ^{
uploadCompleteCallback_ = callback;
});
}
- (void)removeUploadParameterForKey:(NSString*)key {
NSAssert(started_, @"The controller must be started before "
"removeUploadParameterForKey is called");
dispatch_async(queue_, ^{
if (breakpadRef_)
BreakpadRemoveUploadParameter(breakpadRef_, key);
});
}
- (void)withBreakpadRef:(void(^)(BreakpadRef))callback {
dispatch_async(queue_, ^{
callback(started_ ? breakpadRef_ : NULL);
});
}
- (void)hasReportToUpload:(void(^)(BOOL))callback {
NSAssert(started_, @"The controller must be started before "
"hasReportToUpload is called");
dispatch_async(queue_, ^{
callback(breakpadRef_ && (BreakpadGetCrashReportCount(breakpadRef_) > 0));
});
}
- (void)getCrashReportCount:(void(^)(int))callback {
NSAssert(started_, @"The controller must be started before "
"getCrashReportCount is called");
dispatch_async(queue_, ^{
callback(breakpadRef_ ? BreakpadGetCrashReportCount(breakpadRef_) : 0);
});
}
- (void)getNextReportConfigurationOrSendDelay:
(void(^)(NSDictionary*, int))callback {
NSAssert(started_, @"The controller must be started before "
"getNextReportConfigurationOrSendDelay is called");
dispatch_async(queue_, ^{
if (!breakpadRef_) {
callback(nil, -1);
return;
}
int delay = [self sendDelay];
if (delay != 0) {
callback(nil, delay);
return;
}
[self reportWillBeSent];
callback(BreakpadGetNextReportConfiguration(breakpadRef_), 0);
});
}
- (void)getDateOfMostRecentCrashReport:(void(^)(NSDate *))callback {
NSAssert(started_, @"The controller must be started before "
"getDateOfMostRecentCrashReport is called");
dispatch_async(queue_, ^{
if (!breakpadRef_) {
callback(nil);
return;
}
callback(BreakpadGetDateOfMostRecentCrashReport(breakpadRef_));
});
}
#pragma mark -
- (int)sendDelay {
if (!breakpadRef_ || uploadIntervalInSeconds_ <= 0 || !enableUploads_)
return -1;
// To prevent overloading the crash server, crashes are not sent than one
// report every |uploadIntervalInSeconds_|. A value in the user defaults is
// used to keep the time of the last upload.
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSNumber *lastTimeNum = [userDefaults objectForKey:kLastSubmission];
NSTimeInterval lastTime = lastTimeNum ? [lastTimeNum floatValue] : 0;
NSTimeInterval spanSeconds = CFAbsoluteTimeGetCurrent() - lastTime;
if (spanSeconds >= uploadIntervalInSeconds_)
return 0;
return uploadIntervalInSeconds_ - static_cast<int>(spanSeconds);
}
- (void)reportWillBeSent {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:[NSNumber numberWithDouble:CFAbsoluteTimeGetCurrent()]
forKey:kLastSubmission];
[userDefaults synchronize];
}
// This method must be called from the breakpad queue.
- (void)sendStoredCrashReports {
if (BreakpadGetCrashReportCount(breakpadRef_) == 0)
return;
int timeToWait = [self sendDelay];
// Unable to ever send report.
if (timeToWait == -1)
return;
// A report can be sent now.
if (timeToWait == 0) {
[self reportWillBeSent];
BreakpadUploadNextReportWithParameters(breakpadRef_, uploadTimeParameters_,
uploadCompleteCallback_);
// If more reports must be sent, make sure this method is called again.
if (BreakpadGetCrashReportCount(breakpadRef_) > 0)
timeToWait = uploadIntervalInSeconds_;
}
// A report must be sent later.
if (timeToWait > 0) {
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeToWait * NSEC_PER_SEC));
dispatch_after(delay, queue_, ^{
[self sendStoredCrashReports];
});
}
}
@end