DDFileLogger.m 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871
  1. // Software License Agreement (BSD License)
  2. //
  3. // Copyright (c) 2010-2025, Deusty, LLC
  4. // All rights reserved.
  5. //
  6. // Redistribution and use of this software in source and binary forms,
  7. // with or without modification, are permitted provided that the following conditions are met:
  8. //
  9. // * Redistributions of source code must retain the above copyright notice,
  10. // this list of conditions and the following disclaimer.
  11. //
  12. // * Neither the name of Deusty nor the names of its contributors may be used
  13. // to endorse or promote products derived from this software without specific
  14. // prior written permission of Deusty, LLC.
  15. #if !__has_feature(objc_arc)
  16. #error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
  17. #endif
  18. #import <sys/xattr.h>
  19. #import <sys/file.h>
  20. #import <errno.h>
  21. #import <unistd.h>
  22. #import "DDFileLogger+Internal.h"
  23. // We probably shouldn't be using DDLog() statements within the DDLog implementation.
  24. // But we still want to leave our log statements for any future debugging,
  25. // and to allow other developers to trace the implementation (which is a great learning tool).
  26. //
  27. // So we use primitive logging macros around NSLog.
  28. // We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog.
  29. #ifndef DD_NSLOG_LEVEL
  30. #define DD_NSLOG_LEVEL 2
  31. #endif
  32. #define NSLogError(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0)
  33. #define NSLogWarn(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0)
  34. #define NSLogInfo(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0)
  35. #define NSLogDebug(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0)
  36. #define NSLogVerbose(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 5) NSLog((frmt), ##__VA_ARGS__); } while(0)
  37. #if TARGET_OS_IPHONE
  38. BOOL doesAppRunInBackground(void);
  39. #endif
  40. unsigned long long const kDDDefaultLogMaxFileSize = 1024 * 1024; // 1 MB
  41. NSTimeInterval const kDDDefaultLogRollingFrequency = 60 * 60 * 24; // 24 Hours
  42. NSUInteger const kDDDefaultLogMaxNumLogFiles = 5; // 5 Files
  43. unsigned long long const kDDDefaultLogFilesDiskQuota = 20 * 1024 * 1024; // 20 MB
  44. NSTimeInterval const kDDRollingLeeway = 1.0; // 1s
  45. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  46. #pragma mark -
  47. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  48. @implementation DDFileLogPlainTextMessageSerializer
  49. - (instancetype)init {
  50. return [super init];
  51. }
  52. - (NSData *)dataForString:(NSString *)string originatingFromMessage:(DDLogMessage *)message {
  53. return [string dataUsingEncoding:NSUTF8StringEncoding] ?: [NSData data];
  54. }
  55. @end
  56. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  57. #pragma mark -
  58. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  59. @interface DDLogFileManagerDefault () {
  60. NSDateFormatter *_fileDateFormatter;
  61. NSUInteger _maximumNumberOfLogFiles;
  62. unsigned long long _logFilesDiskQuota;
  63. NSString *_logsDirectory;
  64. BOOL _wasAddedToLogger;
  65. #if TARGET_OS_IPHONE
  66. NSFileProtectionType _defaultFileProtectionLevel;
  67. #endif
  68. }
  69. @end
  70. @implementation DDLogFileManagerDefault
  71. @synthesize maximumNumberOfLogFiles = _maximumNumberOfLogFiles;
  72. @synthesize logFilesDiskQuota = _logFilesDiskQuota;
  73. @synthesize logMessageSerializer = _logMessageSerializer;
  74. - (instancetype)initWithLogsDirectory:(nullable NSString *)aLogsDirectory {
  75. if ((self = [super init])) {
  76. _maximumNumberOfLogFiles = kDDDefaultLogMaxNumLogFiles;
  77. _logFilesDiskQuota = kDDDefaultLogFilesDiskQuota;
  78. _wasAddedToLogger = NO;
  79. _fileDateFormatter = [[NSDateFormatter alloc] init];
  80. [_fileDateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
  81. [_fileDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
  82. [_fileDateFormatter setDateFormat: @"yyyy'-'MM'-'dd'--'HH'-'mm'-'ss'-'SSS'"];
  83. if (aLogsDirectory.length > 0) {
  84. _logsDirectory = [aLogsDirectory copy];
  85. } else {
  86. _logsDirectory = [[self defaultLogsDirectory] copy];
  87. }
  88. _logMessageSerializer = [[DDFileLogPlainTextMessageSerializer alloc] init];
  89. NSLogVerbose(@"DDFileLogManagerDefault: logsDirectory:\n%@", [self logsDirectory]);
  90. NSLogVerbose(@"DDFileLogManagerDefault: sortedLogFileNames:\n%@", [self sortedLogFileNames]);
  91. }
  92. return self;
  93. }
  94. #if TARGET_OS_IPHONE
  95. - (instancetype)initWithLogsDirectory:(NSString *)logsDirectory
  96. defaultFileProtectionLevel:(NSFileProtectionType)fileProtectionLevel {
  97. if ((self = [self initWithLogsDirectory:logsDirectory])) {
  98. if ([fileProtectionLevel isEqualToString:NSFileProtectionNone] ||
  99. [fileProtectionLevel isEqualToString:NSFileProtectionComplete] ||
  100. [fileProtectionLevel isEqualToString:NSFileProtectionCompleteUnlessOpen] ||
  101. [fileProtectionLevel isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) {
  102. _defaultFileProtectionLevel = fileProtectionLevel;
  103. }
  104. }
  105. return self;
  106. }
  107. #endif
  108. - (instancetype)init {
  109. return [self initWithLogsDirectory:nil];
  110. }
  111. - (void)didAddToFileLogger:(DDFileLogger *)fileLogger {
  112. _wasAddedToLogger = YES;
  113. }
  114. - (void)deleteOldFilesForConfigurationChange {
  115. if (!_wasAddedToLogger) return;
  116. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  117. @autoreleasepool {
  118. // See method header for queue reasoning.
  119. [self deleteOldLogFilesWithError:nil];
  120. }
  121. });
  122. }
  123. - (void)setLogFilesDiskQuota:(unsigned long long)logFilesDiskQuota {
  124. if (_logFilesDiskQuota != logFilesDiskQuota) {
  125. _logFilesDiskQuota = logFilesDiskQuota;
  126. NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: logFilesDiskQuota");
  127. [self deleteOldFilesForConfigurationChange];
  128. }
  129. }
  130. - (void)setMaximumNumberOfLogFiles:(NSUInteger)maximumNumberOfLogFiles {
  131. if (_maximumNumberOfLogFiles != maximumNumberOfLogFiles) {
  132. _maximumNumberOfLogFiles = maximumNumberOfLogFiles;
  133. NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: maximumNumberOfLogFiles");
  134. [self deleteOldFilesForConfigurationChange];
  135. }
  136. }
  137. #if TARGET_OS_IPHONE
  138. - (NSFileProtectionType)logFileProtection {
  139. if (_defaultFileProtectionLevel.length > 0) {
  140. return _defaultFileProtectionLevel;
  141. } else if (doesAppRunInBackground()) {
  142. return NSFileProtectionCompleteUntilFirstUserAuthentication;
  143. } else {
  144. return NSFileProtectionCompleteUnlessOpen;
  145. }
  146. }
  147. #endif
  148. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  149. #pragma mark File Deleting
  150. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  151. /**
  152. * Deletes archived log files that exceed the maximumNumberOfLogFiles or logFilesDiskQuota configuration values.
  153. * Method may take a while to execute since we're performing IO. It's not critical that this is synchronized with
  154. * log output, since the files we're deleting are all archived and not in use, therefore this method is called on a
  155. * background queue.
  156. **/
  157. - (BOOL)deleteOldLogFilesWithError:(NSError *__autoreleasing _Nullable *)error {
  158. NSLogVerbose(@"DDLogFileManagerDefault: %@", NSStringFromSelector(_cmd));
  159. if (error) *error = nil;
  160. __auto_type sortedLogFileInfos = [self sortedLogFileInfos];
  161. NSUInteger firstIndexToDelete = NSNotFound;
  162. const unsigned long long diskQuota = self.logFilesDiskQuota;
  163. const NSUInteger maxNumLogFiles = self.maximumNumberOfLogFiles;
  164. if (diskQuota) {
  165. unsigned long long used = 0;
  166. for (NSUInteger i = 0; i < sortedLogFileInfos.count; i++) {
  167. DDLogFileInfo *info = sortedLogFileInfos[i];
  168. used += info.fileSize;
  169. if (used > diskQuota) {
  170. firstIndexToDelete = i;
  171. break;
  172. }
  173. }
  174. }
  175. if (maxNumLogFiles) {
  176. if (firstIndexToDelete == NSNotFound) {
  177. firstIndexToDelete = maxNumLogFiles;
  178. } else {
  179. firstIndexToDelete = MIN(firstIndexToDelete, maxNumLogFiles);
  180. }
  181. }
  182. if (firstIndexToDelete == 0) {
  183. // Do we consider the first file?
  184. // We are only supposed to be deleting archived files.
  185. // In most cases, the first file is likely the log file that is currently being written to.
  186. // So in most cases, we do not want to consider this file for deletion.
  187. if (sortedLogFileInfos.count > 0) {
  188. if (!sortedLogFileInfos[0].isArchived) {
  189. // Don't delete active file.
  190. firstIndexToDelete++;
  191. }
  192. }
  193. }
  194. if (firstIndexToDelete != NSNotFound) {
  195. // removing all log files starting with firstIndexToDelete
  196. for (NSUInteger i = firstIndexToDelete; i < sortedLogFileInfos.count; i++) {
  197. __auto_type logFileInfo = sortedLogFileInfos[i];
  198. __autoreleasing NSError *deletionError = nil;
  199. __auto_type success = [[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:&deletionError];
  200. if (success) {
  201. NSLogInfo(@"DDLogFileManagerDefault: Deleting file: %@", logFileInfo.fileName);
  202. } else {
  203. NSLogError(@"DDLogFileManagerDefault: Error deleting file %@", deletionError);
  204. if (error) {
  205. *error = deletionError;
  206. return NO; // If we were given an error, stop after the first failure!
  207. }
  208. }
  209. }
  210. }
  211. return YES;
  212. }
  213. - (BOOL)cleanupLogFilesWithError:(NSError *__autoreleasing _Nullable *)error {
  214. return [self deleteOldLogFilesWithError:error];
  215. }
  216. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  217. #pragma mark Log Files
  218. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  219. /**
  220. * Returns the path to the default logs directory.
  221. * If the logs directory doesn't exist, this method automatically creates it.
  222. **/
  223. - (NSString *)defaultLogsDirectory {
  224. #if TARGET_OS_IPHONE
  225. __auto_type paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  226. __auto_type baseDir = paths.firstObject;
  227. __auto_type logsDirectory = [baseDir stringByAppendingPathComponent:@"Logs"];
  228. #else
  229. __auto_type appName = [[NSProcessInfo processInfo] processName];
  230. __auto_type paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
  231. __auto_type basePath = ([paths count] > 0) ? paths[0] : NSTemporaryDirectory();
  232. __auto_type logsDirectory = [[basePath stringByAppendingPathComponent:@"Logs"] stringByAppendingPathComponent:appName];
  233. #endif
  234. return logsDirectory;
  235. }
  236. - (NSString *)logsDirectory {
  237. // We could do this check once, during initialization, and not bother again.
  238. // But this way the code continues to work if the directory gets deleted while the code is running.
  239. NSAssert(_logsDirectory.length > 0, @"Directory must be set.");
  240. __autoreleasing NSError *error = nil;
  241. __auto_type success = [[NSFileManager defaultManager] createDirectoryAtPath:_logsDirectory
  242. withIntermediateDirectories:YES
  243. attributes:nil
  244. error:&error];
  245. if (!success) {
  246. NSLogError(@"DDFileLogManagerDefault: Error creating logsDirectory: %@", error);
  247. }
  248. return _logsDirectory;
  249. }
  250. - (BOOL)isLogFile:(NSString *)fileName {
  251. __auto_type appName = [self applicationName];
  252. // We need to add a space to the name as otherwise we could match applications that have the name prefix.
  253. return [fileName hasPrefix:[appName stringByAppendingString:@" "]] && [fileName hasSuffix:@".log"];
  254. }
  255. // if you change formatter, then change sortedLogFileInfos method also accordingly
  256. - (NSDateFormatter *)logFileDateFormatter {
  257. return _fileDateFormatter;
  258. }
  259. - (NSArray *)unsortedLogFilePaths {
  260. __auto_type logsDirectory = [self logsDirectory];
  261. __autoreleasing NSError *error = nil;
  262. __auto_type fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:&error];
  263. if (!fileNames && error) {
  264. NSLogError(@"DDFileLogManagerDefault: Error listing log file directory: %@", error);
  265. return [[NSArray alloc] init];
  266. }
  267. __auto_type unsortedLogFilePaths = [NSMutableArray arrayWithCapacity:[fileNames count]];
  268. for (NSString *fileName in fileNames) {
  269. // Filter out any files that aren't log files. (Just for extra safety)
  270. #if TARGET_IPHONE_SIMULATOR
  271. // This is only used on the iPhone simulator for backward compatibility reason.
  272. //
  273. // In case of iPhone simulator there can be 'archived' extension. isLogFile:
  274. // method knows nothing about it. Thus removing it for this method.
  275. __auto_type theFileName = [fileName stringByReplacingOccurrencesOfString:@".archived"
  276. withString:@""];
  277. if ([self isLogFile:theFileName])
  278. #else
  279. if ([self isLogFile:fileName])
  280. #endif
  281. {
  282. __auto_type filePath = [logsDirectory stringByAppendingPathComponent:fileName];
  283. [unsortedLogFilePaths addObject:filePath];
  284. }
  285. }
  286. return unsortedLogFilePaths;
  287. }
  288. - (NSArray *)unsortedLogFileNames {
  289. __auto_type unsortedLogFilePaths = [self unsortedLogFilePaths];
  290. __auto_type unsortedLogFileNames = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
  291. for (NSString *filePath in unsortedLogFilePaths) {
  292. [unsortedLogFileNames addObject:[filePath lastPathComponent]];
  293. }
  294. return unsortedLogFileNames;
  295. }
  296. - (NSArray *)unsortedLogFileInfos {
  297. __auto_type unsortedLogFilePaths = [self unsortedLogFilePaths];
  298. __auto_type unsortedLogFileInfos = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
  299. for (NSString *filePath in unsortedLogFilePaths) {
  300. __auto_type logFileInfo = [[DDLogFileInfo alloc] initWithFilePath:filePath];
  301. [unsortedLogFileInfos addObject:logFileInfo];
  302. }
  303. return unsortedLogFileInfos;
  304. }
  305. - (NSArray *)sortedLogFilePaths {
  306. __auto_type sortedLogFileInfos = [self sortedLogFileInfos];
  307. __auto_type sortedLogFilePaths = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
  308. for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) {
  309. [sortedLogFilePaths addObject:[logFileInfo filePath]];
  310. }
  311. return sortedLogFilePaths;
  312. }
  313. - (NSArray *)sortedLogFileNames {
  314. __auto_type sortedLogFileInfos = [self sortedLogFileInfos];
  315. __auto_type sortedLogFileNames = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
  316. for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) {
  317. [sortedLogFileNames addObject:[logFileInfo fileName]];
  318. }
  319. return sortedLogFileNames;
  320. }
  321. - (NSArray *)sortedLogFileInfos {
  322. return [[self unsortedLogFileInfos] sortedArrayUsingComparator:^NSComparisonResult(DDLogFileInfo *obj1,
  323. DDLogFileInfo *obj2) {
  324. NSDate *date1 = [NSDate date];
  325. NSDate *date2 = [NSDate date];
  326. __auto_type arrayComponent = [[obj1 fileName] componentsSeparatedByString:@" "];
  327. if (arrayComponent.count > 0) {
  328. NSString *stringDate = arrayComponent.lastObject;
  329. stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@""];
  330. #if TARGET_IPHONE_SIMULATOR
  331. // This is only used on the iPhone simulator for backward compatibility reason.
  332. stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@""];
  333. #endif
  334. date1 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj1 creationDate];
  335. }
  336. arrayComponent = [[obj2 fileName] componentsSeparatedByString:@" "];
  337. if (arrayComponent.count > 0) {
  338. NSString *stringDate = arrayComponent.lastObject;
  339. stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@""];
  340. #if TARGET_IPHONE_SIMULATOR
  341. // This is only used on the iPhone simulator for backward compatibility reason.
  342. stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@""];
  343. #endif
  344. date2 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj2 creationDate];
  345. }
  346. return [date2 compare:date1 ?: [NSDate date]];
  347. }];
  348. }
  349. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  350. #pragma mark Creation
  351. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  352. // If you change newLogFileName, then change `isLogFile:` method also accordingly.
  353. - (NSString *)newLogFileName {
  354. __auto_type appName = [self applicationName];
  355. __auto_type dateFormatter = [self logFileDateFormatter];
  356. __auto_type formattedDate = [dateFormatter stringFromDate:[NSDate date]];
  357. return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate];
  358. }
  359. - (nullable NSString *)logFileHeader {
  360. return nil;
  361. }
  362. - (NSData *)logFileHeaderData {
  363. NSString *fileHeaderStr = [self logFileHeader];
  364. if (fileHeaderStr.length == 0) {
  365. return nil;
  366. }
  367. if (![fileHeaderStr hasSuffix:@"\n"]) {
  368. fileHeaderStr = [fileHeaderStr stringByAppendingString:@"\n"];
  369. }
  370. return [_logMessageSerializer dataForString:fileHeaderStr originatingFromMessage:nil];
  371. }
  372. - (NSString *)createNewLogFileWithError:(NSError *__autoreleasing _Nullable *)error {
  373. static NSUInteger MAX_ALLOWED_ERROR = 5;
  374. __auto_type fileName = [self newLogFileName];
  375. __auto_type logsDirectory = [self logsDirectory];
  376. __auto_type fileHeader = [self logFileHeaderData] ?: [NSData data];
  377. NSString *baseName = nil;
  378. NSString *extension;
  379. NSUInteger attempt = 1;
  380. NSUInteger criticalErrors = 0;
  381. NSError *lastCriticalError;
  382. if (error) *error = nil;
  383. do {
  384. if (criticalErrors >= MAX_ALLOWED_ERROR) {
  385. NSLogError(@"DDLogFileManagerDefault: Bailing file creation, encountered %ld errors.",
  386. (unsigned long)criticalErrors);
  387. if (error) *error = lastCriticalError;
  388. return nil;
  389. }
  390. NSString *actualFileName;
  391. if (attempt > 1) {
  392. if (baseName == nil) {
  393. baseName = [fileName stringByDeletingPathExtension];
  394. extension = [fileName pathExtension];
  395. }
  396. actualFileName = [baseName stringByAppendingFormat:@" %lu", (unsigned long)attempt];
  397. if (extension.length) {
  398. actualFileName = [actualFileName stringByAppendingPathExtension:extension];
  399. }
  400. } else {
  401. actualFileName = fileName;
  402. }
  403. __auto_type filePath = [logsDirectory stringByAppendingPathComponent:actualFileName];
  404. __autoreleasing NSError *currentError = nil;
  405. __auto_type success = [fileHeader writeToFile:filePath options:NSDataWritingAtomic error:&currentError];
  406. #if TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST
  407. if (success) {
  408. // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
  409. //
  410. // But in case if app is able to launch from background we need to have an ability to open log file any time we
  411. // want (even if device is locked). Thats why that attribute have to be changed to
  412. // NSFileProtectionCompleteUntilFirstUserAuthentication.
  413. NSDictionary *attributes = @{NSFileProtectionKey: [self logFileProtection]};
  414. success = [[NSFileManager defaultManager] setAttributes:attributes
  415. ofItemAtPath:filePath
  416. error:&currentError];
  417. }
  418. #endif
  419. if (success) {
  420. NSLogVerbose(@"DDLogFileManagerDefault: Created new log file: %@", actualFileName);
  421. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  422. // Since we just created a new log file, we may need to delete some old log files
  423. // Note that we don't on errors here! The new log file was created, so this method technically succeeded!
  424. [self deleteOldLogFilesWithError:nil];
  425. });
  426. return filePath;
  427. } else if (currentError.code == NSFileWriteFileExistsError) {
  428. attempt++;
  429. } else {
  430. NSLogError(@"DDLogFileManagerDefault: Critical error while creating log file: %@", currentError);
  431. criticalErrors++;
  432. lastCriticalError = currentError;
  433. }
  434. } while (YES);
  435. }
  436. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  437. #pragma mark Utility
  438. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  439. - (NSString *)applicationName {
  440. static NSString *_appName;
  441. static dispatch_once_t onceToken;
  442. dispatch_once(&onceToken, ^{
  443. _appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"];
  444. if (_appName.length == 0) {
  445. _appName = [[NSProcessInfo processInfo] processName];
  446. }
  447. if (_appName.length == 0) {
  448. _appName = @"";
  449. }
  450. });
  451. return _appName;
  452. }
  453. @end
  454. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  455. #pragma mark -
  456. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  457. @interface DDLogFileFormatterDefault () {
  458. NSDateFormatter *_dateFormatter;
  459. }
  460. @end
  461. @implementation DDLogFileFormatterDefault
  462. - (instancetype)init {
  463. return [self initWithDateFormatter:nil];
  464. }
  465. - (instancetype)initWithDateFormatter:(nullable NSDateFormatter *)aDateFormatter {
  466. if ((self = [super init])) {
  467. if (aDateFormatter) {
  468. _dateFormatter = aDateFormatter;
  469. } else {
  470. _dateFormatter = [[NSDateFormatter alloc] init];
  471. [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; // 10.4+ style
  472. [_dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
  473. [_dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
  474. [_dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"];
  475. }
  476. }
  477. return self;
  478. }
  479. - (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
  480. __auto_type dateAndTime = [_dateFormatter stringFromDate:logMessage->_timestamp];
  481. // Note: There are two spaces between the date and the message.
  482. return [NSString stringWithFormat:@"%@ %@", dateAndTime, logMessage->_message];
  483. }
  484. @end
  485. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  486. #pragma mark -
  487. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  488. @interface DDFileLogger () {
  489. id <DDLogFileManager> _logFileManager;
  490. DDLogFileInfo *_currentLogFileInfo;
  491. NSFileHandle *_currentLogFileHandle;
  492. dispatch_source_t _currentLogFileVnode;
  493. NSTimeInterval _rollingFrequency;
  494. dispatch_source_t _rollingTimer;
  495. unsigned long long _maximumFileSize;
  496. dispatch_queue_t _completionQueue;
  497. }
  498. @end
  499. #pragma clang diagnostic push
  500. #pragma clang diagnostic ignored "-Wincomplete-implementation"
  501. @implementation DDFileLogger
  502. #pragma clang diagnostic pop
  503. - (instancetype)init {
  504. return [self initWithLogFileManager:[[DDLogFileManagerDefault alloc] init]
  505. completionQueue:nil];
  506. }
  507. - (instancetype)initWithLogFileManager:(id<DDLogFileManager>)logFileManager {
  508. return [self initWithLogFileManager:logFileManager completionQueue:nil];
  509. }
  510. - (instancetype)initWithLogFileManager:(id <DDLogFileManager>)aLogFileManager
  511. completionQueue:(nullable dispatch_queue_t)dispatchQueue {
  512. if ((self = [super init])) {
  513. _completionQueue = dispatchQueue ?: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  514. _maximumFileSize = kDDDefaultLogMaxFileSize;
  515. _rollingFrequency = kDDDefaultLogRollingFrequency;
  516. _automaticallyAppendNewlineForCustomFormatters = YES;
  517. _logFileManager = aLogFileManager;
  518. _logFormatter = [DDLogFileFormatterDefault new];
  519. if ([_logFileManager respondsToSelector:@selector(didAddToFileLogger:)]) {
  520. [_logFileManager didAddToFileLogger:self];
  521. }
  522. }
  523. return self;
  524. }
  525. - (void)lt_cleanup {
  526. DDAbstractLoggerAssertOnInternalLoggerQueue();
  527. if (_currentLogFileHandle != nil) {
  528. if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) {
  529. __autoreleasing NSError *error = nil;
  530. __auto_type success = [_currentLogFileHandle synchronizeAndReturnError:&error];
  531. if (!success) {
  532. NSLogError(@"DDFileLogger: Failed to synchronize file: %@", error);
  533. }
  534. success = [_currentLogFileHandle closeAndReturnError:&error];
  535. if (!success) {
  536. NSLogError(@"DDFileLogger: Failed to close file: %@", error);
  537. }
  538. } else {
  539. @try {
  540. [_currentLogFileHandle synchronizeFile];
  541. }
  542. @catch (NSException *exception) {
  543. NSLogError(@"DDFileLogger: Failed to synchronize file: %@", exception);
  544. }
  545. [_currentLogFileHandle closeFile];
  546. }
  547. _currentLogFileHandle = nil;
  548. }
  549. if (_currentLogFileVnode) {
  550. dispatch_source_cancel(_currentLogFileVnode);
  551. _currentLogFileVnode = NULL;
  552. }
  553. if (_rollingTimer) {
  554. dispatch_source_cancel(_rollingTimer);
  555. _rollingTimer = NULL;
  556. }
  557. }
  558. - (void)dealloc {
  559. if (self.isOnInternalLoggerQueue) {
  560. [self lt_cleanup];
  561. } else {
  562. dispatch_sync(self.loggerQueue, ^{
  563. [self lt_cleanup];
  564. });
  565. }
  566. }
  567. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  568. #pragma mark Properties
  569. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  570. - (unsigned long long)maximumFileSize {
  571. __block unsigned long long result;
  572. __auto_type block = ^{
  573. result = self->_maximumFileSize;
  574. };
  575. // The design of this method is taken from the DDAbstractLogger implementation.
  576. // For extensive documentation please refer to the DDAbstractLogger implementation.
  577. // Note: The internal implementation MUST access the maximumFileSize variable directly,
  578. // This method is designed explicitly for external access.
  579. //
  580. // Using "self." syntax to go through this method will cause immediate deadlock.
  581. // This is the intended result. Fix it by accessing the ivar directly.
  582. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  583. DDAbstractLoggerAssertLockedPropertyAccess();
  584. dispatch_sync(DDLog.loggingQueue, ^{
  585. dispatch_sync(self.loggerQueue, block);
  586. });
  587. return result;
  588. }
  589. - (void)setMaximumFileSize:(unsigned long long)newMaximumFileSize {
  590. __auto_type block = ^{
  591. @autoreleasepool {
  592. self->_maximumFileSize = newMaximumFileSize;
  593. if (self->_currentLogFileHandle != nil) {
  594. [self lt_maybeRollLogFileDueToSize];
  595. }
  596. }
  597. };
  598. // The design of this method is taken from the DDAbstractLogger implementation.
  599. // For extensive documentation please refer to the DDAbstractLogger implementation.
  600. // Note: The internal implementation MUST access the maximumFileSize variable directly,
  601. // This method is designed explicitly for external access.
  602. //
  603. // Using "self." syntax to go through this method will cause immediate deadlock.
  604. // This is the intended result. Fix it by accessing the ivar directly.
  605. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  606. DDAbstractLoggerAssertLockedPropertyAccess();
  607. dispatch_async(DDLog.loggingQueue, ^{
  608. dispatch_async(self.loggerQueue, block);
  609. });
  610. }
  611. - (NSTimeInterval)rollingFrequency {
  612. __block NSTimeInterval result;
  613. __auto_type block = ^{
  614. result = self->_rollingFrequency;
  615. };
  616. // The design of this method is taken from the DDAbstractLogger implementation.
  617. // For extensive documentation please refer to the DDAbstractLogger implementation.
  618. // Note: The internal implementation should access the rollingFrequency variable directly,
  619. // This method is designed explicitly for external access.
  620. //
  621. // Using "self." syntax to go through this method will cause immediate deadlock.
  622. // This is the intended result. Fix it by accessing the ivar directly.
  623. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  624. DDAbstractLoggerAssertLockedPropertyAccess();
  625. dispatch_sync(DDLog.loggingQueue, ^{
  626. dispatch_sync(self.loggerQueue, block);
  627. });
  628. return result;
  629. }
  630. - (void)setRollingFrequency:(NSTimeInterval)newRollingFrequency {
  631. __auto_type block = ^{
  632. @autoreleasepool {
  633. self->_rollingFrequency = newRollingFrequency;
  634. if (self->_currentLogFileHandle != nil) {
  635. [self lt_maybeRollLogFileDueToAge];
  636. }
  637. }
  638. };
  639. // The design of this method is taken from the DDAbstractLogger implementation.
  640. // For extensive documentation please refer to the DDAbstractLogger implementation.
  641. // Note: The internal implementation should access the rollingFrequency variable directly,
  642. // This method is designed explicitly for external access.
  643. //
  644. // Using "self." syntax to go through this method will cause immediate deadlock.
  645. // This is the intended result. Fix it by accessing the ivar directly.
  646. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  647. DDAbstractLoggerAssertLockedPropertyAccess();
  648. dispatch_async(DDLog.loggingQueue, ^{
  649. dispatch_async(self.loggerQueue, block);
  650. });
  651. }
  652. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  653. #pragma mark File Rolling
  654. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  655. - (void)lt_scheduleTimerToRollLogFileDueToAge {
  656. DDAbstractLoggerAssertOnInternalLoggerQueue();
  657. if (_rollingTimer) {
  658. dispatch_source_cancel(_rollingTimer);
  659. _rollingTimer = NULL;
  660. }
  661. if (_currentLogFileInfo == nil || _rollingFrequency <= 0.0) {
  662. return;
  663. }
  664. __auto_type logFileCreationDate = [_currentLogFileInfo creationDate];
  665. __auto_type frequency = MIN(_rollingFrequency, DBL_MAX - [logFileCreationDate timeIntervalSinceReferenceDate]);
  666. __auto_type logFileRollingDate = [logFileCreationDate dateByAddingTimeInterval:frequency];
  667. NSLogVerbose(@"DDFileLogger: scheduleTimerToRollLogFileDueToAge");
  668. NSLogVerbose(@"DDFileLogger: logFileCreationDate : %@", logFileCreationDate);
  669. NSLogVerbose(@"DDFileLogger: actual rollingFrequency: %f", frequency);
  670. NSLogVerbose(@"DDFileLogger: logFileRollingDate : %@", logFileRollingDate);
  671. _rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _loggerQueue);
  672. __weak __auto_type weakSelf = self;
  673. dispatch_source_set_event_handler(_rollingTimer, ^{ @autoreleasepool {
  674. [weakSelf lt_maybeRollLogFileDueToAge];
  675. } });
  676. #if !OS_OBJECT_USE_OBJC
  677. dispatch_source_t theRollingTimer = _rollingTimer;
  678. dispatch_source_set_cancel_handler(_rollingTimer, ^{
  679. dispatch_release(theRollingTimer);
  680. });
  681. #endif
  682. static NSTimeInterval const kDDMaxTimerDelay = LLONG_MAX / NSEC_PER_SEC;
  683. __auto_type delay = (int64_t)(MIN([logFileRollingDate timeIntervalSinceNow], kDDMaxTimerDelay) * (NSTimeInterval)NSEC_PER_SEC);
  684. __auto_type fireTime = dispatch_walltime(NULL, delay); // `NULL` uses `gettimeofday` internally
  685. dispatch_source_set_timer(_rollingTimer, fireTime, DISPATCH_TIME_FOREVER, (uint64_t)kDDRollingLeeway * NSEC_PER_SEC);
  686. dispatch_activate(_rollingTimer);
  687. }
  688. - (void)rollLogFile {
  689. [self rollLogFileWithCompletionBlock:nil];
  690. }
  691. - (void)rollLogFileWithCompletionBlock:(nullable void (^)(void))completionBlock {
  692. // This method is public.
  693. // We need to execute the rolling on our logging thread/queue.
  694. __auto_type block = ^{
  695. @autoreleasepool {
  696. [self lt_rollLogFileNow];
  697. if (completionBlock) {
  698. dispatch_async(self->_completionQueue, ^{
  699. completionBlock();
  700. });
  701. }
  702. }
  703. };
  704. // The design of this method is taken from the DDAbstractLogger implementation.
  705. // For extensive documentation please refer to the DDAbstractLogger implementation.
  706. if ([self isOnInternalLoggerQueue]) {
  707. block();
  708. } else {
  709. DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
  710. dispatch_async(DDLog.loggingQueue, ^{
  711. dispatch_async(self.loggerQueue, block);
  712. });
  713. }
  714. }
  715. - (void)lt_rollLogFileNow {
  716. DDAbstractLoggerAssertOnInternalLoggerQueue();
  717. NSLogVerbose(@"DDFileLogger: %@", NSStringFromSelector(_cmd));
  718. if (_currentLogFileHandle == nil) {
  719. return;
  720. }
  721. if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) {
  722. __autoreleasing NSError *error = nil;
  723. __auto_type success = [_currentLogFileHandle synchronizeAndReturnError:&error];
  724. if (!success) {
  725. NSLogError(@"DDFileLogger: Failed to synchronize file: %@", error);
  726. }
  727. success = [_currentLogFileHandle closeAndReturnError:&error];
  728. if (!success) {
  729. NSLogError(@"DDFileLogger: Failed to close file: %@", error);
  730. }
  731. } else {
  732. @try {
  733. [_currentLogFileHandle synchronizeFile];
  734. }
  735. @catch (NSException *exception) {
  736. NSLogError(@"DDFileLogger: Failed to synchronize file: %@", exception);
  737. }
  738. [_currentLogFileHandle closeFile];
  739. }
  740. _currentLogFileHandle = nil;
  741. _currentLogFileInfo.isArchived = YES;
  742. const __auto_type logFileManagerRespondsToNewArchiveSelector = [_logFileManager respondsToSelector:@selector(didArchiveLogFile:wasRolled:)];
  743. const __auto_type logFileManagerRespondsToSelector = (logFileManagerRespondsToNewArchiveSelector
  744. || [_logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]);
  745. NSString *archivedFilePath = (logFileManagerRespondsToSelector) ? [_currentLogFileInfo.filePath copy] : nil;
  746. _currentLogFileInfo = nil;
  747. if (logFileManagerRespondsToSelector) {
  748. dispatch_block_t block;
  749. if (logFileManagerRespondsToNewArchiveSelector) {
  750. block = ^{
  751. [self->_logFileManager didArchiveLogFile:archivedFilePath wasRolled:YES];
  752. };
  753. } else {
  754. block = ^{
  755. #pragma clang diagnostic push
  756. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  757. [self->_logFileManager didRollAndArchiveLogFile:archivedFilePath];
  758. #pragma clang diagnostic pop
  759. };
  760. }
  761. dispatch_async(_completionQueue, block);
  762. }
  763. if (_currentLogFileVnode) {
  764. dispatch_source_cancel(_currentLogFileVnode);
  765. _currentLogFileVnode = nil;
  766. }
  767. if (_rollingTimer) {
  768. dispatch_source_cancel(_rollingTimer);
  769. _rollingTimer = nil;
  770. }
  771. }
  772. - (void)lt_maybeRollLogFileDueToAge {
  773. DDAbstractLoggerAssertOnInternalLoggerQueue();
  774. if (_rollingFrequency > 0.0 && (_currentLogFileInfo.age + kDDRollingLeeway) >= _rollingFrequency) {
  775. NSLogVerbose(@"DDFileLogger: Rolling log file due to age...");
  776. [self lt_rollLogFileNow];
  777. } else {
  778. [self lt_scheduleTimerToRollLogFileDueToAge];
  779. }
  780. }
  781. - (void)lt_maybeRollLogFileDueToSize {
  782. DDAbstractLoggerAssertOnInternalLoggerQueue();
  783. // This method is called from logMessage.
  784. // Keep it FAST.
  785. // Note: Use direct access to maximumFileSize variable.
  786. // We specifically wrote our own getter/setter method to allow us to do this (for performance reasons).
  787. if (_currentLogFileHandle != nil && _maximumFileSize > 0) {
  788. unsigned long long fileSize;
  789. if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) {
  790. __autoreleasing NSError *error = nil;
  791. __auto_type success = [_currentLogFileHandle getOffset:&fileSize error:&error];
  792. if (!success) {
  793. NSLogError(@"DDFileLogger: Failed to get offset: %@", error);
  794. return;
  795. }
  796. } else {
  797. fileSize = [_currentLogFileHandle offsetInFile];
  798. }
  799. if (fileSize >= _maximumFileSize) {
  800. NSLogVerbose(@"DDFileLogger: Rolling log file due to size (%qu)...", fileSize);
  801. [self lt_rollLogFileNow];
  802. }
  803. }
  804. }
  805. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  806. #pragma mark File Logging
  807. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  808. - (BOOL)lt_shouldLogFileBeArchived:(DDLogFileInfo *)mostRecentLogFileInfo {
  809. DDAbstractLoggerAssertOnInternalLoggerQueue();
  810. if ([self shouldArchiveRecentLogFileInfo:mostRecentLogFileInfo]) {
  811. return YES;
  812. } else if (_maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= _maximumFileSize) {
  813. return YES;
  814. } else if (_rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= _rollingFrequency) {
  815. return YES;
  816. }
  817. #if TARGET_OS_IPHONE
  818. // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
  819. //
  820. // But in case if app is able to launch from background we need to have an ability to open log file any time we
  821. // want (even if device is locked). Thats why that attribute have to be changed to
  822. // NSFileProtectionCompleteUntilFirstUserAuthentication.
  823. //
  824. // If previous log was created when app wasn't running in background, but now it is - we archive it and create
  825. // a new one.
  826. //
  827. // If user has overwritten to NSFileProtectionNone there is no need to create a new one.
  828. if (doesAppRunInBackground()) {
  829. NSFileProtectionType key = mostRecentLogFileInfo.fileAttributes[NSFileProtectionKey];
  830. __auto_type isUntilFirstAuth = [key isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication];
  831. __auto_type isNone = [key isEqualToString:NSFileProtectionNone];
  832. if (key != nil && !isUntilFirstAuth && !isNone) {
  833. return YES;
  834. }
  835. }
  836. #endif
  837. return NO;
  838. }
  839. /**
  840. * Returns the log file that should be used.
  841. * If there is an existing log file that is suitable, within the
  842. * constraints of maximumFileSize and rollingFrequency, then it is returned.
  843. *
  844. * Otherwise a new file is created and returned.
  845. **/
  846. - (DDLogFileInfo *)currentLogFileInfo {
  847. // The design of this method is taken from the DDAbstractLogger implementation.
  848. // For extensive documentation please refer to the DDAbstractLogger implementation.
  849. // Do not access this method on any Lumberjack queue, will deadlock.
  850. DDAbstractLoggerAssertLockedPropertyAccess();
  851. __block DDLogFileInfo *info = nil;
  852. __auto_type block = ^{
  853. info = [self lt_currentLogFileInfo];
  854. };
  855. dispatch_sync(DDLog.loggingQueue, ^{
  856. dispatch_sync(self->_loggerQueue, block);
  857. });
  858. return info;
  859. }
  860. - (DDLogFileInfo *)lt_currentLogFileInfo {
  861. DDAbstractLoggerAssertOnInternalLoggerQueue();
  862. // Get the current log file info ivar (might be nil).
  863. __auto_type newCurrentLogFile = _currentLogFileInfo;
  864. // Check if we're resuming and if so, get the first of the sorted log file infos.
  865. __auto_type isResuming = newCurrentLogFile == nil;
  866. if (isResuming) {
  867. NSArray *sortedLogFileInfos = [_logFileManager sortedLogFileInfos];
  868. newCurrentLogFile = sortedLogFileInfos.firstObject;
  869. }
  870. // Check if the file we've found is still valid. Otherwise create a new one.
  871. if (newCurrentLogFile != nil && [self lt_shouldUseLogFile:newCurrentLogFile isResuming:isResuming]) {
  872. if (isResuming) {
  873. NSLogVerbose(@"DDFileLogger: Resuming logging with file %@", newCurrentLogFile.fileName);
  874. }
  875. _currentLogFileInfo = newCurrentLogFile;
  876. } else {
  877. NSString *currentLogFilePath;
  878. if ([_logFileManager respondsToSelector:@selector(createNewLogFileWithError:)]) {
  879. __autoreleasing NSError *error; // Don't initialize error to nil since it will be done in -createNewLogFileWithError:
  880. currentLogFilePath = [_logFileManager createNewLogFileWithError:&error];
  881. if (!currentLogFilePath) {
  882. NSLogError(@"DDFileLogger: Failed to create new log file: %@", error);
  883. }
  884. } else {
  885. #pragma clang diagnostic push
  886. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  887. NSAssert([_logFileManager respondsToSelector:@selector(createNewLogFile)],
  888. @"Invalid log file manager! Responds neither to `-createNewLogFileWithError:` nor `-createNewLogFile`!");
  889. currentLogFilePath = [_logFileManager createNewLogFile];
  890. #pragma clang diagnostic pop
  891. if (!currentLogFilePath) {
  892. NSLogError(@"DDFileLogger: Failed to create new log file");
  893. }
  894. }
  895. // Use static factory method here, since it checks for nil (and is unavailable to Swift).
  896. _currentLogFileInfo = [DDLogFileInfo logFileWithPath:currentLogFilePath];
  897. }
  898. return _currentLogFileInfo;
  899. }
  900. - (BOOL)lt_shouldUseLogFile:(nonnull DDLogFileInfo *)logFileInfo isResuming:(BOOL)isResuming {
  901. NSParameterAssert(logFileInfo);
  902. DDAbstractLoggerAssertOnInternalLoggerQueue();
  903. // Check if the log file is archived. We must not use archived log files.
  904. if (logFileInfo.isArchived) {
  905. return NO;
  906. }
  907. // Don't follow symlink
  908. if (logFileInfo.isSymlink) {
  909. return NO;
  910. }
  911. // If we're resuming, we need to check if the log file is allowed for reuse or needs to be archived.
  912. if (isResuming && (_doNotReuseLogFiles || [self lt_shouldLogFileBeArchived:logFileInfo])) {
  913. logFileInfo.isArchived = YES;
  914. const __auto_type logFileManagerRespondsToNewArchiveSelector = [_logFileManager respondsToSelector:@selector(didArchiveLogFile:wasRolled:)];
  915. if (logFileManagerRespondsToNewArchiveSelector || [_logFileManager respondsToSelector:@selector(didArchiveLogFile:)]) {
  916. NSString *archivedFilePath = [logFileInfo.filePath copy];
  917. dispatch_block_t block;
  918. if (logFileManagerRespondsToNewArchiveSelector) {
  919. block = ^{
  920. [self->_logFileManager didArchiveLogFile:archivedFilePath wasRolled:NO];
  921. };
  922. } else {
  923. block = ^{
  924. #pragma clang diagnostic push
  925. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  926. [self->_logFileManager didArchiveLogFile:archivedFilePath];
  927. #pragma clang diagnostic pop
  928. };
  929. }
  930. dispatch_async(_completionQueue, block);
  931. }
  932. return NO;
  933. }
  934. // All checks have passed. It's valid.
  935. return YES;
  936. }
  937. - (void)lt_monitorCurrentLogFileForExternalChanges {
  938. DDAbstractLoggerAssertOnInternalLoggerQueue();
  939. NSAssert(_currentLogFileHandle, @"Can not monitor without handle.");
  940. // This seems to work around crashes when an active source is replaced / released.
  941. // See https://github.com/CocoaLumberjack/CocoaLumberjack/issues/1341
  942. // And https://stackoverflow.com/questions/36296528/what-does-this-dispatch-xref-dispose-error-mean
  943. if (_currentLogFileVnode) {
  944. dispatch_source_cancel(_currentLogFileVnode);
  945. }
  946. _currentLogFileVnode = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE,
  947. (uintptr_t)[_currentLogFileHandle fileDescriptor],
  948. DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE,
  949. _loggerQueue);
  950. __weak __auto_type weakSelf = self;
  951. dispatch_source_set_event_handler(_currentLogFileVnode, ^{ @autoreleasepool {
  952. NSLogInfo(@"DDFileLogger: Current logfile was moved. Rolling it and creating a new one");
  953. [weakSelf lt_rollLogFileNow];
  954. } });
  955. #if !OS_OBJECT_USE_OBJC
  956. dispatch_source_t vnode = _currentLogFileVnode;
  957. dispatch_source_set_cancel_handler(_currentLogFileVnode, ^{
  958. dispatch_release(vnode);
  959. });
  960. #endif
  961. dispatch_activate(_currentLogFileVnode);
  962. }
  963. - (NSFileHandle *)lt_currentLogFileHandle {
  964. DDAbstractLoggerAssertOnInternalLoggerQueue();
  965. if (_currentLogFileHandle == nil) {
  966. __auto_type logFilePath = [[self lt_currentLogFileInfo] filePath];
  967. _currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
  968. if (_currentLogFileHandle != nil) {
  969. if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) {
  970. __autoreleasing NSError *error = nil;
  971. __auto_type success = [_currentLogFileHandle seekToEndReturningOffset:nil error:&error];
  972. if (!success) {
  973. NSLogError(@"DDFileLogger: Failed to seek to end of file: %@", error);
  974. }
  975. } else {
  976. [_currentLogFileHandle seekToEndOfFile];
  977. }
  978. [self lt_scheduleTimerToRollLogFileDueToAge];
  979. [self lt_monitorCurrentLogFileForExternalChanges];
  980. } else {
  981. NSLogWarn(@"NSFileHandle returned nil for writing to log file at path: %@", logFilePath);
  982. }
  983. }
  984. return _currentLogFileHandle;
  985. }
  986. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  987. #pragma mark DDLogger Protocol
  988. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  989. static int exception_count = 0;
  990. - (void)logMessage:(DDLogMessage *)logMessage {
  991. // Don't need to check for isOnInternalLoggerQueue, -lt_dataForMessage: will do it for us.
  992. NSData *data = [self lt_dataForMessage:logMessage];
  993. if (data.length == 0) {
  994. return;
  995. }
  996. [self lt_logData:data];
  997. }
  998. - (void)willLogMessage:(DDLogFileInfo *)logFileInfo {}
  999. - (void)didLogMessage:(DDLogFileInfo *)logFileInfo {
  1000. [self lt_maybeRollLogFileDueToSize];
  1001. }
  1002. - (BOOL)shouldArchiveRecentLogFileInfo:(__unused DDLogFileInfo *)recentLogFileInfo {
  1003. return NO;
  1004. }
  1005. - (void)willRemoveLogger {
  1006. [self lt_rollLogFileNow];
  1007. }
  1008. - (void)flush {
  1009. // This method is public.
  1010. // We need to execute the rolling on our logging thread/queue.
  1011. dispatch_block_t block = ^{
  1012. @autoreleasepool {
  1013. [self lt_flush];
  1014. }
  1015. };
  1016. // The design of this method is taken from the DDAbstractLogger implementation.
  1017. // For extensive documentation please refer to the DDAbstractLogger implementation.
  1018. if ([self isOnInternalLoggerQueue]) {
  1019. block();
  1020. } else {
  1021. DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
  1022. dispatch_sync(DDLog.loggingQueue, ^{
  1023. dispatch_sync(self.loggerQueue, block);
  1024. });
  1025. }
  1026. }
  1027. - (void)lt_flush {
  1028. DDAbstractLoggerAssertOnInternalLoggerQueue();
  1029. if (_currentLogFileHandle != nil) {
  1030. if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) {
  1031. __autoreleasing NSError *error = nil;
  1032. __auto_type success = [_currentLogFileHandle synchronizeAndReturnError:&error];
  1033. if (!success) {
  1034. NSLogError(@"DDFileLogger: Failed to synchronize file: %@", error);
  1035. }
  1036. } else {
  1037. @try {
  1038. [_currentLogFileHandle synchronizeFile];
  1039. } @catch (NSException *exception) {
  1040. NSLogError(@"DDFileLogger: Failed to synchronize file: %@", exception);
  1041. }
  1042. }
  1043. }
  1044. }
  1045. - (DDLoggerName)loggerName {
  1046. return DDLoggerNameFile;
  1047. }
  1048. @end
  1049. @implementation DDFileLogger (Internal)
  1050. - (void)logData:(NSData *)data {
  1051. // This method is public.
  1052. // We need to execute the rolling on our logging thread/queue.
  1053. __auto_type block = ^{
  1054. @autoreleasepool {
  1055. [self lt_logData:data];
  1056. }
  1057. };
  1058. // The design of this method is taken from the DDAbstractLogger implementation.
  1059. // For extensive documentation please refer to the DDAbstractLogger implementation.
  1060. if ([self isOnInternalLoggerQueue]) {
  1061. block();
  1062. } else {
  1063. DDAbstractLoggerAssertNotOnGlobalLoggingQueue();
  1064. dispatch_sync(DDLog.loggingQueue, ^{
  1065. dispatch_sync(self.loggerQueue, block);
  1066. });
  1067. }
  1068. }
  1069. - (void)lt_deprecationCatchAll {}
  1070. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  1071. if (aSelector == @selector(willLogMessage) || aSelector == @selector(didLogMessage)) {
  1072. // Ignore calls to deprecated methods.
  1073. return [self methodSignatureForSelector:@selector(lt_deprecationCatchAll)];
  1074. }
  1075. return [super methodSignatureForSelector:aSelector];
  1076. }
  1077. - (void)forwardInvocation:(NSInvocation *)anInvocation {
  1078. if (anInvocation.selector != @selector(lt_deprecationCatchAll)) {
  1079. [super forwardInvocation:anInvocation];
  1080. }
  1081. }
  1082. - (void)lt_logData:(NSData *)data {
  1083. static __auto_type implementsDeprecatedWillLog = NO;
  1084. static __auto_type implementsDeprecatedDidLog = NO;
  1085. static dispatch_once_t onceToken;
  1086. dispatch_once(&onceToken, ^{
  1087. implementsDeprecatedWillLog = [self respondsToSelector:@selector(willLogMessage)];
  1088. implementsDeprecatedDidLog = [self respondsToSelector:@selector(didLogMessage)];
  1089. });
  1090. DDAbstractLoggerAssertOnInternalLoggerQueue();
  1091. if (data.length == 0) {
  1092. return;
  1093. }
  1094. @try {
  1095. // Make sure that _currentLogFileInfo is initialised before being used.
  1096. __auto_type handle = [self lt_currentLogFileHandle];
  1097. if (implementsDeprecatedWillLog) {
  1098. #pragma clang diagnostic push
  1099. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  1100. [self willLogMessage];
  1101. #pragma clang diagnostic pop
  1102. } else {
  1103. [self willLogMessage:_currentLogFileInfo];
  1104. }
  1105. // use an advisory lock to coordinate write with other processes
  1106. __auto_type fd = [handle fileDescriptor];
  1107. while(flock(fd, LOCK_EX) != 0) {
  1108. NSLogError(@"DDFileLogger: Could not lock logfile, retrying in 1ms: %s (%d)", strerror(errno), errno);
  1109. usleep(1000);
  1110. }
  1111. @try {
  1112. if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)) {
  1113. __autoreleasing NSError *error = nil;
  1114. __auto_type success = [handle seekToEndReturningOffset:nil error:&error];
  1115. if (!success) {
  1116. NSLogError(@"DDFileLogger: Failed to seek to end of file: %@", error);
  1117. }
  1118. success = [handle writeData:data error:&error];
  1119. if (!success) {
  1120. NSLogError(@"DDFileLogger: Failed to write data: %@", error);
  1121. }
  1122. } else {
  1123. [handle seekToEndOfFile];
  1124. [handle writeData:data];
  1125. }
  1126. }
  1127. @finally {
  1128. flock(fd, LOCK_UN);
  1129. }
  1130. if (implementsDeprecatedDidLog) {
  1131. #pragma clang diagnostic push
  1132. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  1133. [self didLogMessage];
  1134. #pragma clang diagnostic pop
  1135. } else {
  1136. [self didLogMessage:_currentLogFileInfo];
  1137. }
  1138. }
  1139. @catch (NSException *exception) {
  1140. exception_count++;
  1141. if (exception_count <= 10) {
  1142. NSLogError(@"DDFileLogger.logMessage: %@", exception);
  1143. if (exception_count == 10) {
  1144. NSLogError(@"DDFileLogger.logMessage: Too many exceptions -- will not log any more of them.");
  1145. }
  1146. }
  1147. }
  1148. }
  1149. - (id <DDFileLogMessageSerializer>)lt_logFileSerializer {
  1150. if ([_logFileManager respondsToSelector:@selector(logMessageSerializer)]) {
  1151. return _logFileManager.logMessageSerializer;
  1152. } else {
  1153. return [[DDFileLogPlainTextMessageSerializer alloc] init];
  1154. }
  1155. }
  1156. - (NSData *)lt_dataForMessage:(DDLogMessage *)logMessage {
  1157. DDAbstractLoggerAssertOnInternalLoggerQueue();
  1158. __auto_type messageString = logMessage->_message;
  1159. __auto_type isFormatted = NO;
  1160. if (_logFormatter != nil) {
  1161. messageString = [_logFormatter formatLogMessage:logMessage];
  1162. isFormatted = messageString != logMessage->_message;
  1163. }
  1164. if (messageString.length == 0) {
  1165. return nil;
  1166. }
  1167. __auto_type shouldFormat = !isFormatted || _automaticallyAppendNewlineForCustomFormatters;
  1168. if (shouldFormat && ![messageString hasSuffix:@"\n"]) {
  1169. messageString = [messageString stringByAppendingString:@"\n"];
  1170. }
  1171. return [[self lt_logFileSerializer] dataForString:messageString originatingFromMessage:logMessage];
  1172. }
  1173. @end
  1174. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1175. #pragma mark -
  1176. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1177. static NSString * const kDDXAttrArchivedName = @"lumberjack.log.archived";
  1178. @interface DDLogFileInfo () {
  1179. __strong NSString *_filePath;
  1180. __strong NSString *_fileName;
  1181. __strong NSDictionary *_fileAttributes;
  1182. __strong NSDate *_creationDate;
  1183. __strong NSDate *_modificationDate;
  1184. unsigned long long _fileSize;
  1185. }
  1186. #if TARGET_IPHONE_SIMULATOR
  1187. // Old implementation of extended attributes on the simulator.
  1188. - (BOOL)_hasExtensionAttributeWithName:(NSString *)attrName;
  1189. - (void)_removeExtensionAttributeWithName:(NSString *)attrName;
  1190. #endif
  1191. @end
  1192. @implementation DDLogFileInfo
  1193. @synthesize filePath;
  1194. @dynamic fileName;
  1195. @dynamic fileAttributes;
  1196. @dynamic creationDate;
  1197. @dynamic modificationDate;
  1198. @dynamic fileSize;
  1199. @dynamic age;
  1200. @dynamic isArchived;
  1201. #pragma mark Lifecycle
  1202. + (instancetype)logFileWithPath:(NSString *)aFilePath {
  1203. if (!aFilePath) return nil;
  1204. return [[self alloc] initWithFilePath:aFilePath];
  1205. }
  1206. - (instancetype)initWithFilePath:(NSString *)aFilePath {
  1207. NSParameterAssert(aFilePath);
  1208. if ((self = [super init])) {
  1209. filePath = [aFilePath copy];
  1210. }
  1211. return self;
  1212. }
  1213. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1214. #pragma mark Standard Info
  1215. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1216. - (NSDictionary *)fileAttributes {
  1217. if (_fileAttributes == nil && filePath != nil) {
  1218. __autoreleasing NSError *error = nil;
  1219. _fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
  1220. if (!_fileAttributes) {
  1221. NSLogError(@"DDLogFileInfo: Failed to read file attributes: %@", error);
  1222. }
  1223. }
  1224. return _fileAttributes ?: @{};
  1225. }
  1226. - (NSString *)fileName {
  1227. if (_fileName == nil) {
  1228. _fileName = [filePath lastPathComponent];
  1229. }
  1230. return _fileName;
  1231. }
  1232. - (NSDate *)modificationDate {
  1233. if (_modificationDate == nil) {
  1234. _modificationDate = self.fileAttributes[NSFileModificationDate];
  1235. }
  1236. return _modificationDate;
  1237. }
  1238. - (NSDate *)creationDate {
  1239. if (_creationDate == nil) {
  1240. _creationDate = self.fileAttributes[NSFileCreationDate];
  1241. }
  1242. return _creationDate;
  1243. }
  1244. - (unsigned long long)fileSize {
  1245. if (_fileSize == 0) {
  1246. _fileSize = [self.fileAttributes[NSFileSize] unsignedLongLongValue];
  1247. }
  1248. return _fileSize;
  1249. }
  1250. - (NSTimeInterval)age {
  1251. return -[[self creationDate] timeIntervalSinceNow];
  1252. }
  1253. - (BOOL)isSymlink {
  1254. return self.fileAttributes[NSFileType] == NSFileTypeSymbolicLink;
  1255. }
  1256. - (NSString *)description {
  1257. return [@{ @"filePath": self.filePath ? : @"",
  1258. @"fileName": self.fileName ? : @"",
  1259. @"fileAttributes": self.fileAttributes ? : @"",
  1260. @"creationDate": self.creationDate ? : @"",
  1261. @"modificationDate": self.modificationDate ? : @"",
  1262. @"fileSize": @(self.fileSize),
  1263. @"age": @(self.age),
  1264. @"isArchived": @(self.isArchived) } description];
  1265. }
  1266. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1267. #pragma mark Archiving
  1268. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1269. - (BOOL)isArchived {
  1270. return [self hasExtendedAttributeWithName:kDDXAttrArchivedName];
  1271. }
  1272. - (void)setIsArchived:(BOOL)flag {
  1273. if (flag) {
  1274. [self addExtendedAttributeWithName:kDDXAttrArchivedName];
  1275. } else {
  1276. [self removeExtendedAttributeWithName:kDDXAttrArchivedName];
  1277. }
  1278. }
  1279. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1280. #pragma mark Changes
  1281. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1282. - (void)reset {
  1283. _fileName = nil;
  1284. _fileAttributes = nil;
  1285. _creationDate = nil;
  1286. _modificationDate = nil;
  1287. }
  1288. - (void)renameFile:(NSString *)newFileName {
  1289. // This method is only used on the iPhone simulator, where normal extended attributes are broken.
  1290. // See full explanation in the header file.
  1291. if (![newFileName isEqualToString:[self fileName]]) {
  1292. __auto_type fileManager = [NSFileManager defaultManager];
  1293. __auto_type fileDir = [filePath stringByDeletingLastPathComponent];
  1294. __auto_type newFilePath = [fileDir stringByAppendingPathComponent:newFileName];
  1295. // We only want to assert when we're not using the simulator, as we're "archiving" a log file with this method in the sim
  1296. // (in which case the file might not exist anymore and neither does it parent folder).
  1297. #if defined(DEBUG) && (!defined(TARGET_IPHONE_SIMULATOR) || !TARGET_IPHONE_SIMULATOR)
  1298. __auto_type directory = NO;
  1299. [fileManager fileExistsAtPath:fileDir isDirectory:&directory];
  1300. NSAssert(directory, @"Containing directory must exist.");
  1301. #endif
  1302. __autoreleasing NSError *error = nil;
  1303. __auto_type success = [fileManager removeItemAtPath:newFilePath error:&error];
  1304. if (!success && error.code != NSFileNoSuchFileError) {
  1305. NSLogError(@"DDLogFileInfo: Error deleting archive (%@): %@", self.fileName, error);
  1306. }
  1307. success = [fileManager moveItemAtPath:filePath toPath:newFilePath error:&error];
  1308. // When a log file is deleted, moved or renamed on the simulator, we attempt to rename it as a
  1309. // result of "archiving" it, but since the file doesn't exist anymore, needless error logs are printed
  1310. // We therefore ignore this error, and assert that the directory we are copying into exists (which
  1311. // is the only other case where this error code can come up).
  1312. #if TARGET_IPHONE_SIMULATOR
  1313. if (!success && error.code != NSFileNoSuchFileError)
  1314. #else
  1315. if (!success)
  1316. #endif
  1317. {
  1318. NSLogError(@"DDLogFileInfo: Error renaming file (%@): %@", self.fileName, error);
  1319. }
  1320. filePath = newFilePath;
  1321. [self reset];
  1322. }
  1323. }
  1324. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1325. #pragma mark Attribute Management
  1326. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1327. #if TARGET_IPHONE_SIMULATOR
  1328. // Old implementation of extended attributes on the simulator.
  1329. // Extended attributes were not working properly on the simulator
  1330. // due to misuse of setxattr() function.
  1331. // Now that this is fixed in the new implementation, we want to keep
  1332. // backward compatibility with previous simulator installations.
  1333. static NSString * const kDDExtensionSeparator = @".";
  1334. static NSString *_xattrToExtensionName(NSString *attrName) {
  1335. static NSDictionary<NSString *, NSString *>* _xattrToExtensionNameMap;
  1336. static dispatch_once_t _token;
  1337. dispatch_once(&_token, ^{
  1338. _xattrToExtensionNameMap = @{ kDDXAttrArchivedName: @"archived" };
  1339. });
  1340. return [_xattrToExtensionNameMap objectForKey:attrName];
  1341. }
  1342. - (BOOL)_hasExtensionAttributeWithName:(NSString *)attrName {
  1343. // This method is only used on the iPhone simulator for backward compatibility reason.
  1344. // Split the file name into components. File name may have various format, but generally
  1345. // structure is same:
  1346. //
  1347. // <name part>.<extension part> and <name part>.archived.<extension part>
  1348. // or
  1349. // <name part> and <name part>.archived
  1350. //
  1351. // So we want to search for the attrName in the components (ignoring the first array index).
  1352. __auto_type components = [[self fileName] componentsSeparatedByString:kDDExtensionSeparator];
  1353. // Watch out for file names without an extension
  1354. for (NSUInteger i = 1; i < components.count; i++) {
  1355. if ([attrName isEqualToString:components[i]]) {
  1356. return YES;
  1357. }
  1358. }
  1359. return NO;
  1360. }
  1361. - (void)_removeExtensionAttributeWithName:(NSString *)attrName {
  1362. // This method is only used on the iPhone simulator for backward compatibility reason.
  1363. if ([attrName length] == 0) {
  1364. return;
  1365. }
  1366. // Example:
  1367. // attrName = "archived"
  1368. //
  1369. // "mylog.archived.txt" -> "mylog.txt"
  1370. // "mylog.archived" -> "mylog"
  1371. __auto_type components = [[self fileName] componentsSeparatedByString:kDDExtensionSeparator];
  1372. __auto_type count = [components count];
  1373. __auto_type estimatedNewLength = [[self fileName] length];
  1374. __auto_type newFileName = [NSMutableString stringWithCapacity:estimatedNewLength];
  1375. if (count > 0) {
  1376. [newFileName appendString:components[0]];
  1377. }
  1378. __auto_type found = NO;
  1379. NSUInteger i;
  1380. for (i = 1; i < count; i++) {
  1381. __auto_type attr = components[i];
  1382. if ([attrName isEqualToString:attr]) {
  1383. found = YES;
  1384. } else {
  1385. [newFileName appendString:kDDExtensionSeparator];
  1386. [newFileName appendString:attr];
  1387. }
  1388. }
  1389. if (found) {
  1390. [self renameFile:newFileName];
  1391. }
  1392. }
  1393. #endif /* if TARGET_IPHONE_SIMULATOR */
  1394. - (BOOL)hasExtendedAttributeWithName:(NSString *)attrName {
  1395. __auto_type path = [filePath fileSystemRepresentation];
  1396. __auto_type name = [attrName UTF8String];
  1397. __auto_type hasExtendedAttribute = NO;
  1398. char buffer[1];
  1399. __auto_type result = getxattr(path, name, buffer, 1, 0, 0);
  1400. // Fast path
  1401. if (result > 0 && buffer[0] == '\1') {
  1402. hasExtendedAttribute = YES;
  1403. }
  1404. // Maintain backward compatibility, but fix it for future checks
  1405. else if (result >= 0) {
  1406. hasExtendedAttribute = YES;
  1407. [self addExtendedAttributeWithName:attrName];
  1408. }
  1409. #if TARGET_IPHONE_SIMULATOR
  1410. else if ([self _hasExtensionAttributeWithName:_xattrToExtensionName(attrName)]) {
  1411. hasExtendedAttribute = YES;
  1412. [self addExtendedAttributeWithName:attrName];
  1413. }
  1414. #endif
  1415. return hasExtendedAttribute;
  1416. }
  1417. - (void)addExtendedAttributeWithName:(NSString *)attrName {
  1418. __auto_type path = [filePath fileSystemRepresentation];
  1419. __auto_type name = [attrName UTF8String];
  1420. __auto_type result = setxattr(path, name, "\1", 1, 0, 0);
  1421. if (result < 0) {
  1422. if (errno != ENOENT) {
  1423. NSLogError(@"DDLogFileInfo: setxattr(%@, %@): error = %@",
  1424. attrName,
  1425. filePath,
  1426. @(strerror(errno)));
  1427. } else {
  1428. NSLogDebug(@"DDLogFileInfo: File does not exist in setxattr(%@, %@): error = %@",
  1429. attrName,
  1430. filePath,
  1431. @(strerror(errno)));
  1432. }
  1433. }
  1434. #if TARGET_IPHONE_SIMULATOR
  1435. else {
  1436. [self _removeExtensionAttributeWithName:_xattrToExtensionName(attrName)];
  1437. }
  1438. #endif
  1439. }
  1440. - (void)removeExtendedAttributeWithName:(NSString *)attrName {
  1441. __auto_type path = [filePath fileSystemRepresentation];
  1442. __auto_type name = [attrName UTF8String];
  1443. __auto_type result = removexattr(path, name, 0);
  1444. if (result < 0 && errno != ENOATTR) {
  1445. NSLogError(@"DDLogFileInfo: removexattr(%@, %@): error = %@",
  1446. attrName,
  1447. self.fileName,
  1448. @(strerror(errno)));
  1449. }
  1450. #if TARGET_IPHONE_SIMULATOR
  1451. [self _removeExtensionAttributeWithName:_xattrToExtensionName(attrName)];
  1452. #endif
  1453. }
  1454. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1455. #pragma mark Comparisons
  1456. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1457. - (BOOL)isEqual:(id)object {
  1458. if ([object isKindOfClass:[self class]]) {
  1459. __auto_type another = (DDLogFileInfo *)object;
  1460. return [filePath isEqualToString:[another filePath]];
  1461. }
  1462. return NO;
  1463. }
  1464. - (NSUInteger)hash {
  1465. return [filePath hash];
  1466. }
  1467. - (NSComparisonResult)reverseCompareDatesUs:(NSDate *_Nullable)us them:(NSDate *_Nullable)them {
  1468. if (us != nil && them != nil) {
  1469. return [them compare:(NSDate * _Nonnull)us];
  1470. } else if (us == nil && them == nil) {
  1471. return NSOrderedSame;
  1472. }
  1473. return them == nil ? NSOrderedAscending : NSOrderedDescending;
  1474. }
  1475. - (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another {
  1476. return [self reverseCompareDatesUs:[self creationDate] them:[another creationDate]];
  1477. }
  1478. - (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another {
  1479. return [self reverseCompareDatesUs:[self modificationDate] them:[another modificationDate]];
  1480. }
  1481. @end
  1482. #if TARGET_OS_IPHONE
  1483. /**
  1484. * When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
  1485. *
  1486. * But in case if app is able to launch from background we need to have an ability to open log file any time we
  1487. * want (even if device is locked). Thats why that attribute have to be changed to
  1488. * NSFileProtectionCompleteUntilFirstUserAuthentication.
  1489. */
  1490. BOOL doesAppRunInBackground(void) {
  1491. if ([[[NSBundle mainBundle] executablePath] containsString:@".appex/"]) {
  1492. return YES;
  1493. }
  1494. __auto_type answer = NO;
  1495. NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
  1496. for (NSString *mode in backgroundModes) {
  1497. if (mode.length > 0) {
  1498. answer = YES;
  1499. break;
  1500. }
  1501. }
  1502. return answer;
  1503. }
  1504. #endif