/ src / client / ios / BreakpadController.mm
BreakpadController.mm
  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  
 65  namespace {
 66  
 67  // The name of the user defaults key for the last submission to the crash
 68  // server.
 69  NSString* const kLastSubmission = @"com.google.Breakpad.LastSubmission";
 70  
 71  // Returns a NSString describing the current platform.
 72  NSString* 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