| #import "DDFileLogger.h" |
| |
| #import <unistd.h> |
| #import <sys/attr.h> |
| #import <sys/xattr.h> |
| #import <libkern/OSAtomic.h> |
| |
| /** |
| * 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 "<bundle identifier> <date> <time>.log". |
| * Example: MobileSafari 2013-12-03 17-14.log |
| * |
| * You can change it by overriding newLogFileName and isLogFile: methods. |
| **/ |
| - (BOOL)isLogFile:(NSString *)fileName |
| { |
| NSString *appName = [self applicationName]; |
| |
| BOOL hasProperPrefix = [fileName hasPrefix:appName]; |
| BOOL hasProperSuffix = [fileName hasSuffix:@".log"]; |
| BOOL hasProperDate = NO; |
| |
| if (hasProperPrefix && hasProperSuffix) |
| { |
| NSUInteger lengthOfMiddle = fileName.length - appName.length - @".log".length; |
| |
| // Date string should have at least 16 characters - " 2013-12-03 17-14" |
| if (lengthOfMiddle >= 17) |
| { |
| NSRange range = NSMakeRange(appName.length, lengthOfMiddle); |
| |
| NSString *middle = [fileName substringWithRange:range]; |
| NSArray *components = [middle componentsSeparatedByString:@" "]; |
| |
| // When creating logfile if there is existing file with the same name, we append attemp number at the end. |
| // Thats why here we can have three or four components. For details see createNewLogFile method. |
| // |
| // Components: |
| // "", "2013-12-03", "17-14" |
| // or |
| // "", "2013-12-03", "17-14", "1" |
| if (components.count == 3 || components.count == 4) |
| { |
| NSString *dateString = [NSString stringWithFormat:@"%@ %@", components[1], components[2]]; |
| NSDateFormatter *dateFormatter = [self logFileDateFormatter]; |
| |
| NSDate *date = [dateFormatter dateFromString:dateString]; |
| |
| if (date) |
| { |
| hasProperDate = YES; |
| } |
| } |
| } |
| } |
| |
| return (hasProperPrefix && hasProperDate && hasProperSuffix); |
| } |
| |
| - (NSDateFormatter *)logFileDateFormatter |
| { |
| NSMutableDictionary *dictionary = [[NSThread currentThread] |
| threadDictionary]; |
| NSString *dateFormat = @"yyyy'-'MM'-'dd' 'HH'-'mm'"; |
| NSString *key = [NSString stringWithFormat:@"logFileDateFormatter.%@", dateFormat]; |
| NSDateFormatter *dateFormatter = [dictionary objectForKey:key]; |
| if (dateFormatter == nil) { |
| dateFormatter = [[NSDateFormatter alloc] init]; |
| [dateFormatter setDateFormat:dateFormat]; |
| [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; |
| [dictionary setObject:dateFormatter |
| forKey:key]; |
| } |
| |
| return dateFormatter; |
| } |
| |
| /** |
| * Returns an array of NSString objects, |
| * each of which is the filePath to an existing log file on disk. |
| **/ |
| - (NSArray *)unsortedLogFilePaths |
| { |
| NSString *logsDirectory = [self logsDirectory]; |
| NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:nil]; |
| |
| NSMutableArray *unsortedLogFilePaths = [NSMutableArray arrayWithCapacity:[fileNames count]]; |
| |
| for (NSString *fileName in fileNames) |
| { |
| // Filter out any files that aren't log files. (Just for extra safety) |
| |
| #if TARGET_IPHONE_SIMULATOR |
| // In case of iPhone simulator there can be 'archived' extension. isLogFile: |
| // method knows nothing about it. Thus removing it for this method. |
| // |
| // See full explanation in the header file. |
| NSString *theFileName = [fileName stringByReplacingOccurrencesOfString:@".archived" |
| withString:@""]; |
| |
| if ([self isLogFile:theFileName]) |
| #else |
| if ([self isLogFile:fileName]) |
| #endif |
| { |
| NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName]; |
| |
| [unsortedLogFilePaths addObject:filePath]; |
| } |
| } |
| |
| return unsortedLogFilePaths; |
| } |
| |
| /** |
| * Returns an array of NSString objects, |
| * each of which is the fileName of an existing log file on disk. |
| **/ |
| - (NSArray *)unsortedLogFileNames |
| { |
| NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths]; |
| |
| NSMutableArray *unsortedLogFileNames = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]]; |
| |
| for (NSString *filePath in unsortedLogFilePaths) |
| { |
| [unsortedLogFileNames addObject:[filePath lastPathComponent]]; |
| } |
| |
| return unsortedLogFileNames; |
| } |
| |
| /** |
| * Returns an array of DDLogFileInfo objects, |
| * each representing an existing log file on disk, |
| * and containing important information about the log file such as it's modification date and size. |
| **/ |
| - (NSArray *)unsortedLogFileInfos |
| { |
| NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths]; |
| |
| NSMutableArray *unsortedLogFileInfos = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]]; |
| |
| for (NSString *filePath in unsortedLogFilePaths) |
| { |
| DDLogFileInfo *logFileInfo = [[DDLogFileInfo alloc] initWithFilePath:filePath]; |
| |
| [unsortedLogFileInfos addObject:logFileInfo]; |
| } |
| |
| return unsortedLogFileInfos; |
| } |
| |
| /** |
| * Just like the unsortedLogFilePaths method, but sorts the array. |
| * The items in the array are sorted by creation date. |
| * The first item in the array will be the most recently created log file. |
| **/ |
| - (NSArray *)sortedLogFilePaths |
| { |
| NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; |
| |
| NSMutableArray *sortedLogFilePaths = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]]; |
| |
| for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) |
| { |
| [sortedLogFilePaths addObject:[logFileInfo filePath]]; |
| } |
| |
| return sortedLogFilePaths; |
| } |
| |
| /** |
| * Just like the unsortedLogFileNames method, but sorts the array. |
| * The items in the array are sorted by creation date. |
| * The first item in the array will be the most recently created log file. |
| **/ |
| - (NSArray *)sortedLogFileNames |
| { |
| NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; |
| |
| NSMutableArray *sortedLogFileNames = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]]; |
| |
| for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) |
| { |
| [sortedLogFileNames addObject:[logFileInfo fileName]]; |
| } |
| |
| return sortedLogFileNames; |
| } |
| |
| /** |
| * Just like the unsortedLogFileInfos method, but sorts the array. |
| * The items in the array are sorted by creation date. |
| * The first item in the array will be the most recently created log file. |
| **/ |
| - (NSArray *)sortedLogFileInfos |
| { |
| return [[self unsortedLogFileInfos] sortedArrayUsingSelector:@selector(reverseCompareByCreationDate:)]; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Creation |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Generates log file name with default format "<bundle identifier> <date> <time>.log" |
| * Example: MobileSafari 2013-12-03 17-14.log |
| * |
| * You can change it by overriding newLogFileName and isLogFile: methods. |
| **/ |
| - (NSString *)newLogFileName |
| { |
| NSString *appName = [self applicationName]; |
| |
| NSDateFormatter *dateFormatter = [self logFileDateFormatter]; |
| NSString *formattedDate = [dateFormatter stringFromDate:[NSDate date]]; |
| |
| return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate]; |
| } |
| |
| /** |
| * Generates a new unique log file path, and creates the corresponding log file. |
| **/ |
| - (NSString *)createNewLogFile |
| { |
| NSString *fileName = [self newLogFileName]; |
| NSString *logsDirectory = [self logsDirectory]; |
| |
| NSUInteger attempt = 1; |
| do |
| { |
| NSString *actualFileName = fileName; |
| |
| if (attempt > 1) { |
| NSString *extension = [actualFileName pathExtension]; |
| |
| actualFileName = [actualFileName stringByDeletingPathExtension]; |
| actualFileName = [actualFileName stringByAppendingFormat:@" %lu", (unsigned long)attempt]; |
| if (extension.length) { |
| actualFileName = [actualFileName stringByAppendingPathExtension:extension]; |
| } |
| } |
| |
| NSString *filePath = [logsDirectory stringByAppendingPathComponent:actualFileName]; |
| |
| if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) |
| { |
| NSLogVerbose(@"DDLogFileManagerDefault: Creating new log file: %@", actualFileName); |
| |
| NSDictionary *attributes = nil; |
| |
| #if TARGET_OS_IPHONE |
| // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen. |
| // |
| // But in case if app is able to launch from background we need to have an ability to open log file any time we |
| // want (even if device is locked). Thats why that attribute have to be changed to |
| // NSFileProtectionCompleteUntilFirstUserAuthentication. |
| |
| NSString *key = _defaultFileProtectionLevel ? : |
| (doesAppRunInBackground() ? NSFileProtectionCompleteUntilFirstUserAuthentication : NSFileProtectionCompleteUnlessOpen); |
| |
| attributes = @{ NSFileProtectionKey : key }; |
| #endif |
| |
| [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:attributes]; |
| |
| // Since we just created a new log file, we may need to delete some old log files |
| [self deleteOldLogFiles]; |
| |
| return filePath; |
| } else { |
| attempt++; |
| } |
| |
| } while(YES); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Utility |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| - (NSString *)applicationName |
| { |
| static NSString *_appName; |
| static dispatch_once_t onceToken; |
| |
| dispatch_once(&onceToken, ^{ |
| _appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; |
| |
| if (! _appName) |
| { |
| _appName = [[NSProcessInfo processInfo] processName]; |
| } |
| |
| if (! _appName) |
| { |
| _appName = @""; |
| } |
| }); |
| |
| return _appName; |
| } |
| |
| @end |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark - |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @implementation DDLogFileFormatterDefault |
| |
| - (id)init |
| { |
| return [self initWithDateFormatter:nil]; |
| } |
| |
| - (instancetype)initWithDateFormatter:(NSDateFormatter *)aDateFormatter |
| { |
| if ((self = [super init])) |
| { |
| if (aDateFormatter) |
| { |
| dateFormatter = aDateFormatter; |
| } |
| else |
| { |
| dateFormatter = [[NSDateFormatter alloc] init]; |
| [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; // 10.4+ style |
| [dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"]; |
| } |
| } |
| return self; |
| } |
| |
| - (NSString *)formatLogMessage:(DDLogMessage *)logMessage |
| { |
| NSString *dateAndTime = [dateFormatter stringFromDate:(logMessage->timestamp)]; |
| |
| return [NSString stringWithFormat:@"%@ %@", dateAndTime, logMessage->logMsg]; |
| } |
| |
| @end |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark - |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @implementation DDFileLogger |
| |
| - (id)init |
| { |
| DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init]; |
| |
| return [self initWithLogFileManager:defaultLogFileManager]; |
| } |
| |
| - (instancetype)initWithLogFileManager:(id <DDLogFileManager>)aLogFileManager |
| { |
| if ((self = [super init])) |
| { |
| maximumFileSize = DEFAULT_LOG_MAX_FILE_SIZE; |
| rollingFrequency = DEFAULT_LOG_ROLLING_FREQUENCY; |
| _automaticallyAppendNewlineForCustomFormatters = YES; |
| |
| logFileManager = aLogFileManager; |
| |
| formatter = [[DDLogFileFormatterDefault alloc] init]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc |
| { |
| [currentLogFileHandle synchronizeFile]; |
| [currentLogFileHandle closeFile]; |
| |
| if (currentLogFileVnode) { |
| dispatch_source_cancel(currentLogFileVnode); |
| currentLogFileVnode = NULL; |
| } |
| |
| if (rollingTimer) |
| { |
| dispatch_source_cancel(rollingTimer); |
| rollingTimer = NULL; |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Properties |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @synthesize logFileManager; |
| |
| - (unsigned long long)maximumFileSize |
| { |
| __block unsigned long long result; |
| |
| dispatch_block_t block = ^{ |
| result = maximumFileSize; |
| }; |
| |
| // The design of this method is taken from the DDAbstractLogger implementation. |
| // For extensive documentation please refer to the DDAbstractLogger implementation. |
| |
| // Note: The internal implementation MUST access the maximumFileSize variable directly, |
| // This method is designed explicitly for external access. |
| // |
| // Using "self." syntax to go through this method will cause immediate deadlock. |
| // This is the intended result. Fix it by accessing the ivar directly. |
| // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. |
| |
| NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
| NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); |
| |
| dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
| |
| dispatch_sync(globalLoggingQueue, ^{ |
| dispatch_sync(loggerQueue, block); |
| }); |
| |
| return result; |
| } |
| |
| - (void)setMaximumFileSize:(unsigned long long)newMaximumFileSize |
| { |
| dispatch_block_t block = ^{ @autoreleasepool { |
| |
| maximumFileSize = newMaximumFileSize; |
| [self maybeRollLogFileDueToSize]; |
| |
| }}; |
| |
| // The design of this method is taken from the DDAbstractLogger implementation. |
| // For extensive documentation please refer to the DDAbstractLogger implementation. |
| |
| // Note: The internal implementation MUST access the maximumFileSize variable directly, |
| // This method is designed explicitly for external access. |
| // |
| // Using "self." syntax to go through this method will cause immediate deadlock. |
| // This is the intended result. Fix it by accessing the ivar directly. |
| // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. |
| |
| NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
| NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); |
| |
| dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
| |
| dispatch_async(globalLoggingQueue, ^{ |
| dispatch_async(loggerQueue, block); |
| }); |
| } |
| |
| - (NSTimeInterval)rollingFrequency |
| { |
| __block NSTimeInterval result; |
| |
| dispatch_block_t block = ^{ |
| result = rollingFrequency; |
| }; |
| |
| // The design of this method is taken from the DDAbstractLogger implementation. |
| // For extensive documentation please refer to the DDAbstractLogger implementation. |
| |
| // Note: The internal implementation should access the rollingFrequency variable directly, |
| // This method is designed explicitly for external access. |
| // |
| // Using "self." syntax to go through this method will cause immediate deadlock. |
| // This is the intended result. Fix it by accessing the ivar directly. |
| // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. |
| |
| NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
| NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); |
| |
| dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
| |
| dispatch_sync(globalLoggingQueue, ^{ |
| dispatch_sync(loggerQueue, block); |
| }); |
| |
| return result; |
| } |
| |
| - (void)setRollingFrequency:(NSTimeInterval)newRollingFrequency |
| { |
| dispatch_block_t block = ^{ @autoreleasepool { |
| |
| rollingFrequency = newRollingFrequency; |
| [self maybeRollLogFileDueToAge]; |
| }}; |
| |
| // The design of this method is taken from the DDAbstractLogger implementation. |
| // For extensive documentation please refer to the DDAbstractLogger implementation. |
| |
| // Note: The internal implementation should access the rollingFrequency variable directly, |
| // This method is designed explicitly for external access. |
| // |
| // Using "self." syntax to go through this method will cause immediate deadlock. |
| // This is the intended result. Fix it by accessing the ivar directly. |
| // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. |
| |
| NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
| NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); |
| |
| dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
| |
| dispatch_async(globalLoggingQueue, ^{ |
| dispatch_async(loggerQueue, block); |
| }); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark File Rolling |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| - (void)scheduleTimerToRollLogFileDueToAge |
| { |
| if (rollingTimer) |
| { |
| dispatch_source_cancel(rollingTimer); |
| rollingTimer = NULL; |
| } |
| |
| if (currentLogFileInfo == nil || rollingFrequency <= 0.0) |
| { |
| return; |
| } |
| |
| NSDate *logFileCreationDate = [currentLogFileInfo creationDate]; |
| |
| NSTimeInterval ti = [logFileCreationDate timeIntervalSinceReferenceDate]; |
| ti += rollingFrequency; |
| |
| NSDate *logFileRollingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ti]; |
| |
| NSLogVerbose(@"DDFileLogger: scheduleTimerToRollLogFileDueToAge"); |
| |
| NSLogVerbose(@"DDFileLogger: logFileCreationDate: %@", logFileCreationDate); |
| NSLogVerbose(@"DDFileLogger: logFileRollingDate : %@", logFileRollingDate); |
| |
| rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); |
| |
| dispatch_source_set_event_handler(rollingTimer, ^{ @autoreleasepool { |
| |
| [self maybeRollLogFileDueToAge]; |
| |
| }}); |
| |
| #if !OS_OBJECT_USE_OBJC |
| dispatch_source_t theRollingTimer = rollingTimer; |
| dispatch_source_set_cancel_handler(rollingTimer, ^{ |
| dispatch_release(theRollingTimer); |
| }); |
| #endif |
| |
| uint64_t delay = (uint64_t)([logFileRollingDate timeIntervalSinceNow] * NSEC_PER_SEC); |
| dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, delay); |
| |
| dispatch_source_set_timer(rollingTimer, fireTime, DISPATCH_TIME_FOREVER, 1.0); |
| dispatch_resume(rollingTimer); |
| } |
| |
| |
| - (void)rollLogFile |
| { |
| [self rollLogFileWithCompletionBlock:nil]; |
| } |
| |
| - (void)rollLogFileWithCompletionBlock:(void (^)())completionBlock |
| { |
| // This method is public. |
| // We need to execute the rolling on our logging thread/queue. |
| |
| dispatch_block_t block = ^{ @autoreleasepool { |
| |
| [self rollLogFileNow]; |
| |
| if (completionBlock) |
| { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| completionBlock(); |
| }); |
| } |
| }}; |
| |
| // The design of this method is taken from the DDAbstractLogger implementation. |
| // For extensive documentation please refer to the DDAbstractLogger implementation. |
| |
| if ([self isOnInternalLoggerQueue]) |
| { |
| block(); |
| } |
| else |
| { |
| dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
| NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
| |
| dispatch_async(globalLoggingQueue, ^{ |
| dispatch_async(loggerQueue, block); |
| }); |
| } |
| } |
| |
| - (void)rollLogFileNow |
| { |
| NSLogVerbose(@"DDFileLogger: rollLogFileNow"); |
| |
| |
| if (currentLogFileHandle == nil) return; |
| |
| [currentLogFileHandle synchronizeFile]; |
| [currentLogFileHandle closeFile]; |
| currentLogFileHandle = nil; |
| |
| currentLogFileInfo.isArchived = YES; |
| |
| if ([logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]) |
| { |
| [logFileManager didRollAndArchiveLogFile:(currentLogFileInfo.filePath)]; |
| } |
| |
| currentLogFileInfo = nil; |
| |
| if (currentLogFileVnode) { |
| dispatch_source_cancel(currentLogFileVnode); |
| currentLogFileVnode = NULL; |
| } |
| |
| if (rollingTimer) |
| { |
| dispatch_source_cancel(rollingTimer); |
| rollingTimer = NULL; |
| } |
| } |
| |
| - (void)maybeRollLogFileDueToAge |
| { |
| if (rollingFrequency > 0.0 && currentLogFileInfo.age >= rollingFrequency) |
| { |
| NSLogVerbose(@"DDFileLogger: Rolling log file due to age..."); |
| |
| [self rollLogFileNow]; |
| } |
| else |
| { |
| [self scheduleTimerToRollLogFileDueToAge]; |
| } |
| } |
| |
| - (void)maybeRollLogFileDueToSize |
| { |
| // This method is called from logMessage. |
| // Keep it FAST. |
| |
| // Note: Use direct access to maximumFileSize variable. |
| // We specifically wrote our own getter/setter method to allow us to do this (for performance reasons). |
| |
| if (maximumFileSize > 0) |
| { |
| unsigned long long fileSize = [currentLogFileHandle offsetInFile]; |
| |
| if (fileSize >= maximumFileSize) |
| { |
| NSLogVerbose(@"DDFileLogger: Rolling log file due to size (%qu)...", fileSize); |
| |
| [self rollLogFileNow]; |
| } |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark File Logging |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Returns the log file that should be used. |
| * If there is an existing log file that is suitable, |
| * within the constraints of maximumFileSize and rollingFrequency, then it is returned. |
| * |
| * Otherwise a new file is created and returned. |
| **/ |
| - (DDLogFileInfo *)currentLogFileInfo |
| { |
| if (currentLogFileInfo == nil) |
| { |
| NSArray *sortedLogFileInfos = [logFileManager sortedLogFileInfos]; |
| |
| if ([sortedLogFileInfos count] > 0) |
| { |
| DDLogFileInfo *mostRecentLogFileInfo = [sortedLogFileInfos objectAtIndex:0]; |
| |
| BOOL shouldArchiveMostRecent = NO; |
| |
| if (mostRecentLogFileInfo.isArchived) |
| { |
| shouldArchiveMostRecent = NO; |
| } |
| else if (maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= maximumFileSize) |
| { |
| shouldArchiveMostRecent = YES; |
| } |
| else if (rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= rollingFrequency) |
| { |
| shouldArchiveMostRecent = YES; |
| } |
| |
| |
| #if TARGET_OS_IPHONE |
| // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen. |
| // |
| // But in case if app is able to launch from background we need to have an ability to open log file any time we |
| // want (even if device is locked). Thats why that attribute have to be changed to |
| // NSFileProtectionCompleteUntilFirstUserAuthentication. |
| // |
| // If previous log was created when app wasn't running in background, but now it is - we archive it and create |
| // a new one. |
| // |
| // If user has owerwritten to NSFileProtectionNone there is no neeed to create a new one. |
| |
| if (!_doNotReuseLogFiles && doesAppRunInBackground()) { |
| NSString *key = mostRecentLogFileInfo.fileAttributes[NSFileProtectionKey]; |
| |
| if (! ([key isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication] || [key isEqualToString:NSFileProtectionNone])) { |
| shouldArchiveMostRecent = YES; |
| } |
| } |
| #endif |
| |
| if (!_doNotReuseLogFiles && !mostRecentLogFileInfo.isArchived && !shouldArchiveMostRecent) |
| { |
| NSLogVerbose(@"DDFileLogger: Resuming logging with file %@", mostRecentLogFileInfo.fileName); |
| |
| currentLogFileInfo = mostRecentLogFileInfo; |
| } |
| else |
| { |
| if (shouldArchiveMostRecent) |
| { |
| mostRecentLogFileInfo.isArchived = YES; |
| |
| if ([logFileManager respondsToSelector:@selector(didArchiveLogFile:)]) |
| { |
| [logFileManager didArchiveLogFile:(mostRecentLogFileInfo.filePath)]; |
| } |
| } |
| } |
| } |
| |
| if (currentLogFileInfo == nil) |
| { |
| NSString *currentLogFilePath = [logFileManager createNewLogFile]; |
| |
| currentLogFileInfo = [[DDLogFileInfo alloc] initWithFilePath:currentLogFilePath]; |
| } |
| } |
| |
| return currentLogFileInfo; |
| } |
| |
| - (NSFileHandle *)currentLogFileHandle |
| { |
| if (currentLogFileHandle == nil) |
| { |
| NSString *logFilePath = [[self currentLogFileInfo] filePath]; |
| |
| currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath]; |
| [currentLogFileHandle seekToEndOfFile]; |
| |
| if (currentLogFileHandle) |
| { |
| [self scheduleTimerToRollLogFileDueToAge]; |
| |
| // Here we are monitoring the log file. In case if it would be deleted ormoved |
| // somewhere we want to roll it and use a new one. |
| currentLogFileVnode = dispatch_source_create( |
| DISPATCH_SOURCE_TYPE_VNODE, |
| [currentLogFileHandle fileDescriptor], |
| DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME, |
| loggerQueue |
| ); |
| |
| dispatch_source_set_event_handler(currentLogFileVnode, ^{ @autoreleasepool { |
| NSLogInfo(@"DDFileLogger: Current logfile was moved. Rolling it and creating a new one"); |
| [self rollLogFileNow]; |
| }}); |
| |
| #if !OS_OBJECT_USE_OBJC |
| dispatch_source_t vnode = currentLogFileVnode; |
| dispatch_source_set_cancel_handler(currentLogFileVnode, ^{ |
| dispatch_release(vnode); |
| }); |
| #endif |
| |
| dispatch_resume(currentLogFileVnode); |
| } |
| } |
| |
| return currentLogFileHandle; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark DDLogger Protocol |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| static int exception_count = 0; |
| - (void)logMessage:(DDLogMessage *)logMessage |
| { |
| NSString *logMsg = logMessage->logMsg; |
| BOOL isFormatted = NO; |
| |
| if (formatter) |
| { |
| logMsg = [formatter formatLogMessage:logMessage]; |
| isFormatted = logMsg != logMessage->logMsg; |
| } |
| |
| if (logMsg) |
| { |
| if ((!isFormatted || _automaticallyAppendNewlineForCustomFormatters) && |
| (![logMsg hasSuffix:@"\n"])) |
| { |
| logMsg = [logMsg stringByAppendingString:@"\n"]; |
| } |
| |
| NSData *logData = [logMsg dataUsingEncoding:NSUTF8StringEncoding]; |
| |
| @try { |
| [[self currentLogFileHandle] writeData:logData]; |
| |
| [self maybeRollLogFileDueToSize]; |
| } |
| @catch (NSException *exception) { |
| exception_count++; |
| if (exception_count <= 10) { |
| NSLogError(@"DDFileLogger.logMessage: %@", exception); |
| if (exception_count == 10) |
| NSLogError(@"DDFileLogger.logMessage: Too many exceptions -- will not log any more of them."); |
| } |
| } |
| } |
| } |
| |
| - (void)willRemoveLogger |
| { |
| // If you override me be sure to invoke [super willRemoveLogger]; |
| |
| [self rollLogFileNow]; |
| } |
| |
| - (NSString *)loggerName |
| { |
| return @"cocoa.lumberjack.fileLogger"; |
| } |
| |
| @end |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark - |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| #if TARGET_IPHONE_SIMULATOR |
| #define XATTR_ARCHIVED_NAME @"archived" |
| #else |
| #define XATTR_ARCHIVED_NAME @"lumberjack.log.archived" |
| #endif |
| |
| @implementation DDLogFileInfo |
| |
| @synthesize filePath; |
| |
| @dynamic fileName; |
| @dynamic fileAttributes; |
| @dynamic creationDate; |
| @dynamic modificationDate; |
| @dynamic fileSize; |
| @dynamic age; |
| |
| @dynamic isArchived; |
| |
| |
| #pragma mark Lifecycle |
| |
| + (instancetype)logFileWithPath:(NSString *)aFilePath |
| { |
| return [[self alloc] initWithFilePath:aFilePath]; |
| } |
| |
| - (instancetype)initWithFilePath:(NSString *)aFilePath |
| { |
| if ((self = [super init])) |
| { |
| filePath = [aFilePath copy]; |
| } |
| return self; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Standard Info |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| - (NSDictionary *)fileAttributes |
| { |
| if (fileAttributes == nil) |
| { |
| fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil]; |
| } |
| return fileAttributes; |
| } |
| |
| - (NSString *)fileName |
| { |
| if (fileName == nil) |
| { |
| fileName = [filePath lastPathComponent]; |
| } |
| return fileName; |
| } |
| |
| - (NSDate *)modificationDate |
| { |
| if (modificationDate == nil) |
| { |
| modificationDate = [[self fileAttributes] objectForKey:NSFileModificationDate]; |
| } |
| |
| return modificationDate; |
| } |
| |
| - (NSDate *)creationDate |
| { |
| if (creationDate == nil) |
| { |
| creationDate = [[self fileAttributes] objectForKey:NSFileCreationDate]; |
| } |
| return creationDate; |
| } |
| |
| - (unsigned long long)fileSize |
| { |
| if (fileSize == 0) |
| { |
| fileSize = [[[self fileAttributes] objectForKey:NSFileSize] unsignedLongLongValue]; |
| } |
| |
| return fileSize; |
| } |
| |
| - (NSTimeInterval)age |
| { |
| return [[self creationDate] timeIntervalSinceNow] * -1.0; |
| } |
| |
| - (NSString *)description |
| { |
| return [@{@"filePath": self.filePath ?: @"", |
| @"fileName": self.fileName ?: @"", |
| @"fileAttributes": self.fileAttributes ?: @"", |
| @"creationDate": self.creationDate ?: @"", |
| @"modificationDate": self.modificationDate ?: @"", |
| @"fileSize": @(self.fileSize), |
| @"age": @(self.age), |
| @"isArchived": @(self.isArchived)} description]; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Archiving |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| - (BOOL)isArchived |
| { |
| |
| #if TARGET_IPHONE_SIMULATOR |
| |
| // Extended attributes don't work properly on the simulator. |
| // So we have to use a less attractive alternative. |
| // See full explanation in the header file. |
| |
| return [self hasExtensionAttributeWithName:XATTR_ARCHIVED_NAME]; |
| |
| #else |
| |
| return [self hasExtendedAttributeWithName:XATTR_ARCHIVED_NAME]; |
| |
| #endif |
| } |
| |
| - (void)setIsArchived:(BOOL)flag |
| { |
| |
| #if TARGET_IPHONE_SIMULATOR |
| |
| // Extended attributes don't work properly on the simulator. |
| // So we have to use a less attractive alternative. |
| // See full explanation in the header file. |
| |
| if (flag) |
| [self addExtensionAttributeWithName:XATTR_ARCHIVED_NAME]; |
| else |
| [self removeExtensionAttributeWithName:XATTR_ARCHIVED_NAME]; |
| |
| #else |
| |
| if (flag) |
| [self addExtendedAttributeWithName:XATTR_ARCHIVED_NAME]; |
| else |
| [self removeExtendedAttributeWithName:XATTR_ARCHIVED_NAME]; |
| |
| #endif |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Changes |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| - (void)reset |
| { |
| fileName = nil; |
| fileAttributes = nil; |
| creationDate = nil; |
| modificationDate = nil; |
| } |
| |
| - (void)renameFile:(NSString *)newFileName |
| { |
| // This method is only used on the iPhone simulator, where normal extended attributes are broken. |
| // See full explanation in the header file. |
| |
| if (![newFileName isEqualToString:[self fileName]]) |
| { |
| NSString *fileDir = [filePath stringByDeletingLastPathComponent]; |
| |
| NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName]; |
| |
| NSLogVerbose(@"DDLogFileInfo: Renaming file: '%@' -> '%@'", self.fileName, newFileName); |
| |
| NSError *error = nil; |
| if ([[NSFileManager defaultManager] fileExistsAtPath:newFilePath] && |
| ![[NSFileManager defaultManager] removeItemAtPath:newFilePath error:&error]) |
| { |
| NSLogError(@"DDLogFileInfo: Error deleting archive (%@): %@", self.fileName, error); |
| } |
| |
| if (![[NSFileManager defaultManager] moveItemAtPath:filePath toPath:newFilePath error:&error]) |
| { |
| NSLogError(@"DDLogFileInfo: Error renaming file (%@): %@", self.fileName, error); |
| } |
| |
| filePath = newFilePath; |
| [self reset]; |
| } |
| } |
| |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Attribute Management |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| #if TARGET_IPHONE_SIMULATOR |
| |
| // Extended attributes don't work properly on the simulator. |
| // So we have to use a less attractive alternative. |
| // See full explanation in the header file. |
| |
| - (BOOL)hasExtensionAttributeWithName:(NSString *)attrName |
| { |
| // This method is only used on the iPhone simulator, where normal extended attributes are broken. |
| // See full explanation in the header file. |
| |
| // Split the file name into components. File name may have various format, but generally |
| // structure is same: |
| // |
| // <name part>.<extension part> and <name part>.archived.<extension part> |
| // or |
| // <name part> and <name part>.archived |
| // |
| // So we want to search for the attrName in the components (ignoring the first array index). |
| |
| NSArray *components = [[self fileName] componentsSeparatedByString:@"."]; |
| |
| // Watch out for file names without an extension |
| |
| for (NSUInteger i = 1; i < components.count; i++) |
| { |
| NSString *attr = [components objectAtIndex:i]; |
| |
| if ([attrName isEqualToString:attr]) |
| { |
| return YES; |
| } |
| } |
| |
| return NO; |
| } |
| |
| - (void)addExtensionAttributeWithName:(NSString *)attrName |
| { |
| // This method is only used on the iPhone simulator, where normal extended attributes are broken. |
| // See full explanation in the header file. |
| |
| if ([attrName length] == 0) return; |
| |
| // Example: |
| // attrName = "archived" |
| // |
| // "mylog.txt" -> "mylog.archived.txt" |
| // "mylog" -> "mylog.archived" |
| |
| NSArray *components = [[self fileName] componentsSeparatedByString:@"."]; |
| |
| NSUInteger count = [components count]; |
| |
| NSUInteger estimatedNewLength = [[self fileName] length] + [attrName length] + 1; |
| NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength]; |
| |
| if (count > 0) |
| { |
| [newFileName appendString:[components objectAtIndex:0]]; |
| } |
| |
| NSString *lastExt = @""; |
| |
| NSUInteger i; |
| for (i = 1; i < count; i++) |
| { |
| NSString *attr = [components objectAtIndex:i]; |
| if ([attr length] == 0) |
| { |
| continue; |
| } |
| |
| if ([attrName isEqualToString:attr]) |
| { |
| // Extension attribute already exists in file name |
| return; |
| } |
| |
| if ([lastExt length] > 0) |
| { |
| [newFileName appendFormat:@".%@", lastExt]; |
| } |
| |
| lastExt = attr; |
| } |
| |
| [newFileName appendFormat:@".%@", attrName]; |
| |
| if ([lastExt length] > 0) |
| { |
| [newFileName appendFormat:@".%@", lastExt]; |
| } |
| |
| [self renameFile:newFileName]; |
| } |
| |
| - (void)removeExtensionAttributeWithName:(NSString *)attrName |
| { |
| // This method is only used on the iPhone simulator, where normal extended attributes are broken. |
| // See full explanation in the header file. |
| |
| if ([attrName length] == 0) return; |
| |
| // Example: |
| // attrName = "archived" |
| // |
| // "mylog.archived.txt" -> "mylog.txt" |
| // "mylog.archived" -> "mylog" |
| |
| NSArray *components = [[self fileName] componentsSeparatedByString:@"."]; |
| |
| NSUInteger count = [components count]; |
| |
| NSUInteger estimatedNewLength = [[self fileName] length]; |
| NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength]; |
| |
| if (count > 0) |
| { |
| [newFileName appendString:[components objectAtIndex:0]]; |
| } |
| |
| BOOL found = NO; |
| |
| NSUInteger i; |
| for (i = 1; i < count; i++) |
| { |
| NSString *attr = [components objectAtIndex:i]; |
| |
| if ([attrName isEqualToString:attr]) |
| { |
| found = YES; |
| } |
| else |
| { |
| [newFileName appendFormat:@".%@", attr]; |
| } |
| } |
| |
| if (found) |
| { |
| [self renameFile:newFileName]; |
| } |
| } |
| |
| #else |
| |
| - (BOOL)hasExtendedAttributeWithName:(NSString *)attrName |
| { |
| const char *path = [filePath UTF8String]; |
| const char *name = [attrName UTF8String]; |
| |
| ssize_t result = getxattr(path, name, NULL, 0, 0, 0); |
| |
| return (result >= 0); |
| } |
| |
| - (void)addExtendedAttributeWithName:(NSString *)attrName |
| { |
| const char *path = [filePath UTF8String]; |
| const char *name = [attrName UTF8String]; |
| |
| int result = setxattr(path, name, NULL, 0, 0, 0); |
| |
| if (result < 0) |
| { |
| NSLogError(@"DDLogFileInfo: setxattr(%@, %@): error = %s", |
| attrName, |
| self.fileName, |
| strerror(errno)); |
| } |
| } |
| |
| - (void)removeExtendedAttributeWithName:(NSString *)attrName |
| { |
| const char *path = [filePath UTF8String]; |
| const char *name = [attrName UTF8String]; |
| |
| int result = removexattr(path, name, 0); |
| |
| if (result < 0 && errno != ENOATTR) |
| { |
| NSLogError(@"DDLogFileInfo: removexattr(%@, %@): error = %s", |
| attrName, |
| self.fileName, |
| strerror(errno)); |
| } |
| } |
| |
| #endif |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Comparisons |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| - (BOOL)isEqual:(id)object |
| { |
| if ([object isKindOfClass:[self class]]) |
| { |
| DDLogFileInfo *another = (DDLogFileInfo *)object; |
| |
| return [filePath isEqualToString:[another filePath]]; |
| } |
| |
| return NO; |
| } |
| |
| - (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another |
| { |
| NSDate *us = [self creationDate]; |
| NSDate *them = [another creationDate]; |
| |
| NSComparisonResult result = [us compare:them]; |
| |
| if (result == NSOrderedAscending) |
| return NSOrderedDescending; |
| |
| if (result == NSOrderedDescending) |
| return NSOrderedAscending; |
| |
| return NSOrderedSame; |
| } |
| |
| - (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another |
| { |
| NSDate *us = [self modificationDate]; |
| NSDate *them = [another modificationDate]; |
| |
| NSComparisonResult result = [us compare:them]; |
| |
| if (result == NSOrderedAscending) |
| return NSOrderedDescending; |
| |
| if (result == NSOrderedDescending) |
| return NSOrderedAscending; |
| |
| return NSOrderedSame; |
| } |
| |
| @end |
| |
| #if TARGET_OS_IPHONE |
| /** |
| * When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen. |
| * |
| * But in case if app is able to launch from background we need to have an ability to open log file any time we |
| * want (even if device is locked). Thats why that attribute have to be changed to |
| * NSFileProtectionCompleteUntilFirstUserAuthentication. |
| */ |
| BOOL doesAppRunInBackground() |
| { |
| BOOL answer = NO; |
| |
| NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"]; |
| |
| for (NSString *mode in backgroundModes) { |
| if (mode.length > 0) { |
| answer = YES; |
| break; |
| } |
| } |
| |
| return answer; |
| } |
| #endif |
| |