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 66namespace { 67 68// The name of the user defaults key for the last submission to the crash 69// server. 70NSString* const kLastSubmission = @"com.google.Breakpad.LastSubmission"; 71 72// Returns a NSString describing the current platform. 73NSString* 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 static dispatch_once_t onceToken; 97 static BreakpadController* sharedInstance ; 98 dispatch_once(&onceToken, ^{ 99 sharedInstance = [[BreakpadController alloc] initSingleton]; 100 }); 101 return sharedInstance; 102} 103 104- (id)init { 105 return nil; 106} 107 108- (id)initSingleton { 109 self = [super init]; 110 if (self) { 111 queue_ = dispatch_queue_create("com.google.BreakpadQueue", NULL); 112 enableUploads_ = NO; 113 started_ = NO; 114 [self resetConfiguration]; 115 } 116 return self; 117} 118 119// Since this class is a singleton, this method is not expected to be called. 120- (void)dealloc { 121 assert(!breakpadRef_); 122 dispatch_release(queue_); 123 [configuration_ release]; 124 [uploadTimeParameters_ release]; 125 [super dealloc]; 126} 127 128#pragma mark - 129 130- (void)start:(BOOL)onCurrentThread { 131 if (started_) 132 return; 133 started_ = YES; 134 void(^startBlock)() = ^{ 135 assert(!breakpadRef_); 136 breakpadRef_ = BreakpadCreate(configuration_); 137 if (breakpadRef_) { 138 BreakpadAddUploadParameter(breakpadRef_, @"platform", GetPlatform()); 139 } 140 }; 141 if (onCurrentThread) 142 startBlock(); 143 else 144 dispatch_async(queue_, startBlock); 145} 146 147- (void)stop { 148 if (!started_) 149 return; 150 started_ = NO; 151 dispatch_sync(queue_, ^{ 152 if (breakpadRef_) { 153 BreakpadRelease(breakpadRef_); 154 breakpadRef_ = NULL; 155 } 156 }); 157} 158 159- (BOOL)isStarted { 160 return started_; 161} 162 163// This method must be called from the breakpad queue. 164- (void)threadUnsafeSendReportWithConfiguration:(NSDictionary*)configuration 165 withBreakpadRef:(BreakpadRef)ref { 166 NSAssert(started_, @"The controller must be started before " 167 "threadUnsafeSendReportWithConfiguration is called"); 168 if (breakpadRef_) { 169 BreakpadUploadReportWithParametersAndConfiguration( 170 breakpadRef_, uploadTimeParameters_, configuration, 171 uploadCompleteCallback_); 172 } 173} 174 175- (void)setUploadingEnabled:(BOOL)enabled { 176 NSAssert(started_, 177 @"The controller must be started before setUploadingEnabled is called"); 178 dispatch_async(queue_, ^{ 179 if (enabled == enableUploads_) 180 return; 181 if (enabled) { 182 // Set this before calling doSendStoredCrashReport, because that 183 // calls sendDelay, which in turn checks this flag. 184 enableUploads_ = YES; 185 [self sendStoredCrashReports]; 186 } else { 187 // disable the enableUpload_ flag. 188 // sendDelay checks this flag and disables the upload of logs by sendStoredCrashReports 189 enableUploads_ = NO; 190 } 191 }); 192} 193 194- (void)updateConfiguration:(NSDictionary*)configuration { 195 NSAssert(!started_, 196 @"The controller must not be started when updateConfiguration is called"); 197 [configuration_ addEntriesFromDictionary:configuration]; 198 NSString *uploadInterval = 199 [configuration_ valueForKey:@BREAKPAD_REPORT_INTERVAL]; 200 if (uploadInterval) 201 [self setUploadInterval:[uploadInterval intValue]]; 202} 203 204- (void)resetConfiguration { 205 NSAssert(!started_, 206 @"The controller must not be started when resetConfiguration is called"); 207 [configuration_ autorelease]; 208 configuration_ = [[[NSBundle mainBundle] infoDictionary] mutableCopy]; 209 NSString *uploadInterval = 210 [configuration_ valueForKey:@BREAKPAD_REPORT_INTERVAL]; 211 [self setUploadInterval:[uploadInterval intValue]]; 212 [self setParametersToAddAtUploadTime:nil]; 213} 214 215- (void)setUploadingURL:(NSString*)url { 216 NSAssert(!started_, 217 @"The controller must not be started when setUploadingURL is called"); 218 [configuration_ setValue:url forKey:@BREAKPAD_URL]; 219} 220 221- (void)setUploadInterval:(int)intervalInSeconds { 222 NSAssert(!started_, 223 @"The controller must not be started when setUploadInterval is called"); 224 [configuration_ removeObjectForKey:@BREAKPAD_REPORT_INTERVAL]; 225 uploadIntervalInSeconds_ = intervalInSeconds; 226 if (uploadIntervalInSeconds_ < 0) 227 uploadIntervalInSeconds_ = 0; 228} 229 230- (void)setParametersToAddAtUploadTime:(NSDictionary*)uploadTimeParameters { 231 NSAssert(!started_, @"The controller must not be started when " 232 "setParametersToAddAtUploadTime is called"); 233 [uploadTimeParameters_ autorelease]; 234 uploadTimeParameters_ = [uploadTimeParameters copy]; 235} 236 237- (void)addUploadParameter:(NSString*)value forKey:(NSString*)key { 238 NSAssert(started_, 239 @"The controller must be started before addUploadParameter is called"); 240 dispatch_async(queue_, ^{ 241 if (breakpadRef_) 242 BreakpadAddUploadParameter(breakpadRef_, key, value); 243 }); 244} 245 246- (void)setUploadCallback:(BreakpadUploadCompletionCallback)callback { 247 NSAssert(started_, 248 @"The controller must not be started before setUploadCallback is " 249 "called"); 250 dispatch_async(queue_, ^{ 251 uploadCompleteCallback_ = callback; 252 }); 253} 254 255- (void)removeUploadParameterForKey:(NSString*)key { 256 NSAssert(started_, @"The controller must be started before " 257 "removeUploadParameterForKey is called"); 258 dispatch_async(queue_, ^{ 259 if (breakpadRef_) 260 BreakpadRemoveUploadParameter(breakpadRef_, key); 261 }); 262} 263 264- (void)withBreakpadRef:(void(^)(BreakpadRef))callback { 265 dispatch_async(queue_, ^{ 266 callback(started_ ? breakpadRef_ : NULL); 267 }); 268} 269 270- (void)hasReportToUpload:(void(^)(BOOL))callback { 271 NSAssert(started_, @"The controller must be started before " 272 "hasReportToUpload is called"); 273 dispatch_async(queue_, ^{ 274 callback(breakpadRef_ && (BreakpadGetCrashReportCount(breakpadRef_) > 0)); 275 }); 276} 277 278- (void)getCrashReportCount:(void(^)(int))callback { 279 NSAssert(started_, @"The controller must be started before " 280 "getCrashReportCount is called"); 281 dispatch_async(queue_, ^{ 282 callback(breakpadRef_ ? BreakpadGetCrashReportCount(breakpadRef_) : 0); 283 }); 284} 285 286- (void)getNextReportConfigurationOrSendDelay: 287 (void(^)(NSDictionary*, int))callback { 288 NSAssert(started_, @"The controller must be started before " 289 "getNextReportConfigurationOrSendDelay is called"); 290 dispatch_async(queue_, ^{ 291 if (!breakpadRef_) { 292 callback(nil, -1); 293 return; 294 } 295 int delay = [self sendDelay]; 296 if (delay != 0) { 297 callback(nil, delay); 298 return; 299 } 300 [self reportWillBeSent]; 301 callback(BreakpadGetNextReportConfiguration(breakpadRef_), 0); 302 }); 303} 304 305- (void)getDateOfMostRecentCrashReport:(void(^)(NSDate *))callback { 306 NSAssert(started_, @"The controller must be started before " 307 "getDateOfMostRecentCrashReport is called"); 308 dispatch_async(queue_, ^{ 309 if (!breakpadRef_) { 310 callback(nil); 311 return; 312 } 313 callback(BreakpadGetDateOfMostRecentCrashReport(breakpadRef_)); 314 }); 315} 316 317#pragma mark - 318 319- (int)sendDelay { 320 if (!breakpadRef_ || uploadIntervalInSeconds_ <= 0 || !enableUploads_) 321 return -1; 322 323 // To prevent overloading the crash server, crashes are not sent than one 324 // report every |uploadIntervalInSeconds_|. A value in the user defaults is 325 // used to keep the time of the last upload. 326 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 327 NSNumber *lastTimeNum = [userDefaults objectForKey:kLastSubmission]; 328 NSTimeInterval lastTime = lastTimeNum ? [lastTimeNum floatValue] : 0; 329 NSTimeInterval spanSeconds = CFAbsoluteTimeGetCurrent() - lastTime; 330 331 if (spanSeconds >= uploadIntervalInSeconds_) 332 return 0; 333 return uploadIntervalInSeconds_ - static_cast<int>(spanSeconds); 334} 335 336- (void)reportWillBeSent { 337 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 338 [userDefaults setObject:[NSNumber numberWithDouble:CFAbsoluteTimeGetCurrent()] 339 forKey:kLastSubmission]; 340 [userDefaults synchronize]; 341} 342 343// This method must be called from the breakpad queue. 344- (void)sendStoredCrashReports { 345 if (BreakpadGetCrashReportCount(breakpadRef_) == 0) 346 return; 347 348 int timeToWait = [self sendDelay]; 349 350 // Unable to ever send report. 351 if (timeToWait == -1) 352 return; 353 354 // A report can be sent now. 355 if (timeToWait == 0) { 356 [self reportWillBeSent]; 357 BreakpadUploadNextReportWithParameters(breakpadRef_, uploadTimeParameters_, 358 uploadCompleteCallback_); 359 360 // If more reports must be sent, make sure this method is called again. 361 if (BreakpadGetCrashReportCount(breakpadRef_) > 0) 362 timeToWait = uploadIntervalInSeconds_; 363 } 364 365 // A report must be sent later. 366 if (timeToWait > 0) { 367 dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeToWait * NSEC_PER_SEC)); 368 dispatch_after(delay, queue_, ^{ 369 [self sendStoredCrashReports]; 370 }); 371 } 372} 373 374@end 375