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 @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