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_IPHONENSString *const FIRCLSDeviceOrientationKey = @"com.crashlytics.device-orientation";NSString *const FIRCLSUIOrientationKey = @"com.crashlytics.ui-orientation";#endifNSString *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 pathNSString *const FIRCLSSynchronizedPathKey = @"";const uint32_t FIRCLSUserLoggingMaxKVEntries = 64;#pragma mark - Prototypesstatic 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 - Setupvoid 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 Loggingvoid 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 queueNSDictionary *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 thisfor (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 nilconst 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 nilif (!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 compactionsanitizedKeysAndValues[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 Loggingstatic 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);// addressesFIRCLSFileWriteHashKey(file, "stacktrace");FIRCLSFileWriteArrayStart(file);for (NSNumber *address in addresses) {FIRCLSFileWriteArrayEntryUint64(file, [address unsignedLongLongValue]);}FIRCLSFileWriteArrayEnd(file);// user-infoFIRCLSUserLoggingRecordErrorUserInfo(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 statementNSArray *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 Supportvoid 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 - Propertiesuint32_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 penaltyuint32_t size = 1024 * 64;return size * 2;}uint32_t FIRCLSUserLoggingMaxErrorSize(void) {return FIRCLSUserLoggingMaxLogSize();}#pragma mark - AB Loggingvoid 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 activeconst char *otherPath = NULL;if (*activePath == storage->aPath) {otherPath = storage->bPath;} else {// take this path if the pointer is invalid as well, to resetotherPath = storage->aPath;}// guard here against path being nil or emptyNSString *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 writtenif (!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 - abortif (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);});}