// Software License Agreement (BSD License) // // Copyright (c) 2010-2015, Deusty, LLC // All rights reserved. // // Redistribution and use of this software in source and binary forms, // with or without modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // * Neither the name of Deusty nor the names of its contributors may be used // to endorse or promote products derived from this software without specific // prior written permission of Deusty, LLC. #import "DDFileLogger.h" #import #import #import #import #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) #if TARGET_OS_IPHONE BOOL doesAppRunInBackground(void); #endif unsigned long long const kDDDefaultLogMaxFileSize = 1024 * 1024; // 1 MB NSTimeInterval const kDDDefaultLogRollingFrequency = 60 * 60 * 24; // 24 Hours NSUInteger const kDDDefaultLogMaxNumLogFiles = 5; // 5 Files unsigned long long const kDDDefaultLogFilesDiskQuota = 20 * 1024 * 1024; // 20 MB //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @interface DDLogFileManagerDefault () { NSUInteger _maximumNumberOfLogFiles; unsigned long long _logFilesDiskQuota; NSString *_logsDirectory; #if TARGET_OS_IPHONE NSString *_defaultFileProtectionLevel; #endif } - (void)deleteOldLogFiles; - (NSString *)defaultLogsDirectory; @end @implementation DDLogFileManagerDefault @synthesize maximumNumberOfLogFiles = _maximumNumberOfLogFiles; @synthesize logFilesDiskQuota = _logFilesDiskQuota; - (instancetype)init { return [self initWithLogsDirectory:nil]; } - (instancetype)initWithLogsDirectory:(NSString *)aLogsDirectory { if ((self = [super init])) { _maximumNumberOfLogFiles = kDDDefaultLogMaxNumLogFiles; _logFilesDiskQuota = kDDDefaultLogFilesDiskQuota; 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; } + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"maximumNumberOfLogFiles"] || [theKey isEqualToString:@"logFilesDiskQuota"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; } #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[NSKeyValueChangeOldKey]; NSNumber *new = change[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[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[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 "