Index: ios/chrome/browser/crash_report/crash_report_background_uploader.mm |
diff --git a/ios/chrome/browser/crash_report/crash_report_background_uploader.mm b/ios/chrome/browser/crash_report/crash_report_background_uploader.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..b538de0e04883a1abeefdf6b18e763933b450190 |
--- /dev/null |
+++ b/ios/chrome/browser/crash_report/crash_report_background_uploader.mm |
@@ -0,0 +1,366 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/browser/crash_report/crash_report_background_uploader.h" |
+ |
+#import <UIKit/UIKit.h> |
+ |
+#include "base/logging.h" |
+#include "base/mac/scoped_block.h" |
+#include "base/mac/scoped_nsobject.h" |
+#include "base/metrics/histogram.h" |
+#include "base/metrics/user_metrics_action.h" |
+#include "base/time/time.h" |
+#import "breakpad/src/client/ios/BreakpadController.h" |
+#include "ios/chrome/browser/experimental_flags.h" |
+#include "ios/web/public/user_metrics.h" |
+ |
+using base::UserMetricsAction; |
+ |
+namespace { |
+ |
+NSString* const kBackgroundReportUploader = |
+ @"com.google.chrome.breakpad.backgroundupload"; |
+const char* const kUMAMobileCrashBackgroundUploadDelay = |
+ "CrashReport.CrashBackgroundUploadDelay"; |
+const char* const kUMAMobilePendingReportsOnBackgroundWakeUp = |
+ "CrashReport.PendingReportsOnBackgroundWakeUp"; |
+NSString* const kUploadedInBackground = @"uploaded_in_background"; |
+NSString* const kReportsUploadedInBackground = @"ReportsUploadedInBackground"; |
+ |
+NSString* CreateSessionIdentifierFromTask(NSURLSessionTask* task) { |
+ return [NSString stringWithFormat:@"%@.%ld", kBackgroundReportUploader, |
+ (unsigned long)[task taskIdentifier]]; |
+} |
+ |
+} // namespace |
+ |
+@interface UrlSessionDelegate : NSObject<NSURLSessionDelegate, |
+ NSURLSessionTaskDelegate, |
+ NSURLSessionDataDelegate> |
++ (instancetype)sharedInstance; |
+ |
+// Sets the completion handler for the URL session current tasks. The |
+// |completionHandler| cannot be nil. |
+- (void)setSessionCompletionHandler:(ProceduralBlock)completionHandler; |
+ |
+@end |
+ |
+@implementation UrlSessionDelegate { |
+ // The completion handler to call when all tasks are completed. |
+ base::mac::ScopedBlock<ProceduralBlock> _sessionCompletionHandler; |
+ // The number of tasks in progress for the session. |
+ int _tasks; |
+ // Flag to indicate that URLSessionDidFinishEventsForBackgroundURLSession |
+ // has been called, so that no new task will be launched for this session. |
+ // It is safe to call completion handler when the pending tasks are completed. |
+ BOOL _didFinishEventsCalled; |
+} |
+ |
++ (instancetype)sharedInstance { |
+ static UrlSessionDelegate* instance = [[UrlSessionDelegate alloc] init]; |
+ return instance; |
+} |
+ |
+- (void)setSessionCompletionHandler:(ProceduralBlock)completionHandler { |
+ DCHECK(completionHandler); |
+ _sessionCompletionHandler.reset(completionHandler, |
+ base::scoped_policy::RETAIN); |
+ _didFinishEventsCalled = NO; |
+} |
+ |
+- (void)URLSession:(NSURLSession*)session |
+ task:(NSURLSessionTask*)dataTask |
+ didReceiveChallenge:(NSURLAuthenticationChallenge*)challenge |
+ completionHandler: |
+ (void (^)(NSURLSessionAuthChallengeDisposition disposition, |
+ NSURLCredential* credential))completionHandler { |
+ if (![challenge.protectionSpace.authenticationMethod |
+ isEqualToString:NSURLAuthenticationMethodServerTrust]) { |
+ completionHandler(NSURLSessionAuthChallengeUseCredential, nil); |
+ return; |
+ } |
+ NSString* identifier = CreateSessionIdentifierFromTask(dataTask); |
+ |
+ NSDictionary* configuration = |
+ [[NSUserDefaults standardUserDefaults] dictionaryForKey:identifier]; |
+ NSString* host = |
+ [[NSURL URLWithString:[configuration objectForKey:@BREAKPAD_URL]] host]; |
+ if ([challenge.protectionSpace.host isEqualToString:host]) { |
+ NSURLCredential* credential = [NSURLCredential |
+ credentialForTrust:challenge.protectionSpace.serverTrust]; |
+ completionHandler(NSURLSessionAuthChallengeUseCredential, credential); |
+ return; |
+ } |
+ completionHandler(NSURLSessionAuthChallengeUseCredential, nil); |
+} |
+ |
+- (void)URLSessionDidFinishEventsForBackgroundURLSession: |
+ (NSURLSession*)session { |
+ _didFinishEventsCalled = YES; |
+ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ |
+ [self callCompletionHandler]; |
+ }]; |
+} |
+ |
+- (void)taskFinished { |
+ DCHECK_GT(_tasks, 0); |
+ _tasks--; |
+ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ |
+ [self callCompletionHandler]; |
+ }]; |
+} |
+ |
+- (void)callCompletionHandler { |
+ if (_tasks > 0 || !_didFinishEventsCalled) |
+ return; |
+ if (_sessionCompletionHandler) { |
+ void (^completionHandler)() = _sessionCompletionHandler.get(); |
+ completionHandler(); |
+ _sessionCompletionHandler.reset(); |
+ } |
+} |
+ |
+- (void)URLSession:(NSURLSession*)session |
+ dataTask:(NSURLSessionDataTask*)dataTask |
+ didReceiveResponse:(NSURLResponse*)response |
+ completionHandler: |
+ (void (^)(NSURLSessionResponseDisposition disposition))handler { |
+ handler(NSURLSessionResponseAllow); |
+} |
+ |
+- (void)URLSession:(NSURLSession*)session |
+ dataTask:(NSURLSessionDataTask*)dataTask |
+ didReceiveData:(NSData*)data { |
+ NSString* identifier = CreateSessionIdentifierFromTask(dataTask); |
+ |
+ NSDictionary* configuration = |
+ [[NSUserDefaults standardUserDefaults] dictionaryForKey:identifier]; |
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:identifier]; |
+ _tasks++; |
+ |
+ if (experimental_flags::IsAlertOnBackgroundUploadEnabled()) { |
+ base::scoped_nsobject<UILocalNotification> localNotification( |
+ [[UILocalNotification alloc] init]); |
+ localNotification.get().fireDate = [NSDate date]; |
+ base::scoped_nsobject<NSString> reportId( |
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); |
+ localNotification.get().alertBody = [NSString |
+ stringWithFormat:@"Crash report uploaded: %@", reportId.get()]; |
+ [[UIApplication sharedApplication] |
+ scheduleLocalNotification:localNotification]; |
+ } |
+ |
+ [[BreakpadController sharedInstance] withBreakpadRef:^(BreakpadRef ref) { |
+ BreakpadHandleNetworkResponse(ref, configuration, data, nil); |
+ dispatch_async(dispatch_get_main_queue(), ^{ |
+ [self taskFinished]; |
+ }); |
+ }]; |
+} |
+ |
+@end |
+ |
+@implementation CrashReportBackgroundUploader |
+ |
+@synthesize hasPendingCrashReportsToUploadAtStartup; |
+ |
++ (instancetype)sharedInstance { |
+ static CrashReportBackgroundUploader* instance = |
+ [[CrashReportBackgroundUploader alloc] init]; |
+ return instance; |
+} |
+ |
++ (NSURLSession*)BreakpadBackgroundURLSessionWithCompletionHandler: |
+ (ProceduralBlock)completionHandler { |
+ static NSURLSession* session = nil; |
+ static dispatch_once_t onceToken; |
+ dispatch_once(&onceToken, ^{ |
+ |
+ // TODO(olivierrobin) When all bots compile with iOS8 release SDK, use |
+ // only backgroundSessionConfigurationWithIdentifier. |
+ NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration |
+ backgroundSessionConfiguration:kBackgroundReportUploader]; |
+ |
+ session = [NSURLSession |
+ sessionWithConfiguration:sessionConfig |
+ delegate:[UrlSessionDelegate sharedInstance] |
+ delegateQueue:[NSOperationQueue mainQueue]]; |
+ }); |
+ DCHECK(session); |
+ if (completionHandler) { |
+ [[UrlSessionDelegate sharedInstance] |
+ setSessionCompletionHandler:completionHandler]; |
+ } |
+ return session; |
+} |
+ |
++ (BOOL)sendNextReport:(NSDictionary*)nextReport |
+ withBreakpadRef:(BreakpadRef)ref { |
+ NSString* uploadURL = |
+ [NSString stringWithString:[nextReport valueForKey:@BREAKPAD_URL]]; |
+ NSString* tmpDir = NSTemporaryDirectory(); |
+ NSString* tmpFile = [tmpDir |
+ stringByAppendingPathComponent: |
+ [NSString |
+ stringWithFormat:@"%.0f.%@", |
+ [NSDate timeIntervalSinceReferenceDate] * 1000.0, |
+ @"txt"]]; |
+ NSURL* fileURL = [NSURL fileURLWithPath:tmpFile]; |
+ [nextReport setValue:[fileURL absoluteString] forKey:@BREAKPAD_URL]; |
+ |
+#ifndef NDEBUG |
+ NSString* BreakpadMinidumpLocation = [NSHomeDirectory() |
+ stringByAppendingPathComponent:@"Library/Caches/Breakpad"]; |
+ [nextReport setValue:BreakpadMinidumpLocation |
+ forKey:@kReporterMinidumpDirectoryKey]; |
+ [nextReport setValue:BreakpadMinidumpLocation |
+ forKey:@BREAKPAD_DUMP_DIRECTORY]; |
+#endif |
+ |
+ [[BreakpadController sharedInstance] |
+ threadUnsafeSendReportWithConfiguration:nextReport |
+ withBreakpadRef:ref]; |
+ |
+ NSFileManager* fileManager = [NSFileManager defaultManager]; |
+ if (![fileManager fileExistsAtPath:tmpFile]) { |
+ return NO; |
+ } |
+ |
+ NSError* error; |
+ NSString* fileString = |
+ [NSString stringWithContentsOfFile:tmpFile |
+ encoding:NSISOLatin1StringEncoding |
+ error:&error]; |
+ |
+ // The HTTP content is a MIME multipart. The delimiter of the mime body must |
+ // be added to the HTTP headers. |
+ // A mime body is of the form |
+ // --{delimiter} |
+ // content 1 |
+ // --{delimiter} |
+ // content 2 |
+ // --{delimiter}-- |
+ // The delimiter can be read on the first line of the file. |
+ NSString* delimiter = |
+ [[fileString componentsSeparatedByCharactersInSet: |
+ [NSCharacterSet newlineCharacterSet]] firstObject]; |
+ if (![delimiter hasPrefix:@"--"]) { |
+ [fileManager removeItemAtPath:tmpFile error:&error]; |
+ return NO; |
+ } |
+ delimiter = [[delimiter |
+ stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]] |
+ substringFromIndex:2]; |
+ |
+ NSMutableURLRequest* request = |
+ [NSMutableURLRequest requestWithURL:[NSURL URLWithString:uploadURL]]; |
+ [request setHTTPMethod:@"POST"]; |
+ [request setValue:[NSString |
+ stringWithFormat:@"multipart/form-data; boundary=%@", |
+ delimiter] |
+ forHTTPHeaderField:@"Content-type"]; |
+ [request setHTTPBody:[NSData dataWithContentsOfFile:tmpFile]]; |
+ |
+ NSURLSession* session = [CrashReportBackgroundUploader |
+ BreakpadBackgroundURLSessionWithCompletionHandler:nil]; |
+ NSURLSessionDataTask* dataTask = |
+ [session uploadTaskWithRequest:request fromFile:fileURL]; |
+ |
+ NSString* identifier = CreateSessionIdentifierFromTask(dataTask); |
+ [[NSUserDefaults standardUserDefaults] setObject:nextReport |
+ forKey:identifier]; |
+ |
+ [dataTask resume]; |
+ return YES; |
+} |
+ |
++ (void)performFetchWithCompletionHandler: |
+ (BackgroundFetchCompletionBlock)completionHandler { |
+ [[BreakpadController sharedInstance] stop]; |
+ [[BreakpadController sharedInstance] setParametersToAddAtUploadTime:@{ |
+ kUploadedInBackground : @"yes" |
+ }]; |
+ [[BreakpadController sharedInstance] start:YES]; |
+ [[BreakpadController sharedInstance] withBreakpadRef:^(BreakpadRef ref) { |
+ // Note that this processing will be done before |sendNextCrashReport| |
+ // starts uploading the crashes. The ordering is ensured here because both |
+ // the crash report processing and the upload enabling are handled by |
+ // posting blocks to a single |dispath_queue_t| in BreakpadController. |
+ [[BreakpadController sharedInstance] setUploadingEnabled:YES]; |
+ [[BreakpadController sharedInstance] |
+ getNextReportConfigurationOrSendDelay:^(NSDictionary* nextReport, |
+ int delay) { |
+ BOOL reportToSend = NO; |
+ BOOL uploaded = NO; |
+ UMA_HISTOGRAM_COUNTS_100(kUMAMobilePendingReportsOnBackgroundWakeUp, |
+ BreakpadGetCrashReportCount(ref)); |
+ if (delay == 0 && nextReport) { |
+ reportToSend = YES; |
+ NSNumber* crashTimeNum = |
+ [nextReport valueForKey:@BREAKPAD_PROCESS_CRASH_TIME]; |
+ base::Time crashTime = |
+ base::Time::FromTimeT([crashTimeNum intValue]); |
+ base::Time now = base::Time::Now(); |
+ UMA_HISTOGRAM_LONG_TIMES_100(kUMAMobileCrashBackgroundUploadDelay, |
+ now - crashTime); |
+ uploaded = [self sendNextReport:nextReport withBreakpadRef:ref]; |
+ } |
+ int pendingReports = BreakpadGetCrashReportCount(ref); |
+ [[BreakpadController sharedInstance] setUploadingEnabled:NO]; |
+ dispatch_async(dispatch_get_main_queue(), ^{ |
+ if (reportToSend) { |
+ if (uploaded) { |
+ NSUserDefaults* defaults = |
+ [NSUserDefaults standardUserDefaults]; |
+ NSInteger uploadedCrashes = |
+ [defaults integerForKey:kReportsUploadedInBackground]; |
+ [defaults setInteger:(uploadedCrashes + 1) |
+ forKey:kReportsUploadedInBackground]; |
+ web::RecordAction( |
+ UserMetricsAction("BackgroundUploadReportSucceeded")); |
+ |
+ } else { |
+ web::RecordAction( |
+ UserMetricsAction("BackgroundUploadReportAborted")); |
+ } |
+ } |
+ if (uploaded && pendingReports) { |
+ completionHandler(UIBackgroundFetchResultNewData); |
+ } else if (pendingReports) { |
+ completionHandler(UIBackgroundFetchResultFailed); |
+ } else { |
+ [[UIApplication sharedApplication] |
+ setMinimumBackgroundFetchInterval: |
+ UIApplicationBackgroundFetchIntervalNever]; |
+ completionHandler(UIBackgroundFetchResultNoData); |
+ } |
+ }); |
+ }]; |
+ }]; |
+} |
+ |
++ (BOOL)canHandleBackgroundURLSession:(NSString*)identifier { |
+ return [identifier isEqualToString:kBackgroundReportUploader]; |
+} |
+ |
++ (void)handleEventsForBackgroundURLSession:(NSString*)identifier |
+ completionHandler:(ProceduralBlock)completionHandler { |
+ [CrashReportBackgroundUploader |
+ BreakpadBackgroundURLSessionWithCompletionHandler:completionHandler]; |
+} |
+ |
++ (BOOL)hasUploadedCrashReportsInBackground { |
+ NSInteger uploadedCrashReportsInBackgroundCount = |
+ [[NSUserDefaults standardUserDefaults] |
+ integerForKey:kReportsUploadedInBackground]; |
+ return uploadedCrashReportsInBackgroundCount > 0; |
+} |
+ |
++ (void)resetReportsUploadedInBackgroundCount { |
+ [[NSUserDefaults standardUserDefaults] |
+ removeObjectForKey:kReportsUploadedInBackground]; |
+} |
+ |
+@end |