| 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
|
|
|