#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)
#warning 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;
- (id)init
{
return [self initWithLogsDirectory:nil];
}
- (instancetype)initWithLogsDirectory:(NSString *)aLogsDirectory
{
if ((self = [super init]))
{
maximumNumberOfLogFiles = DEFAULT_LOG_MAX_NUM_LOG_FILES;
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];
NSLogVerbose(@"DDFileLogManagerDefault: logsDirectory:\n%@", [self logsDirectory]);
NSLogVerbose(@"DDFileLogManagerDefault: sortedLogFileNames:\n%@", [self sortedLogFileNames]);
}
return self;
}
- (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))];
}
@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))])
{
NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: maximumNumberOfLogFiles");
dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool {
[self deleteOldLogFiles];
}});
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark File Deleting
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Deletes archived
log files that exceed the maximumNumberOfLogFiles configuration value.
**/
- (void)deleteOldLogFiles
{
NSLogVerbose(@"DDLogFileManagerDefault: deleteOldLogFiles");
NSUInteger maxNumLogFiles = self.maximumNumberOfLogFiles;
if (maxNumLogFiles == 0)
{
return;
}
NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
// 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.
NSUInteger count = [sortedLogFileInfos count];
BOOL excludeFirstFile = NO;
if (count > 0)
{
DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:0];
if (!logFileInfo.isArchived)
{
excludeFirstFile = YES;
}
}
NSArray *sortedArchivedLogFileInfos;
if (excludeFirstFile)
{
count--;
sortedArchivedLogFileInfos = [sortedLogFileInfos subarrayWithRange:NSMakeRange(1, count)];
}
else
{
sortedArchivedLogFileInfos = sortedLogFileInfos;
}
for (i = maxNumLogFiles;
i < count;
i++
)
{
DDLogFileInfo *logFileInfo =
[sortedArchivedLogFileInfos objectAtIndex:
i];
NSLogInfo(@"DDLogFileManagerDefault: Deleting file: %@", logFileInfo.fileName);
[[NSFileManager defaultManager
] removeItemAtPath:logFileInfo.
filePath error:nil
];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* 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;
}
/**
* A
log file has a name like "<app name> <date> <time>.
log".
* Example: MobileSafari
2013-
12-03
17-
14.
log
**/
- (BOOL)isLogFile:(NSString *)fileName
{
NSString *appName = [self applicationName];
BOOL hasProperPrefix = [fileName hasPrefix:appName];
BOOL hasProperSuffix = [fileName hasSuffix:@".log"];
BOOL hasProperDate = NO;
if (hasProperPrefix && hasProperSuffix)
{
//
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 generateLogFileNameWithAttempt: 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
];
{
hasProperDate = YES;
}
}
}
}
return (hasProperPrefix && hasProperDate && hasProperSuffix);
}
- (NSDateFormatter *)logFileDateFormatter
{
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy'-'MM'-'dd' 'HH'-'mm'"];
[dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
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 ([self isLogFile:fileName])
{
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
format "<app name> <date> <time>.
log"
* Example: MobileSafari
2013-
12-03
17-
14.
log
**/
- (NSString *)generateLogFileNameWithAttempt:(NSUInteger)attempt
{
NSString *appName = [self applicationName];
NSDateFormatter *dateFormatter = [self logFileDateFormatter];
NSString *formattedDate =
[dateFormatter stringFromDate:
[NSDate
date]];
if (attempt > 1)
{
return [NSString stringWithFormat:@"%@ %@ %lu.log", appName, formattedDate, (unsigned long)attempt];
}
else
{
return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate];
}
}
/**
**/
- (NSString *)createNewLogFile
{
// Generate a random
log file name, and create the file
(if there isn't a collision
)
NSString *logsDirectory = [self logsDirectory];
NSUInteger attempt = 1;
do
{
NSString *fileName = [self generateLogFileNameWithAttempt:attempt];
NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName];
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath])
{
NSLogVerbose
(@"DDLogFileManagerDefault: Creating new
log file:
%@", fileName);
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 = 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, ^{
#if TARGET_OS_IPHONE
_appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
if (! _appName)
{
_appName = [[NSProcessInfo processInfo] processName];
}
#else
_appName = [[NSProcessInfo processInfo] processName];
#endif
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;
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);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#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);
}
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);
}
}
- (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 useExistingLogFile = YES;
BOOL shouldArchiveMostRecent = NO;
if (mostRecentLogFileInfo.isArchived)
{
useExistingLogFile = NO;
shouldArchiveMostRecent = NO;
}
else if (maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= maximumFileSize)
{
useExistingLogFile = NO;
shouldArchiveMostRecent = YES;
}
else if (rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= rollingFrequency)
{
useExistingLogFile = NO;
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 (useExistingLogFile && doesAppRunInBackground()) {
NSString *key = mostRecentLogFileInfo.fileAttributes[NSFileProtectionKey];
if (! [key isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) {
useExistingLogFile = NO;
shouldArchiveMostRecent = YES;
}
}
#endif
if (useExistingLogFile)
{
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;
if (formatter)
{
logMsg = [formatter formatLogMessage:logMessage];
}
if (logMsg)
{
if (![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.
[self addExtensionAttributeWithName:XATTR_ARCHIVED_NAME];
else
[self removeExtensionAttributeWithName:XATTR_ARCHIVED_NAME];
#else
[self addExtendedAttributeWithName:XATTR_ARCHIVED_NAME];
else
[self removeExtendedAttributeWithName:XATTR_ARCHIVED_NAME];
#endif
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Changes
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
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);
if (!
[[NSFileManager defaultManager
] moveItemAtPath:filePath toPath:newFilePath
error:&
error])
{
NSLogError
(@"DDLogFileInfo:
Error renaming file
(%@): %@", self.fileName, error);
}
filePath = newFilePath;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#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.
//
// log-ABC123.archived.uploaded.txt
//
// 0. log-ABC123
// 1. archived
// 2. uploaded
// 3. txt
//
// So we want to search for the attrName in the components (ignoring the first and last array indexes).
NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
// Watch out for file names without an extension
NSUInteger count = [components count];
NSUInteger
max =
(count >=
2) ? count-
1 : count;
{
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"
//
// "log-ABC123.txt" -> "log-ABC123.archived.txt"
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 = @"";
for (i =
1;
i < count;
i++
)
{
NSString *attr =
[components objectAtIndex:
i];
{
continue;
}
if ([attrName isEqualToString:attr])
{
// Extension attribute already exists in file name
return;
}
{
[newFileName appendFormat:@".%@", lastExt];
}
lastExt = attr;
}
[newFileName appendFormat:@".%@", attrName];
{
[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"
//
// "log-ABC123.archived.txt" -> "log-ABC123.txt"
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;
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 = %i", attrName, self.fileName, result);
}
}
- (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 = %i", attrName, self.fileName, result);
}
}
#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) {
answer = YES;
break;
}
}
return answer;
}
#endif