499 lines
18 KiB
Objective-C
499 lines
18 KiB
Objective-C
//
|
|
// DDFileLoggerManager.m
|
|
// NVLogManagerDemo
|
|
//
|
|
// Created by cxb on 2023/5/10.
|
|
// Copyright © 2023 com.zhouxi. All rights reserved.
|
|
//
|
|
|
|
#import "DDLogFileManagerDefault.h"
|
|
|
|
|
|
@interface DDLogFileManagerDefault () {
|
|
NSDateFormatter *_fileDateFormatter;
|
|
NSUInteger _maximumNumberOfLogFiles;
|
|
// unsigned long long _logFilesDiskQuota;
|
|
NSString *_logsDirectory;
|
|
NSString *_directoryName;
|
|
#if TARGET_OS_IPHONE
|
|
NSFileProtectionType _defaultFileProtectionLevel;
|
|
#endif
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation DDLogFileManagerDefault
|
|
|
|
@synthesize maximumNumberOfLogFiles = _maximumNumberOfLogFiles;
|
|
//@synthesize logFilesDiskQuota = _logFilesDiskQuota;
|
|
|
|
|
|
|
|
- (instancetype)initWithLogsDirectory:(nullable NSString *)aLogsDirectory {
|
|
if ((self = [super init])) {
|
|
_maximumNumberOfLogFiles = kDDDefaultLogMaxNumLogFiles;
|
|
// _logFilesDiskQuota = kDDDefaultLogFilesDiskQuota;
|
|
_directoryName = aLogsDirectory;
|
|
_fileDateFormatter = [[NSDateFormatter alloc] init];
|
|
// [_fileDateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
|
|
// [_fileDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
|
|
[_fileDateFormatter setDateFormat: @"yyyy'-'MM'-'dd'--'HH'-'mm'-'ss'-'SSS'"];
|
|
|
|
if (aLogsDirectory.length > 0) {
|
|
_logsDirectory = [[self generateLogsDirectoryWithPath:aLogsDirectory] copy];
|
|
} else {
|
|
_logsDirectory = [[self generateLogsDirectoryWithPath:@"default"] copy];
|
|
}
|
|
|
|
NSLogVerbose(@"DDFileLogManagerDefault: logsDirectory:\n%@", [self logsDirectory]);
|
|
NSLogVerbose(@"DDFileLogManagerDefault: sortedLogFileNames:\n%@", [self sortedLogFileNames]);
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
#if TARGET_OS_IPHONE
|
|
- (instancetype)initWithLogsDirectory:(NSString *)logsDirectory
|
|
defaultFileProtectionLevel:(NSFileProtectionType)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)deleteOldFilesForConfigurationChange {
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
@autoreleasepool {
|
|
// See method header for queue reasoning.
|
|
[self deleteOldLogFiles];
|
|
}
|
|
});
|
|
}
|
|
|
|
//- (void)setLogFilesDiskQuota:(unsigned long long)logFilesDiskQuota {
|
|
// if (_logFilesDiskQuota != logFilesDiskQuota) {
|
|
// _logFilesDiskQuota = logFilesDiskQuota;
|
|
// NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: logFilesDiskQuota");
|
|
// [self deleteOldFilesForConfigurationChange];
|
|
// }
|
|
//}
|
|
|
|
- (void)setMaximumNumberOfLogFiles:(NSUInteger)maximumNumberOfLogFiles {
|
|
if (_maximumNumberOfLogFiles != maximumNumberOfLogFiles) {
|
|
_maximumNumberOfLogFiles = maximumNumberOfLogFiles;
|
|
NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: maximumNumberOfLogFiles");
|
|
[self deleteOldFilesForConfigurationChange];
|
|
}
|
|
}
|
|
|
|
#if TARGET_OS_IPHONE
|
|
- (NSFileProtectionType)logFileProtection {
|
|
if (_defaultFileProtectionLevel.length > 0) {
|
|
return _defaultFileProtectionLevel;
|
|
} else if (__doesAppRunInBackground()) {
|
|
return NSFileProtectionCompleteUntilFirstUserAuthentication;
|
|
} else {
|
|
return NSFileProtectionCompleteUnlessOpen;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark File Deleting
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Deletes archived log files that exceed the maximumNumberOfLogFiles or logFilesDiskQuota configuration values.
|
|
* Method may take a while to execute since we're performing IO. It's not critical that this is synchronized with
|
|
* log output, since the files we're deleting are all archived and not in use, therefore this method is called on a
|
|
* background queue.
|
|
**/
|
|
- (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 log files starting with firstIndexToDelete
|
|
|
|
for (NSUInteger i = firstIndexToDelete; i < sortedLogFileInfos.count; i++) {
|
|
DDLogFileInfo *logFileInfo = sortedLogFileInfos[i];
|
|
|
|
__autoreleasing NSError *error = nil;
|
|
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:&error];
|
|
if (success) {
|
|
NSLogInfo(@"DDLogFileManagerDefault: Deleting file: %@", logFileInfo.fileName);
|
|
} else {
|
|
NSLogError(@"DDLogFileManagerDefault: Error deleting file %@", error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Log Files
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
-(NSString *)generateLogsDirectoryWithPath:(NSString *)filePath{
|
|
NSString *logsDirectory = [[DDLog rootLogsDirectory] stringByAppendingPathComponent:[NSString stringWithFormat:@"/%@",filePath]];
|
|
return logsDirectory;
|
|
}
|
|
|
|
- (NSString *)logsDirectory {
|
|
// We could do this check once, during initialization, and not bother again.
|
|
// But this way the code continues to work if the directory gets deleted while the code is running.
|
|
|
|
NSAssert(_logsDirectory.length > 0, @"Directory must be set.");
|
|
|
|
__autoreleasing NSError *error = nil;
|
|
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:_logsDirectory
|
|
withIntermediateDirectories:YES
|
|
attributes:nil
|
|
error:&error];
|
|
if (!success) {
|
|
NSLogError(@"DDFileLogManagerDefault: Error creating logsDirectory: %@", error);
|
|
}
|
|
|
|
return _logsDirectory;
|
|
}
|
|
|
|
- (NSString *)directoryName{
|
|
if(_directoryName != nil){
|
|
return _directoryName;
|
|
}
|
|
return @"";
|
|
}
|
|
|
|
- (BOOL)isLogFile:(NSString *)fileName {
|
|
NSString *appName = [self applicationName];
|
|
|
|
// We need to add a space to the name as otherwise we could match applications that have the name prefix.
|
|
BOOL hasProperPrefix = [fileName hasPrefix:[appName stringByAppendingString:@" "]];
|
|
BOOL hasProperSuffix = [fileName hasSuffix:@".log"];
|
|
|
|
return (hasProperPrefix && hasProperSuffix);
|
|
}
|
|
|
|
// if you change formatter, then change sortedLogFileInfos method also accordingly
|
|
- (NSDateFormatter *)logFileDateFormatter {
|
|
return _fileDateFormatter;
|
|
}
|
|
|
|
- (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
|
|
// This is only used on the iPhone simulator for backward compatibility reason.
|
|
//
|
|
// In case of iPhone simulator there can be 'archived' extension. isLogFile:
|
|
// method knows nothing about it. Thus removing it for this method.
|
|
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;
|
|
}
|
|
|
|
- (NSArray *)unsortedLogFileNames {
|
|
NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths];
|
|
|
|
NSMutableArray *unsortedLogFileNames = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
|
|
|
|
for (NSString *filePath in unsortedLogFilePaths) {
|
|
[unsortedLogFileNames addObject:[filePath lastPathComponent]];
|
|
}
|
|
|
|
return unsortedLogFileNames;
|
|
}
|
|
|
|
- (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;
|
|
}
|
|
|
|
- (NSArray *)sortedLogFilePaths {
|
|
NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
|
|
|
|
NSMutableArray *sortedLogFilePaths = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
|
|
|
|
for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) {
|
|
[sortedLogFilePaths addObject:[logFileInfo filePath]];
|
|
}
|
|
|
|
return sortedLogFilePaths;
|
|
}
|
|
|
|
- (NSArray *)sortedLogFileNames {
|
|
NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
|
|
|
|
NSMutableArray *sortedLogFileNames = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
|
|
|
|
for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) {
|
|
[sortedLogFileNames addObject:[logFileInfo fileName]];
|
|
}
|
|
|
|
return sortedLogFileNames;
|
|
}
|
|
|
|
- (NSArray *)sortedLogFileInfos {
|
|
return [[self unsortedLogFileInfos] sortedArrayUsingComparator:^NSComparisonResult(DDLogFileInfo *obj1,
|
|
DDLogFileInfo *obj2) {
|
|
NSDate *date1 = [NSDate new];
|
|
NSDate *date2 = [NSDate new];
|
|
|
|
NSArray<NSString *> *arrayComponent = [[obj1 fileName] componentsSeparatedByString:@" "];
|
|
if (arrayComponent.count > 0) {
|
|
NSString *stringDate = arrayComponent.lastObject;
|
|
stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@""];
|
|
#if TARGET_IPHONE_SIMULATOR
|
|
// This is only used on the iPhone simulator for backward compatibility reason.
|
|
stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@""];
|
|
#endif
|
|
date1 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj1 creationDate];
|
|
}
|
|
|
|
arrayComponent = [[obj2 fileName] componentsSeparatedByString:@" "];
|
|
if (arrayComponent.count > 0) {
|
|
NSString *stringDate = arrayComponent.lastObject;
|
|
stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@""];
|
|
#if TARGET_IPHONE_SIMULATOR
|
|
// This is only used on the iPhone simulator for backward compatibility reason.
|
|
stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@""];
|
|
#endif
|
|
date2 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj2 creationDate];
|
|
}
|
|
|
|
return [date2 compare:date1 ?: [NSDate new]];
|
|
}];
|
|
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Creation
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
//if you change newLogFileName , then change isLogFile method also accordingly
|
|
- (NSString *)newLogFileName {
|
|
NSString *appName = [self applicationName];
|
|
|
|
NSDateFormatter *dateFormatter = [self logFileDateFormatter];
|
|
NSString *formattedDate = [dateFormatter stringFromDate:[NSDate date]];
|
|
|
|
return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate];
|
|
}
|
|
|
|
- (nullable NSString *)logFileHeader {
|
|
return nil;
|
|
}
|
|
|
|
- (NSData *)logFileHeaderData {
|
|
NSString *fileHeaderStr = [self logFileHeader];
|
|
|
|
if (fileHeaderStr.length == 0) {
|
|
return nil;
|
|
}
|
|
|
|
if (![fileHeaderStr hasSuffix:@"\n"]) {
|
|
fileHeaderStr = [fileHeaderStr stringByAppendingString:@"\n"];
|
|
}
|
|
|
|
return [fileHeaderStr dataUsingEncoding:NSUTF8StringEncoding];
|
|
}
|
|
|
|
- (NSString *)createNewLogFileWithError:(NSError *__autoreleasing _Nullable *)error {
|
|
static NSUInteger MAX_ALLOWED_ERROR = 5;
|
|
|
|
NSString *fileName = [self newLogFileName];
|
|
NSString *logsDirectory = [self logsDirectory];
|
|
NSData *fileHeader = [self logFileHeaderData] ?: [NSData new];
|
|
|
|
NSString *baseName = nil;
|
|
NSString *extension;
|
|
NSUInteger attempt = 1;
|
|
NSUInteger criticalErrors = 0;
|
|
NSError *lastCriticalError;
|
|
|
|
if (error) *error = nil;
|
|
do {
|
|
if (criticalErrors >= MAX_ALLOWED_ERROR) {
|
|
NSLogError(@"DDLogFileManagerDefault: Bailing file creation, encountered %ld errors.",
|
|
(unsigned long)criticalErrors);
|
|
if (error) *error = lastCriticalError;
|
|
return nil;
|
|
}
|
|
|
|
NSString *actualFileName;
|
|
if (attempt > 1) {
|
|
if (baseName == nil) {
|
|
baseName = [fileName stringByDeletingPathExtension];
|
|
extension = [fileName pathExtension];
|
|
}
|
|
|
|
actualFileName = [baseName stringByAppendingFormat:@" %lu", (unsigned long)attempt];
|
|
if (extension.length) {
|
|
actualFileName = [actualFileName stringByAppendingPathExtension:extension];
|
|
}
|
|
} else {
|
|
actualFileName = fileName;
|
|
}
|
|
|
|
NSString *filePath = [logsDirectory stringByAppendingPathComponent:actualFileName];
|
|
|
|
__autoreleasing NSError *currentError = nil;
|
|
BOOL success = [fileHeader writeToFile:filePath options:NSDataWritingAtomic error:¤tError];
|
|
|
|
#if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST
|
|
if (success) {
|
|
// 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.
|
|
NSDictionary *attributes = @{NSFileProtectionKey: [self logFileProtection]};
|
|
success = [[NSFileManager defaultManager] setAttributes:attributes
|
|
ofItemAtPath:filePath
|
|
error:¤tError];
|
|
}
|
|
#endif
|
|
|
|
if (success) {
|
|
NSLogVerbose(@"DDLogFileManagerDefault: Created new log file: %@", actualFileName);
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
// Since we just created a new log file, we may need to delete some old log files
|
|
[self deleteOldLogFiles];
|
|
});
|
|
return filePath;
|
|
} else if (currentError.code == NSFileWriteFileExistsError) {
|
|
attempt++;
|
|
} else {
|
|
NSLogError(@"DDLogFileManagerDefault: Critical error while creating log file: %@", currentError);
|
|
criticalErrors++;
|
|
lastCriticalError = currentError;
|
|
}
|
|
} 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.length == 0) {
|
|
_appName = [[NSProcessInfo processInfo] processName];
|
|
}
|
|
|
|
if (_appName.length == 0) {
|
|
_appName = @"";
|
|
}
|
|
});
|
|
|
|
return _appName;
|
|
}
|
|
|
|
|
|
|
|
BOOL __doesAppRunInBackground(void) {
|
|
BOOL answer = NO;
|
|
|
|
NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
|
|
|
|
for (NSString *mode in backgroundModes) {
|
|
if (mode.length > 0) {
|
|
answer = YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return answer;
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|