Proyectos de Subversion Iphone Microlearning

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// Copyright 2021 Google LLC
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//      http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
 
15
#import <Foundation/Foundation.h>
16
 
17
#import "Crashlytics/Crashlytics/Controllers/FIRCLSMetricKitManager.h"
18
 
19
#if CLS_METRICKIT_SUPPORTED
20
 
21
#import "Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h"
22
#include "Crashlytics/Crashlytics/Handlers/FIRCLSMachException.h"
23
#include "Crashlytics/Crashlytics/Handlers/FIRCLSSignal.h"
24
#import "Crashlytics/Crashlytics/Helpers/FIRCLSCallStackTree.h"
25
#import "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h"
26
#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
27
#import "Crashlytics/Crashlytics/Models/FIRCLSExecutionIdentifierModel.h"
28
#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
29
#import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlytics.h"
30
#import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h"
31
 
32
@interface FIRCLSMetricKitManager ()
33
 
34
@property FBLPromise *metricKitDataAvailable;
35
@property FIRCLSExistingReportManager *existingReportManager;
36
@property FIRCLSFileManager *fileManager;
37
@property FIRCLSManagerData *managerData;
38
@property BOOL metricKitPromiseFulfilled;
39
 
40
@end
41
 
42
@implementation FIRCLSMetricKitManager
43
 
44
- (instancetype)initWithManagerData:(FIRCLSManagerData *)managerData
45
              existingReportManager:(FIRCLSExistingReportManager *)existingReportManager
46
                        fileManager:(FIRCLSFileManager *)fileManager {
47
  _existingReportManager = existingReportManager;
48
  _fileManager = fileManager;
49
  _managerData = managerData;
50
  _metricKitPromiseFulfilled = NO;
51
  return self;
52
}
53
 
54
/*
55
 * Registers the MetricKit manager to receive MetricKit reports by adding self to the
56
 * MXMetricManager subscribers. Also initializes the promise that we'll use to ensure that any
57
 * MetricKit report files are included in Crashylytics fatal reports. If no crash occurred on the
58
 * last run of the app, this promise is immediately resolved so that the upload of any nonfatal
59
 * events can proceed.
60
 */
61
- (void)registerMetricKitManager API_AVAILABLE(ios(14)) {
62
  [[MXMetricManager sharedManager] addSubscriber:self];
63
  self.metricKitDataAvailable = [FBLPromise pendingPromise];
64
 
65
  // If there was no crash on the last run of the app or there's no diagnostic report in the
66
  // MetricKit directory, then we aren't expecting a MetricKit diagnostic report and should resolve
67
  // the promise immediately. If MetricKit captured a fatal event and Crashlytics did not, then
68
  // we'll still process the MetricKit crash but won't upload it until the app restarts again.
69
  if (![self.fileManager didCrashOnPreviousExecution] ||
70
      ![self.fileManager metricKitDiagnosticFileExists]) {
71
    @synchronized(self) {
72
      [self fulfillMetricKitPromise];
73
    }
74
  }
75
 
76
  // If we haven't resolved this promise within three seconds, resolve it now so that we're not
77
  // waiting indefinitely for MetricKit payloads that won't arrive.
78
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), self.managerData.dispatchQueue,
79
                 ^{
80
                   @synchronized(self) {
81
                     if (!self.metricKitPromiseFulfilled) {
82
                       FIRCLSDebugLog(@"Resolving MetricKit promise after three seconds");
83
                       [self fulfillMetricKitPromise];
84
                     }
85
                   }
86
                 });
87
 
88
  FIRCLSDebugLog(@"Finished registering metrickit manager");
89
}
90
 
91
/*
92
 * This method receives diagnostic payloads from MetricKit whenever a fatal or nonfatal MetricKit
93
 * event occurs. If a fatal event, this method will be called when the app restarts. Since we're
94
 * including a MetricKit report file in the Crashlytics report to be sent to the backend, we need
95
 * to make sure that we process the payloads and write the included information to file before
96
 * the report is sent up. If this method is called due to a nonfatal event, it will be called
97
 * immediately after the event. Since we send nonfatal events on the next run of the app, we can
98
 * write out the information but won't need to resolve the promise.
99
 */
