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;
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]];
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;
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.
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];
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
77 (void (^)(NSURLSessionAuthChallengeDisposition disposition,
78 NSURLCredential* credential))completionHandler {
79 if (![challenge.protectionSpace.authenticationMethod
80 isEqualToString:NSURLAuthenticationMethodServerTrust]) {
81 completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
84 NSString* identifier = CreateSessionIdentifierFromTask(dataTask);
86 NSDictionary* configuration =
87 [[NSUserDefaults standardUserDefaults] dictionaryForKey:identifier];
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);
96 completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
99 - (void)URLSessionDidFinishEventsForBackgroundURLSession:
100 (NSURLSession*)session {
101 _didFinishEventsCalled = YES;
102 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
103 [self callCompletionHandler];
107 - (void)taskFinished {
108 DCHECK_GT(_tasks, 0);
110 [[NSOperationQueue mainQueue] addOperationWithBlock:^{
111 [self callCompletionHandler];
115 - (void)callCompletionHandler {
116 if (_tasks > 0 || !_didFinishEventsCalled)
118 if (_sessionCompletionHandler) {
119 void (^completionHandler)() = _sessionCompletionHandler.get();
121 _sessionCompletionHandler.reset();
125 - (void)URLSession:(NSURLSession*)session
126 dataTask:(NSURLSessionDataTask*)dataTask
127 didReceiveResponse:(NSURLResponse*)response
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];
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];
155 [[BreakpadController sharedInstance] withBreakpadRef:^(BreakpadRef ref) {
156 BreakpadHandleNetworkResponse(ref, configuration, data, nil);
157 dispatch_async(dispatch_get_main_queue(), ^{
165 @implementation CrashReportBackgroundUploader
167 @synthesize hasPendingCrashReportsToUploadAtStartup;
169 + (instancetype)sharedInstance {
170 static CrashReportBackgroundUploader* instance =
171 [[CrashReportBackgroundUploader alloc] init];
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]];
192 if (completionHandler) {
193 [[UrlSessionDelegate sharedInstance]
194 setSessionCompletionHandler:completionHandler];
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:
207 stringWithFormat:@"%.0f.%@",
208 [NSDate timeIntervalSinceReferenceDate] * 1000.0,
210 NSURL* fileURL = [NSURL fileURLWithPath:tmpFile];
211 [nextReport setValue:[fileURL absoluteString] forKey:@BREAKPAD_URL];
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];
222 [[BreakpadController sharedInstance]
223 threadUnsafeSendReportWithConfiguration:nextReport
224 withBreakpadRef:ref];
226 NSFileManager* fileManager = [NSFileManager defaultManager];
227 if (![fileManager fileExistsAtPath:tmpFile]) {
232 NSString* fileString =
233 [NSString stringWithContentsOfFile:tmpFile
234 encoding:NSISOLatin1StringEncoding
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
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];
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=%@",
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
279 + (void)performFetchWithCompletionHandler:
280 (BackgroundFetchCompletionBlock)completionHandler {
281 [[BreakpadController sharedInstance] stop];
282 [[BreakpadController sharedInstance] setParametersToAddAtUploadTime:@{
283 kUploadedInBackground : @"yes"
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,
295 BOOL reportToSend = NO;
297 UMA_HISTOGRAM_COUNTS_100(kUMAMobilePendingReportsOnBackgroundWakeUp,
298 BreakpadGetCrashReportCount(ref));
299 if (delay == 0 && nextReport) {
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,
308 uploaded = [self sendNextReport:nextReport withBreakpadRef:ref];
310 int pendingReports = BreakpadGetCrashReportCount(ref);
311 [[BreakpadController sharedInstance] setUploadingEnabled:NO];
312 dispatch_async(dispatch_get_main_queue(), ^{
315 NSUserDefaults* defaults =
316 [NSUserDefaults standardUserDefaults];
317 NSInteger uploadedCrashes =
318 [defaults integerForKey:kReportsUploadedInBackground];
319 [defaults setInteger:(uploadedCrashes + 1)
320 forKey:kReportsUploadedInBackground];
322 UserMetricsAction("BackgroundUploadReportSucceeded"));
326 UserMetricsAction("BackgroundUploadReportAborted"));
329 if (uploaded && pendingReports) {
330 completionHandler(UIBackgroundFetchResultNewData);
331 } else if (pendingReports) {
332 completionHandler(UIBackgroundFetchResultFailed);
334 [[UIApplication sharedApplication]
335 setMinimumBackgroundFetchInterval:
336 UIApplicationBackgroundFetchIntervalNever];
337 completionHandler(UIBackgroundFetchResultNoData);
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];