AutorÃa | Ultima modificación | Ver Log |
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "Crashlytics/Crashlytics/Controllers/FIRCLSExistingReportManager.h"
#import "Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h"
#import "Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.h"
#import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionArbiter.h"
#import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionToken.h"
#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
#import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h"
#import "Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h"
#import "Crashlytics/Crashlytics/Private/FIRCrashlyticsReport_Private.h"
#import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h"
// This value should stay in sync with the Android SDK
NSUInteger const FIRCLSMaxUnsentReports = 4;
@interface FIRCLSExistingReportManager ()
@property(nonatomic, strong) FIRCLSFileManager *fileManager;
@property(nonatomic, strong) FIRCLSReportUploader *reportUploader;
@property(nonatomic, strong) NSOperationQueue *operationQueue;
@property(nonatomic, strong) FIRCLSSettings *settings;
@property(nonatomic, strong) FIRCLSDataCollectionArbiter *dataArbiter;
@property(nonatomic, strong) FIRCLSOnDemandModel *onDemandModel;
// This list of active reports excludes the brand new active report that will be created this run of
// the app.
@property(nonatomic, strong) NSArray *existingUnemptyActiveReportPaths;
@property(nonatomic, strong) NSArray *processingReportPaths;
@property(nonatomic, strong) NSArray *preparedReportPaths;
@property(nonatomic, strong) FIRCLSInternalReport *newestInternalReport;
@end
@implementation FIRCLSExistingReportManager
- (instancetype)initWithManagerData:(FIRCLSManagerData *)managerData
reportUploader:(FIRCLSReportUploader *)reportUploader {
self = [super init];
if (!self) {
return nil;
}
_fileManager = managerData.fileManager;
_settings = managerData.settings;
_operationQueue = managerData.operationQueue;
_dataArbiter = managerData.dataArbiter;
_reportUploader = reportUploader;
_onDemandModel = managerData.onDemandModel;
return self;
}
NSInteger compareNewer(FIRCLSInternalReport *reportA,
FIRCLSInternalReport *reportB,
void *context) {
// Compare naturally sorts with oldest first, so swap A and B
return [reportB.dateCreated compare:reportA.dateCreated];
}
- (void)collectExistingReports {
self.existingUnemptyActiveReportPaths =
[self getUnsentActiveReportsAndDeleteEmptyOrOld:self.fileManager.activePathContents];
self.processingReportPaths = self.fileManager.processingPathContents;
self.preparedReportPaths = self.fileManager.preparedPathContents;
}
- (FIRCrashlyticsReport *)newestUnsentReport {
if (self.unsentReportsCount <= 0) {
return nil;
}
return [[FIRCrashlyticsReport alloc] initWithInternalReport:self.newestInternalReport];
}
- (NSUInteger)unsentReportsCount {
// There are nuances about why we only count active reports.
// See the header comment for more information.
return self.existingUnemptyActiveReportPaths.count;
}
/*
* This has the side effect of deleting any reports over the max, starting with oldest reports.
*/
- (NSArray<NSString *> *)getUnsentActiveReportsAndDeleteEmptyOrOld:(NSArray *)reportPaths {
NSMutableArray<FIRCLSInternalReport *> *validReports = [NSMutableArray array];
NSMutableArray<FIRCLSInternalReport *> *reports = [NSMutableArray array];
for (NSString *path in reportPaths) {
FIRCLSInternalReport *_Nullable report = [FIRCLSInternalReport reportWithPath:path];
if (!report) {
continue;
}
[reports addObject:report];
}
if (reports.count == 0) {
return @[];
}
[reports sortUsingFunction:compareNewer context:nil];
NSString *newestReportPath = [reports firstObject].path;
// If there was a MetricKit event recorded on the last run of the app, add it to the newest
// report.
if (self.settings.metricKitCollectionEnabled &&
[self.fileManager metricKitDiagnosticFileExists]) {
[self.fileManager createEmptyMetricKitFile:newestReportPath];
}
for (FIRCLSInternalReport *report in reports) {
// Delete reports without any crashes or non-fatals
if (![report hasAnyEvents]) {
[self.operationQueue addOperationWithBlock:^{
[self.fileManager removeItemAtPath:report.path];
}];
continue;
}
[validReports addObject:report];
}
if (validReports.count == 0) {
return @[];
}
// Sort with the newest at the end
[validReports sortUsingFunction:compareNewer context:nil];
// Set our report for updating in checkAndUpdateUnsentReports
self.newestInternalReport = [validReports firstObject];
// Delete any reports above the limit, starting with the oldest
// which should be at the start of the array.
if (validReports.count > FIRCLSMaxUnsentReports) {
NSUInteger deletingCount = validReports.count - FIRCLSMaxUnsentReports;
FIRCLSInfoLog(@"Deleting %lu unsent reports over the limit of %lu to prevent disk space from "
@"filling up. To prevent this make sure to call send/deleteUnsentReports.",
deletingCount, FIRCLSMaxUnsentReports);
}
// Not that validReports is sorted, delete any reports at indices > MAX_UNSENT_REPORTS, and
// collect the rest of the reports to return.
NSMutableArray<NSString *> *validReportPaths = [NSMutableArray array];
for (int i = 0; i < validReports.count; i++) {
if (i >= FIRCLSMaxUnsentReports) {
[self.operationQueue addOperationWithBlock:^{
NSString *path = [[validReports objectAtIndex:i] path];
[self.fileManager removeItemAtPath:path];
}];
} else {
[validReportPaths addObject:[[validReports objectAtIndex:i] path]];
}
}
return validReportPaths;
}
- (void)sendUnsentReportsWithToken:(FIRCLSDataCollectionToken *)dataCollectionToken
asUrgent:(BOOL)urgent {
for (NSString *path in self.existingUnemptyActiveReportPaths) {
[self processExistingActiveReportPath:path
dataCollectionToken:dataCollectionToken
asUrgent:urgent];
}
for (NSString *path in self.onDemandModel.storedActiveReportPaths) {
[self processExistingActiveReportPath:path
dataCollectionToken:dataCollectionToken
asUrgent:urgent];
}
[self.onDemandModel.storedActiveReportPaths removeAllObjects];
// deal with stuff in processing more carefully - do not process again
[self.operationQueue addOperationWithBlock:^{
for (NSString *path in self.processingReportPaths) {
FIRCLSInternalReport *report = [FIRCLSInternalReport reportWithPath:path];
[self.reportUploader prepareAndSubmitReport:report
dataCollectionToken:dataCollectionToken
asUrgent:NO
withProcessing:NO];
}
}];
// Because this could happen quite a bit after the initial set of files was
// captured, some could be completed (deleted). So, just double-check to make sure
// the file still exists.
[self.operationQueue addOperationWithBlock:^{
for (NSString *path in self.preparedReportPaths) {
if (![[self.fileManager underlyingFileManager] fileExistsAtPath:path]) {
continue;
}
[self.reportUploader uploadPackagedReportAtPath:path
dataCollectionToken:dataCollectionToken
asUrgent:NO];
}
}];
}
- (void)processExistingActiveReportPath:(NSString *)path
dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken
asUrgent:(BOOL)urgent {
FIRCLSInternalReport *report = [FIRCLSInternalReport reportWithPath:path];
// TODO: hasAnyEvents should really be called on the background queue.
if (![report hasAnyEvents]) {
[self.operationQueue addOperationWithBlock:^{
[self.fileManager removeItemAtPath:path];
}];
return;
}
if (urgent && [dataCollectionToken isValid]) {
// We can proceed without the delegate.
[self.reportUploader prepareAndSubmitReport:report
dataCollectionToken:dataCollectionToken
asUrgent:urgent
withProcessing:YES];
return;
}
[self.operationQueue addOperationWithBlock:^{
[self.reportUploader prepareAndSubmitReport:report
dataCollectionToken:dataCollectionToken
asUrgent:NO
withProcessing:YES];
}];
}
- (void)deleteUnsentReports {
NSArray<NSString *> *reportPaths = @[];
reportPaths = [reportPaths arrayByAddingObjectsFromArray:self.existingUnemptyActiveReportPaths];
reportPaths = [reportPaths arrayByAddingObjectsFromArray:self.processingReportPaths];
reportPaths = [reportPaths arrayByAddingObjectsFromArray:self.preparedReportPaths];
[self.operationQueue addOperationWithBlock:^{
for (NSString *path in reportPaths) {
[self.fileManager removeItemAtPath:path];
}
}];
}
- (void)handleOnDemandReportUpload:(NSString *)path
dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken
asUrgent:(BOOL)urgent {
dispatch_async(self.operationQueue.underlyingQueue, ^{
[self processExistingActiveReportPath:path
dataCollectionToken:dataCollectionToken
asUrgent:YES];
});
}
@end