100
- (void)didReceiveDiagnosticPayloads:(NSArray<MXDiagnosticPayload *> *)payloads
101
    API_AVAILABLE(ios(14)) {
102
  BOOL processedFatalPayload = NO;
103
  for (MXDiagnosticPayload *diagnosticPayload in payloads) {
104
    if (!diagnosticPayload) {
105
      continue;
106
    }
107
 
108
    BOOL processedPayload = [self processMetricKitPayload:diagnosticPayload
109
                                           skipCrashEvent:processedFatalPayload];
110
    if (processedPayload && ([diagnosticPayload.crashDiagnostics count] > 0)) {
111
      processedFatalPayload = YES;
112
    }
113
  }
114
  // Once we've processed all the payloads, resolve the promise so that reporting uploading
115
  // continues. If there was not a crash on the previous run of the app, the promise will already
116
  // have been resolved.
117
  @synchronized(self) {
118
    [self fulfillMetricKitPromise];
119
  }
120
}
121
 
122
// Helper method to write a MetricKit payload's data to file.
123
- (BOOL)processMetricKitPayload:(MXDiagnosticPayload *)diagnosticPayload
124
                 skipCrashEvent:(BOOL)skipCrashEvent API_AVAILABLE(ios(14)) {
125
  BOOL writeFailed = NO;
126
 
127
  // Write out each type of diagnostic if it exists in the report
128
  BOOL hasCrash = [diagnosticPayload.crashDiagnostics count] > 0;
129
  BOOL hasHang = [diagnosticPayload.hangDiagnostics count] > 0;
130
  BOOL hasCPUException = [diagnosticPayload.cpuExceptionDiagnostics count] > 0;
131
  BOOL hasDiskWriteException = [diagnosticPayload.diskWriteExceptionDiagnostics count] > 0;
132
 
133
  // If there are no diagnostics in the report, return before writing out any files.
134
  if (!hasCrash && !hasHang && !hasCPUException && !hasDiskWriteException) {
135
    return false;
136
  }
137
 
138
  // MXDiagnosticPayload have both a timeStampBegin and timeStampEnd. Now that these events are
139
  // real-time, both refer to the same time - record both values anyway.
140
  NSTimeInterval beginSecondsSince1970 = [diagnosticPayload.timeStampBegin timeIntervalSince1970];
141
  NSTimeInterval endSecondsSince1970 = [diagnosticPayload.timeStampEnd timeIntervalSince1970];
142
 
143
  // Get file path for the active reports directory.
144
  NSString *activePath = [[self.fileManager activePath] stringByAppendingString:@"/"];
145
 
146
  // If there is a crash diagnostic in the payload, then this method was called for a fatal event.
147
  // Also ensure that there is a report from the last run of the app that we can write to.
148
  NSString *metricKitFatalReportFile;
149
  NSString *metricKitNonfatalReportFile;
150
 
151
  NSString *newestUnsentReportID =
152
      self.existingReportManager.newestUnsentReport.reportID
153
          ? [self.existingReportManager.newestUnsentReport.reportID stringByAppendingString:@"/"]
154
          : nil;
155
  NSString *currentReportID =
156
      [_managerData.executionIDModel.executionID stringByAppendingString:@"/"];
157
  BOOL crashlyticsFatalReported =
158
      ([diagnosticPayload.crashDiagnostics count] > 0) && (newestUnsentReportID != nil) &&
159
      ([self.fileManager
160
          fileExistsAtPath:[activePath stringByAppendingString:newestUnsentReportID]]);
161
 
162
  // Set the MetricKit fatal path appropriately depending on whether we also captured a Crashlytics
163
  // fatal event and whether the diagnostic report came from a fatal or nonfatal event.
164
  if (crashlyticsFatalReported) {
165
    metricKitFatalReportFile = [[activePath stringByAppendingString:newestUnsentReportID]
166
        stringByAppendingString:FIRCLSMetricKitFatalReportFile];
167
  } else {
168
    metricKitFatalReportFile = [[activePath stringByAppendingString:currentReportID]
169
        stringByAppendingString:FIRCLSMetricKitFatalReportFile];
170
  }
171
  metricKitNonfatalReportFile = [[activePath stringByAppendingString:currentReportID]
172
      stringByAppendingString:FIRCLSMetricKitNonfatalReportFile];
173
 
174
  if (!metricKitFatalReportFile || !metricKitNonfatalReportFile) {
175
    FIRCLSDebugLog(@"Error finding MetricKit files");
176
    return NO;
177
  }
178
 
179
  FIRCLSDebugLog(@"File paths for MetricKit report:  %@, %@", metricKitFatalReportFile,
180
                 metricKitNonfatalReportFile);
181
  if (hasCrash && ![_fileManager fileExistsAtPath:metricKitFatalReportFile]) {
182
    [_fileManager createFileAtPath:metricKitFatalReportFile contents:nil attributes:nil];
183
  }
184
  if ((hasHang | hasCPUException | hasDiskWriteException) &&
185
      ![_fileManager fileExistsAtPath:metricKitNonfatalReportFile]) {
186
    [_fileManager createFileAtPath:metricKitNonfatalReportFile contents:nil attributes:nil];
187
  }
188
  NSFileHandle *nonfatalFile =
189
      [NSFileHandle fileHandleForUpdatingAtPath:metricKitNonfatalReportFile];
190
  if ((hasHang | hasCPUException | hasDiskWriteException) && nonfatalFile == nil) {
191
    FIRCLSDebugLog(@"Unable to create or open nonfatal MetricKit file.");
192
    return false;
193
  }
194
  NSFileHandle *fatalFile = [NSFileHandle fileHandleForUpdatingAtPath:metricKitFatalReportFile];
195
  if (hasCrash && fatalFile == nil) {
196
    FIRCLSDebugLog(@"Unable to create or open fatal MetricKit file.");
197
    return false;
198
  }
199
 
200
  NSData *newLineData = [@"\n" dataUsingEncoding:NSUTF8StringEncoding];
201
 
202
  // For each diagnostic type, write out a section in the MetricKit report file. This section will
203
  // have subsections for threads, metadata, and event specific metadata.
204
  if (hasCrash && !skipCrashEvent) {
205
    // Write out time information to the MetricKit report file. Time needs to be a value for
206
    // backend serialization, so we write out end_time separately.
207
 
208
    MXCrashDiagnostic *crashDiagnostic = [diagnosticPayload.crashDiagnostics objectAtIndex:0];
209
 
210
    NSArray *threadArray = [self convertThreadsToArray:crashDiagnostic.callStackTree];
211
    NSDictionary *metadataDict = [self convertMetadataToDictionary:crashDiagnostic.metaData];
212
 
213
    NSString *nilString = @"";
214
 
215
    // On the backend, we process name, code name, and address into the subtitle of an issue.
216
    // Mach exception name and code should be preferred over signal name and code if available.
217
    const char *signalName = NULL;
218
    const char *signalCodeName = NULL;
219
    FIRCLSSignalNameLookup([crashDiagnostic.signal intValue], 0, &signalName, &signalCodeName);
220
    // signalName is the default name, so should never be NULL
221
    if (signalName == NULL) {
222
      signalName = "UNKNOWN";
223
    }
224
    if (signalCodeName == NULL) {
225
      signalCodeName = "";
226
    }
227
 
228
    const char *machExceptionName = NULL;
229
    const char *machExceptionCodeName = NULL;
230
#if CLS_MACH_EXCEPTION_SUPPORTED
231
    FIRCLSMachExceptionNameLookup(
232
        [crashDiagnostic.exceptionType intValue],
233
        (mach_exception_data_type_t)[crashDiagnostic.exceptionCode intValue], &machExceptionName,
234
        &machExceptionCodeName);
235
#endif
236
    if (machExceptionCodeName == NULL) {
237
      machExceptionCodeName = "";
238
    }
239
 
240
    NSString *name = machExceptionName != NULL ? [NSString stringWithUTF8String:machExceptionName]
241
                                               : [NSString stringWithUTF8String:signalName];
242
    NSString *codeName = machExceptionName != NULL
243
                             ? [NSString stringWithUTF8String:machExceptionCodeName]
244
                             : [NSString stringWithUTF8String:signalCodeName];
245
 
246
    NSDictionary *crashDictionary = @{
247
      @"metric_kit_fatal" : @{
248
        @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
249
        @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
250
        @"metadata" : metadataDict,
251
        @"termination_reason" :
252
                (crashDiagnostic.terminationReason) ? crashDiagnostic.terminationReason : nilString,
253
        @"virtual_memory_region_info" : (crashDiagnostic.virtualMemoryRegionInfo)
254
            ? crashDiagnostic.virtualMemoryRegionInfo
255
            : nilString,
256
        @"exception_type" : crashDiagnostic.exceptionType,
257
        @"exception_code" : crashDiagnostic.exceptionCode,
258
        @"signal" : crashDiagnostic.signal,
259
        @"app_version" : crashDiagnostic.applicationVersion,
260
        @"code_name" : codeName,
261
        @"name" : name
262
      }
263
    };
264
    writeFailed = ![self writeDictionaryToFile:crashDictionary
265
                                          file:fatalFile
266
                                   newLineData:newLineData];
267
    writeFailed = writeFailed | ![self writeDictionaryToFile:@{@"threads" : threadArray}
268
                                                        file:fatalFile
269
                                                 newLineData:newLineData];
270
  }
271
 
272
  if (hasHang) {
273
    MXHangDiagnostic *hangDiagnostic = [diagnosticPayload.hangDiagnostics objectAtIndex:0];
274
 
275
    NSArray *threadArray = [self convertThreadsToArray:hangDiagnostic.callStackTree];
276
    NSDictionary *metadataDict = [self convertMetadataToDictionary:hangDiagnostic.metaData];
277
 
278
    NSDictionary *hangDictionary = @{
279
      @"exception" : @{
280
        @"type" : @"metrickit_nonfatal",
281
        @"name" : @"hang_event",
282
        @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
283
        @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
284
        @"threads" : threadArray,
285
        @"metadata" : metadataDict,
286
        @"hang_duration" : [NSNumber numberWithDouble:[hangDiagnostic.hangDuration doubleValue]],
287
        @"app_version" : hangDiagnostic.applicationVersion
288
      }
289
    };
290
 
291
    writeFailed = ![self writeDictionaryToFile:hangDictionary
292
                                          file:nonfatalFile
293
                                   newLineData:newLineData];
294
  }
295
 
296
  if (hasCPUException) {
297
    MXCPUExceptionDiagnostic *cpuExceptionDiagnostic =
298
        [diagnosticPayload.cpuExceptionDiagnostics objectAtIndex:0];
299
 
300
    NSArray *threadArray = [self convertThreadsToArray:cpuExceptionDiagnostic.callStackTree];
301
    NSDictionary *metadataDict = [self convertMetadataToDictionary:cpuExceptionDiagnostic.metaData];
302
 
303
    NSDictionary *cpuDictionary = @{
304
      @"exception" : @{
305
        @"type" : @"metrickit_nonfatal",
306
        @"name" : @"cpu_exception_event",
307
        @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
308
        @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
309
        @"threads" : threadArray,
310
        @"metadata" : metadataDict,
311
        @"total_cpu_time" :
312
            [NSNumber numberWithDouble:[cpuExceptionDiagnostic.totalCPUTime doubleValue]],
313
        @"total_sampled_time" :
314
            [NSNumber numberWithDouble:[cpuExceptionDiagnostic.totalSampledTime doubleValue]],
315
        @"app_version" : cpuExceptionDiagnostic.applicationVersion
316
      }
317
    };
318
    writeFailed = ![self writeDictionaryToFile:cpuDictionary
319
                                          file:nonfatalFile
320
                                   newLineData:newLineData];
321
  }
322
 
323
  if (hasDiskWriteException) {
324
    MXDiskWriteExceptionDiagnostic *diskWriteExceptionDiagnostic =
325
        [diagnosticPayload.diskWriteExceptionDiagnostics objectAtIndex:0];
326
 
327
    NSArray *threadArray = [self convertThreadsToArray:diskWriteExceptionDiagnostic.callStackTree];
328
    NSDictionary *metadataDict =
329
        [self convertMetadataToDictionary:diskWriteExceptionDiagnostic.metaData];
330
 
331
    NSDictionary *diskWriteDictionary = @{
332
      @"exception" : @{
333
        @"type" : @"metrickit_nonfatal",
334
        @"name" : @"disk_write_exception_event",
335
        @"time" : [NSNumber numberWithLong:beginSecondsSince1970],
336
        @"end_time" : [NSNumber numberWithLong:endSecondsSince1970],
337
        @"threads" : threadArray,
338
        @"metadata" : metadataDict,
339
        @"app_version" : diskWriteExceptionDiagnostic.applicationVersion,
340
        @"total_writes_caused" :
341
            [NSNumber numberWithDouble:[diskWriteExceptionDiagnostic.totalWritesCaused doubleValue]]
342
      }
343
    };
344
    writeFailed = ![self writeDictionaryToFile:diskWriteDictionary
345
                                          file:nonfatalFile
346
                                   newLineData:newLineData];
347
  }
348
 
349
  return !writeFailed;
350
}
351
/*
352
 * Required for MXMetricManager subscribers. Since we aren't currently collecting any MetricKit
353
 * metrics, this method is left empty.
354
 */
