// 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 { @synchronized(self) { static BreakpadController* 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; } }); } // 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); } } - (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 { enableUploads_ = NO; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(sendStoredCrashReports) object:nil]; } }); } - (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)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 { NSAssert(started_, @"The controller must be started before withBreakpadRef is called"); dispatch_async(queue_, ^{ callback(breakpadRef_); }); } - (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); }); } #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]; } - (void)sendStoredCrashReports { dispatch_async(queue_, ^{ 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_); // 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) { // performSelector: doesn't work on queue_ dispatch_async(dispatch_get_main_queue(), ^{ [self performSelector:@selector(sendStoredCrashReports) withObject:nil afterDelay:timeToWait]; }); } }); } @end