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