FBControlCore/Crashes/FBCrashLog.m (343 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBCrashLog.h"
#import <stdio.h>
#import "FBControlCoreGlobalConfiguration.h"
#import "FBConcurrentCollectionOperations.h"
#import "NSPredicate+FBControlCore.h"
#import "FBControlCoreError.h"
#import "FBControlCoreLogger.h"
@implementation FBCrashLog
#pragma mark Initializers
- (instancetype)initWithInfo:(FBCrashLogInfo *)info contents:(NSString *)contents
{
self = [super init];
if (!self) {
return nil;
}
_info = info;
_contents = contents;
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"Crash Info: %@ \n Crash Report: %@\n", _info, _contents];
}
#pragma mark NSCopying
- (instancetype)copyWithZone:(NSZone *)zone
{
// Is immutable
return self;
}
@end
@implementation FBCrashLogInfo
#pragma mark Initializers
+ (instancetype)fromCrashLogAtPath:(NSString *)crashPath error:(NSError **)error
{
if (!crashPath) {
return [[FBControlCoreError
describe:@"No crash path provided"]
fail:error];
}
FILE *file = fopen(crashPath.UTF8String, "r");
if (!file) {
return [[FBControlCoreError
describeFormat:@"Could not open file at path %@: %s", crashPath, strerror(errno)]
fail:error];
}
NSString *executablePath = nil;
NSString *identifier = nil;
NSString *processName = nil;
NSString *parentProcessName = nil;
NSDate *date = nil;
pid_t processIdentifier = -1;
pid_t parentProcessIdentifier = -1;
if (![self extractFromFile:file executablePathOut:&executablePath identifierOut:&identifier processNameOut:&processName parentProcessNameOut:&parentProcessName processIdentifierOut:&processIdentifier parentProcessIdentifierOut:&parentProcessIdentifier dateOut:&date error:error]) {
fclose(file);
return nil;
}
FBCrashLogInfoProcessType processType = [self processTypeForExecutablePath:executablePath];
fclose(file);
return [[FBCrashLogInfo alloc]
initWithCrashPath:crashPath
executablePath:executablePath
identifier:identifier
processName:processName
processIdentifier:processIdentifier
parentProcessName:parentProcessName
parentProcessIdentifier:parentProcessIdentifier
date:date
processType:processType];
}
- (instancetype)initWithCrashPath:(NSString *)crashPath executablePath:(NSString *)executablePath identifier:(NSString *)identifier processName:(NSString *)processName processIdentifier:(pid_t)processIdentifer parentProcessName:(NSString *)parentProcessName parentProcessIdentifier:(pid_t)parentProcessIdentifier date:(NSDate *)date processType:(FBCrashLogInfoProcessType)processType
{
self = [super init];
if (!self) {
return nil;
}
_crashPath = crashPath;
_executablePath = executablePath;
_identifier = identifier;
_processName = processName;
_processIdentifier = processIdentifer;
_parentProcessName = parentProcessName;
_parentProcessIdentifier = parentProcessIdentifier;
_date = date;
_processType = processType;
return self;
}
#pragma mark Public
+ (BOOL)isParsableCrashLog:(NSData *)data
{
#if defined(__apple_build_version__)
if (@available(macOS 10.13, *)) {
FILE *file = fmemopen((void *)data.bytes, data.length, "r");
if (!file) {
return NO;
}
BOOL parsable = [self extractFromFile:file executablePathOut:nil identifierOut:nil processNameOut:nil parentProcessNameOut:nil processIdentifierOut:nil parentProcessIdentifierOut:nil dateOut:nil error:nil];
fclose(file);
return parsable;
} else {
return NO;
}
#else
return NO;
#endif
}
#pragma mark NSObject
- (NSString *)description
{
return [NSString stringWithFormat:
@"Identifier %@ | Executable Path %@ | Process %@ | pid %d | Parent %@ | ppid %d | Date %@ | Path %@",
self.identifier,
self.executablePath,
self.processName,
self.processIdentifier,
self.parentProcessName,
self.parentProcessIdentifier,
self.date,
self.crashPath
];
}
#pragma mark NSCopying
- (instancetype)copyWithZone:(NSZone *)zone
{
// Is immutable
return self;
}
#pragma mark Properties
- (NSString *)name
{
return self.crashPath.lastPathComponent;
}
#pragma mark Bulk Collection
+ (NSArray<FBCrashLogInfo *> *)crashInfoAfterDate:(NSDate *)date logger:(id<FBControlCoreLogger>)logger
{
NSMutableArray<FBCrashLogInfo *> *allCrashInfos = NSMutableArray.new;
for (NSString *basePath in self.diagnosticReportsPaths) {
NSArray<FBCrashLogInfo *> *crashInfos = [[FBConcurrentCollectionOperations
filterMap:[NSFileManager.defaultManager contentsOfDirectoryAtPath:basePath error:nil]
predicate:[FBCrashLogInfo predicateForFilesWithBasePath:basePath afterDate:date withExtension:@"crash"]
map:^ FBCrashLogInfo * (NSString *fileName) {
NSString *path = [basePath stringByAppendingPathComponent:fileName];
NSError *error = nil;
FBCrashLogInfo *info = [FBCrashLogInfo fromCrashLogAtPath:path error:&error];
if (!info) {
[logger logFormat:@"Error parsing log %@", error];
}
return info;
}]
filteredArrayUsingPredicate:NSPredicate.notNullPredicate];
[allCrashInfos addObjectsFromArray:crashInfos];
}
return [allCrashInfos copy];
}
#pragma mark Contents
- (FBCrashLog *)obtainCrashLogWithError:(NSError **)error
{
NSError *innerError = nil;
NSString *contents = [NSString stringWithContentsOfFile:self.crashPath encoding:NSUTF8StringEncoding error:&innerError];
if (!contents) {
return [[[FBControlCoreError
describeFormat:@"Failed to read crash log at path %@", self.crashPath]
causedBy:innerError]
fail:error];
}
return [[FBCrashLog alloc] initWithInfo:self contents:contents];
}
#pragma mark Predicates
+ (NSPredicate *)predicateForCrashLogsWithProcessID:(pid_t)processID
{
return [NSPredicate predicateWithBlock:^ BOOL (FBCrashLogInfo *crashLog, id _) {
return crashLog.processIdentifier == processID;
}];
}
+ (NSPredicate *)predicateNewerThanDate:(NSDate *)date
{
return [NSPredicate predicateWithBlock:^ BOOL (FBCrashLogInfo *crashLog, id _) {
return [date compare:crashLog.date] == NSOrderedAscending;
}];
}
+ (NSPredicate *)predicateOlderThanDate:(NSDate *)date
{
return [NSCompoundPredicate notPredicateWithSubpredicate:[self predicateNewerThanDate:date]];
}
+ (NSPredicate *)predicateForIdentifier:(NSString *)identifier
{
return [NSPredicate predicateWithBlock:^ BOOL (FBCrashLogInfo *crashLog, id _) {
return [identifier isEqualToString:crashLog.identifier];
}];
}
+ (NSPredicate *)predicateForName:(NSString *)name
{
return [NSPredicate predicateWithBlock:^ BOOL (FBCrashLogInfo *crashLog, id _) {
return [name isEqualToString:crashLog.name];
}];
}
+ (NSPredicate *)predicateForExecutablePathContains:(NSString *)contains
{
return [NSPredicate predicateWithBlock:^ BOOL (FBCrashLogInfo *crashLog, id _) {
return [crashLog.executablePath containsString:contains];
}];
}
#pragma mark Helpers
+ (NSArray<NSString *> *)diagnosticReportsPaths
{
return @[
[NSHomeDirectory() stringByAppendingPathComponent:@"Library/Logs/DiagnosticReports"],
@"/Library/Logs/DiagnosticReports", // diagnostic reports path when ReportCrash is running as root.
];
}
#pragma mark Private
static NSUInteger MaxLineSearch = 20;
+ (BOOL)extractFromFile:(FILE *)file executablePathOut:(NSString **)executablePathOut identifierOut:(NSString **)identifierOut processNameOut:(NSString **)processNameOut parentProcessNameOut:(NSString **)parentProcessNameOut processIdentifierOut:(pid_t *)processIdentifierOut parentProcessIdentifierOut:(pid_t *)parentProcessIdentifierOut dateOut:(NSDate **)dateOut error:(NSError **)error
{
// Buffers for the sscanf
size_t lineSize = sizeof(char) * 4098;
char *line = malloc(lineSize);
char value[lineSize];
// Values that should exist after scanning
NSDate *date = nil;
NSString *executablePath = nil;
NSString *identifier = nil;
NSString *parentProcessName = nil;
NSString *processName = nil;
pid_t processIdentifier = -1;
pid_t parentProcessIdentifier = -1;
NSUInteger lineNumber = 0;
while (lineNumber++ < MaxLineSearch && getline(&line, &lineSize, file) > 0) {
if (sscanf(line, "Process: %s [%d]", value, &processIdentifier) > 0) {
processName = [[NSString alloc] initWithCString:value encoding:NSUTF8StringEncoding];
continue;
}
if (sscanf(line, "Identifier: %s", value) > 0) {
identifier = [[NSString alloc] initWithCString:value encoding:NSUTF8StringEncoding];
continue;
}
if (sscanf(line, "Parent Process: %s [%d]", value, &parentProcessIdentifier) > 0) {
parentProcessName = [[NSString alloc] initWithCString:value encoding:NSUTF8StringEncoding];
continue;
}
if (sscanf(line, "Path: %s", value) > 0) {
executablePath = [[NSString alloc] initWithCString:value encoding:NSUTF8StringEncoding];
continue;
}
if (sscanf(line, "Date/Time: %[^\n]", value) > 0) {
NSString *dateString = [[NSString alloc] initWithCString:value encoding:NSUTF8StringEncoding];
date = [self.dateFormatter dateFromString:dateString];
continue;
}
}
free(line);
if (processName == nil) {
return [[FBControlCoreError
describe:@"Missing process name in crash log"]
failBool:error];
}
if (identifier == nil) {
return [[FBControlCoreError
describe:@"Missing identifier in crash log"]
failBool:error];
}
if (parentProcessName == nil) {
return [[FBControlCoreError
describe:@"Missing process name in crash log"]
failBool:error];
}
if (executablePath == nil) {
return [[FBControlCoreError
describe:@"Missing executable path in crash log"]
failBool:error];
}
if (processIdentifier == -1) {
return [[FBControlCoreError
describe:@"Missing process identifier in crash log"]
failBool:error];
}
if (parentProcessIdentifier == -1) {
return [[FBControlCoreError
describe:@"Missing parent process identifier in crash log"]
failBool:error];
}
if (date == nil) {
return [[FBControlCoreError
describe:@"Missing date in crash log"]
failBool:error];
}
if (executablePathOut) {
*executablePathOut = executablePath;
}
if (identifierOut) {
*identifierOut = identifier;
}
if (processNameOut) {
*processNameOut = processName;
}
if (parentProcessNameOut) {
*parentProcessNameOut = parentProcessName;
}
if (processIdentifierOut) {
*processIdentifierOut = processIdentifier;
}
if (parentProcessIdentifierOut) {
*parentProcessIdentifierOut = parentProcessIdentifier;
}
if (dateOut) {
*dateOut = date;
}
return YES;
}
+ (FBCrashLogInfoProcessType)processTypeForExecutablePath:(NSString *)executablePath
{
if ([executablePath containsString:@"Platforms/iPhoneSimulator.platform"]) {
return FBCrashLogInfoProcessTypeSystem;
}
if ([executablePath containsString:@".app"]) {
return FBCrashLogInfoProcessTypeApplication;
}
return FBCrashLogInfoProcessTypeCustom;
}
+ (NSPredicate *)predicateForFilesWithBasePath:(NSString *)basePath afterDate:(NSDate *)date withExtension:(NSString *)extension
{
NSFileManager *fileManager = NSFileManager.defaultManager;
NSPredicate *datePredicate = [NSPredicate predicateWithValue:YES];
if (date) {
datePredicate = [NSPredicate predicateWithBlock:^ BOOL (NSString *fileName, NSDictionary *_) {
NSString *path = [basePath stringByAppendingPathComponent:fileName];
NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:nil];
return [attributes.fileModificationDate compare:date] != NSOrderedAscending;
}];
}
return [NSCompoundPredicate andPredicateWithSubpredicates:@[
[NSPredicate predicateWithFormat:@"pathExtension == %@", extension],
datePredicate
]];
}
+ (NSDateFormatter *)dateFormatter
{
static dispatch_once_t onceToken;
static NSDateFormatter *dateFormatter = nil;
dispatch_once(&onceToken, ^{
dateFormatter = [NSDateFormatter new];
dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS Z";
dateFormatter.lenient = YES;
dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US"];
});
return dateFormatter;
}
@end