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 |