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