You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1528 lines
51 KiB

  1. // Software License Agreement (BSD License)
  2. //
  3. // Copyright (c) 2010-2015, 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. NSString *_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:(NSString *)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. /**
  220. * Default log file name is "<bundle identifier> <date> <time>.log".
  221. * Example: MobileSafari 2013-12-03 17-14.log
  222. *
  223. * You can change it by overriding newLogFileName and isLogFile: methods.
  224. **/
  225. - (BOOL)isLogFile:(NSString *)fileName {
  226. NSString *appName = [self applicationName];
  227. BOOL hasProperPrefix = [fileName hasPrefix:appName];
  228. BOOL hasProperSuffix = [fileName hasSuffix:@".log"];
  229. BOOL hasProperDate = NO;
  230. if (hasProperPrefix && hasProperSuffix) {
  231. NSUInteger lengthOfMiddle = fileName.length - appName.length - @".log".length;
  232. // Date string should have at least 16 characters - " 2013-12-03 17-14"
  233. if (lengthOfMiddle >= 17) {
  234. NSRange range = NSMakeRange(appName.length, lengthOfMiddle);
  235. NSString *middle = [fileName substringWithRange:range];
  236. NSArray *components = [middle componentsSeparatedByString:@" "];
  237. // When creating logfile if there is existing file with the same name, we append attemp number at the end.
  238. // Thats why here we can have three or four components. For details see createNewLogFile method.
  239. //
  240. // Components:
  241. // "", "2013-12-03", "17-14"
  242. // or
  243. // "", "2013-12-03", "17-14", "1"
  244. if (components.count == 3 || components.count == 4) {
  245. NSString *dateString = [NSString stringWithFormat:@"%@ %@", components[1], components[2]];
  246. NSDateFormatter *dateFormatter = [self logFileDateFormatter];
  247. NSDate *date = [dateFormatter dateFromString:dateString];
  248. if (date) {
  249. hasProperDate = YES;
  250. }
  251. }
  252. }
  253. }
  254. return (hasProperPrefix && hasProperDate && hasProperSuffix);
  255. }
  256. - (NSDateFormatter *)logFileDateFormatter {
  257. NSMutableDictionary *dictionary = [[NSThread currentThread]
  258. threadDictionary];
  259. NSString *dateFormat = @"yyyy'-'MM'-'dd' 'HH'-'mm'";
  260. NSString *key = [NSString stringWithFormat:@"logFileDateFormatter.%@", dateFormat];
  261. NSDateFormatter *dateFormatter = dictionary[key];
  262. if (dateFormatter == nil) {
  263. dateFormatter = [[NSDateFormatter alloc] init];
  264. [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
  265. [dateFormatter setDateFormat:dateFormat];
  266. [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
  267. dictionary[key] = dateFormatter;
  268. }
  269. return dateFormatter;
  270. }
  271. /**
  272. * Returns an array of NSString objects,
  273. * each of which is the filePath to an existing log file on disk.
  274. **/
  275. - (NSArray *)unsortedLogFilePaths {
  276. NSString *logsDirectory = [self logsDirectory];
  277. NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:nil];
  278. NSMutableArray *unsortedLogFilePaths = [NSMutableArray arrayWithCapacity:[fileNames count]];
  279. for (NSString *fileName in fileNames) {
  280. // Filter out any files that aren't log files. (Just for extra safety)
  281. #if TARGET_IPHONE_SIMULATOR
  282. // In case of iPhone simulator there can be 'archived' extension. isLogFile:
  283. // method knows nothing about it. Thus removing it for this method.
  284. //
  285. // See full explanation in the header file.
  286. NSString *theFileName = [fileName stringByReplacingOccurrencesOfString:@".archived"
  287. withString:@""];
  288. if ([self isLogFile:theFileName])
  289. #else
  290. if ([self isLogFile:fileName])
  291. #endif
  292. {
  293. NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName];
  294. [unsortedLogFilePaths addObject:filePath];
  295. }
  296. }
  297. return unsortedLogFilePaths;
  298. }
  299. /**
  300. * Returns an array of NSString objects,
  301. * each of which is the fileName of an existing log file on disk.
  302. **/
  303. - (NSArray *)unsortedLogFileNames {
  304. NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths];
  305. NSMutableArray *unsortedLogFileNames = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
  306. for (NSString *filePath in unsortedLogFilePaths) {
  307. [unsortedLogFileNames addObject:[filePath lastPathComponent]];
  308. }
  309. return unsortedLogFileNames;
  310. }
  311. /**
  312. * Returns an array of DDLogFileInfo objects,
  313. * each representing an existing log file on disk,
  314. * and containing important information about the log file such as it's modification date and size.
  315. **/
  316. - (NSArray *)unsortedLogFileInfos {
  317. NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths];
  318. NSMutableArray *unsortedLogFileInfos = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
  319. for (NSString *filePath in unsortedLogFilePaths) {
  320. DDLogFileInfo *logFileInfo = [[DDLogFileInfo alloc] initWithFilePath:filePath];
  321. [unsortedLogFileInfos addObject:logFileInfo];
  322. }
  323. return unsortedLogFileInfos;
  324. }
  325. /**
  326. * Just like the unsortedLogFilePaths method, but sorts the array.
  327. * The items in the array are sorted by creation date.
  328. * The first item in the array will be the most recently created log file.
  329. **/
  330. - (NSArray *)sortedLogFilePaths {
  331. NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
  332. NSMutableArray *sortedLogFilePaths = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
  333. for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) {
  334. [sortedLogFilePaths addObject:[logFileInfo filePath]];
  335. }
  336. return sortedLogFilePaths;
  337. }
  338. /**
  339. * Just like the unsortedLogFileNames method, but sorts the array.
  340. * The items in the array are sorted by creation date.
  341. * The first item in the array will be the most recently created log file.
  342. **/
  343. - (NSArray *)sortedLogFileNames {
  344. NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
  345. NSMutableArray *sortedLogFileNames = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
  346. for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) {
  347. [sortedLogFileNames addObject:[logFileInfo fileName]];
  348. }
  349. return sortedLogFileNames;
  350. }
  351. /**
  352. * Just like the unsortedLogFileInfos method, but sorts the array.
  353. * The items in the array are sorted by creation date.
  354. * The first item in the array will be the most recently created log file.
  355. **/
  356. - (NSArray *)sortedLogFileInfos {
  357. return [[self unsortedLogFileInfos] sortedArrayUsingSelector:@selector(reverseCompareByCreationDate:)];
  358. }
  359. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  360. #pragma mark Creation
  361. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  362. /**
  363. * Generates log file name with default format "<bundle identifier> <date> <time>.log"
  364. * Example: MobileSafari 2013-12-03 17-14.log
  365. *
  366. * You can change it by overriding newLogFileName and isLogFile: methods.
  367. **/
  368. - (NSString *)newLogFileName {
  369. NSString *appName = [self applicationName];
  370. NSDateFormatter *dateFormatter = [self logFileDateFormatter];
  371. NSString *formattedDate = [dateFormatter stringFromDate:[NSDate date]];
  372. return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate];
  373. }
  374. /**
  375. * Generates a new unique log file path, and creates the corresponding log file.
  376. **/
  377. - (NSString *)createNewLogFile {
  378. NSString *fileName = [self newLogFileName];
  379. NSString *logsDirectory = [self logsDirectory];
  380. NSUInteger attempt = 1;
  381. do {
  382. NSString *actualFileName = fileName;
  383. if (attempt > 1) {
  384. NSString *extension = [actualFileName pathExtension];
  385. actualFileName = [actualFileName stringByDeletingPathExtension];
  386. actualFileName = [actualFileName stringByAppendingFormat:@" %lu", (unsigned long)attempt];
  387. if (extension.length) {
  388. actualFileName = [actualFileName stringByAppendingPathExtension:extension];
  389. }
  390. }
  391. NSString *filePath = [logsDirectory stringByAppendingPathComponent:actualFileName];
  392. if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
  393. NSLogVerbose(@"DDLogFileManagerDefault: Creating new log file: %@", actualFileName);
  394. NSDictionary *attributes = nil;
  395. #if TARGET_OS_IPHONE
  396. // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
  397. //
  398. // But in case if app is able to launch from background we need to have an ability to open log file any time we
  399. // want (even if device is locked). Thats why that attribute have to be changed to
  400. // NSFileProtectionCompleteUntilFirstUserAuthentication.
  401. NSString *key = _defaultFileProtectionLevel ? :
  402. (doesAppRunInBackground() ? NSFileProtectionCompleteUntilFirstUserAuthentication : NSFileProtectionCompleteUnlessOpen);
  403. attributes = @{
  404. NSFileProtectionKey: key
  405. };
  406. #endif
  407. [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:attributes];
  408. // Since we just created a new log file, we may need to delete some old log files
  409. [self deleteOldLogFiles];
  410. return filePath;
  411. } else {
  412. attempt++;
  413. }
  414. } while (YES);
  415. }
  416. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  417. #pragma mark Utility
  418. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  419. - (NSString *)applicationName {
  420. static NSString *_appName;
  421. static dispatch_once_t onceToken;
  422. dispatch_once(&onceToken, ^{
  423. _appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"];
  424. if (!_appName) {
  425. _appName = [[NSProcessInfo processInfo] processName];
  426. }
  427. if (!_appName) {
  428. _appName = @"";
  429. }
  430. });
  431. return _appName;
  432. }
  433. @end
  434. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  435. #pragma mark -
  436. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  437. @interface DDLogFileFormatterDefault () {
  438. NSDateFormatter *_dateFormatter;
  439. }
  440. @end
  441. @implementation DDLogFileFormatterDefault
  442. - (instancetype)init {
  443. return [self initWithDateFormatter:nil];
  444. }
  445. - (instancetype)initWithDateFormatter:(NSDateFormatter *)aDateFormatter {
  446. if ((self = [super init])) {
  447. if (aDateFormatter) {
  448. _dateFormatter = aDateFormatter;
  449. } else {
  450. _dateFormatter = [[NSDateFormatter alloc] init];
  451. [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; // 10.4+ style
  452. [_dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"];
  453. }
  454. }
  455. return self;
  456. }
  457. - (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
  458. NSString *dateAndTime = [_dateFormatter stringFromDate:(logMessage->_timestamp)];
  459. return [NSString stringWithFormat:@"%@ %@", dateAndTime, logMessage->_message];
  460. }
  461. @end
  462. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  463. #pragma mark -
  464. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  465. @interface DDFileLogger () {
  466. __strong id <DDLogFileManager> _logFileManager;
  467. DDLogFileInfo *_currentLogFileInfo;
  468. NSFileHandle *_currentLogFileHandle;
  469. dispatch_source_t _currentLogFileVnode;
  470. dispatch_source_t _rollingTimer;
  471. unsigned long long _maximumFileSize;
  472. NSTimeInterval _rollingFrequency;
  473. }
  474. - (void)rollLogFileNow;
  475. - (void)maybeRollLogFileDueToAge;
  476. - (void)maybeRollLogFileDueToSize;
  477. @end
  478. @implementation DDFileLogger
  479. - (instancetype)init {
  480. DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init];
  481. return [self initWithLogFileManager:defaultLogFileManager];
  482. }
  483. - (instancetype)initWithLogFileManager:(id <DDLogFileManager>)aLogFileManager {
  484. if ((self = [super init])) {
  485. _maximumFileSize = kDDDefaultLogMaxFileSize;
  486. _rollingFrequency = kDDDefaultLogRollingFrequency;
  487. _automaticallyAppendNewlineForCustomFormatters = YES;
  488. logFileManager = aLogFileManager;
  489. self.logFormatter = [DDLogFileFormatterDefault new];
  490. }
  491. return self;
  492. }
  493. - (void)dealloc {
  494. [_currentLogFileHandle synchronizeFile];
  495. [_currentLogFileHandle closeFile];
  496. if (_currentLogFileVnode) {
  497. dispatch_source_cancel(_currentLogFileVnode);
  498. _currentLogFileVnode = NULL;
  499. }
  500. if (_rollingTimer) {
  501. dispatch_source_cancel(_rollingTimer);
  502. _rollingTimer = NULL;
  503. }
  504. }
  505. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  506. #pragma mark Properties
  507. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  508. @synthesize logFileManager;
  509. - (unsigned long long)maximumFileSize {
  510. __block unsigned long long result;
  511. dispatch_block_t block = ^{
  512. result = _maximumFileSize;
  513. };
  514. // The design of this method is taken from the DDAbstractLogger implementation.
  515. // For extensive documentation please refer to the DDAbstractLogger implementation.
  516. // Note: The internal implementation MUST access the maximumFileSize variable directly,
  517. // This method is designed explicitly for external access.
  518. //
  519. // Using "self." syntax to go through this method will cause immediate deadlock.
  520. // This is the intended result. Fix it by accessing the ivar directly.
  521. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  522. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  523. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  524. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  525. dispatch_sync(globalLoggingQueue, ^{
  526. dispatch_sync(self.loggerQueue, block);
  527. });
  528. return result;
  529. }
  530. - (void)setMaximumFileSize:(unsigned long long)newMaximumFileSize {
  531. dispatch_block_t block = ^{
  532. @autoreleasepool {
  533. _maximumFileSize = newMaximumFileSize;
  534. [self maybeRollLogFileDueToSize];
  535. }
  536. };
  537. // The design of this method is taken from the DDAbstractLogger implementation.
  538. // For extensive documentation please refer to the DDAbstractLogger implementation.
  539. // Note: The internal implementation MUST access the maximumFileSize variable directly,
  540. // This method is designed explicitly for external access.
  541. //
  542. // Using "self." syntax to go through this method will cause immediate deadlock.
  543. // This is the intended result. Fix it by accessing the ivar directly.
  544. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  545. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  546. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  547. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  548. dispatch_async(globalLoggingQueue, ^{
  549. dispatch_async(self.loggerQueue, block);
  550. });
  551. }
  552. - (NSTimeInterval)rollingFrequency {
  553. __block NSTimeInterval result;
  554. dispatch_block_t block = ^{
  555. result = _rollingFrequency;
  556. };
  557. // The design of this method is taken from the DDAbstractLogger implementation.
  558. // For extensive documentation please refer to the DDAbstractLogger implementation.
  559. // Note: The internal implementation should access the rollingFrequency variable directly,
  560. // This method is designed explicitly for external access.
  561. //
  562. // Using "self." syntax to go through this method will cause immediate deadlock.
  563. // This is the intended result. Fix it by accessing the ivar directly.
  564. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  565. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  566. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  567. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  568. dispatch_sync(globalLoggingQueue, ^{
  569. dispatch_sync(self.loggerQueue, block);
  570. });
  571. return result;
  572. }
  573. - (void)setRollingFrequency:(NSTimeInterval)newRollingFrequency {
  574. dispatch_block_t block = ^{
  575. @autoreleasepool {
  576. _rollingFrequency = newRollingFrequency;
  577. [self maybeRollLogFileDueToAge];
  578. }
  579. };
  580. // The design of this method is taken from the DDAbstractLogger implementation.
  581. // For extensive documentation please refer to the DDAbstractLogger implementation.
  582. // Note: The internal implementation should access the rollingFrequency variable directly,
  583. // This method is designed explicitly for external access.
  584. //
  585. // Using "self." syntax to go through this method will cause immediate deadlock.
  586. // This is the intended result. Fix it by accessing the ivar directly.
  587. // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
  588. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  589. NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
  590. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  591. dispatch_async(globalLoggingQueue, ^{
  592. dispatch_async(self.loggerQueue, block);
  593. });
  594. }
  595. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  596. #pragma mark File Rolling
  597. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  598. - (void)scheduleTimerToRollLogFileDueToAge {
  599. if (_rollingTimer) {
  600. dispatch_source_cancel(_rollingTimer);
  601. _rollingTimer = NULL;
  602. }
  603. if (_currentLogFileInfo == nil || _rollingFrequency <= 0.0) {
  604. return;
  605. }
  606. NSDate *logFileCreationDate = [_currentLogFileInfo creationDate];
  607. NSTimeInterval ti = [logFileCreationDate timeIntervalSinceReferenceDate];
  608. ti += _rollingFrequency;
  609. NSDate *logFileRollingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ti];
  610. NSLogVerbose(@"DDFileLogger: scheduleTimerToRollLogFileDueToAge");
  611. NSLogVerbose(@"DDFileLogger: logFileCreationDate: %@", logFileCreationDate);
  612. NSLogVerbose(@"DDFileLogger: logFileRollingDate : %@", logFileRollingDate);
  613. _rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue);
  614. dispatch_source_set_event_handler(_rollingTimer, ^{ @autoreleasepool {
  615. [self maybeRollLogFileDueToAge];
  616. } });
  617. #if !OS_OBJECT_USE_OBJC
  618. dispatch_source_t theRollingTimer = _rollingTimer;
  619. dispatch_source_set_cancel_handler(_rollingTimer, ^{
  620. dispatch_release(theRollingTimer);
  621. });
  622. #endif
  623. uint64_t delay = (uint64_t)([logFileRollingDate timeIntervalSinceNow] * NSEC_PER_SEC);
  624. dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, delay);
  625. dispatch_source_set_timer(_rollingTimer, fireTime, DISPATCH_TIME_FOREVER, 1.0);
  626. dispatch_resume(_rollingTimer);
  627. }
  628. - (void)rollLogFile {
  629. [self rollLogFileWithCompletionBlock:nil];
  630. }
  631. - (void)rollLogFileWithCompletionBlock:(void (^)())completionBlock {
  632. // This method is public.
  633. // We need to execute the rolling on our logging thread/queue.
  634. dispatch_block_t block = ^{
  635. @autoreleasepool {
  636. [self rollLogFileNow];
  637. if (completionBlock) {
  638. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  639. completionBlock();
  640. });
  641. }
  642. }
  643. };
  644. // The design of this method is taken from the DDAbstractLogger implementation.
  645. // For extensive documentation please refer to the DDAbstractLogger implementation.
  646. if ([self isOnInternalLoggerQueue]) {
  647. block();
  648. } else {
  649. dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
  650. NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
  651. dispatch_async(globalLoggingQueue, ^{
  652. dispatch_async(self.loggerQueue, block);
  653. });
  654. }
  655. }
  656. - (void)rollLogFileNow {
  657. NSLogVerbose(@"DDFileLogger: rollLogFileNow");
  658. if (_currentLogFileHandle == nil) {
  659. return;
  660. }
  661. [_currentLogFileHandle synchronizeFile];
  662. [_currentLogFileHandle closeFile];
  663. _currentLogFileHandle = nil;
  664. _currentLogFileInfo.isArchived = YES;
  665. if ([logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]) {
  666. [logFileManager didRollAndArchiveLogFile:(_currentLogFileInfo.filePath)];
  667. }
  668. _currentLogFileInfo = nil;
  669. if (_currentLogFileVnode) {
  670. dispatch_source_cancel(_currentLogFileVnode);
  671. _currentLogFileVnode = NULL;
  672. }
  673. if (_rollingTimer) {
  674. dispatch_source_cancel(_rollingTimer);
  675. _rollingTimer = NULL;
  676. }
  677. }
  678. - (void)maybeRollLogFileDueToAge {
  679. if (_rollingFrequency > 0.0 && _currentLogFileInfo.age >= _rollingFrequency) {
  680. NSLogVerbose(@"DDFileLogger: Rolling log file due to age...");
  681. [self rollLogFileNow];
  682. } else {
  683. [self scheduleTimerToRollLogFileDueToAge];
  684. }
  685. }
  686. - (void)maybeRollLogFileDueToSize {
  687. // This method is called from logMessage.
  688. // Keep it FAST.
  689. // Note: Use direct access to maximumFileSize variable.
  690. // We specifically wrote our own getter/setter method to allow us to do this (for performance reasons).
  691. if (_maximumFileSize > 0) {
  692. unsigned long long fileSize = [_currentLogFileHandle offsetInFile];
  693. if (fileSize >= _maximumFileSize) {
  694. NSLogVerbose(@"DDFileLogger: Rolling log file due to size (%qu)...", fileSize);
  695. [self rollLogFileNow];
  696. }
  697. }
  698. }
  699. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  700. #pragma mark File Logging
  701. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  702. /**
  703. * Returns the log file that should be used.
  704. * If there is an existing log file that is suitable,
  705. * within the constraints of maximumFileSize and rollingFrequency, then it is returned.
  706. *
  707. * Otherwise a new file is created and returned.
  708. **/
  709. - (DDLogFileInfo *)currentLogFileInfo {
  710. if (_currentLogFileInfo == nil) {
  711. NSArray *sortedLogFileInfos = [logFileManager sortedLogFileInfos];
  712. if ([sortedLogFileInfos count] > 0) {
  713. DDLogFileInfo *mostRecentLogFileInfo = sortedLogFileInfos[0];
  714. BOOL shouldArchiveMostRecent = NO;
  715. if (mostRecentLogFileInfo.isArchived) {
  716. shouldArchiveMostRecent = NO;
  717. } else if (_maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= _maximumFileSize) {
  718. shouldArchiveMostRecent = YES;
  719. } else if (_rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= _rollingFrequency) {
  720. shouldArchiveMostRecent = YES;
  721. }
  722. #if TARGET_OS_IPHONE
  723. // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
  724. //
  725. // But in case if app is able to launch from background we need to have an ability to open log file any time we
  726. // want (even if device is locked). Thats why that attribute have to be changed to
  727. // NSFileProtectionCompleteUntilFirstUserAuthentication.
  728. //
  729. // If previous log was created when app wasn't running in background, but now it is - we archive it and create
  730. // a new one.
  731. //
  732. // If user has owerwritten to NSFileProtectionNone there is no neeed to create a new one.
  733. if (!_doNotReuseLogFiles && doesAppRunInBackground()) {
  734. NSString *key = mostRecentLogFileInfo.fileAttributes[NSFileProtectionKey];
  735. if ([key length] > 0 && !([key isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication] || [key isEqualToString:NSFileProtectionNone])) {
  736. shouldArchiveMostRecent = YES;
  737. }
  738. }
  739. #endif
  740. if (!_doNotReuseLogFiles && !mostRecentLogFileInfo.isArchived && !shouldArchiveMostRecent) {
  741. NSLogVerbose(@"DDFileLogger: Resuming logging with file %@", mostRecentLogFileInfo.fileName);
  742. _currentLogFileInfo = mostRecentLogFileInfo;
  743. } else {
  744. if (shouldArchiveMostRecent) {
  745. mostRecentLogFileInfo.isArchived = YES;
  746. if ([logFileManager respondsToSelector:@selector(didArchiveLogFile:)]) {
  747. [logFileManager didArchiveLogFile:(mostRecentLogFileInfo.filePath)];
  748. }
  749. }
  750. }
  751. }
  752. if (_currentLogFileInfo == nil) {
  753. NSString *currentLogFilePath = [logFileManager createNewLogFile];
  754. _currentLogFileInfo = [[DDLogFileInfo alloc] initWithFilePath:currentLogFilePath];
  755. }
  756. }
  757. return _currentLogFileInfo;
  758. }
  759. - (NSFileHandle *)currentLogFileHandle {
  760. if (_currentLogFileHandle == nil) {
  761. NSString *logFilePath = [[self currentLogFileInfo] filePath];
  762. _currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
  763. [_currentLogFileHandle seekToEndOfFile];
  764. if (_currentLogFileHandle) {
  765. [self scheduleTimerToRollLogFileDueToAge];
  766. // Here we are monitoring the log file. In case if it would be deleted ormoved
  767. // somewhere we want to roll it and use a new one.
  768. _currentLogFileVnode = dispatch_source_create(
  769. DISPATCH_SOURCE_TYPE_VNODE,
  770. [_currentLogFileHandle fileDescriptor],
  771. DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME,
  772. self.loggerQueue
  773. );
  774. dispatch_source_set_event_handler(_currentLogFileVnode, ^{ @autoreleasepool {
  775. NSLogInfo(@"DDFileLogger: Current logfile was moved. Rolling it and creating a new one");
  776. [self rollLogFileNow];
  777. } });
  778. #if !OS_OBJECT_USE_OBJC
  779. dispatch_source_t vnode = _currentLogFileVnode;
  780. dispatch_source_set_cancel_handler(_currentLogFileVnode, ^{
  781. dispatch_release(vnode);
  782. });
  783. #endif
  784. dispatch_resume(_currentLogFileVnode);
  785. }
  786. }
  787. return _currentLogFileHandle;
  788. }
  789. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  790. #pragma mark DDLogger Protocol
  791. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  792. static int exception_count = 0;
  793. - (void)logMessage:(DDLogMessage *)logMessage {
  794. NSString *message = logMessage->_message;
  795. BOOL isFormatted = NO;
  796. if (_logFormatter) {
  797. message = [_logFormatter formatLogMessage:logMessage];
  798. isFormatted = message != logMessage->_message;
  799. }
  800. if (message) {
  801. if ((!isFormatted || _automaticallyAppendNewlineForCustomFormatters) &&
  802. (![message hasSuffix:@"\n"])) {
  803. message = [message stringByAppendingString:@"\n"];
  804. }
  805. NSData *logData = [message dataUsingEncoding:NSUTF8StringEncoding];
  806. @try {
  807. [[self currentLogFileHandle] writeData:logData];
  808. [self maybeRollLogFileDueToSize];
  809. } @catch (NSException *exception) {
  810. exception_count++;
  811. if (exception_count <= 10) {
  812. NSLogError(@"DDFileLogger.logMessage: %@", exception);
  813. if (exception_count == 10) {
  814. NSLogError(@"DDFileLogger.logMessage: Too many exceptions -- will not log any more of them.");
  815. }
  816. }
  817. }
  818. }
  819. }
  820. - (void)willRemoveLogger {
  821. // If you override me be sure to invoke [super willRemoveLogger];
  822. [self rollLogFileNow];
  823. }
  824. - (NSString *)loggerName {
  825. return @"cocoa.lumberjack.fileLogger";
  826. }
  827. @end
  828. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  829. #pragma mark -
  830. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  831. #if TARGET_IPHONE_SIMULATOR
  832. static NSString * const kDDXAttrArchivedName = @"archived";
  833. #else
  834. static NSString * const kDDXAttrArchivedName = @"lumberjack.log.archived";
  835. #endif
  836. @interface DDLogFileInfo () {
  837. __strong NSString *_filePath;
  838. __strong NSString *_fileName;
  839. __strong NSDictionary *_fileAttributes;
  840. __strong NSDate *_creationDate;
  841. __strong NSDate *_modificationDate;
  842. unsigned long long _fileSize;
  843. }
  844. @end
  845. @implementation DDLogFileInfo
  846. @synthesize filePath;
  847. @dynamic fileName;
  848. @dynamic fileAttributes;
  849. @dynamic creationDate;
  850. @dynamic modificationDate;
  851. @dynamic fileSize;
  852. @dynamic age;
  853. @dynamic isArchived;
  854. #pragma mark Lifecycle
  855. + (instancetype)logFileWithPath:(NSString *)aFilePath {
  856. return [[self alloc] initWithFilePath:aFilePath];
  857. }
  858. - (instancetype)initWithFilePath:(NSString *)aFilePath {
  859. if ((self = [super init])) {
  860. filePath = [aFilePath copy];
  861. }
  862. return self;
  863. }
  864. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  865. #pragma mark Standard Info
  866. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  867. - (NSDictionary *)fileAttributes {
  868. if (_fileAttributes == nil) {
  869. _fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
  870. }
  871. return _fileAttributes;
  872. }
  873. - (NSString *)fileName {
  874. if (_fileName == nil) {
  875. _fileName = [filePath lastPathComponent];
  876. }
  877. return _fileName;
  878. }
  879. - (NSDate *)modificationDate {
  880. if (_modificationDate == nil) {
  881. _modificationDate = self.fileAttributes[NSFileModificationDate];
  882. }
  883. return _modificationDate;
  884. }
  885. - (NSDate *)creationDate {
  886. if (_creationDate == nil) {
  887. _creationDate = self.fileAttributes[NSFileCreationDate];
  888. }
  889. return _creationDate;
  890. }
  891. - (unsigned long long)fileSize {
  892. if (_fileSize == 0) {
  893. _fileSize = [self.fileAttributes[NSFileSize] unsignedLongLongValue];
  894. }
  895. return _fileSize;
  896. }
  897. - (NSTimeInterval)age {
  898. return [[self creationDate] timeIntervalSinceNow] * -1.0;
  899. }
  900. - (NSString *)description {
  901. return [@{ @"filePath": self.filePath ? : @"",
  902. @"fileName": self.fileName ? : @"",
  903. @"fileAttributes": self.fileAttributes ? : @"",
  904. @"creationDate": self.creationDate ? : @"",
  905. @"modificationDate": self.modificationDate ? : @"",
  906. @"fileSize": @(self.fileSize),
  907. @"age": @(self.age),
  908. @"isArchived": @(self.isArchived) } description];
  909. }
  910. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  911. #pragma mark Archiving
  912. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  913. - (BOOL)isArchived {
  914. #if TARGET_IPHONE_SIMULATOR
  915. // Extended attributes don't work properly on the simulator.
  916. // So we have to use a less attractive alternative.
  917. // See full explanation in the header file.
  918. return [self hasExtensionAttributeWithName:kDDXAttrArchivedName];
  919. #else
  920. return [self hasExtendedAttributeWithName:kDDXAttrArchivedName];
  921. #endif
  922. }
  923. - (void)setIsArchived:(BOOL)flag {
  924. #if TARGET_IPHONE_SIMULATOR
  925. // Extended attributes don't work properly on the simulator.
  926. // So we have to use a less attractive alternative.
  927. // See full explanation in the header file.
  928. if (flag) {
  929. [self addExtensionAttributeWithName:kDDXAttrArchivedName];
  930. } else {
  931. [self removeExtensionAttributeWithName:kDDXAttrArchivedName];
  932. }
  933. #else
  934. if (flag) {
  935. [self addExtendedAttributeWithName:kDDXAttrArchivedName];
  936. } else {
  937. [self removeExtendedAttributeWithName:kDDXAttrArchivedName];
  938. }
  939. #endif
  940. }
  941. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  942. #pragma mark Changes
  943. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  944. - (void)reset {
  945. _fileName = nil;
  946. _fileAttributes = nil;
  947. _creationDate = nil;
  948. _modificationDate = nil;
  949. }
  950. - (void)renameFile:(NSString *)newFileName {
  951. // This method is only used on the iPhone simulator, where normal extended attributes are broken.
  952. // See full explanation in the header file.
  953. if (![newFileName isEqualToString:[self fileName]]) {
  954. NSString *fileDir = [filePath stringByDeletingLastPathComponent];
  955. NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName];
  956. NSLogVerbose(@"DDLogFileInfo: Renaming file: '%@' -> '%@'", self.fileName, newFileName);
  957. NSError *error = nil;
  958. if ([[NSFileManager defaultManager] fileExistsAtPath:newFilePath] &&
  959. ![[NSFileManager defaultManager] removeItemAtPath:newFilePath error:&error]) {
  960. NSLogError(@"DDLogFileInfo: Error deleting archive (%@): %@", self.fileName, error);
  961. }
  962. if (![[NSFileManager defaultManager] moveItemAtPath:filePath toPath:newFilePath error:&error]) {
  963. NSLogError(@"DDLogFileInfo: Error renaming file (%@): %@", self.fileName, error);
  964. }
  965. filePath = newFilePath;
  966. [self reset];
  967. }
  968. }
  969. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  970. #pragma mark Attribute Management
  971. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  972. #if TARGET_IPHONE_SIMULATOR
  973. // Extended attributes don't work properly on the simulator.
  974. // So we have to use a less attractive alternative.
  975. // See full explanation in the header file.
  976. - (BOOL)hasExtensionAttributeWithName:(NSString *)attrName {
  977. // This method is only used on the iPhone simulator, where normal extended attributes are broken.
  978. // See full explanation in the header file.
  979. // Split the file name into components. File name may have various format, but generally
  980. // structure is same:
  981. //
  982. // <name part>.<extension part> and <name part>.archived.<extension part>
  983. // or
  984. // <name part> and <name part>.archived
  985. //
  986. // So we want to search for the attrName in the components (ignoring the first array index).
  987. NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
  988. // Watch out for file names without an extension
  989. for (NSUInteger i = 1; i < components.count; i++) {
  990. NSString *attr = components[i];
  991. if ([attrName isEqualToString:attr]) {
  992. return YES;
  993. }
  994. }
  995. return NO;
  996. }
  997. - (void)addExtensionAttributeWithName:(NSString *)attrName {
  998. // This method is only used on the iPhone simulator, where normal extended attributes are broken.
  999. // See full explanation in the header file.
  1000. if ([attrName length] == 0) {
  1001. return;
  1002. }
  1003. // Example:
  1004. // attrName = "archived"
  1005. //
  1006. // "mylog.txt" -> "mylog.archived.txt"
  1007. // "mylog" -> "mylog.archived"
  1008. NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
  1009. NSUInteger count = [components count];
  1010. NSUInteger estimatedNewLength = [[self fileName] length] + [attrName length] + 1;
  1011. NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength];
  1012. if (count > 0) {
  1013. [newFileName appendString:components.firstObject];
  1014. }
  1015. NSString *lastExt = @"";
  1016. NSUInteger i;
  1017. for (i = 1; i < count; i++) {
  1018. NSString *attr = components[i];
  1019. if ([attr length] == 0) {
  1020. continue;
  1021. }
  1022. if ([attrName isEqualToString:attr]) {
  1023. // Extension attribute already exists in file name
  1024. return;
  1025. }
  1026. if ([lastExt length] > 0) {
  1027. [newFileName appendFormat:@".%@", lastExt];
  1028. }
  1029. lastExt = attr;
  1030. }
  1031. [newFileName appendFormat:@".%@", attrName];
  1032. if ([lastExt length] > 0) {
  1033. [newFileName appendFormat:@".%@", lastExt];
  1034. }
  1035. [self renameFile:newFileName];
  1036. }
  1037. - (void)removeExtensionAttributeWithName:(NSString *)attrName {
  1038. // This method is only used on the iPhone simulator, where normal extended attributes are broken.
  1039. // See full explanation in the header file.
  1040. if ([attrName length] == 0) {
  1041. return;
  1042. }
  1043. // Example:
  1044. // attrName = "archived"
  1045. //
  1046. // "mylog.archived.txt" -> "mylog.txt"
  1047. // "mylog.archived" -> "mylog"
  1048. NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
  1049. NSUInteger count = [components count];
  1050. NSUInteger estimatedNewLength = [[self fileName] length];
  1051. NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength];
  1052. if (count > 0) {
  1053. [newFileName appendString:components.firstObject];
  1054. }
  1055. BOOL found = NO;
  1056. NSUInteger i;
  1057. for (i = 1; i < count; i++) {
  1058. NSString *attr = components[i];
  1059. if ([attrName isEqualToString:attr]) {
  1060. found = YES;
  1061. } else {
  1062. [newFileName appendFormat:@".%@", attr];
  1063. }
  1064. }
  1065. if (found) {
  1066. [self renameFile:newFileName];
  1067. }
  1068. }
  1069. #else /* if TARGET_IPHONE_SIMULATOR */
  1070. - (BOOL)hasExtendedAttributeWithName:(NSString *)attrName {
  1071. const char *path = [filePath UTF8String];
  1072. const char *name = [attrName UTF8String];
  1073. ssize_t result = getxattr(path, name, NULL, 0, 0, 0);
  1074. return (result >= 0);
  1075. }
  1076. - (void)addExtendedAttributeWithName:(NSString *)attrName {
  1077. const char *path = [filePath UTF8String];
  1078. const char *name = [attrName UTF8String];
  1079. int result = setxattr(path, name, NULL, 0, 0, 0);
  1080. if (result < 0) {
  1081. NSLogError(@"DDLogFileInfo: setxattr(%@, %@): error = %s",
  1082. attrName,
  1083. filePath,
  1084. strerror(errno));
  1085. }
  1086. }
  1087. - (void)removeExtendedAttributeWithName:(NSString *)attrName {
  1088. const char *path = [filePath UTF8String];
  1089. const char *name = [attrName UTF8String];
  1090. int result = removexattr(path, name, 0);
  1091. if (result < 0 && errno != ENOATTR) {
  1092. NSLogError(@"DDLogFileInfo: removexattr(%@, %@): error = %s",
  1093. attrName,
  1094. self.fileName,
  1095. strerror(errno));
  1096. }
  1097. }
  1098. #endif /* if TARGET_IPHONE_SIMULATOR */
  1099. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1100. #pragma mark Comparisons
  1101. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  1102. - (BOOL)isEqual:(id)object {
  1103. if ([object isKindOfClass:[self class]]) {
  1104. DDLogFileInfo *another = (DDLogFileInfo *)object;
  1105. return [filePath isEqualToString:[another filePath]];
  1106. }
  1107. return NO;
  1108. }
  1109. - (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another {
  1110. NSDate *us = [self creationDate];
  1111. NSDate *them = [another creationDate];
  1112. NSComparisonResult result = [us compare:them];
  1113. if (result == NSOrderedAscending) {
  1114. return NSOrderedDescending;
  1115. }
  1116. if (result == NSOrderedDescending) {
  1117. return NSOrderedAscending;
  1118. }
  1119. return NSOrderedSame;
  1120. }
  1121. - (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another {
  1122. NSDate *us = [self modificationDate];
  1123. NSDate *them = [another modificationDate];
  1124. NSComparisonResult result = [us compare:them];
  1125. if (result == NSOrderedAscending) {
  1126. return NSOrderedDescending;
  1127. }
  1128. if (result == NSOrderedDescending) {
  1129. return NSOrderedAscending;
  1130. }
  1131. return NSOrderedSame;
  1132. }
  1133. @end
  1134. #if TARGET_OS_IPHONE
  1135. /**
  1136. * When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
  1137. *
  1138. * But in case if app is able to launch from background we need to have an ability to open log file any time we
  1139. * want (even if device is locked). Thats why that attribute have to be changed to
  1140. * NSFileProtectionCompleteUntilFirstUserAuthentication.
  1141. */
  1142. BOOL doesAppRunInBackground() {
  1143. BOOL answer = NO;
  1144. NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
  1145. for (NSString *mode in backgroundModes) {
  1146. if (mode.length > 0) {
  1147. answer = YES;
  1148. break;
  1149. }
  1150. }
  1151. return answer;
  1152. }
  1153. #endif