OLD | NEW |
(Empty) | |
| 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. |
| 4 |
| 5 #import "ios/chrome/browser/crash_report/crash_report_background_uploader.h" |
| 6 |
| 7 #import <UIKit/UIKit.h> |
| 8 |
| 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" |
| 18 |
| 19 using base::UserMetricsAction; |
| 20 |
| 21 namespace { |
| 22 |
| 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"; |
| 31 |
| 32 NSString* CreateSessionIdentifierFromTask(NSURLSessionTask* task) { |
| 33 return [NSString stringWithFormat:@"%@.%ld", kBackgroundReportUploader, |
| 34 (unsigned long)[task taskIdentifier]]; |
| 35 } |
| 36 |
| 37 } // namespace |
| 38 |
| 39 @interface UrlSessionDelegate : NSObject<NSURLSessionDelegate, |
| 40 NSURLSessionTaskDelegate, |
| 41 NSURLSessionDataDelegate> |
| 42 + (instancetype)sharedInstance; |
| 43 |
| 44 // Sets the completion handler for the URL session current tasks. The |
| 45 // |completionHandler| cannot be nil. |
| 46 - (void)setSessionCompletionHandler:(ProceduralBlock)completionHandler; |
| 47 |
| 48 @end |
| 49 |
| 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; |
| 59 } |
| 60 |
| 61 + (instancetype)sharedInstance { |
| 62 static UrlSessionDelegate* instance = [[UrlSessionDelegate alloc] init]; |
| 63 return instance; |
| 64 } |
| 65 |
| 66 - (void)setSessionCompletionHandler:(ProceduralBlock)completionHandler { |
| 67 DCHECK(completionHandler); |
| 68 _sessionCompletionHandler.reset(completionHandler, |
| 69 base::scoped_policy::RETAIN); |
| 70 _didFinishEventsCalled = NO; |
| 71 } |
| 72 |
| 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); |
| 85 |
| 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); |
| 97 } |
| 98 |
| 99 - (void)URLSessionDidFinishEventsForBackgroundURLSession: |
| 100 (NSURLSession*)session { |
| 101 _didFinishEventsCalled = YES; |
| 102 [[NSOperationQueue mainQueue] addOperationWithBlock:^{ |
| 103 [self callCompletionHandler]; |
| 104 }]; |
| 105 } |
| 106 |
| 107 - (void)taskFinished { |
| 108 DCHECK_GT(_tasks, 0); |
| 109 _tasks--; |
| 110 [[NSOperationQueue mainQueue] addOperationWithBlock:^{ |
| 111 [self callCompletionHandler]; |
| 112 }]; |
| 113 } |
| 114 |
| 115 - (void)callCompletionHandler { |
| 116 if (_tasks > 0 || !_didFinishEventsCalled) |
| 117 return; |
| 118 if (_sessionCompletionHandler) { |
| 119 void (^completionHandler)() = _sessionCompletionHandler.get(); |
| 120 completionHandler(); |
| 121 _sessionCompletionHandler.reset(); |
| 122 } |
| 123 } |
| 124 |
| 125 - (void)URLSession:(NSURLSession*)session |
| 126 dataTask:(NSURLSessionDataTask*)dataTask |
| 127 didReceiveResponse:(NSURLResponse*)response |
| 128 completionHandler: |
| 129 (void (^)(NSURLSessionResponseDisposition disposition))handler { |
| 130 handler(NSURLSessionResponseAllow); |
| 131 } |
| 132 |
| 133 - (void)URLSession:(NSURLSession*)session |
| 134 dataTask:(NSURLSessionDataTask*)dataTask |
| 135 didReceiveData:(NSData*)data { |
| 136 NSString* identifier = CreateSessionIdentifierFromTask(dataTask); |
| 137 |
| 138 NSDictionary* configuration = |
| 139 [[NSUserDefaults standardUserDefaults] dictionaryForKey:identifier]; |
| 140 [[NSUserDefaults standardUserDefaults] removeObjectForKey:identifier]; |
| 141 _tasks++; |
| 142 |
| 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 } |
| 154 |
| 155 [[BreakpadController sharedInstance] withBreakpadRef:^(BreakpadRef ref) { |
| 156 BreakpadHandleNetworkResponse(ref, configuration, data, nil); |
| 157 dispatch_async(dispatch_get_main_queue(), ^{ |
| 158 [self taskFinished]; |
| 159 }); |
| 160 }]; |
| 161 } |
| 162 |
| 163 @end |
| 164 |
| 165 @implementation CrashReportBackgroundUploader |
| 166 |
| 167 @synthesize hasPendingCrashReportsToUploadAtStartup; |
| 168 |
| 169 + (instancetype)sharedInstance { |
| 170 static CrashReportBackgroundUploader* instance = |
| 171 [[CrashReportBackgroundUploader alloc] init]; |
| 172 return instance; |
| 173 } |
| 174 |
| 175 + (NSURLSession*)BreakpadBackgroundURLSessionWithCompletionHandler: |
| 176 (ProceduralBlock)completionHandler { |
| 177 static NSURLSession* session = nil; |
| 178 static dispatch_once_t onceToken; |
| 179 dispatch_once(&onceToken, ^{ |
| 180 |
| 181 // TODO(olivierrobin) When all bots compile with iOS8 release SDK, use |
| 182 // only backgroundSessionConfigurationWithIdentifier. |
| 183 NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration |
| 184 backgroundSessionConfiguration:kBackgroundReportUploader]; |
| 185 |
| 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; |
| 197 } |
| 198 |
| 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]; |
| 212 |
| 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 |
| 221 |
| 222 [[BreakpadController sharedInstance] |
| 223 threadUnsafeSendReportWithConfiguration:nextReport |
| 224 withBreakpadRef:ref]; |
| 225 |
| 226 NSFileManager* fileManager = [NSFileManager defaultManager]; |
| 227 if (![fileManager fileExistsAtPath:tmpFile]) { |
| 228 return NO; |
| 229 } |
| 230 |
| 231 NSError* error; |
| 232 NSString* fileString = |
| 233 [NSString stringWithContentsOfFile:tmpFile |
| 234 encoding:NSISOLatin1StringEncoding |
| 235 error:&error]; |
| 236 |
| 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]; |
| 256 |
| 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]]; |
| 265 |
| 266 NSURLSession* session = [CrashReportBackgroundUploader |
| 267 BreakpadBackgroundURLSessionWithCompletionHandler:nil]; |
| 268 NSURLSessionDataTask* dataTask = |
| 269 [session uploadTaskWithRequest:request fromFile:fileURL]; |
| 270 |
| 271 NSString* identifier = CreateSessionIdentifierFromTask(dataTask); |
| 272 [[NSUserDefaults standardUserDefaults] setObject:nextReport |
| 273 forKey:identifier]; |
| 274 |
| 275 [dataTask resume]; |
| 276 return YES; |
| 277 } |
| 278 |
| 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")); |
| 323 |
| 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 }]; |
| 342 } |
| 343 |
| 344 + (BOOL)canHandleBackgroundURLSession:(NSString*)identifier { |
| 345 return [identifier isEqualToString:kBackgroundReportUploader]; |
| 346 } |
| 347 |
| 348 + (void)handleEventsForBackgroundURLSession:(NSString*)identifier |
| 349 completionHandler:(ProceduralBlock)completionHandler { |
| 350 [CrashReportBackgroundUploader |
| 351 BreakpadBackgroundURLSessionWithCompletionHandler:completionHandler]; |
| 352 } |
| 353 |
| 354 + (BOOL)hasUploadedCrashReportsInBackground { |
| 355 NSInteger uploadedCrashReportsInBackgroundCount = |
| 356 [[NSUserDefaults standardUserDefaults] |
| 357 integerForKey:kReportsUploadedInBackground]; |
| 358 return uploadedCrashReportsInBackgroundCount > 0; |
| 359 } |
| 360 |
| 361 + (void)resetReportsUploadedInBackgroundCount { |
| 362 [[NSUserDefaults standardUserDefaults] |
| 363 removeObjectForKey:kReportsUploadedInBackground]; |
| 364 } |
| 365 |
| 366 @end |
OLD | NEW |