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