guru_sdk/guru_app/plugins/persistent/ios/Classes/LogLibrary/DDLogFileManagerDefault.m

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:&currentError];
#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:&currentError];
}
#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