DDFileLogger.m 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495
  1. // Software License Agreement (BSD License)
  2. //
  3. // Copyright (c) 2010-2016, 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. #import "DDFileLogger.h"
  16. #import <unistd.h>
  17. #import <sys/attr.h>
  18. #import <sys/xattr.h>
  19. #import <libkern/OSAtomic.h>
  20. #if !__has_feature(objc_arc)
  21. #error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
  22. #endif
  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. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  45. #pragma mark -
  46. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  47. @interface DDLogFileManagerDefault () {
  48. NSUInteger _maximumNumberOfLogFiles;
  49. unsigned long long _logFilesDiskQuota;
  50. NSString *_logsDirectory;
  51. #if TARGET_OS_IPHONE
  52. NSFileProtectionType _defaultFileProtectionLevel;
  53. #endif
  54. }
  55. - (void)deleteOldLogFiles;
  56. - (NSString *)defaultLogsDirectory;
  57. @end
  58. @implementation DDLogFileManagerDefault
  59. @synthesize maximumNumberOfLogFiles = _maximumNumberOfLogFiles;
  60. @synthesize logFilesDiskQuota = _logFilesDiskQuota;
  61. - (instancetype)init {
  62. return [self initWithLogsDirectory:nil];
  63. }
  64. - (instancetype)initWithLogsDirectory:(NSString *)aLogsDirectory {
  65. if ((self = [super init])) {
  66. _maximumNumberOfLogFiles = kDDDefaultLogMaxNumLogFiles;
  67. _logFilesDiskQuota = kDDDefaultLogFilesDiskQuota;
  68. if (aLogsDirectory) {
  69. _logsDirectory = [aLogsDirectory copy];
  70. } else {
  71. _logsDirectory = [[self defaultLogsDirectory] copy];
  72. }
  73. NSKeyValueObservingOptions kvoOptions = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
  74. [self addObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles)) options:kvoOptions context:nil];
  75. [self addObserver:self forKeyPath:NSStringFromSelector(@selector(logFilesDiskQuota)) options:kvoOptions context:nil];
  76. NSLogVerbose(@"DDFileLogManagerDefault: logsDirectory:\n%@", [self logsDirectory]);
  77. NSLogVerbose(@"DDFileLogManagerDefault: sortedLogFileNames:\n%@", [self sortedLogFileNames]);
  78. }
  79. return self;
  80. }
  81. + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey
  82. {
  83. BOOL automatic = NO;
  84. if ([theKey isEqualToString:@"maximumNumberOfLogFiles"] || [theKey isEqualToString:@"logFilesDiskQuota"]) {
  85. automatic = NO;
  86. } else {
  87. automatic = [super automaticallyNotifiesObserversForKey:theKey];
  88. }
  89. return automatic;
  90. }
  91. #if TARGET_OS_IPHONE
  92. - (instancetype)initWithLogsDirectory:(NSString *)logsDirectory defaultFileProtectionLevel:(NSFileProtectionType)fileProtectionLevel {
  93. if ((self = [self initWithLogsDirectory:logsDirectory])) {
  94. if ([fileProtectionLevel isEqualToString:NSFileProtectionNone] ||
  95. [fileProtectionLevel isEqualToString:NSFileProtectionComplete] ||
  96. [fileProtectionLevel isEqualToString:NSFileProtectionCompleteUnlessOpen] ||
  97. [fileProtectionLevel isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) {
  98. _defaultFileProtectionLevel = fileProtectionLevel;
  99. }
  100. }
  101. return self;
  102. }
  103. #endif
  104. - (void)dealloc {
  105. // try-catch because the observer might be removed or never added. In this case, removeObserver throws and exception
  106. @try {
  107. [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles))];
  108. [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(logFilesDiskQuota))];
  109. } @catch (NSException *exception) {
  110. }
  111. }
  112. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  113. #pragma mark Configuration
  114. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  115. - (void)observeValueForKeyPath:(NSString *)keyPath
  116. ofObject:(id)object
  117. change:(NSDictionary *)change
  118. context:(void *)context {
  119. NSNumber *old = change[NSKeyValueChangeOldKey];
  120. NSNumber *new = change[NSKeyValueChangeNewKey];
  121. if ([old isEqual:new]) {
  122. // No change in value - don't bother with any processing.
  123. return;
  124. }
  125. if ([keyPath isEqualToString:NSStringFromSelector(@selector(maximumNumberOfLogFiles))] ||
  126. [keyPath isEqualToString:NSStringFromSelector(@selector(logFilesDiskQuota))]) {
  127. NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: %@", keyPath);
  128. dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool {
  129. [self deleteOldLogFiles];
  130. } });
  131. }
  132. }
  133. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  134. #pragma mark File Deleting
  135. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  136. /**
  137. * Deletes archived log files that exceed the maximumNumberOfLogFiles or logFilesDiskQuota configuration values.
  138. **/
  139. - (void)deleteOldLogFiles {
  140. NSLogVerbose(@"DDLogFileManagerDefault: deleteOldLogFiles");
  141. NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
  142. NSUInteger firstIndexToDelete = NSNotFound;
  143. const unsigned long long diskQuota = self.logFilesDiskQuota;
  144. const NSUInteger maxNumLogFiles = self.maximumNumberOfLogFiles;
  145. if (diskQuota) {
  146. unsigned long long used = 0;
  147. for (NSUInteger i = 0; i < sortedLogFileInfos.count; i++) {
  148. DDLogFileInfo *info = sortedLogFileInfos[i];
  149. used += info.fileSize;
  150. if (used > diskQuota) {
  151. firstIndexToDelete = i;
  152. break;
  153. }
  154. }
  155. }
  156. if (maxNumLogFiles) {
  157. if (firstIndexToDelete == NSNotFound) {
  158. firstIndexToDelete = maxNumLogFiles;
  159. } else {
  160. firstIndexToDelete = MIN(firstIndexToDelete, maxNumLogFiles);
  161. }
  162. }
  163. if (firstIndexToDelete == 0) {
  164. // Do we consider the first file?
  165. // We are only supposed to be deleting archived files.
  166. // In most cases, the first file is likely the log file that is currently being written to.
  167. // So in most cases, we do not want to consider this file for deletion.
  168. if (sortedLogFileInfos.count > 0) {
  169. DDLogFileInfo *logFileInfo = sortedLogFileInfos[0];
  170. if (!logFileInfo.isArchived) {
  171. // Don't delete active file.
  172. ++firstIndexToDelete;
  173. }
  174. }
  175. }
  176. if (firstIndexToDelete != NSNotFound) {
  177. // removing all logfiles starting with firstIndexToDelete
  178. for (NSUInteger i = firstIndexToDelete; i < sortedLogFileInfos.count; i++) {
  179. DDLogFileInfo *logFileInfo = sortedLogFileInfos[i];
  180. NSLogInfo(@"DDLogFileManagerDefault: Deleting file: %@", logFileInfo.fileName);
  181. [[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:nil];
  182. }
  183. }
  184. }
  185. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  186. #pragma mark Log Files
  187. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  188. /**
  189. * Returns the path to the default logs directory.
  190. * If the logs directory doesn't exist, this method automatically creates it.
  191. **/
  192. - (NSString *)defaultLogsDirectory {
  193. #if TARGET_OS_IPHONE
  194. NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  195. NSString *baseDir = paths.firstObject;
  196. NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"Logs"];
  197. #else
  198. NSString *appName = [[NSProcessInfo processInfo] processName];
  199. NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
  200. NSString *basePath = ([paths count] > 0) ? paths[0] : NSTemporaryDirectory();
  201. NSString *logsDirectory = [[basePath stringByAppendingPathComponent:@"Logs"] stringByAppendingPathComponent:appName];
  202. #endif
  203. return logsDirectory;
  204. }
  205. - (NSString *)logsDirectory {
  206. // We could do this check once, during initalization, and not bother again.
  207. // But this way the code continues to work if the directory gets deleted while the code is running.
  208. if (![[NSFileManager defaultManager] fileExistsAtPath:_logsDirectory]) {
  209. NSError *err = nil;
  210. if (![[NSFileManager defaultManager] createDirectoryAtPath:_logsDirectory
  211. withIntermediateDirectories:YES
  212. attributes:nil
  213. error:&err]) {
  214. NSLogError(@"DDFileLogManagerDefault: Error creating logsDirectory: %@", err);
  215. }
  216. }
  217. return _logsDirectory;
  218. }
  219. - (BOOL)isLogFile:(NSString *)fileName {
  220. NSString *appName = [self applicationName];
  221. BOOL hasProperPrefix = [fileName hasPrefix:appName];
  222. BOOL hasProperSuffix = [fileName hasSuffix:@".log"];
  223. return (hasProperPrefix && hasProperSuffix);
  224. }
  225. //if you change formater , then change sortedLogFileInfos method also accordingly
  226. - (NSDateFormatter *)logFileDateFormatter {
  227. NSMutableDictionary *dictionary = [[NSThread currentThread]
  228. threadDictionary];
  229. NSString *dateFormat = @"yyyy'-'MM'-'dd'--'HH'-'mm'-'ss'-'SSS'";
  230. NSString *key = [NSString stringWithFormat:@"logFileDateFormatter.%@", dateFormat];
  231. NSDateFormatter *dateFormatter = dictionary[key];
  232. if (dateFormatter == nil) {
  233. dateFormatter = [[NSDateFormatter alloc] init];
  234. [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
  235. [dateFormatter setDateFormat:dateFormat];
  236. [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
  237. dictionary[key] = dateFormatter;
  238. }
  239. return dateFormatter;
  240. }
  241. - (NSArray *)unsortedLogFilePaths {
  242. NSString *logsDirectory = [self logsDirectory];
  243. NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:nil];
  244. NSMutableArray *unsortedLogFilePaths = [NSMutableArray arrayWithCapacity:[fileNames count]];
  245. for (NSString *fileName in fileNames) {
  246. // Filter out any files that aren't log files. (Just for extra safety)
  247. #if TARGET_IPHONE_SIMULATOR
  248. // In case of iPhone simulator there can be 'archived' extension. isLogFile:
  249. // method knows nothing about it. Thus removing it for this method.
  250. //
  251. // See full explanation in the header file.
  252. NSString *theFileName = [fileName stringByReplacingOccurrencesOfString:@".archived"
  253. withString:@""];
  254. if ([self isLogFile:theFileName])
  255. #else
  256. if ([self isLogFile:fileName])
  257. #endif
  258. {
  259. NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName];
  260. [unsortedLogFilePaths addObject:filePath];
  261. }
  262. }
  263. return unsortedLogFilePaths;
  264. }
  265. - (NSArray *)unsortedLogFileNames {
  266. NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths];
  267. NSMutableArray *unsortedLogFileNames = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
  268. for (NSString *filePath in unsortedLogFilePaths) {
  269. [unsortedLogFileNames addObject:[filePath lastPathComponent]];
  270. }
  271. return unsortedLogFileNames;
  272. }
  273. - (NSArray *)unsortedLogFileInfos {
  274. NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths];
  275. NSMutableArray *unsortedLogFileInfos = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
  276. for (NSString *filePath in unsortedLogFilePaths) {
  277. DDLogFileInfo *logFileInfo = [[DDLogFileInfo alloc] initWithFilePath:filePath];
  278. [unsortedLogFileInfos addObject:logFileInfo];
  279. }
  280. return unsortedLogFileInfos;
  281. }
  282. - (NSArray *)sortedLogFilePaths {
  283. NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
  284. NSMutableArray *sortedLogFilePaths = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
  285. for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) {
  286. [sortedLogFilePaths addObject:[logFileInfo filePath]];
  287. }
  288. return sortedLogFilePaths;
  289. }
  290. - (NSArray *)sortedLogFileNames {
  291. NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
  292. NSMutableArray *sortedLogFileNames = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
  293. for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) {
  294. [sortedLogFileNames addObject:[logFileInfo fileName]];
  295. }
  296. return sortedLogFileNames;
  297. }
  298. - (NSArray *)sortedLogFileInfos {
  299. return [[self unsortedLogFileInfos] sortedArrayUsingComparator:^NSComparisonResult(DDLogFileInfo * _Nonnull obj1, DDLogFileInfo * _Nonnull obj2) {
  300. NSDate *date1 = [NSDate new];
  301. NSDate *date2 = [NSDate new];
  302. NSArray<NSString *> *arrayComponent = [[obj1 fileName] componentsSeparatedByString:@" "];
  303. if (arrayComponent.count > 0) {
  304. NSString *stringDate = arrayComponent.lastObject;
  305. stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@""];
  306. stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@""];
  307. date1 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj1 creationDate];
  308. }
  309. arrayComponent = [[obj2 fileName] componentsSeparatedByString:@" "];
  310. if (arrayComponent.count > 0) {
  311. NSString *stringDate = arrayComponent.lastObject;
  312. stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@""];
  313. stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@""];
  314. date2 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj2 creationDate];
  315. }
  316. return [date2 compare:date1];
  317. }];
  318. }
  319. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  320. #pragma mark Creation
  321. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  322. //if you change newLogFileName , then change isLogFile method also accordingly
  323. - (NSString *)newLogFileName {
  324. NSString *appName = [self applicationName];
  325. NSDateFormatter *dateFormatter = [self logFileDateFormatter];
  326. NSString *formattedDate = [dateFormatter stringFromDate:[NSDate date]];
  327. return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate];
  328. }
  329. - (NSString *)createNewLogFile {
  330. NSString *fileName = [self newLogFileName];
  331. NSString *logsDirectory = [self logsDirectory];
  332. NSUInteger attempt = 1;
  333. do {
  334. NSString *actualFileName = fileName;
  335. if (attempt > 1) {
  336. NSString *extension = [actualFileName pathExtension];
  337. actualFileName = [actualFileName stringByDeletingPathExtension];
  338. actualFileName = [actualFileName stringByAppendingFormat:@" %lu", (unsigned long)attempt];
  339. if (extension.length) {
  340. actualFileName = [actualFileName stringByAppendingPathExtension:extension];
  341. }
  342. }
  343. NSString *filePath = [logsDirectory stringByAppendingPathComponent:actualFileName];
  344. if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
  345. NSLogVerbose(@"DDLogFileManagerDefault: Creating new log file: %@", actualFileName);
  346. NSDictionary *attributes = nil;
  347. #if TARGET_OS_IPHONE
  348. // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
  349. //
  350. // But in case if app is able to launch from background we need to have an ability to open log file any time we
  351. // want (even if device is locked). Thats why that attribute have to be changed to
  352. // NSFileProtectionCompleteUntilFirstUserAuthentication.
  353. NSFileProtectionType key = _defaultFileProtectionLevel ? :
  354. (doesAppRunInBackground() ? NSFileProtectionCompleteUntilFirstUserAuthentication : NSFileProtectionCompleteUnlessOpen);
  355. attributes = @{
  356. NSFileProtectionKey: key
  357. };
  358. #endif
  359. [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:attributes];
  360. // Since we just created a new log file, we may need to delete some old log files
  361. [self deleteOldLogFiles];
  362. return filePath;
  363. } else {
  364. attempt++;
  365. }
  366. } while (YES);
  367. }
  368. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  369. #pragma mark Utility
  370. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  371. - (NSString *)applicationName {
  372. static NSString *_appName;
  373. static dispatch_once_t onceToken;
  374. dispatch_once(&onceToken, ^{
  375. _appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"];
  376. if (!_appName) {
  377. _appName = [[NSProcessInfo processInfo] processName];
  378. }
  379. if (!_appName) {
  380. _appName = @"";
  381. }
  382. });
  383. return _appName;
  384. }
  385. @end
  386. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  387. #pragma mark -
  388. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  389. @interface DDLogFileFormatterDefault () {
  390. NSDateFormatter *_dateFormatter;
  391. }
  392. @end
  393. @implementation DDLogFileFormatterDefault
  394. - (instancetype)init {
  395. return [self initWithDateFormatter:nil];
  396. }
  397. - (instancetype)initWithDateFormatter:(NSDateFormatter *)aDateFormatter {
  398. if ((self = [super init])) {
  399. if (aDateFormatter) {
  400. _dateFormatter = aDateFormatter;
  401. } else {
  402. _dateFormatter = [[NSDateFormatter alloc] init];
  403. [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; // 10.4+ style
  404. [_dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"];
  405. }
  406. }
  407. return self;
  408. }
  409. - (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
  410. NSString *dateAndTime = [_dateFormatter stringFromDate:(logMessage->_timestamp)];
  411. return [NSString stringWithFormat:@"%@ %@", dateAndTime, logMessage->_message];
  412. }
  413. @end
  414. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  415. #pragma mark -
  416. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  417. @interface DDFileLogger () {
  418. __strong id <DDLogFileManager> _logFileManager;
  419. NSFileHandle *_currentLogFileHandle;
  420. dispatch_source_t _currentLogFileVnode;
  421. dispatch_source_t _rollingTimer;
  422. unsigned long long _maximumFileSize;
  423. NSTimeInterval _rollingFrequency;
  424. }
  425. - (void)rollLogFileNow;
  426. - (void)maybeRollLogFileDueToAge;
  427. - (void)maybeRollLogFileDueToSize;
  428. @end
  429. @implementation DDFileLogger
  430. - (instancetype)init {
  431. DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init];
  432. return [self initWithLogFileManager:defaultLogFileManager];
  433. }
  434. - (instancetype)initWithLogFileManager:(id <DDLogFileManager>)aLogFileManager {
  435. if ((self = [super init])) {
  436. _maximumFileSize = kDDDefaultLogMaxFileSize;
  437. _rollingFrequency = kDDDefaultLogRollingFrequency;
  438. _automaticallyAppendNewlineForCustomFormatters = YES;
  439. logFileManager = aLogFileManager;
  440. self.logFormatter = [DDLogFileFormatterDefault new];
  441. }
  442. return self;
  443. }
  444. - (void)dealloc {
  445. [_currentLogFileHandle synchronizeFile];
  446. [_currentLogFileHandle closeFile];
  447. if (_currentLogFileVnode) {
  448. dispatch_source_cancel(_currentLogFileVnode);
  449. _currentLogFileVnode = NULL;
  450. }
  451. if (_rollingTimer) {
  452. dispatch_source_cancel(_rollingTimer);
  453. _rollingTimer = NULL;
  454. }
  455. }
  456. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  457. #pragma mark Properties
  458. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  459. @synthesize logFileManager;
  460. - (unsigned long long)maximumFileSize {
  461. __block unsigned long long result;
  462. dispatch_block_t block = ^{
  463. result = _maximumFileSize;
  464. };
  465. // The design of this method is taken from the DDAbstractLogger implementation.
  466. // For extensive documentation please refer to the DDAbstractLogger implementation.
  467. // Note: The internal implementation MUST access the maximumFileSize variable directly,
  468. // This method is designed explicitly for external access.
  469. //
  470. // Using "self." syntax to go through this method will cause immediate deadlock.
  471. // This is the intended result. Fix it by accessing the ivar directly.
  472. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  473. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  474. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  475. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  476. dispatch_sync(globalLoggingQueue, ^{
  477. dispatch_sync(self.loggerQueue, block);
  478. });
  479. return result;
  480. }
  481. - (void)setMaximumFileSize:(unsigned long long)newMaximumFileSize {
  482. dispatch_block_t block = ^{
  483. @autoreleasepool {
  484. _maximumFileSize = newMaximumFileSize;
  485. [self maybeRollLogFileDueToSize];
  486. }
  487. };
  488. // The design of this method is taken from the DDAbstractLogger implementation.
  489. // For extensive documentation please refer to the DDAbstractLogger implementation.
  490. // Note: The internal implementation MUST access the maximumFileSize variable directly,
  491. // This method is designed explicitly for external access.
  492. //
  493. // Using "self." syntax to go through this method will cause immediate deadlock.
  494. // This is the intended result. Fix it by accessing the ivar directly.
  495. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  496. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  497. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  498. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  499. dispatch_async(globalLoggingQueue, ^{
  500. dispatch_async(self.loggerQueue, block);
  501. });
  502. }
  503. - (NSTimeInterval)rollingFrequency {
  504. __block NSTimeInterval result;
  505. dispatch_block_t block = ^{
  506. result = _rollingFrequency;
  507. };
  508. // The design of this method is taken from the DDAbstractLogger implementation.
  509. // For extensive documentation please refer to the DDAbstractLogger implementation.
  510. // Note: The internal implementation should access the rollingFrequency variable directly,
  511. // This method is designed explicitly for external access.
  512. //
  513. // Using "self." syntax to go through this method will cause immediate deadlock.
  514. // This is the intended result. Fix it by accessing the ivar directly.
  515. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  516. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  517. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  518. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  519. dispatch_sync(globalLoggingQueue, ^{
  520. dispatch_sync(self.loggerQueue, block);
  521. });
  522. return result;
  523. }
  524. - (void)setRollingFrequency:(NSTimeInterval)newRollingFrequency {
  525. dispatch_block_t block = ^{
  526. @autoreleasepool {
  527. _rollingFrequency = newRollingFrequency;
  528. [self maybeRollLogFileDueToAge];
  529. }
  530. };
  531. // The design of this method is taken from the DDAbstractLogger implementation.
  532. // For extensive documentation please refer to the DDAbstractLogger implementation.
  533. // Note: The internal implementation should access the rollingFrequency variable directly,
  534. // This method is designed explicitly for external access.
  535. //
  536. // Using "self." syntax to go through this method will cause immediate deadlock.
  537. // This is the intended result. Fix it by accessing the ivar directly.
  538. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  539. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  540. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  541. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  542. dispatch_async(globalLoggingQueue, ^{
  543. dispatch_async(self.loggerQueue, block);
  544. });
  545. }
  546. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  547. #pragma mark File Rolling
  548. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  549. - (void)scheduleTimerToRollLogFileDueToAge {
  550. if (_rollingTimer) {
  551. dispatch_source_cancel(_rollingTimer);
  552. _rollingTimer = NULL;
  553. }
  554. if (_currentLogFileInfo == nil || _rollingFrequency <= 0.0) {
  555. return;
  556. }
  557. NSDate *logFileCreationDate = [_currentLogFileInfo creationDate];
  558. NSTimeInterval ti = [logFileCreationDate timeIntervalSinceReferenceDate];
  559. ti += _rollingFrequency;
  560. NSDate *logFileRollingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ti];
  561. NSLogVerbose(@"DDFileLogger: scheduleTimerToRollLogFileDueToAge");
  562. NSLogVerbose(@"DDFileLogger: logFileCreationDate: %@", logFileCreationDate);
  563. NSLogVerbose(@"DDFileLogger: logFileRollingDate : %@", logFileRollingDate);
  564. _rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue);
  565. dispatch_source_set_event_handler(_rollingTimer, ^{ @autoreleasepool {
  566. [self maybeRollLogFileDueToAge];
  567. } });
  568. #if !OS_OBJECT_USE_OBJC
  569. dispatch_source_t theRollingTimer = _rollingTimer;
  570. dispatch_source_set_cancel_handler(_rollingTimer, ^{
  571. dispatch_release(theRollingTimer);
  572. });
  573. #endif
  574. uint64_t delay = (uint64_t)([logFileRollingDate timeIntervalSinceNow] * (NSTimeInterval) NSEC_PER_SEC);
  575. dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, delay);
  576. dispatch_source_set_timer(_rollingTimer, fireTime, DISPATCH_TIME_FOREVER, 1ull * NSEC_PER_SEC);
  577. dispatch_resume(_rollingTimer);
  578. }
  579. - (void)rollLogFile {
  580. [self rollLogFileWithCompletionBlock:nil];
  581. }
  582. - (void)rollLogFileWithCompletionBlock:(void (^)(void))completionBlock {
  583. // This method is public.
  584. // We need to execute the rolling on our logging thread/queue.
  585. dispatch_block_t block = ^{
  586. @autoreleasepool {
  587. [self rollLogFileNow];
  588. if (completionBlock) {
  589. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  590. completionBlock();
  591. });
  592. }
  593. }
  594. };
  595. // The design of this method is taken from the DDAbstractLogger implementation.
  596. // For extensive documentation please refer to the DDAbstractLogger implementation.
  597. if ([self isOnInternalLoggerQueue]) {
  598. block();
  599. } else {
  600. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  601. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  602. dispatch_async(globalLoggingQueue, ^{
  603. dispatch_async(self.loggerQueue, block);
  604. });
  605. }
  606. }
  607. - (void)rollLogFileNow {
  608. NSLogVerbose(@"DDFileLogger: rollLogFileNow");
  609. if (_currentLogFileHandle == nil) {
  610. return;
  611. }
  612. [_currentLogFileHandle synchronizeFile];
  613. [_currentLogFileHandle closeFile];
  614. _currentLogFileHandle = nil;
  615. _currentLogFileInfo.isArchived = YES;
  616. if ([logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]) {
  617. [logFileManager didRollAndArchiveLogFile:(_currentLogFileInfo.filePath)];
  618. }
  619. _currentLogFileInfo = nil;
  620. if (_currentLogFileVnode) {
  621. dispatch_source_cancel(_currentLogFileVnode);
  622. _currentLogFileVnode = NULL;
  623. }
  624. if (_rollingTimer) {
  625. dispatch_source_cancel(_rollingTimer);
  626. _rollingTimer = NULL;
  627. }
  628. }
  629. - (void)maybeRollLogFileDueToAge {
  630. if (_rollingFrequency > 0.0 && _currentLogFileInfo.age >= _rollingFrequency) {
  631. NSLogVerbose(@"DDFileLogger: Rolling log file due to age...");
  632. [self rollLogFileNow];
  633. } else {
  634. [self scheduleTimerToRollLogFileDueToAge];
  635. }
  636. }
  637. - (void)maybeRollLogFileDueToSize {
  638. // This method is called from logMessage.
  639. // Keep it FAST.
  640. // Note: Use direct access to maximumFileSize variable.
  641. // We specifically wrote our own getter/setter method to allow us to do this (for performance reasons).
  642. if (_maximumFileSize > 0) {
  643. unsigned long long fileSize = [_currentLogFileHandle offsetInFile];
  644. if (fileSize >= _maximumFileSize) {
  645. NSLogVerbose(@"DDFileLogger: Rolling log file due to size (%qu)...", fileSize);
  646. [self rollLogFileNow];
  647. }
  648. }
  649. }
  650. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  651. #pragma mark File Logging
  652. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  653. /**
  654. * Returns the log file that should be used.
  655. * If there is an existing log file that is suitable,
  656. * within the constraints of maximumFileSize and rollingFrequency, then it is returned.
  657. *
  658. * Otherwise a new file is created and returned.
  659. **/
  660. - (DDLogFileInfo *)currentLogFileInfo {
  661. if (_currentLogFileInfo == nil) {
  662. NSArray *sortedLogFileInfos = [logFileManager sortedLogFileInfos];
  663. if ([sortedLogFileInfos count] > 0) {
  664. DDLogFileInfo *mostRecentLogFileInfo = sortedLogFileInfos[0];
  665. BOOL shouldArchiveMostRecent = NO;
  666. if (mostRecentLogFileInfo.isArchived) {
  667. shouldArchiveMostRecent = NO;
  668. } else if ([self shouldArchiveRecentLogFileInfo:mostRecentLogFileInfo]) {
  669. shouldArchiveMostRecent = YES;
  670. } else if (_maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= _maximumFileSize) {
  671. shouldArchiveMostRecent = YES;
  672. } else if (_rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= _rollingFrequency) {
  673. shouldArchiveMostRecent = YES;
  674. }
  675. #if TARGET_OS_IPHONE
  676. // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
  677. //
  678. // But in case if app is able to launch from background we need to have an ability to open log file any time we
  679. // want (even if device is locked). Thats why that attribute have to be changed to
  680. // NSFileProtectionCompleteUntilFirstUserAuthentication.
  681. //
  682. // If previous log was created when app wasn't running in background, but now it is - we archive it and create
  683. // a new one.
  684. //
  685. // If user has overwritten to NSFileProtectionNone there is no neeed to create a new one.
  686. if (!_doNotReuseLogFiles && doesAppRunInBackground()) {
  687. NSFileProtectionType key = mostRecentLogFileInfo.fileAttributes[NSFileProtectionKey];
  688. if ([key length] > 0 && !([key isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication] || [key isEqualToString:NSFileProtectionNone])) {
  689. shouldArchiveMostRecent = YES;
  690. }
  691. }
  692. #endif
  693. if (!_doNotReuseLogFiles && !mostRecentLogFileInfo.isArchived && !shouldArchiveMostRecent) {
  694. NSLogVerbose(@"DDFileLogger: Resuming logging with file %@", mostRecentLogFileInfo.fileName);
  695. _currentLogFileInfo = mostRecentLogFileInfo;
  696. } else {
  697. if (shouldArchiveMostRecent) {
  698. mostRecentLogFileInfo.isArchived = YES;
  699. if ([logFileManager respondsToSelector:@selector(didArchiveLogFile:)]) {
  700. [logFileManager didArchiveLogFile:(mostRecentLogFileInfo.filePath)];
  701. }
  702. }
  703. }
  704. }
  705. if (_currentLogFileInfo == nil) {
  706. NSString *currentLogFilePath = [logFileManager createNewLogFile];
  707. _currentLogFileInfo = [[DDLogFileInfo alloc] initWithFilePath:currentLogFilePath];
  708. }
  709. }
  710. return _currentLogFileInfo;
  711. }
  712. - (NSFileHandle *)currentLogFileHandle {
  713. if (_currentLogFileHandle == nil) {
  714. NSString *logFilePath = [[self currentLogFileInfo] filePath];
  715. _currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
  716. [_currentLogFileHandle seekToEndOfFile];
  717. if (_currentLogFileHandle) {
  718. [self scheduleTimerToRollLogFileDueToAge];
  719. // Here we are monitoring the log file. In case if it would be deleted ormoved
  720. // somewhere we want to roll it and use a new one.
  721. _currentLogFileVnode = dispatch_source_create(
  722. DISPATCH_SOURCE_TYPE_VNODE,
  723. [_currentLogFileHandle fileDescriptor],
  724. DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME,
  725. self.loggerQueue
  726. );
  727. dispatch_source_set_event_handler(_currentLogFileVnode, ^{ @autoreleasepool {
  728. NSLogInfo(@"DDFileLogger: Current logfile was moved. Rolling it and creating a new one");
  729. [self rollLogFileNow];
  730. } });
  731. #if !OS_OBJECT_USE_OBJC
  732. dispatch_source_t vnode = _currentLogFileVnode;
  733. dispatch_source_set_cancel_handler(_currentLogFileVnode, ^{
  734. dispatch_release(vnode);
  735. });
  736. #endif
  737. dispatch_resume(_currentLogFileVnode);
  738. }
  739. }
  740. return _currentLogFileHandle;
  741. }
  742. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  743. #pragma mark DDLogger Protocol
  744. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  745. static int exception_count = 0;
  746. - (void)logMessage:(DDLogMessage *)logMessage {
  747. NSString *message = logMessage->_message;
  748. BOOL isFormatted = NO;
  749. if (_logFormatter) {
  750. message = [_logFormatter formatLogMessage:logMessage];
  751. isFormatted = message != logMessage->_message;
  752. }
  753. if (message) {
  754. if ((!isFormatted || _automaticallyAppendNewlineForCustomFormatters) &&
  755. (![message hasSuffix:@"\n"])) {
  756. message = [message stringByAppendingString:@"\n"];
  757. }
  758. NSData *logData = [message dataUsingEncoding:NSUTF8StringEncoding];
  759. @try {
  760. [self willLogMessage];
  761. [[self currentLogFileHandle] writeData:logData];
  762. [self didLogMessage];
  763. } @catch (NSException *exception) {
  764. exception_count++;
  765. if (exception_count <= 10) {
  766. NSLogError(@"DDFileLogger.logMessage: %@", exception);
  767. if (exception_count == 10) {
  768. NSLogError(@"DDFileLogger.logMessage: Too many exceptions -- will not log any more of them.");
  769. }
  770. }
  771. }
  772. }
  773. }
  774. - (void)willLogMessage {
  775. }
  776. - (void)didLogMessage {
  777. [self maybeRollLogFileDueToSize];
  778. }
  779. - (BOOL)shouldArchiveRecentLogFileInfo:(DDLogFileInfo *)recentLogFileInfo {
  780. return NO;
  781. }
  782. - (void)willRemoveLogger {
  783. // If you override me be sure to invoke [super willRemoveLogger];
  784. [self rollLogFileNow];
  785. }
  786. - (NSString *)loggerName {
  787. return @"cocoa.lumberjack.fileLogger";
  788. }
  789. @end
  790. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  791. #pragma mark -
  792. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  793. #if TARGET_IPHONE_SIMULATOR
  794. static NSString * const kDDXAttrArchivedName = @"archived";
  795. #else
  796. static NSString * const kDDXAttrArchivedName = @"lumberjack.log.archived";
  797. #endif
  798. @interface DDLogFileInfo () {
  799. __strong NSString *_filePath;
  800. __strong NSString *_fileName;
  801. __strong NSDictionary *_fileAttributes;
  802. __strong NSDate *_creationDate;
  803. __strong NSDate *_modificationDate;
  804. unsigned long long _fileSize;
  805. }
  806. @end
  807. @implementation DDLogFileInfo
  808. @synthesize filePath;
  809. @dynamic fileName;
  810. @dynamic fileAttributes;
  811. @dynamic creationDate;
  812. @dynamic modificationDate;
  813. @dynamic fileSize;
  814. @dynamic age;
  815. @dynamic isArchived;
  816. #pragma mark Lifecycle
  817. + (instancetype)logFileWithPath:(NSString *)aFilePath {
  818. return [[self alloc] initWithFilePath:aFilePath];
  819. }
  820. - (instancetype)initWithFilePath:(NSString *)aFilePath {
  821. if ((self = [super init])) {
  822. filePath = [aFilePath copy];
  823. }
  824. return self;
  825. }
  826. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  827. #pragma mark Standard Info
  828. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  829. - (NSDictionary *)fileAttributes {
  830. if (_fileAttributes == nil && filePath != nil) {
  831. _fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
  832. }
  833. return _fileAttributes;
  834. }
  835. - (NSString *)fileName {
  836. if (_fileName == nil) {
  837. _fileName = [filePath lastPathComponent];
  838. }
  839. return _fileName;
  840. }
  841. - (NSDate *)modificationDate {
  842. if (_modificationDate == nil) {
  843. _modificationDate = self.fileAttributes[NSFileModificationDate];
  844. }
  845. return _modificationDate;
  846. }
  847. - (NSDate *)creationDate {
  848. if (_creationDate == nil) {
  849. _creationDate = self.fileAttributes[NSFileCreationDate];
  850. }
  851. return _creationDate;
  852. }
  853. - (unsigned long long)fileSize {
  854. if (_fileSize == 0) {
  855. _fileSize = [self.fileAttributes[NSFileSize] unsignedLongLongValue];
  856. }
  857. return _fileSize;
  858. }
  859. - (NSTimeInterval)age {
  860. return [[self creationDate] timeIntervalSinceNow] * -1.0;
  861. }
  862. - (NSString *)description {
  863. return [@{ @"filePath": self.filePath ? : @"",
  864. @"fileName": self.fileName ? : @"",
  865. @"fileAttributes": self.fileAttributes ? : @"",
  866. @"creationDate": self.creationDate ? : @"",
  867. @"modificationDate": self.modificationDate ? : @"",
  868. @"fileSize": @(self.fileSize),
  869. @"age": @(self.age),
  870. @"isArchived": @(self.isArchived) } description];
  871. }
  872. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  873. #pragma mark Archiving
  874. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  875. - (BOOL)isArchived {
  876. #if TARGET_IPHONE_SIMULATOR
  877. // Extended attributes don't work properly on the simulator.
  878. // So we have to use a less attractive alternative.
  879. // See full explanation in the header file.
  880. return [self hasExtensionAttributeWithName:kDDXAttrArchivedName];
  881. #else
  882. return [self hasExtendedAttributeWithName:kDDXAttrArchivedName];
  883. #endif
  884. }
  885. - (void)setIsArchived:(BOOL)flag {
  886. #if TARGET_IPHONE_SIMULATOR
  887. // Extended attributes don't work properly on the simulator.
  888. // So we have to use a less attractive alternative.
  889. // See full explanation in the header file.
  890. if (flag) {
  891. [self addExtensionAttributeWithName:kDDXAttrArchivedName];
  892. } else {
  893. [self removeExtensionAttributeWithName:kDDXAttrArchivedName];
  894. }
  895. #else
  896. if (flag) {
  897. [self addExtendedAttributeWithName:kDDXAttrArchivedName];
  898. } else {
  899. [self removeExtendedAttributeWithName:kDDXAttrArchivedName];
  900. }
  901. #endif
  902. }
  903. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  904. #pragma mark Changes
  905. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  906. - (void)reset {
  907. _fileName = nil;
  908. _fileAttributes = nil;
  909. _creationDate = nil;
  910. _modificationDate = nil;
  911. }
  912. - (void)renameFile:(NSString *)newFileName {
  913. // This method is only used on the iPhone simulator, where normal extended attributes are broken.
  914. // See full explanation in the header file.
  915. if (![newFileName isEqualToString:[self fileName]]) {
  916. NSString *fileDir = [filePath stringByDeletingLastPathComponent];
  917. NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName];
  918. NSLogVerbose(@"DDLogFileInfo: Renaming file: '%@' -> '%@'", self.fileName, newFileName);
  919. NSError *error = nil;
  920. if ([[NSFileManager defaultManager] fileExistsAtPath:newFilePath] &&
  921. ![[NSFileManager defaultManager] removeItemAtPath:newFilePath error:&error]) {
  922. NSLogError(@"DDLogFileInfo: Error deleting archive (%@): %@", self.fileName, error);
  923. }
  924. if (![[NSFileManager defaultManager] moveItemAtPath:filePath toPath:newFilePath error:&error]) {
  925. NSLogError(@"DDLogFileInfo: Error renaming file (%@): %@", self.fileName, error);
  926. }
  927. filePath = newFilePath;
  928. [self reset];
  929. }
  930. }
  931. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  932. #pragma mark Attribute Management
  933. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  934. #if TARGET_IPHONE_SIMULATOR
  935. // Extended attributes don't work properly on the simulator.
  936. // So we have to use a less attractive alternative.
  937. // See full explanation in the header file.
  938. - (BOOL)hasExtensionAttributeWithName:(NSString *)attrName {
  939. // This method is only used on the iPhone simulator, where normal extended attributes are broken.
  940. // See full explanation in the header file.
  941. // Split the file name into components. File name may have various format, but generally
  942. // structure is same:
  943. //
  944. // <name part>.<extension part> and <name part>.archived.<extension part>
  945. // or
  946. // <name part> and <name part>.archived
  947. //
  948. // So we want to search for the attrName in the components (ignoring the first array index).
  949. NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
  950. // Watch out for file names without an extension
  951. for (NSUInteger i = 1; i < components.count; i++) {
  952. NSString *attr = components[i];
  953. if ([attrName isEqualToString:attr]) {
  954. return YES;
  955. }
  956. }
  957. return NO;
  958. }
  959. - (void)addExtensionAttributeWithName:(NSString *)attrName {
  960. // This method is only used on the iPhone simulator, where normal extended attributes are broken.
  961. // See full explanation in the header file.
  962. if ([attrName length] == 0) {
  963. return;
  964. }
  965. // Example:
  966. // attrName = "archived"
  967. //
  968. // "mylog.txt" -> "mylog.archived.txt"
  969. // "mylog" -> "mylog.archived"
  970. NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
  971. NSUInteger count = [components count];
  972. NSUInteger estimatedNewLength = [[self fileName] length] + [attrName length] + 1;
  973. NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength];
  974. if (count > 0) {
  975. [newFileName appendString:components.firstObject];
  976. }
  977. NSString *lastExt = @"";
  978. NSUInteger i;
  979. for (i = 1; i < count; i++) {
  980. NSString *attr = components[i];
  981. if ([attr length] == 0) {
  982. continue;
  983. }
  984. if ([attrName isEqualToString:attr]) {
  985. // Extension attribute already exists in file name
  986. return;
  987. }
  988. if ([lastExt length] > 0) {
  989. [newFileName appendFormat:@".%@", lastExt];
  990. }
  991. lastExt = attr;
  992. }
  993. [newFileName appendFormat:@".%@", attrName];
  994. if ([lastExt length] > 0) {
  995. [newFileName appendFormat:@".%@", lastExt];
  996. }
  997. [self renameFile:newFileName];
  998. }
  999. - (void)removeExtensionAttributeWithName:(NSString *)attrName {
  1000. // This method is only used on the iPhone simulator, where normal extended attributes are broken.
  1001. // See full explanation in the header file.
  1002. if ([attrName length] == 0) {
  1003. return;
  1004. }
  1005. // Example:
  1006. // attrName = "archived"
  1007. //
  1008. // "mylog.archived.txt" -> "mylog.txt"
  1009. // "mylog.archived" -> "mylog"
  1010. NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
  1011. NSUInteger count = [components count];
  1012. NSUInteger estimatedNewLength = [[self fileName] length];
  1013. NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength];
  1014. if (count > 0) {
  1015. [newFileName appendString:components.firstObject];
  1016. }
  1017. BOOL found = NO;
  1018. NSUInteger i;
  1019. for (i = 1; i < count; i++) {
  1020. NSString *attr = components[i];
  1021. if ([attrName isEqualToString:attr]) {
  1022. found = YES;
  1023. } else {
  1024. [newFileName appendFormat:@".%@", attr];
  1025. }
  1026. }
  1027. if (found) {
  1028. [self renameFile:newFileName];
  1029. }
  1030. }
  1031. #else /* if TARGET_IPHONE_SIMULATOR */
  1032. - (BOOL)hasExtendedAttributeWithName:(NSString *)attrName {
  1033. const char *path = [filePath UTF8String];
  1034. const char *name = [attrName UTF8String];
  1035. ssize_t result = getxattr(path, name, NULL, 0, 0, 0);
  1036. return (result >= 0);
  1037. }
  1038. - (void)addExtendedAttributeWithName:(NSString *)attrName {
  1039. const char *path = [filePath UTF8String];
  1040. const char *name = [attrName UTF8String];
  1041. int result = setxattr(path, name, NULL, 0, 0, 0);
  1042. if (result < 0) {
  1043. NSLogError(@"DDLogFileInfo: setxattr(%@, %@): error = %s",
  1044. attrName,
  1045. filePath,
  1046. strerror(errno));
  1047. }
  1048. }
  1049. - (void)removeExtendedAttributeWithName:(NSString *)attrName {
  1050. const char *path = [filePath UTF8String];
  1051. const char *name = [attrName UTF8String];
  1052. int result = removexattr(path, name, 0);
  1053. if (result < 0 && errno != ENOATTR) {
  1054. NSLogError(@"DDLogFileInfo: removexattr(%@, %@): error = %s",
  1055. attrName,
  1056. self.fileName,
  1057. strerror(errno));
  1058. }
  1059. }
  1060. #endif /* if TARGET_IPHONE_SIMULATOR */
  1061. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1062. #pragma mark Comparisons
  1063. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1064. - (BOOL)isEqual:(id)object {
  1065. if ([object isKindOfClass:[self class]]) {
  1066. DDLogFileInfo *another = (DDLogFileInfo *)object;
  1067. return [filePath isEqualToString:[another filePath]];
  1068. }
  1069. return NO;
  1070. }
  1071. -(NSUInteger)hash {
  1072. return [filePath hash];
  1073. }
  1074. - (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another {
  1075. NSDate *us = [self creationDate];
  1076. NSDate *them = [another creationDate];
  1077. NSComparisonResult result = [us compare:them];
  1078. if (result == NSOrderedAscending) {
  1079. return NSOrderedDescending;
  1080. }
  1081. if (result == NSOrderedDescending) {
  1082. return NSOrderedAscending;
  1083. }
  1084. return NSOrderedSame;
  1085. }
  1086. - (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another {
  1087. NSDate *us = [self modificationDate];
  1088. NSDate *them = [another modificationDate];
  1089. NSComparisonResult result = [us compare:them];
  1090. if (result == NSOrderedAscending) {
  1091. return NSOrderedDescending;
  1092. }
  1093. if (result == NSOrderedDescending) {
  1094. return NSOrderedAscending;
  1095. }
  1096. return NSOrderedSame;
  1097. }
  1098. @end
  1099. #if TARGET_OS_IPHONE
  1100. /**
  1101. * When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
  1102. *
  1103. * But in case if app is able to launch from background we need to have an ability to open log file any time we
  1104. * want (even if device is locked). Thats why that attribute have to be changed to
  1105. * NSFileProtectionCompleteUntilFirstUserAuthentication.
  1106. */
  1107. BOOL doesAppRunInBackground() {
  1108. BOOL answer = NO;
  1109. NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
  1110. for (NSString *mode in backgroundModes) {
  1111. if (mode.length > 0) {
  1112. answer = YES;
  1113. break;
  1114. }
  1115. }
  1116. return answer;
  1117. }
  1118. #endif