355
- (void)didReceiveMetricPayloads:(NSArray<MXMetricPayload *> *)payloads API_AVAILABLE(ios(13)) {
356
}
357
 
358
- (FBLPromise *)waitForMetricKitDataAvailable {
359
  FBLPromise *result = nil;
360
  @synchronized(self) {
361
    result = self.metricKitDataAvailable;
362
  }
363
  return result;
364
}
365
 
366
/*
367
 * Helper method to convert threads for a MetricKit fatal diagnostic event to an array of threads.
368
 */
369
- (NSArray *)convertThreadsToArray:(MXCallStackTree *)mxCallStackTree API_AVAILABLE(ios(14)) {
370
  FIRCLSCallStackTree *tree = [[FIRCLSCallStackTree alloc] initWithMXCallStackTree:mxCallStackTree];
371
  return [tree getArrayRepresentation];
372
}
373
 
374
/*
375
 * Helper method to convert threads for a MetricKit nonfatal diagnostic event to an array of frames.
376
 */
377
- (NSArray *)convertThreadsToArrayForNonfatal:(MXCallStackTree *)mxCallStackTree
378
    API_AVAILABLE(ios(14)) {
379
  FIRCLSCallStackTree *tree = [[FIRCLSCallStackTree alloc] initWithMXCallStackTree:mxCallStackTree];
380
  return [tree getFramesOfBlamedThread];
381
}
382
 
