AutorÃa | Ultima modificación | Ver Log |
// Copyright 2019 Google
//
// 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.
#include "Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h"
#include <sys/time.h>
#include "Crashlytics/Crashlytics/Components/FIRCLSGlobals.h"
#include "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h"
#import "Crashlytics/Crashlytics/Controllers/FIRCLSReportManager_Private.h"
NSString *const FIRCLSStartTimeKey = @"com.crashlytics.kit-start-time";
NSString *const FIRCLSFirstRunloopTurnTimeKey = @"com.crashlytics.first-run-loop-time";
NSString *const FIRCLSInBackgroundKey = @"com.crashlytics.in-background";
#if TARGET_OS_IPHONE
NSString *const FIRCLSDeviceOrientationKey = @"com.crashlytics.device-orientation";
NSString *const FIRCLSUIOrientationKey = @"com.crashlytics.ui-orientation";
#endif
NSString *const FIRCLSUserIdentifierKey = @"com.crashlytics.user-id";
NSString *const FIRCLSDevelopmentPlatformNameKey = @"com.crashlytics.development-platform-name";
NSString *const FIRCLSDevelopmentPlatformVersionKey =
@"com.crashlytics.development-platform-version";
NSString *const FIRCLSOnDemandRecordedExceptionsKey =
@"com.crashlytics.on-demand.recorded-exceptions";
NSString *const FIRCLSOnDemandDroppedExceptionsKey =
@"com.crashlytics.on-demand.dropped-exceptions";
// Empty string object synchronized on to prevent a race condition when accessing AB file path
NSString *const FIRCLSSynchronizedPathKey = @"";
const uint32_t FIRCLSUserLoggingMaxKVEntries = 64;
#pragma mark - Prototypes
static void FIRCLSUserLoggingWriteKeysAndValues(NSDictionary *keysAndValues,
FIRCLSUserLoggingKVStorage *storage,
uint32_t *counter,
BOOL containsNullValue);
static void FIRCLSUserLoggingCheckAndSwapABFiles(FIRCLSUserLoggingABStorage *storage,
const char **activePath,
off_t fileSize);
void FIRCLSLogInternal(FIRCLSUserLoggingABStorage *storage,
const char **activePath,
NSString *message);
#pragma mark - Setup
void FIRCLSUserLoggingInit(FIRCLSUserLoggingReadOnlyContext *roContext,
FIRCLSUserLoggingWritableContext *rwContext) {
rwContext->activeUserLogPath = roContext->logStorage.aPath;
rwContext->activeErrorLogPath = roContext->errorStorage.aPath;
rwContext->activeCustomExceptionPath = roContext->customExceptionStorage.aPath;
rwContext->userKVCount = 0;
rwContext->internalKVCount = 0;
rwContext->errorsCount = 0;
roContext->userKVStorage.maxIncrementalCount = FIRCLSUserLoggingMaxKVEntries;
roContext->internalKVStorage.maxIncrementalCount = roContext->userKVStorage.maxIncrementalCount;
}
#pragma mark - KV Logging
void FIRCLSUserLoggingRecordInternalKeyValue(NSString *key, id value) {
FIRCLSUserLoggingRecordKeyValue(key, value, &_firclsContext.readonly->logging.internalKVStorage,
&_firclsContext.writable->logging.internalKVCount);
}
void FIRCLSUserLoggingWriteInternalKeyValue(NSString *key, NSString *value) {
// Unsynchronized - must be run on the correct queue
NSDictionary *keysAndValues = key ? @{key : value ?: [NSNull null]} : nil;
FIRCLSUserLoggingWriteKeysAndValues(keysAndValues,
&_firclsContext.readonly->logging.internalKVStorage,
&_firclsContext.writable->logging.internalKVCount, NO);
}
void FIRCLSUserLoggingRecordUserKeyValue(NSString *key, id value) {
FIRCLSUserLoggingRecordKeyValue(key, value, &_firclsContext.readonly->logging.userKVStorage,
&_firclsContext.writable->logging.userKVCount);
}
void FIRCLSUserLoggingRecordUserKeysAndValues(NSDictionary *keysAndValues) {
FIRCLSUserLoggingRecordKeysAndValues(keysAndValues,
&_firclsContext.readonly->logging.userKVStorage,
&_firclsContext.writable->logging.userKVCount);
}
static id FIRCLSUserLoggingGetComponent(NSDictionary *entry,
NSString *componentName,
bool decodeHex) {
id value = [entry objectForKey:componentName];
return (decodeHex && value != [NSNull null]) ? FIRCLSFileHexDecodeString([value UTF8String])
: value;
}
static NSString *FIRCLSUserLoggingGetKey(NSDictionary *entry, bool decodeHex) {
return FIRCLSUserLoggingGetComponent(entry, @"key", decodeHex);
}
static id FIRCLSUserLoggingGetValue(NSDictionary *entry, bool decodeHex) {
return FIRCLSUserLoggingGetComponent(entry, @"value", decodeHex);
}
NSDictionary *FIRCLSUserLoggingGetCompactedKVEntries(FIRCLSUserLoggingKVStorage *storage,
bool decodeHex) {
if (!FIRCLSIsValidPointer(storage)) {
FIRCLSSDKLogError("storage invalid\n");
return nil;
}
NSArray *incrementalKVs = FIRCLSUserLoggingStoredKeyValues(storage->incrementalPath);
NSArray *compactedKVs = FIRCLSUserLoggingStoredKeyValues(storage->compactedPath);
NSMutableDictionary *finalKVSet = [NSMutableDictionary new];
// These should all be unique, so there might be a more efficient way to
// do this
for (NSDictionary *entry in compactedKVs) {
NSString *key = FIRCLSUserLoggingGetKey(entry, decodeHex);
NSString *value = FIRCLSUserLoggingGetValue(entry, decodeHex);
if (!key || !value) {
FIRCLSSDKLogError("compacted key/value contains a nil and must be dropped\n");
continue;
}
[finalKVSet setObject:value forKey:key];
}
// Now, assign the incremental values, in file order, so we overwrite any older values.
for (NSDictionary *entry in incrementalKVs) {
NSString *key = FIRCLSUserLoggingGetKey(entry, decodeHex);
NSString *value = FIRCLSUserLoggingGetValue(entry, decodeHex);
if (!key || !value) {
FIRCLSSDKLogError("incremental key/value contains a nil and must be dropped\n");
continue;
}
if ([value isEqual:[NSNull null]]) {
[finalKVSet removeObjectForKey:key];
} else {
[finalKVSet setObject:value forKey:key];
}
}
return finalKVSet;
}
static void FIRCLSUserLoggingWriteKVEntriesToFile(
NSDictionary<NSString *, NSString *> *keysAndValues, BOOL shouldHexEncode, FIRCLSFile *file) {
for (NSString *key in keysAndValues) {
NSString *valueObject = [keysAndValues objectForKey:key];
// map `NSNull` into nil
const char *value = (valueObject == (NSString *)[NSNull null] ? nil : [valueObject UTF8String]);
FIRCLSFileWriteSectionStart(file, "kv");
FIRCLSFileWriteHashStart(file);
if (shouldHexEncode) {
FIRCLSFileWriteHashEntryHexEncodedString(file, "key", [key UTF8String]);
FIRCLSFileWriteHashEntryHexEncodedString(file, "value", value);
} else {
FIRCLSFileWriteHashEntryString(file, "key", [key UTF8String]);
FIRCLSFileWriteHashEntryString(file, "value", value);
}
FIRCLSFileWriteHashEnd(file);
FIRCLSFileWriteSectionEnd(file);
}
}
void FIRCLSUserLoggingCompactKVEntries(FIRCLSUserLoggingKVStorage *storage) {
if (!FIRCLSIsValidPointer(storage)) {
FIRCLSSDKLogError("Error: storage invalid\n");
return;
}
NSDictionary *finalKVs = FIRCLSUserLoggingGetCompactedKVEntries(storage, false);
if (unlink(storage->compactedPath) != 0) {
FIRCLSSDKLog("Error: Unable to remove compacted KV store before compaction %s\n",
strerror(errno));
}
FIRCLSFile file;
if (!FIRCLSFileInitWithPath(&file, storage->compactedPath, true)) {
FIRCLSSDKLog("Error: Unable to open compacted k-v file\n");
return;
}
uint32_t maxCount = storage->maxCount;
if ([finalKVs count] > maxCount) {
// We need to remove keys, to avoid going over the max.
// This is just about the worst way to go about doing this. There are lots of smarter ways,
// but it's very uncommon to go down this path.
NSArray *keys = [finalKVs allKeys];
FIRCLSSDKLogInfo("Truncating %d keys from KV set, which is above max %d\n",
(uint32_t)(finalKVs.count - maxCount), maxCount);
finalKVs =
[finalKVs dictionaryWithValuesForKeys:[keys subarrayWithRange:NSMakeRange(0, maxCount)]];
}
FIRCLSUserLoggingWriteKVEntriesToFile(finalKVs, false, &file);
FIRCLSFileClose(&file);
if (unlink(storage->incrementalPath) != 0) {
FIRCLSSDKLog("Error: Unable to remove incremental KV store after compaction %s\n",
strerror(errno));
}
}
void FIRCLSUserLoggingRecordKeyValue(NSString *key,
id value,
FIRCLSUserLoggingKVStorage *storage,
uint32_t *counter) {
if (!FIRCLSIsValidPointer(key)) {
FIRCLSSDKLogWarn("User provided bad key\n");
return;
}
NSDictionary *keysAndValues = @{key : (value ?: [NSNull null])};
FIRCLSUserLoggingRecordKeysAndValues(keysAndValues, storage, counter);
}
void FIRCLSUserLoggingRecordKeysAndValues(NSDictionary *keysAndValues,
FIRCLSUserLoggingKVStorage *storage,
uint32_t *counter) {
if (!FIRCLSContextIsInitialized()) {
return;
}
if (keysAndValues.count == 0) {
FIRCLSSDKLogWarn("User provided empty key/value dictionary\n");
return;
}
if (!FIRCLSIsValidPointer(keysAndValues)) {
FIRCLSSDKLogWarn("User provided bad key/value dictionary\n");
return;
}
NSMutableDictionary *sanitizedKeysAndValues = [keysAndValues mutableCopy];
BOOL containsNullValue = NO;
for (NSString *key in keysAndValues) {
if (!FIRCLSIsValidPointer(key)) {
FIRCLSSDKLogWarn("User provided bad key\n");
return;
}
id value = keysAndValues[key];
// ensure that any invalid pointer is actually set to nil
if (!FIRCLSIsValidPointer(value) && value != nil) {
FIRCLSSDKLogWarn("Bad value pointer being clamped to nil\n");
sanitizedKeysAndValues[key] = [NSNull null];
}
if ([value respondsToSelector:@selector(description)] && ![value isEqual:[NSNull null]]) {
sanitizedKeysAndValues[key] = [value description];
} else {
// passing nil will result in a JSON null being written, which is deserialized as [NSNull
// null], signaling to remove the key during compaction
sanitizedKeysAndValues[key] = [NSNull null];
containsNullValue = YES;
}
}
dispatch_sync(FIRCLSGetLoggingQueue(), ^{
FIRCLSUserLoggingWriteKeysAndValues(sanitizedKeysAndValues, storage, counter,
containsNullValue);
});
}
static void FIRCLSUserLoggingWriteKeysAndValues(NSDictionary *keysAndValues,
FIRCLSUserLoggingKVStorage *storage,
uint32_t *counter,
BOOL containsNullValue) {
FIRCLSFile file;
if (!FIRCLSIsValidPointer(storage) || !FIRCLSIsValidPointer(counter)) {
FIRCLSSDKLogError("Bad parameters\n");
return;
}
if (!FIRCLSFileInitWithPath(&file, storage->incrementalPath, true)) {
FIRCLSSDKLogError("Unable to open k-v file\n");
return;
}
FIRCLSUserLoggingWriteKVEntriesToFile(keysAndValues, true, &file);
FIRCLSFileClose(&file);
*counter += keysAndValues.count;
if (*counter >= storage->maxIncrementalCount || containsNullValue) {
dispatch_async(FIRCLSGetLoggingQueue(), ^{
FIRCLSUserLoggingCompactKVEntries(storage);
*counter = 0;
});
}
}
NSArray *FIRCLSUserLoggingStoredKeyValues(const char *path) {
if (!FIRCLSContextIsInitialized()) {
return nil;
}
return FIRCLSFileReadSections(path, true, ^NSObject *(id obj) {
return [obj objectForKey:@"kv"];
});
}
#pragma mark - NSError Logging
static void FIRCLSUserLoggingRecordErrorUserInfo(FIRCLSFile *file,
const char *fileKey,
NSDictionary<NSString *, id> *userInfo) {
if ([userInfo count] == 0) {
return;
}
FIRCLSFileWriteHashKey(file, fileKey);
FIRCLSFileWriteArrayStart(file);
for (id key in userInfo) {
id value = [userInfo objectForKey:key];
if (![value respondsToSelector:@selector(description)]) {
continue;
}
FIRCLSFileWriteArrayStart(file);
FIRCLSFileWriteArrayEntryHexEncodedString(file, [key UTF8String]);
FIRCLSFileWriteArrayEntryHexEncodedString(file, [[value description] UTF8String]);
FIRCLSFileWriteArrayEnd(file);
}
FIRCLSFileWriteArrayEnd(file);
}
static void FIRCLSUserLoggingWriteError(FIRCLSFile *file,
NSError *error,
NSDictionary<NSString *, id> *additionalUserInfo,
NSArray *addresses,
uint64_t timestamp) {
FIRCLSFileWriteSectionStart(file, "error");
FIRCLSFileWriteHashStart(file);
FIRCLSFileWriteHashEntryHexEncodedString(file, "domain", [[error domain] UTF8String]);
FIRCLSFileWriteHashEntryInt64(file, "code", [error code]);
FIRCLSFileWriteHashEntryUint64(file, "time", timestamp);
// addresses
FIRCLSFileWriteHashKey(file, "stacktrace");
FIRCLSFileWriteArrayStart(file);
for (NSNumber *address in addresses) {
FIRCLSFileWriteArrayEntryUint64(file, [address unsignedLongLongValue]);
}
FIRCLSFileWriteArrayEnd(file);
// user-info
FIRCLSUserLoggingRecordErrorUserInfo(file, "info", [error userInfo]);
FIRCLSUserLoggingRecordErrorUserInfo(file, "extra_info", additionalUserInfo);
FIRCLSFileWriteHashEnd(file);
FIRCLSFileWriteSectionEnd(file);
}
void FIRCLSUserLoggingRecordError(NSError *error,
NSDictionary<NSString *, id> *additionalUserInfo) {
if (!error) {
return;
}
if (!FIRCLSContextIsInitialized()) {
return;
}
// record the stacktrace and timestamp here, so we
// are as close as possible to the user's log statement
NSArray *addresses = [NSThread callStackReturnAddresses];
uint64_t timestamp = time(NULL);
FIRCLSUserLoggingWriteAndCheckABFiles(
&_firclsContext.readonly->logging.errorStorage,
&_firclsContext.writable->logging.activeErrorLogPath, ^(FIRCLSFile *file) {
FIRCLSUserLoggingWriteError(file, error, additionalUserInfo, addresses, timestamp);
});
}
#pragma mark - CLSLog Support
void FIRCLSLog(NSString *format, ...) {
// If the format is nil do nothing just like NSLog.
if (!format) {
return;
}
va_list args;
va_start(args, format);
NSString *msg = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
FIRCLSUserLoggingABStorage *currentStorage = &_firclsContext.readonly->logging.logStorage;
const char **activePath = &_firclsContext.writable->logging.activeUserLogPath;
FIRCLSLogInternal(currentStorage, activePath, msg);
}
void FIRCLSLogToStorage(FIRCLSUserLoggingABStorage *storage,
const char **activePath,
NSString *format,
...) {
// If the format is nil do nothing just like NSLog.
if (!format) {
return;
}
va_list args;
va_start(args, format);
NSString *msg = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
FIRCLSLogInternal(storage, activePath, msg);
}
#pragma mark - Properties
uint32_t FIRCLSUserLoggingMaxLogSize(void) {
// don't forget that the message encoding overhead is 2x, and we
// wrap everything in a json structure with time. So, there is
// quite a penalty
uint32_t size = 1024 * 64;
return size * 2;
}
uint32_t FIRCLSUserLoggingMaxErrorSize(void) {
return FIRCLSUserLoggingMaxLogSize();
}
#pragma mark - AB Logging
void FIRCLSUserLoggingCheckAndSwapABFiles(FIRCLSUserLoggingABStorage *storage,
const char **activePath,
off_t fileSize) {
if (!activePath || !storage) {
return;
}
if (!*activePath) {
return;
}
if (storage->restrictBySize) {
if (fileSize <= storage->maxSize) {
return;
}
} else {
if (!FIRCLSIsValidPointer(storage->entryCount)) {
FIRCLSSDKLogError("Error: storage has invalid pointer, but is restricted by entry count\n");
return;
}
if (*storage->entryCount < storage->maxEntries) {
return;
}
// Here we have rolled over, so we have to reset our counter.
*storage->entryCount = 0;
}
// if it is too big:
// - reset the other log
// - make it active
const char *otherPath = NULL;
if (*activePath == storage->aPath) {
otherPath = storage->bPath;
} else {
// take this path if the pointer is invalid as well, to reset
otherPath = storage->aPath;
}
// guard here against path being nil or empty
NSString *pathString = [NSString stringWithUTF8String:otherPath];
if ([pathString length] > 0) {
// ignore the error, because there is nothing we can do to recover here, and its likely
// any failures would be intermittent
[[NSFileManager defaultManager] removeItemAtPath:pathString error:nil];
}
@synchronized(FIRCLSSynchronizedPathKey) {
*activePath = otherPath;
}
}
void FIRCLSUserLoggingWriteAndCheckABFiles(FIRCLSUserLoggingABStorage *storage,
const char **activePath,
void (^openedFileBlock)(FIRCLSFile *file)) {
if (!storage || !activePath || !openedFileBlock) {
return;
}
@synchronized(FIRCLSSynchronizedPathKey) {
if (!*activePath) {
return;
}
}
if (storage->restrictBySize) {
if (storage->maxSize == 0) {
return;
}
} else {
if (storage->maxEntries == 0) {
return;
}
}
dispatch_sync(FIRCLSGetLoggingQueue(), ^{
FIRCLSFile file;
if (!FIRCLSFileInitWithPath(&file, *activePath, true)) {
FIRCLSSDKLog("Unable to open log file\n");
return;
}
openedFileBlock(&file);
off_t fileSize = 0;
FIRCLSFileCloseWithOffset(&file, &fileSize);
// increment the count before calling FIRCLSUserLoggingCheckAndSwapABFiles, so the value
// reflects the actual amount of stuff written
if (!storage->restrictBySize && FIRCLSIsValidPointer(storage->entryCount)) {
*storage->entryCount += 1;
}
dispatch_async(FIRCLSGetLoggingQueue(), ^{
FIRCLSUserLoggingCheckAndSwapABFiles(storage, activePath, fileSize);
});
});
}
void FIRCLSLogInternalWrite(FIRCLSFile *file, NSString *message, uint64_t time) {
FIRCLSFileWriteSectionStart(file, "log");
FIRCLSFileWriteHashStart(file);
FIRCLSFileWriteHashEntryHexEncodedString(file, "msg", [message UTF8String]);
FIRCLSFileWriteHashEntryUint64(file, "time", time);
FIRCLSFileWriteHashEnd(file);
FIRCLSFileWriteSectionEnd(file);
}
void FIRCLSLogInternal(FIRCLSUserLoggingABStorage *storage,
const char **activePath,
NSString *message) {
if (!message) {
return;
}
if (!FIRCLSContextIsInitialized()) {
FIRCLSWarningLog(@"WARNING: FIRCLSLog has been used before (or concurrently with) "
@"Crashlytics initialization and cannot be recorded. The message was: \n%@",
message);
return;
}
struct timeval te;
NSUInteger messageLength = [message length];
int maxLogSize = storage->maxSize;
if (messageLength > maxLogSize) {
FIRCLSWarningLog(
@"WARNING: Attempted to write %zd bytes, but %d is the maximum size of the log. "
@"Truncating to %d bytes.\n",
messageLength, maxLogSize, maxLogSize);
message = [message substringToIndex:maxLogSize];
}
// unable to get time - abort
if (gettimeofday(&te, NULL) != 0) {
return;
}
const uint64_t time = te.tv_sec * 1000LL + te.tv_usec / 1000;
FIRCLSUserLoggingWriteAndCheckABFiles(storage, activePath, ^(FIRCLSFile *file) {
FIRCLSLogInternalWrite(file, message, time);
});
}