Chromium Code Reviews| 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, | |
|
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.
| |
| 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 |