Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ios / chrome / browser / crash_report / crash_report_background_uploader.mm
blobb538de0e04883a1abeefdf6b18e763933b450190
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ios/chrome/browser/crash_report/crash_report_background_uploader.h"
7 #import <UIKit/UIKit.h>
9 #include "base/logging.h"
10 #include "base/mac/scoped_block.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/metrics/histogram.h"
13 #include "base/metrics/user_metrics_action.h"
14 #include "base/time/time.h"
15 #import "breakpad/src/client/ios/BreakpadController.h"
16 #include "ios/chrome/browser/experimental_flags.h"
17 #include "ios/web/public/user_metrics.h"
19 using base::UserMetricsAction;
21 namespace {
23 NSString* const kBackgroundReportUploader =
24     @"com.google.chrome.breakpad.backgroundupload";
25 const char* const kUMAMobileCrashBackgroundUploadDelay =
26     "CrashReport.CrashBackgroundUploadDelay";
27 const char* const kUMAMobilePendingReportsOnBackgroundWakeUp =
28     "CrashReport.PendingReportsOnBackgroundWakeUp";
29 NSString* const kUploadedInBackground = @"uploaded_in_background";
30 NSString* const kReportsUploadedInBackground = @"ReportsUploadedInBackground";
32 NSString* CreateSessionIdentifierFromTask(NSURLSessionTask* task) {
33   return [NSString stringWithFormat:@"%@.%ld", kBackgroundReportUploader,
34                                     (unsigned long)[task taskIdentifier]];
37 }  // namespace
39 @interface UrlSessionDelegate : NSObject<NSURLSessionDelegate,
40                                          NSURLSessionTaskDelegate,
41                                          NSURLSessionDataDelegate>
42 + (instancetype)sharedInstance;
44 // Sets the completion handler for the URL session current tasks. The
45 // |completionHandler| cannot be nil.
46 - (void)setSessionCompletionHandler:(ProceduralBlock)completionHandler;
48 @end
50 @implementation UrlSessionDelegate {
51   // The completion handler to call when all tasks are completed.
52   base::mac::ScopedBlock<ProceduralBlock> _sessionCompletionHandler;
53   // The number of tasks in progress for the session.
54   int _tasks;
55   // Flag to indicate that URLSessionDidFinishEventsForBackgroundURLSession
56   // has been called, so that no new task will be launched for this session.
57   // It is safe to call completion handler when the pending tasks are completed.
58   BOOL _didFinishEventsCalled;
61 + (instancetype)sharedInstance {
62   static UrlSessionDelegate* instance = [[UrlSessionDelegate alloc] init];
63   return instance;
66 - (void)setSessionCompletionHandler:(ProceduralBlock)completionHandler {
67   DCHECK(completionHandler);
68   _sessionCompletionHandler.reset(completionHandler,
69                                   base::scoped_policy::RETAIN);
70   _didFinishEventsCalled = NO;
73 - (void)URLSession:(NSURLSession*)session
74                    task:(NSURLSessionTask*)dataTask
75     didReceiveChallenge:(NSURLAuthenticationChallenge*)challenge
76       completionHandler:
77           (void (^)(NSURLSessionAuthChallengeDisposition disposition,
78                     NSURLCredential* credential))completionHandler {
79   if (![challenge.protectionSpace.authenticationMethod
80           isEqualToString:NSURLAuthenticationMethodServerTrust]) {
81     completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
82     return;
83   }
84   NSString* identifier = CreateSessionIdentifierFromTask(dataTask);
86   NSDictionary* configuration =
87       [[NSUserDefaults standardUserDefaults] dictionaryForKey:identifier];
88   NSString* host =
89       [[NSURL URLWithString:[configuration objectForKey:@BREAKPAD_URL]] host];
90   if ([challenge.protectionSpace.host isEqualToString:host]) {
91     NSURLCredential* credential = [NSURLCredential
92         credentialForTrust:challenge.protectionSpace.serverTrust];
93     completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
94     return;
95   }
96   completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
99 - (void)URLSessionDidFinishEventsForBackgroundURLSession:
100         (NSURLSession*)session {
101   _didFinishEventsCalled = YES;
102   [[NSOperationQueue mainQueue] addOperationWithBlock:^{
103     [self callCompletionHandler];
104   }];
107 - (void)taskFinished {
108   DCHECK_GT(_tasks, 0);
109   _tasks--;
110   [[NSOperationQueue mainQueue] addOperationWithBlock:^{
111     [self callCompletionHandler];
112   }];
115 - (void)callCompletionHandler {
116   if (_tasks > 0 || !_didFinishEventsCalled)
117     return;
118   if (_sessionCompletionHandler) {
119     void (^completionHandler)() = _sessionCompletionHandler.get();
120     completionHandler();
121     _sessionCompletionHandler.reset();
122   }
125 - (void)URLSession:(NSURLSession*)session
126               dataTask:(NSURLSessionDataTask*)dataTask
127     didReceiveResponse:(NSURLResponse*)response
128      completionHandler:
129          (void (^)(NSURLSessionResponseDisposition disposition))handler {
130   handler(NSURLSessionResponseAllow);
133 - (void)URLSession:(NSURLSession*)session
134           dataTask:(NSURLSessionDataTask*)dataTask
135     didReceiveData:(NSData*)data {
136   NSString* identifier = CreateSessionIdentifierFromTask(dataTask);
138   NSDictionary* configuration =
139       [[NSUserDefaults standardUserDefaults] dictionaryForKey:identifier];
140   [[NSUserDefaults standardUserDefaults] removeObjectForKey:identifier];
141   _tasks++;
143   if (experimental_flags::IsAlertOnBackgroundUploadEnabled()) {
144     base::scoped_nsobject<UILocalNotification> localNotification(
145         [[UILocalNotification alloc] init]);
146     localNotification.get().fireDate = [NSDate date];
147     base::scoped_nsobject<NSString> reportId(
148         [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
149     localNotification.get().alertBody = [NSString
150         stringWithFormat:@"Crash report uploaded: %@", reportId.get()];
151     [[UIApplication sharedApplication]
152         scheduleLocalNotification:localNotification];
153   }
155   [[BreakpadController sharedInstance] withBreakpadRef:^(BreakpadRef ref) {
156     BreakpadHandleNetworkResponse(ref, configuration, data, nil);
157     dispatch_async(dispatch_get_main_queue(), ^{
158       [self taskFinished];
159     });
160   }];
163 @end
165 @implementation CrashReportBackgroundUploader
167 @synthesize hasPendingCrashReportsToUploadAtStartup;
169 + (instancetype)sharedInstance {
170   static CrashReportBackgroundUploader* instance =
171       [[CrashReportBackgroundUploader alloc] init];
172   return instance;
175 + (NSURLSession*)BreakpadBackgroundURLSessionWithCompletionHandler:
176         (ProceduralBlock)completionHandler {
177   static NSURLSession* session = nil;
178   static dispatch_once_t onceToken;
179   dispatch_once(&onceToken, ^{
181     // TODO(olivierrobin) When all bots compile with iOS8 release SDK, use
182     // only backgroundSessionConfigurationWithIdentifier.
183     NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration
184         backgroundSessionConfiguration:kBackgroundReportUploader];
186     session = [NSURLSession
187         sessionWithConfiguration:sessionConfig
188                         delegate:[UrlSessionDelegate sharedInstance]
189                    delegateQueue:[NSOperationQueue mainQueue]];
190   });
191   DCHECK(session);
192   if (completionHandler) {
193     [[UrlSessionDelegate sharedInstance]
194         setSessionCompletionHandler:completionHandler];
195   }
196   return session;
199 + (BOOL)sendNextReport:(NSDictionary*)nextReport
200        withBreakpadRef:(BreakpadRef)ref {
201   NSString* uploadURL =
202       [NSString stringWithString:[nextReport valueForKey:@BREAKPAD_URL]];
203   NSString* tmpDir = NSTemporaryDirectory();
204   NSString* tmpFile = [tmpDir
205       stringByAppendingPathComponent:
206           [NSString
207               stringWithFormat:@"%.0f.%@",
208                                [NSDate timeIntervalSinceReferenceDate] * 1000.0,
209                                @"txt"]];
210   NSURL* fileURL = [NSURL fileURLWithPath:tmpFile];
211   [nextReport setValue:[fileURL absoluteString] forKey:@BREAKPAD_URL];
213 #ifndef NDEBUG
214   NSString* BreakpadMinidumpLocation = [NSHomeDirectory()
215       stringByAppendingPathComponent:@"Library/Caches/Breakpad"];
216   [nextReport setValue:BreakpadMinidumpLocation
217                 forKey:@kReporterMinidumpDirectoryKey];
218   [nextReport setValue:BreakpadMinidumpLocation
219                 forKey:@BREAKPAD_DUMP_DIRECTORY];
220 #endif
222   [[BreakpadController sharedInstance]
223       threadUnsafeSendReportWithConfiguration:nextReport
224                               withBreakpadRef:ref];
226   NSFileManager* fileManager = [NSFileManager defaultManager];
227   if (![fileManager fileExistsAtPath:tmpFile]) {
228     return NO;
229   }
231   NSError* error;
232   NSString* fileString =
233       [NSString stringWithContentsOfFile:tmpFile
234                                 encoding:NSISOLatin1StringEncoding
235                                    error:&error];
237   // The HTTP content is a MIME multipart. The delimiter of the mime body must
238   // be added to the HTTP headers.
239   // A mime body is of the form
240   // --{delimiter}
241   // content 1
242   // --{delimiter}
243   // content 2
244   // --{delimiter}--
245   // The delimiter can be read on the first line of the file.
246   NSString* delimiter =
247       [[fileString componentsSeparatedByCharactersInSet:
248                        [NSCharacterSet newlineCharacterSet]] firstObject];
249   if (![delimiter hasPrefix:@"--"]) {
250     [fileManager removeItemAtPath:tmpFile error:&error];
251     return NO;
252   }
253   delimiter = [[delimiter
254       stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]
255       substringFromIndex:2];
257   NSMutableURLRequest* request =
258       [NSMutableURLRequest requestWithURL:[NSURL URLWithString:uploadURL]];
259   [request setHTTPMethod:@"POST"];
260   [request setValue:[NSString
261                         stringWithFormat:@"multipart/form-data; boundary=%@",
262                                          delimiter]
263       forHTTPHeaderField:@"Content-type"];
264   [request setHTTPBody:[NSData dataWithContentsOfFile:tmpFile]];
266   NSURLSession* session = [CrashReportBackgroundUploader
267       BreakpadBackgroundURLSessionWithCompletionHandler:nil];
268   NSURLSessionDataTask* dataTask =
269       [session uploadTaskWithRequest:request fromFile:fileURL];
271   NSString* identifier = CreateSessionIdentifierFromTask(dataTask);
272   [[NSUserDefaults standardUserDefaults] setObject:nextReport
273                                             forKey:identifier];
275   [dataTask resume];
276   return YES;
279 + (void)performFetchWithCompletionHandler:
280         (BackgroundFetchCompletionBlock)completionHandler {
281   [[BreakpadController sharedInstance] stop];
282   [[BreakpadController sharedInstance] setParametersToAddAtUploadTime:@{
283     kUploadedInBackground : @"yes"
284   }];
285   [[BreakpadController sharedInstance] start:YES];
286   [[BreakpadController sharedInstance] withBreakpadRef:^(BreakpadRef ref) {
287     // Note that this processing will be done before |sendNextCrashReport|
288     // starts uploading the crashes. The ordering is ensured here because both
289     // the crash report processing and the upload enabling are handled by
290     // posting blocks to a single |dispath_queue_t| in BreakpadController.
291     [[BreakpadController sharedInstance] setUploadingEnabled:YES];
292     [[BreakpadController sharedInstance]
293         getNextReportConfigurationOrSendDelay:^(NSDictionary* nextReport,
294                                                 int delay) {
295           BOOL reportToSend = NO;
296           BOOL uploaded = NO;
297           UMA_HISTOGRAM_COUNTS_100(kUMAMobilePendingReportsOnBackgroundWakeUp,
298                                    BreakpadGetCrashReportCount(ref));
299           if (delay == 0 && nextReport) {
300             reportToSend = YES;
301             NSNumber* crashTimeNum =
302                 [nextReport valueForKey:@BREAKPAD_PROCESS_CRASH_TIME];
303             base::Time crashTime =
304                 base::Time::FromTimeT([crashTimeNum intValue]);
305             base::Time now = base::Time::Now();
306             UMA_HISTOGRAM_LONG_TIMES_100(kUMAMobileCrashBackgroundUploadDelay,
307                                          now - crashTime);
308             uploaded = [self sendNextReport:nextReport withBreakpadRef:ref];
309           }
310           int pendingReports = BreakpadGetCrashReportCount(ref);
311           [[BreakpadController sharedInstance] setUploadingEnabled:NO];
312           dispatch_async(dispatch_get_main_queue(), ^{
313             if (reportToSend) {
314               if (uploaded) {
315                 NSUserDefaults* defaults =
316                     [NSUserDefaults standardUserDefaults];
317                 NSInteger uploadedCrashes =
318                     [defaults integerForKey:kReportsUploadedInBackground];
319                 [defaults setInteger:(uploadedCrashes + 1)
320                               forKey:kReportsUploadedInBackground];
321                 web::RecordAction(
322                     UserMetricsAction("BackgroundUploadReportSucceeded"));
324               } else {
325                 web::RecordAction(
326                     UserMetricsAction("BackgroundUploadReportAborted"));
327               }
328             }
329             if (uploaded && pendingReports) {
330               completionHandler(UIBackgroundFetchResultNewData);
331             } else if (pendingReports) {
332               completionHandler(UIBackgroundFetchResultFailed);
333             } else {
334               [[UIApplication sharedApplication]
335                   setMinimumBackgroundFetchInterval:
336                       UIApplicationBackgroundFetchIntervalNever];
337               completionHandler(UIBackgroundFetchResultNoData);
338             }
339           });
340         }];
341   }];
344 + (BOOL)canHandleBackgroundURLSession:(NSString*)identifier {
345   return [identifier isEqualToString:kBackgroundReportUploader];
348 + (void)handleEventsForBackgroundURLSession:(NSString*)identifier
349                           completionHandler:(ProceduralBlock)completionHandler {
350   [CrashReportBackgroundUploader
351       BreakpadBackgroundURLSessionWithCompletionHandler:completionHandler];
354 + (BOOL)hasUploadedCrashReportsInBackground {
355   NSInteger uploadedCrashReportsInBackgroundCount =
356       [[NSUserDefaults standardUserDefaults]
357           integerForKey:kReportsUploadedInBackground];
358   return uploadedCrashReportsInBackgroundCount > 0;
361 + (void)resetReportsUploadedInBackgroundCount {
362   [[NSUserDefaults standardUserDefaults]
363       removeObjectForKey:kReportsUploadedInBackground];
366 @end