Chromium Code Reviews| 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, |
|
Alexei Svitkine (slow)
2015/05/13 14:33:52
If this code is being upstreamed, can you move the
sdefresne
2015/05/13 14:48:23
Sure, done in next patchset.
|
| + 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 |