// // 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 *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