#import "DDFileLogger.h" #import #import #import #import /** * Welcome to Cocoa Lumberjack! * * The project page has a wealth of documentation if you have any questions. * https://github.com/CocoaLumberjack/CocoaLumberjack * * If you're new to the project you may wish to read the "Getting Started" wiki. * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted **/ #if ! __has_feature(objc_arc) #error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif // We probably shouldn't be using DDLog() statements within the DDLog implementation. // But we still want to leave our log statements for any future debugging, // and to allow other developers to trace the implementation (which is a great learning tool). // // So we use primitive logging macros around NSLog. // We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. #define LOG_LEVEL 2 #define NSLogError(frmt, ...) do{ if(LOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0) #define NSLogWarn(frmt, ...) do{ if(LOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0) #define NSLogInfo(frmt, ...) do{ if(LOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0) #define NSLogDebug(frmt, ...) do{ if(LOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0) #define NSLogVerbose(frmt, ...) do{ if(LOG_LEVEL >= 5) NSLog((frmt), ##__VA_ARGS__); } while(0) @interface DDLogFileManagerDefault (PrivateAPI) - (void)deleteOldLogFiles; - (NSString *)defaultLogsDirectory; @end @interface DDFileLogger (PrivateAPI) - (void)rollLogFileNow; - (void)maybeRollLogFileDueToAge; - (void)maybeRollLogFileDueToSize; @end #if TARGET_OS_IPHONE BOOL doesAppRunInBackground(void); #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation DDLogFileManagerDefault @synthesize maximumNumberOfLogFiles; @synthesize logFilesDiskQuota; - (id)init { return [self initWithLogsDirectory:nil]; } - (instancetype)initWithLogsDirectory:(NSString *)aLogsDirectory { if ((self = [super init])) { maximumNumberOfLogFiles = DEFAULT_LOG_MAX_NUM_LOG_FILES; logFilesDiskQuota = DEFAULT_LOG_FILES_DISK_QUOTA; if (aLogsDirectory) _logsDirectory = [aLogsDirectory copy]; else _logsDirectory = [[self defaultLogsDirectory] copy]; NSKeyValueObservingOptions kvoOptions = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; [self addObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles)) options:kvoOptions context:nil]; [self addObserver:self forKeyPath:NSStringFromSelector(@selector(logFilesDiskQuota)) options:kvoOptions context:nil]; NSLogVerbose(@"DDFileLogManagerDefault: logsDirectory:\n%@", [self logsDirectory]); NSLogVerbose(@"DDFileLogManagerDefault: sortedLogFileNames:\n%@", [self sortedLogFileNames]); } return self; } #if TARGET_OS_IPHONE - (instancetype)initWithLogsDirectory:(NSString *)logsDirectory defaultFileProtectionLevel:(NSString*)fileProtectionLevel { if ((self = [self initWithLogsDirectory:logsDirectory])) { if ([fileProtectionLevel isEqualToString:NSFileProtectionNone] || [fileProtectionLevel isEqualToString:NSFileProtectionComplete] || [fileProtectionLevel isEqualToString:NSFileProtectionCompleteUnlessOpen] || [fileProtectionLevel isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) { _defaultFileProtectionLevel = fileProtectionLevel; } } return self; } #endif - (void)dealloc { // try-catch because the observer might be removed or never added. In this case, removeObserver throws and exception @try { [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles))]; [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(logFilesDiskQuota))]; } @catch (NSException *exception) { } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Configuration //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSNumber *old = [change objectForKey:NSKeyValueChangeOldKey]; NSNumber *new = [change objectForKey:NSKeyValueChangeNewKey]; if ([old isEqual:new]) { // No change in value - don't bother with any processing. return; } if ([keyPath isEqualToString:NSStringFromSelector(@selector(maximumNumberOfLogFiles))] || [keyPath isEqualToString:NSStringFromSelector(@selector(logFilesDiskQuota))]) { NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: %@", keyPath); dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool { [self deleteOldLogFiles]; }}); } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark File Deleting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Deletes archived log files that exceed the maximumNumberOfLogFiles or logFilesDiskQuota configuration values. **/ - (void)deleteOldLogFiles { NSLogVerbose(@"DDLogFileManagerDefault: deleteOldLogFiles"); NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; NSUInteger firstIndexToDelete = NSNotFound; const unsigned long long diskQuota = self.logFilesDiskQuota; const NSUInteger maxNumLogFiles = self.maximumNumberOfLogFiles; if (diskQuota) { unsigned long long used = 0; for (NSUInteger i = 0; i < sortedLogFileInfos.count; i++) { DDLogFileInfo *info = sortedLogFileInfos[i]; used += info.fileSize; if (used > diskQuota) { firstIndexToDelete = i; break; } } } if (maxNumLogFiles) { if (firstIndexToDelete == NSNotFound) { firstIndexToDelete = maxNumLogFiles; } else { firstIndexToDelete = MIN(firstIndexToDelete, maxNumLogFiles); } } if (firstIndexToDelete == 0) { // Do we consider the first file? // We are only supposed to be deleting archived files. // In most cases, the first file is likely the log file that is currently being written to. // So in most cases, we do not want to consider this file for deletion. if (sortedLogFileInfos.count > 0) { DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:0]; if (! logFileInfo.isArchived) { // Don't delete active file. ++firstIndexToDelete; } } } if (firstIndexToDelete != NSNotFound) { // removing all logfiles starting with firstIndexToDelete for (NSUInteger i = firstIndexToDelete; i < sortedLogFileInfos.count; i++) { DDLogFileInfo *logFileInfo = sortedLogFileInfos[i]; NSLogInfo(@"DDLogFileManagerDefault: Deleting file: %@", logFileInfo.fileName); [[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:nil]; } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Log Files //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Returns the path to the default logs directory. * If the logs directory doesn't exist, this method automatically creates it. **/ - (NSString *)defaultLogsDirectory { #if TARGET_OS_IPHONE NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); NSString *baseDir = ([paths count] > 0) ? [paths objectAtIndex:0] : nil; NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"Logs"]; #else NSString *appName = [[NSProcessInfo processInfo] processName]; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : NSTemporaryDirectory(); NSString *logsDirectory = [[basePath stringByAppendingPathComponent:@"Logs"] stringByAppendingPathComponent:appName]; #endif return logsDirectory; } - (NSString *)logsDirectory { // We could do this check once, during initalization, and not bother again. // But this way the code continues to work if the directory gets deleted while the code is running. if (![[NSFileManager defaultManager] fileExistsAtPath:_logsDirectory]) { NSError *err = nil; if (![[NSFileManager defaultManager] createDirectoryAtPath:_logsDirectory withIntermediateDirectories:YES attributes:nil error:&err]) { NSLogError(@"DDFileLogManagerDefault: Error creating logsDirectory: %@", err); } } return _logsDirectory; } /** * Default log file name is "