Home | History | Annotate | Download | only in ios
      1 // Copyright (c) 2012, Google Inc.
      2 // All rights reserved.
      3 //
      4 // Redistribution and use in source and binary forms, with or without
      5 // modification, are permitted provided that the following conditions are
      6 // met:
      7 //
      8 //     * Redistributions of source code must retain the above copyright
      9 // notice, this list of conditions and the following disclaimer.
     10 //     * Redistributions in binary form must reproduce the above
     11 // copyright notice, this list of conditions and the following disclaimer
     12 // in the documentation and/or other materials provided with the
     13 // distribution.
     14 //     * Neither the name of Google Inc. nor the names of its
     15 // contributors may be used to endorse or promote products derived from
     16 // this software without specific prior written permission.
     17 //
     18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 
     30 #import "BreakpadController.h"
     31 
     32 #import <UIKit/UIKit.h>
     33 #include <asl.h>
     34 #include <execinfo.h>
     35 #include <signal.h>
     36 #include <unistd.h>
     37 #include <sys/sysctl.h>
     38 
     39 #include <common/scoped_ptr.h>
     40 
     41 #pragma mark -
     42 #pragma mark Private Methods
     43 
     44 @interface BreakpadController ()
     45 
     46 // Init the singleton instance.
     47 - (id)initSingleton;
     48 
     49 // Load a crash report and send it to the server.
     50 - (void)sendStoredCrashReports;
     51 
     52 // Returns when a report can be sent. |-1| means never, |0| means that a report
     53 // can be sent immediately, a positive number is the number of seconds to wait
     54 // before being allowed to upload a report.
     55 - (int)sendDelay;
     56 
     57 // Notifies that a report will be sent, and update the last sending time
     58 // accordingly.
     59 - (void)reportWillBeSent;
     60 
     61 @end
     62 
     63 #pragma mark -
     64 #pragma mark Anonymous namespace
     65 
     66 namespace {
     67 
     68 // The name of the user defaults key for the last submission to the crash
     69 // server.
     70 NSString* const kLastSubmission = @"com.google.Breakpad.LastSubmission";
     71 
     72 // Returns a NSString describing the current platform.
     73 NSString* GetPlatform() {
     74   // Name of the system call for getting the platform.
     75   static const char kHwMachineSysctlName[] = "hw.machine";
     76 
     77   NSString* result = nil;
     78 
     79   size_t size = 0;
     80   if (sysctlbyname(kHwMachineSysctlName, NULL, &size, NULL, 0) || size == 0)
     81     return nil;
     82   google_breakpad::scoped_array<char> machine(new char[size]);
     83   if (sysctlbyname(kHwMachineSysctlName, machine.get(), &size, NULL, 0) == 0)
     84     result = [NSString stringWithUTF8String:machine.get()];
     85   return result;
     86 }
     87 
     88 }  // namespace
     89 
     90 #pragma mark -
     91 #pragma mark BreakpadController Implementation
     92 
     93 @implementation BreakpadController
     94 
     95 + (BreakpadController*)sharedInstance {
     96   @synchronized(self) {
     97     static BreakpadController* sharedInstance_ =
     98         [[BreakpadController alloc] initSingleton];
     99     return sharedInstance_;
    100   }
    101 }
    102 
    103 - (id)init {
    104   return nil;
    105 }
    106 
    107 - (id)initSingleton {
    108   self = [super init];
    109   if (self) {
    110     queue_ = dispatch_queue_create("com.google.BreakpadQueue", NULL);
    111     enableUploads_ = NO;
    112     started_ = NO;
    113     [self resetConfiguration];
    114   }
    115   return self;
    116 }
    117 
    118 // Since this class is a singleton, this method is not expected to be called.
    119 - (void)dealloc {
    120   assert(!breakpadRef_);
    121   dispatch_release(queue_);
    122   [configuration_ release];
    123   [uploadTimeParameters_ release];
    124   [super dealloc];
    125 }
    126 
    127 #pragma mark -
    128 
    129 - (void)start:(BOOL)onCurrentThread {
    130   if (started_)
    131     return;
    132   started_ = YES;
    133   void(^startBlock)() = ^{
    134       assert(!breakpadRef_);
    135       breakpadRef_ = BreakpadCreate(configuration_);
    136       if (breakpadRef_) {
    137         BreakpadAddUploadParameter(breakpadRef_, @"platform", GetPlatform());
    138       }
    139   };
    140   if (onCurrentThread)
    141     startBlock();
    142   else
    143     dispatch_async(queue_, startBlock);
    144 }
    145 
    146 - (void)stop {
    147   if (!started_)
    148     return;
    149   started_ = NO;
    150   dispatch_sync(queue_, ^{
    151       if (breakpadRef_) {
    152         BreakpadRelease(breakpadRef_);
    153         breakpadRef_ = NULL;
    154       }
    155   });
    156 }
    157 
    158 // This method must be called from the breakpad queue.
    159 - (void)threadUnsafeSendReportWithConfiguration:(NSDictionary*)configuration
    160                                 withBreakpadRef:(BreakpadRef)ref {
    161   NSAssert(started_, @"The controller must be started before "
    162                      "threadUnsafeSendReportWithConfiguration is called");
    163   if (breakpadRef_) {
    164     BreakpadUploadReportWithParametersAndConfiguration(breakpadRef_,
    165                                                        uploadTimeParameters_,
    166                                                        configuration);
    167   }
    168 }
    169 
    170 - (void)setUploadingEnabled:(BOOL)enabled {
    171   NSAssert(started_,
    172       @"The controller must be started before setUploadingEnabled is called");
    173   dispatch_async(queue_, ^{
    174       if (enabled == enableUploads_)
    175         return;
    176       if (enabled) {
    177         // Set this before calling doSendStoredCrashReport, because that
    178         // calls sendDelay, which in turn checks this flag.
    179         enableUploads_ = YES;
    180         [self sendStoredCrashReports];
    181       } else {
    182         enableUploads_ = NO;
    183         [NSObject cancelPreviousPerformRequestsWithTarget:self
    184             selector:@selector(sendStoredCrashReports)
    185             object:nil];
    186       }
    187   });
    188 }
    189 
    190 - (void)updateConfiguration:(NSDictionary*)configuration {
    191   NSAssert(!started_,
    192       @"The controller must not be started when updateConfiguration is called");
    193   [configuration_ addEntriesFromDictionary:configuration];
    194   NSString* uploadInterval =
    195       [configuration_ valueForKey:@BREAKPAD_REPORT_INTERVAL];
    196   if (uploadInterval)
    197     [self setUploadInterval:[uploadInterval intValue]];
    198 }
    199 
    200 - (void)resetConfiguration {
    201   NSAssert(!started_,
    202       @"The controller must not be started when resetConfiguration is called");
    203   [configuration_ autorelease];
    204   configuration_ = [[[NSBundle mainBundle] infoDictionary] mutableCopy];
    205   NSString* uploadInterval =
    206       [configuration_ valueForKey:@BREAKPAD_REPORT_INTERVAL];
    207   [self setUploadInterval:[uploadInterval intValue]];
    208   [self setParametersToAddAtUploadTime:nil];
    209 }
    210 
    211 - (void)setUploadingURL:(NSString*)url {
    212   NSAssert(!started_,
    213       @"The controller must not be started when setUploadingURL is called");
    214   [configuration_ setValue:url forKey:@BREAKPAD_URL];
    215 }
    216 
    217 - (void)setUploadInterval:(int)intervalInSeconds {
    218   NSAssert(!started_,
    219       @"The controller must not be started when setUploadInterval is called");
    220   [configuration_ removeObjectForKey:@BREAKPAD_REPORT_INTERVAL];
    221   uploadIntervalInSeconds_ = intervalInSeconds;
    222   if (uploadIntervalInSeconds_ < 0)
    223     uploadIntervalInSeconds_ = 0;
    224 }
    225 
    226 - (void)setParametersToAddAtUploadTime:(NSDictionary*)uploadTimeParameters {
    227   NSAssert(!started_, @"The controller must not be started when "
    228                       "setParametersToAddAtUploadTime is called");
    229   [uploadTimeParameters_ autorelease];
    230   uploadTimeParameters_ = [uploadTimeParameters copy];
    231 }
    232 
    233 - (void)addUploadParameter:(NSString*)value forKey:(NSString*)key {
    234   NSAssert(started_,
    235       @"The controller must be started before addUploadParameter is called");
    236   dispatch_async(queue_, ^{
    237       if (breakpadRef_)
    238         BreakpadAddUploadParameter(breakpadRef_, key, value);
    239   });
    240 }
    241 
    242 - (void)removeUploadParameterForKey:(NSString*)key {
    243   NSAssert(started_, @"The controller must be started before "
    244                      "removeUploadParameterForKey is called");
    245   dispatch_async(queue_, ^{
    246       if (breakpadRef_)
    247         BreakpadRemoveUploadParameter(breakpadRef_, key);
    248   });
    249 }
    250 
    251 - (void)withBreakpadRef:(void(^)(BreakpadRef))callback {
    252   NSAssert(started_,
    253       @"The controller must be started before withBreakpadRef is called");
    254   dispatch_async(queue_, ^{
    255       callback(breakpadRef_);
    256   });
    257 }
    258 
    259 - (void)hasReportToUpload:(void(^)(BOOL))callback {
    260   NSAssert(started_, @"The controller must be started before "
    261                      "hasReportToUpload is called");
    262   dispatch_async(queue_, ^{
    263       callback(breakpadRef_ && (BreakpadGetCrashReportCount(breakpadRef_) > 0));
    264   });
    265 }
    266 
    267 - (void)getCrashReportCount:(void(^)(int))callback {
    268   NSAssert(started_, @"The controller must be started before "
    269                      "getCrashReportCount is called");
    270   dispatch_async(queue_, ^{
    271       callback(breakpadRef_ ? BreakpadGetCrashReportCount(breakpadRef_) : 0);
    272   });
    273 }
    274 
    275 - (void)getNextReportConfigurationOrSendDelay:
    276     (void(^)(NSDictionary*, int))callback {
    277   NSAssert(started_, @"The controller must be started before "
    278                      "getNextReportConfigurationOrSendDelay is called");
    279   dispatch_async(queue_, ^{
    280       if (!breakpadRef_) {
    281         callback(nil, -1);
    282         return;
    283       }
    284       int delay = [self sendDelay];
    285       if (delay != 0) {
    286         callback(nil, delay);
    287         return;
    288       }
    289       [self reportWillBeSent];
    290       callback(BreakpadGetNextReportConfiguration(breakpadRef_), 0);
    291   });
    292 }
    293 
    294 #pragma mark -
    295 
    296 - (int)sendDelay {
    297   if (!breakpadRef_ || uploadIntervalInSeconds_ <= 0 || !enableUploads_)
    298     return -1;
    299 
    300   // To prevent overloading the crash server, crashes are not sent than one
    301   // report every |uploadIntervalInSeconds_|. A value in the user defaults is
    302   // used to keep the time of the last upload.
    303   NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    304   NSNumber *lastTimeNum = [userDefaults objectForKey:kLastSubmission];
    305   NSTimeInterval lastTime = lastTimeNum ? [lastTimeNum floatValue] : 0;
    306   NSTimeInterval spanSeconds = CFAbsoluteTimeGetCurrent() - lastTime;
    307 
    308   if (spanSeconds >= uploadIntervalInSeconds_)
    309     return 0;
    310   return uploadIntervalInSeconds_ - static_cast<int>(spanSeconds);
    311 }
    312 
    313 - (void)reportWillBeSent {
    314   NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    315   [userDefaults setObject:[NSNumber numberWithDouble:CFAbsoluteTimeGetCurrent()]
    316                    forKey:kLastSubmission];
    317   [userDefaults synchronize];
    318 }
    319 
    320 - (void)sendStoredCrashReports {
    321   dispatch_async(queue_, ^{
    322       if (BreakpadGetCrashReportCount(breakpadRef_) == 0)
    323         return;
    324 
    325       int timeToWait = [self sendDelay];
    326 
    327       // Unable to ever send report.
    328       if (timeToWait == -1)
    329         return;
    330 
    331       // A report can be sent now.
    332       if (timeToWait == 0) {
    333         [self reportWillBeSent];
    334         BreakpadUploadNextReportWithParameters(breakpadRef_,
    335                                                uploadTimeParameters_);
    336 
    337         // If more reports must be sent, make sure this method is called again.
    338         if (BreakpadGetCrashReportCount(breakpadRef_) > 0)
    339           timeToWait = uploadIntervalInSeconds_;
    340       }
    341 
    342       // A report must be sent later.
    343       if (timeToWait > 0) {
    344         // performSelector: doesn't work on queue_
    345         dispatch_async(dispatch_get_main_queue(), ^{
    346             [self performSelector:@selector(sendStoredCrashReports)
    347                        withObject:nil
    348                        afterDelay:timeToWait];
    349         });
    350      }
    351   });
    352 }
    353 
    354 @end
    355