383
/*
384
 * Helper method to convert metadata for a MetricKit diagnostic event to a dictionary. MXMetadata
385
 * has a dictionaryRepresentation method but it is deprecated.
386
 */
387
- (NSDictionary *)convertMetadataToDictionary:(MXMetaData *)metadata API_AVAILABLE(ios(14)) {
388
  NSError *error = nil;
389
  NSDictionary *metadataDictionary =
390
      [NSJSONSerialization JSONObjectWithData:[metadata JSONRepresentation] options:0 error:&error];
391
  return metadataDictionary;
392
}
393
 
394
/*
395
 * Helper method to fulfill the metricKitDataAvailable promise and track that it has been fulfilled.
396
 */
397
- (void)fulfillMetricKitPromise {
398
  if (self.metricKitPromiseFulfilled) return;
399
 
400
  [self.metricKitDataAvailable fulfill:nil];
401
  self.metricKitPromiseFulfilled = YES;
402
}
403
 
404
/*
405
 * Helper method to write a dictionary of event information to file. Returns whether it succeeded.
406
 */
407
- (BOOL)writeDictionaryToFile:(NSDictionary *)dictionary
408
                         file:(NSFileHandle *)file
409
                  newLineData:(NSData *)newLineData {
410
  NSError *dataError = nil;
411
  NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&dataError];
412
  if (dataError) {
413
    FIRCLSDebugLog(@"Unable to write out dictionary.");
414
    return NO;
415
  }
416
 
417
  [file seekToEndOfFile];
418
  [file writeData:data];
419
  [file writeData:newLineData];
420
 
421
  return YES;
422
}
423
 
424
- (NSString *)getSignalName:(NSNumber *)signalCode {
425
  int signal = [signalCode intValue];
426
  switch (signal) {
427
    case SIGABRT:
428
      return @"SIGABRT";
429
    case SIGBUS:
430
      return @"SIGBUS";
431
    case SIGFPE:
432
      return @"SIGFPE";
433
    case SIGILL:
434
      return @"SIGILL";
435
    case SIGSEGV:
436
      return @"SIGSEGV";
437
    case SIGSYS:
438
      return @"SIGSYS";
439
    case SIGTRAP:
440
      return @"SIGTRAP";
441
    default:
442
      return @"UNKNOWN";
443
  }
444
  return @"UNKNOWN";
445
}
446
 
447
@end
448
 
449
#endif  // CLS_METRICKIT_SUPPORTED