Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
2 | pmbaty | 1 | #import "DDFileLogger.h" |
2 | |||
3 | #import <unistd.h> |
||
4 | #import <sys/attr.h> |
||
5 | #import <sys/xattr.h> |
||
6 | #import <libkern/OSAtomic.h> |
||
7 | |||
8 | /** |
||
9 | * Welcome to Cocoa Lumberjack! |
||
10 | * |
||
11 | * The project page has a wealth of documentation if you have any questions. |
||
12 | * https://github.com/CocoaLumberjack/CocoaLumberjack |
||
13 | * |
||
14 | * If you're new to the project you may wish to read the "Getting Started" wiki. |
||
15 | * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted |
||
16 | **/ |
||
17 | |||
18 | #if ! __has_feature(objc_arc) |
||
19 | #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). |
||
20 | #endif |
||
21 | |||
22 | // We probably shouldn't be using DDLog() statements within the DDLog implementation. |
||
23 | // But we still want to leave our log statements for any future debugging, |
||
24 | // and to allow other developers to trace the implementation (which is a great learning tool). |
||
25 | // |
||
26 | // So we use primitive logging macros around NSLog. |
||
27 | // We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. |
||
28 | |||
29 | #define LOG_LEVEL 2 |
||
30 | |||
31 | #define NSLogError(frmt, ...) do{ if(LOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0) |
||
32 | #define NSLogWarn(frmt, ...) do{ if(LOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0) |
||
33 | #define NSLogInfo(frmt, ...) do{ if(LOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0) |
||
34 | #define NSLogDebug(frmt, ...) do{ if(LOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0) |
||
35 | #define NSLogVerbose(frmt, ...) do{ if(LOG_LEVEL >= 5) NSLog((frmt), ##__VA_ARGS__); } while(0) |
||
36 | |||
37 | @interface DDLogFileManagerDefault (PrivateAPI) |
||
38 | |||
39 | - (void)deleteOldLogFiles; |
||
40 | - (NSString *)defaultLogsDirectory; |
||
41 | |||
42 | @end |
||
43 | |||
44 | @interface DDFileLogger (PrivateAPI) |
||
45 | |||
46 | - (void)rollLogFileNow; |
||
47 | - (void)maybeRollLogFileDueToAge; |
||
48 | - (void)maybeRollLogFileDueToSize; |
||
49 | |||
50 | @end |
||
51 | |||
52 | #if TARGET_OS_IPHONE |
||
53 | BOOL doesAppRunInBackground(void); |
||
54 | #endif |
||
55 | |||
56 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
57 | #pragma mark - |
||
58 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
59 | |||
60 | @implementation DDLogFileManagerDefault |
||
61 | |||
62 | @synthesize maximumNumberOfLogFiles; |
||
63 | |||
64 | - (id)init |
||
65 | { |
||
66 | return [self initWithLogsDirectory:nil]; |
||
67 | } |
||
68 | |||
69 | - (instancetype)initWithLogsDirectory:(NSString *)aLogsDirectory |
||
70 | { |
||
71 | if ((self = [super init])) |
||
72 | { |
||
73 | maximumNumberOfLogFiles = DEFAULT_LOG_MAX_NUM_LOG_FILES; |
||
74 | |||
75 | if (aLogsDirectory) |
||
76 | _logsDirectory = [aLogsDirectory copy]; |
||
77 | else |
||
78 | _logsDirectory = [[self defaultLogsDirectory] copy]; |
||
79 | |||
80 | NSKeyValueObservingOptions kvoOptions = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; |
||
81 | |||
82 | [self addObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles)) options:kvoOptions context:nil]; |
||
83 | |||
84 | NSLogVerbose(@"DDFileLogManagerDefault: logsDirectory:\n%@", [self logsDirectory]); |
||
85 | NSLogVerbose(@"DDFileLogManagerDefault: sortedLogFileNames:\n%@", [self sortedLogFileNames]); |
||
86 | } |
||
87 | return self; |
||
88 | } |
||
89 | |||
90 | - (void)dealloc |
||
91 | { |
||
92 | // try-catch because the observer might be removed or never added. In this case, removeObserver throws and exception |
||
93 | @try { |
||
94 | [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles))]; |
||
95 | } |
||
96 | @catch (NSException *exception) { |
||
97 | |||
98 | } |
||
99 | } |
||
100 | |||
101 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
102 | #pragma mark Configuration |
||
103 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
104 | |||
105 | - (void)observeValueForKeyPath:(NSString *)keyPath |
||
106 | ofObject:(id)object |
||
107 | change:(NSDictionary *)change |
||
108 | context:(void *)context |
||
109 | { |
||
110 | NSNumber *old = [change objectForKey:NSKeyValueChangeOldKey]; |
||
111 | NSNumber *new = [change objectForKey:NSKeyValueChangeNewKey]; |
||
112 | |||
113 | if ([old isEqual:new]) |
||
114 | { |
||
115 | // No change in value - don't bother with any processing. |
||
116 | return; |
||
117 | } |
||
118 | |||
119 | if ([keyPath isEqualToString:NSStringFromSelector(@selector(maximumNumberOfLogFiles))]) |
||
120 | { |
||
121 | NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: maximumNumberOfLogFiles"); |
||
122 | |||
123 | dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool { |
||
124 | |||
125 | [self deleteOldLogFiles]; |
||
126 | }}); |
||
127 | } |
||
128 | } |
||
129 | |||
130 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
131 | #pragma mark File Deleting |
||
132 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
133 | |||
134 | /** |
||
135 | * Deletes archived log files that exceed the maximumNumberOfLogFiles configuration value. |
||
136 | **/ |
||
137 | - (void)deleteOldLogFiles |
||
138 | { |
||
139 | NSLogVerbose(@"DDLogFileManagerDefault: deleteOldLogFiles"); |
||
140 | |||
141 | NSUInteger maxNumLogFiles = self.maximumNumberOfLogFiles; |
||
142 | if (maxNumLogFiles == 0) |
||
143 | { |
||
144 | // Unlimited - don't delete any log files |
||
145 | return; |
||
146 | } |
||
147 | |||
148 | NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; |
||
149 | |||
150 | // Do we consider the first file? |
||
151 | // We are only supposed to be deleting archived files. |
||
152 | // In most cases, the first file is likely the log file that is currently being written to. |
||
153 | // So in most cases, we do not want to consider this file for deletion. |
||
154 | |||
155 | NSUInteger count = [sortedLogFileInfos count]; |
||
156 | BOOL excludeFirstFile = NO; |
||
157 | |||
158 | if (count > 0) |
||
159 | { |
||
160 | DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:0]; |
||
161 | |||
162 | if (!logFileInfo.isArchived) |
||
163 | { |
||
164 | excludeFirstFile = YES; |
||
165 | } |
||
166 | } |
||
167 | |||
168 | NSArray *sortedArchivedLogFileInfos; |
||
169 | if (excludeFirstFile) |
||
170 | { |
||
171 | count--; |
||
172 | sortedArchivedLogFileInfos = [sortedLogFileInfos subarrayWithRange:NSMakeRange(1, count)]; |
||
173 | } |
||
174 | else |
||
175 | { |
||
176 | sortedArchivedLogFileInfos = sortedLogFileInfos; |
||
177 | } |
||
178 | |||
179 | NSUInteger i; |
||
180 | for (i = maxNumLogFiles; i < count; i++) |
||
181 | { |
||
182 | DDLogFileInfo *logFileInfo = [sortedArchivedLogFileInfos objectAtIndex:i]; |
||
183 | |||
184 | NSLogInfo(@"DDLogFileManagerDefault: Deleting file: %@", logFileInfo.fileName); |
||
185 | |||
186 | [[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:nil]; |
||
187 | } |
||
188 | } |
||
189 | |||
190 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
191 | #pragma mark Log Files |
||
192 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
193 | |||
194 | /** |
||
195 | * Returns the path to the default logs directory. |
||
196 | * If the logs directory doesn't exist, this method automatically creates it. |
||
197 | **/ |
||
198 | - (NSString *)defaultLogsDirectory |
||
199 | { |
||
200 | #if TARGET_OS_IPHONE |
||
201 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); |
||
202 | NSString *baseDir = ([paths count] > 0) ? [paths objectAtIndex:0] : nil; |
||
203 | NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"Logs"]; |
||
204 | |||
205 | #else |
||
206 | NSString *appName = [[NSProcessInfo processInfo] processName]; |
||
207 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); |
||
208 | NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : NSTemporaryDirectory(); |
||
209 | NSString *logsDirectory = [[basePath stringByAppendingPathComponent:@"Logs"] stringByAppendingPathComponent:appName]; |
||
210 | |||
211 | #endif |
||
212 | |||
213 | return logsDirectory; |
||
214 | } |
||
215 | |||
216 | - (NSString *)logsDirectory |
||
217 | { |
||
218 | // We could do this check once, during initalization, and not bother again. |
||
219 | // But this way the code continues to work if the directory gets deleted while the code is running. |
||
220 | |||
221 | if (![[NSFileManager defaultManager] fileExistsAtPath:_logsDirectory]) |
||
222 | { |
||
223 | NSError *err = nil; |
||
224 | if (![[NSFileManager defaultManager] createDirectoryAtPath:_logsDirectory |
||
225 | withIntermediateDirectories:YES attributes:nil error:&err]) |
||
226 | { |
||
227 | NSLogError(@"DDFileLogManagerDefault: Error creating logsDirectory: %@", err); |
||
228 | } |
||
229 | } |
||
230 | |||
231 | return _logsDirectory; |
||
232 | } |
||
233 | |||
234 | /** |
||
235 | * A log file has a name like "<app name> <date> <time>.log". |
||
236 | * Example: MobileSafari 2013-12-03 17-14.log |
||
237 | **/ |
||
238 | - (BOOL)isLogFile:(NSString *)fileName |
||
239 | { |
||
240 | NSString *appName = [self applicationName]; |
||
241 | |||
242 | BOOL hasProperPrefix = [fileName hasPrefix:appName]; |
||
243 | BOOL hasProperSuffix = [fileName hasSuffix:@".log"]; |
||
244 | BOOL hasProperDate = NO; |
||
245 | |||
246 | if (hasProperPrefix && hasProperSuffix) |
||
247 | { |
||
248 | NSUInteger lengthOfMiddle = fileName.length - appName.length - @".log".length; |
||
249 | |||
250 | // Date string should have at least 16 characters - " 2013-12-03 17-14" |
||
251 | if (lengthOfMiddle >= 17) |
||
252 | { |
||
253 | NSRange range = NSMakeRange(appName.length, lengthOfMiddle); |
||
254 | |||
255 | NSString *middle = [fileName substringWithRange:range]; |
||
256 | NSArray *components = [middle componentsSeparatedByString:@" "]; |
||
257 | |||
258 | // When creating logfile if there is existing file with the same name, we append attemp number at the end. |
||
259 | // Thats why here we can have three or four components. For details see generateLogFileNameWithAttempt: method. |
||
260 | // |
||
261 | // Components: |
||
262 | // "", "2013-12-03", "17-14" |
||
263 | // or |
||
264 | // "", "2013-12-03", "17-14", "1" |
||
265 | if (components.count == 3 || components.count == 4) |
||
266 | { |
||
267 | NSString *dateString = [NSString stringWithFormat:@"%@ %@", components[1], components[2]]; |
||
268 | NSDateFormatter *dateFormatter = [self logFileDateFormatter]; |
||
269 | |||
270 | NSDate *date = [dateFormatter dateFromString:dateString]; |
||
271 | |||
272 | if (date) |
||
273 | { |
||
274 | hasProperDate = YES; |
||
275 | } |
||
276 | } |
||
277 | } |
||
278 | } |
||
279 | |||
280 | return (hasProperPrefix && hasProperDate && hasProperSuffix); |
||
281 | } |
||
282 | |||
283 | - (NSDateFormatter *)logFileDateFormatter |
||
284 | { |
||
285 | NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; |
||
286 | [dateFormatter setDateFormat:@"yyyy'-'MM'-'dd' 'HH'-'mm'"]; |
||
287 | [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; |
||
288 | |||
289 | return dateFormatter; |
||
290 | } |
||
291 | |||
292 | /** |
||
293 | * Returns an array of NSString objects, |
||
294 | * each of which is the filePath to an existing log file on disk. |
||
295 | **/ |
||
296 | - (NSArray *)unsortedLogFilePaths |
||
297 | { |
||
298 | NSString *logsDirectory = [self logsDirectory]; |
||
299 | NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:nil]; |
||
300 | |||
301 | NSMutableArray *unsortedLogFilePaths = [NSMutableArray arrayWithCapacity:[fileNames count]]; |
||
302 | |||
303 | for (NSString *fileName in fileNames) |
||
304 | { |
||
305 | // Filter out any files that aren't log files. (Just for extra safety) |
||
306 | |||
307 | if ([self isLogFile:fileName]) |
||
308 | { |
||
309 | NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName]; |
||
310 | |||
311 | [unsortedLogFilePaths addObject:filePath]; |
||
312 | } |
||
313 | } |
||
314 | |||
315 | return unsortedLogFilePaths; |
||
316 | } |
||
317 | |||
318 | /** |
||
319 | * Returns an array of NSString objects, |
||
320 | * each of which is the fileName of an existing log file on disk. |
||
321 | **/ |
||
322 | - (NSArray *)unsortedLogFileNames |
||
323 | { |
||
324 | NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths]; |
||
325 | |||
326 | NSMutableArray *unsortedLogFileNames = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]]; |
||
327 | |||
328 | for (NSString *filePath in unsortedLogFilePaths) |
||
329 | { |
||
330 | [unsortedLogFileNames addObject:[filePath lastPathComponent]]; |
||
331 | } |
||
332 | |||
333 | return unsortedLogFileNames; |
||
334 | } |
||
335 | |||
336 | /** |
||
337 | * Returns an array of DDLogFileInfo objects, |
||
338 | * each representing an existing log file on disk, |
||
339 | * and containing important information about the log file such as it's modification date and size. |
||
340 | **/ |
||
341 | - (NSArray *)unsortedLogFileInfos |
||
342 | { |
||
343 | NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths]; |
||
344 | |||
345 | NSMutableArray *unsortedLogFileInfos = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]]; |
||
346 | |||
347 | for (NSString *filePath in unsortedLogFilePaths) |
||
348 | { |
||
349 | DDLogFileInfo *logFileInfo = [[DDLogFileInfo alloc] initWithFilePath:filePath]; |
||
350 | |||
351 | [unsortedLogFileInfos addObject:logFileInfo]; |
||
352 | } |
||
353 | |||
354 | return unsortedLogFileInfos; |
||
355 | } |
||
356 | |||
357 | /** |
||
358 | * Just like the unsortedLogFilePaths method, but sorts the array. |
||
359 | * The items in the array are sorted by creation date. |
||
360 | * The first item in the array will be the most recently created log file. |
||
361 | **/ |
||
362 | - (NSArray *)sortedLogFilePaths |
||
363 | { |
||
364 | NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; |
||
365 | |||
366 | NSMutableArray *sortedLogFilePaths = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]]; |
||
367 | |||
368 | for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) |
||
369 | { |
||
370 | [sortedLogFilePaths addObject:[logFileInfo filePath]]; |
||
371 | } |
||
372 | |||
373 | return sortedLogFilePaths; |
||
374 | } |
||
375 | |||
376 | /** |
||
377 | * Just like the unsortedLogFileNames method, but sorts the array. |
||
378 | * The items in the array are sorted by creation date. |
||
379 | * The first item in the array will be the most recently created log file. |
||
380 | **/ |
||
381 | - (NSArray *)sortedLogFileNames |
||
382 | { |
||
383 | NSArray *sortedLogFileInfos = [self sortedLogFileInfos]; |
||
384 | |||
385 | NSMutableArray *sortedLogFileNames = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]]; |
||
386 | |||
387 | for (DDLogFileInfo *logFileInfo in sortedLogFileInfos) |
||
388 | { |
||
389 | [sortedLogFileNames addObject:[logFileInfo fileName]]; |
||
390 | } |
||
391 | |||
392 | return sortedLogFileNames; |
||
393 | } |
||
394 | |||
395 | /** |
||
396 | * Just like the unsortedLogFileInfos method, but sorts the array. |
||
397 | * The items in the array are sorted by creation date. |
||
398 | * The first item in the array will be the most recently created log file. |
||
399 | **/ |
||
400 | - (NSArray *)sortedLogFileInfos |
||
401 | { |
||
402 | return [[self unsortedLogFileInfos] sortedArrayUsingSelector:@selector(reverseCompareByCreationDate:)]; |
||
403 | } |
||
404 | |||
405 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
406 | #pragma mark Creation |
||
407 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
408 | |||
409 | /** |
||
410 | * Generates log file name with format "<app name> <date> <time>.log" |
||
411 | * Example: MobileSafari 2013-12-03 17-14.log |
||
412 | **/ |
||
413 | - (NSString *)generateLogFileNameWithAttempt:(NSUInteger)attempt |
||
414 | { |
||
415 | NSString *appName = [self applicationName]; |
||
416 | |||
417 | NSDateFormatter *dateFormatter = [self logFileDateFormatter]; |
||
418 | NSString *formattedDate = [dateFormatter stringFromDate:[NSDate date]]; |
||
419 | |||
420 | if (attempt > 1) |
||
421 | { |
||
422 | return [NSString stringWithFormat:@"%@ %@ %lu.log", appName, formattedDate, (unsigned long)attempt]; |
||
423 | } |
||
424 | else |
||
425 | { |
||
426 | return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate]; |
||
427 | } |
||
428 | } |
||
429 | |||
430 | /** |
||
431 | * Generates a new unique log file path, and creates the corresponding log file. |
||
432 | **/ |
||
433 | - (NSString *)createNewLogFile |
||
434 | { |
||
435 | // Generate a random log file name, and create the file (if there isn't a collision) |
||
436 | |||
437 | NSString *logsDirectory = [self logsDirectory]; |
||
438 | NSUInteger attempt = 1; |
||
439 | do |
||
440 | { |
||
441 | NSString *fileName = [self generateLogFileNameWithAttempt:attempt]; |
||
442 | |||
443 | NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName]; |
||
444 | |||
445 | if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) |
||
446 | { |
||
447 | NSLogVerbose(@"DDLogFileManagerDefault: Creating new log file: %@", fileName); |
||
448 | |||
449 | NSDictionary *attributes = nil; |
||
450 | |||
451 | #if TARGET_OS_IPHONE |
||
452 | // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen. |
||
453 | // |
||
454 | // But in case if app is able to launch from background we need to have an ability to open log file any time we |
||
455 | // want (even if device is locked). Thats why that attribute have to be changed to |
||
456 | // NSFileProtectionCompleteUntilFirstUserAuthentication. |
||
457 | |||
458 | NSString *key = doesAppRunInBackground() ? |
||
459 | NSFileProtectionCompleteUntilFirstUserAuthentication : NSFileProtectionCompleteUnlessOpen; |
||
460 | |||
461 | attributes = @{ NSFileProtectionKey : key }; |
||
462 | #endif |
||
463 | |||
464 | [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:attributes]; |
||
465 | |||
466 | // Since we just created a new log file, we may need to delete some old log files |
||
467 | [self deleteOldLogFiles]; |
||
468 | |||
469 | return filePath; |
||
470 | } else { |
||
471 | attempt++; |
||
472 | } |
||
473 | |||
474 | } while(YES); |
||
475 | } |
||
476 | |||
477 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
478 | #pragma mark Utility |
||
479 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
480 | |||
481 | - (NSString *)applicationName |
||
482 | { |
||
483 | static NSString *_appName; |
||
484 | static dispatch_once_t onceToken; |
||
485 | |||
486 | dispatch_once(&onceToken, ^{ |
||
487 | #if TARGET_OS_IPHONE |
||
488 | _appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; |
||
489 | |||
490 | if (! _appName) |
||
491 | { |
||
492 | _appName = [[NSProcessInfo processInfo] processName]; |
||
493 | } |
||
494 | #else |
||
495 | _appName = [[NSProcessInfo processInfo] processName]; |
||
496 | #endif |
||
497 | |||
498 | if (! _appName) |
||
499 | { |
||
500 | _appName = @""; |
||
501 | } |
||
502 | }); |
||
503 | |||
504 | return _appName; |
||
505 | } |
||
506 | |||
507 | @end |
||
508 | |||
509 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
510 | #pragma mark - |
||
511 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
512 | |||
513 | @implementation DDLogFileFormatterDefault |
||
514 | |||
515 | - (id)init |
||
516 | { |
||
517 | return [self initWithDateFormatter:nil]; |
||
518 | } |
||
519 | |||
520 | - (instancetype)initWithDateFormatter:(NSDateFormatter *)aDateFormatter |
||
521 | { |
||
522 | if ((self = [super init])) |
||
523 | { |
||
524 | if (aDateFormatter) |
||
525 | { |
||
526 | dateFormatter = aDateFormatter; |
||
527 | } |
||
528 | else |
||
529 | { |
||
530 | dateFormatter = [[NSDateFormatter alloc] init]; |
||
531 | [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; // 10.4+ style |
||
532 | [dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"]; |
||
533 | } |
||
534 | } |
||
535 | return self; |
||
536 | } |
||
537 | |||
538 | - (NSString *)formatLogMessage:(DDLogMessage *)logMessage |
||
539 | { |
||
540 | NSString *dateAndTime = [dateFormatter stringFromDate:(logMessage->timestamp)]; |
||
541 | |||
542 | return [NSString stringWithFormat:@"%@ %@", dateAndTime, logMessage->logMsg]; |
||
543 | } |
||
544 | |||
545 | @end |
||
546 | |||
547 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
548 | #pragma mark - |
||
549 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
550 | |||
551 | @implementation DDFileLogger |
||
552 | |||
553 | - (id)init |
||
554 | { |
||
555 | DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init]; |
||
556 | |||
557 | return [self initWithLogFileManager:defaultLogFileManager]; |
||
558 | } |
||
559 | |||
560 | - (instancetype)initWithLogFileManager:(id <DDLogFileManager>)aLogFileManager |
||
561 | { |
||
562 | if ((self = [super init])) |
||
563 | { |
||
564 | maximumFileSize = DEFAULT_LOG_MAX_FILE_SIZE; |
||
565 | rollingFrequency = DEFAULT_LOG_ROLLING_FREQUENCY; |
||
566 | |||
567 | logFileManager = aLogFileManager; |
||
568 | |||
569 | formatter = [[DDLogFileFormatterDefault alloc] init]; |
||
570 | } |
||
571 | return self; |
||
572 | } |
||
573 | |||
574 | - (void)dealloc |
||
575 | { |
||
576 | [currentLogFileHandle synchronizeFile]; |
||
577 | [currentLogFileHandle closeFile]; |
||
578 | |||
579 | if (currentLogFileVnode) { |
||
580 | dispatch_source_cancel(currentLogFileVnode); |
||
581 | currentLogFileVnode = NULL; |
||
582 | } |
||
583 | |||
584 | if (rollingTimer) |
||
585 | { |
||
586 | dispatch_source_cancel(rollingTimer); |
||
587 | rollingTimer = NULL; |
||
588 | } |
||
589 | } |
||
590 | |||
591 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
592 | #pragma mark Properties |
||
593 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
594 | |||
595 | @synthesize logFileManager; |
||
596 | |||
597 | - (unsigned long long)maximumFileSize |
||
598 | { |
||
599 | __block unsigned long long result; |
||
600 | |||
601 | dispatch_block_t block = ^{ |
||
602 | result = maximumFileSize; |
||
603 | }; |
||
604 | |||
605 | // The design of this method is taken from the DDAbstractLogger implementation. |
||
606 | // For extensive documentation please refer to the DDAbstractLogger implementation. |
||
607 | |||
608 | // Note: The internal implementation MUST access the maximumFileSize variable directly, |
||
609 | // This method is designed explicitly for external access. |
||
610 | // |
||
611 | // Using "self." syntax to go through this method will cause immediate deadlock. |
||
612 | // This is the intended result. Fix it by accessing the ivar directly. |
||
613 | // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. |
||
614 | |||
615 | NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
||
616 | NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); |
||
617 | |||
618 | dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
||
619 | |||
620 | dispatch_sync(globalLoggingQueue, ^{ |
||
621 | dispatch_sync(loggerQueue, block); |
||
622 | }); |
||
623 | |||
624 | return result; |
||
625 | } |
||
626 | |||
627 | - (void)setMaximumFileSize:(unsigned long long)newMaximumFileSize |
||
628 | { |
||
629 | dispatch_block_t block = ^{ @autoreleasepool { |
||
630 | |||
631 | maximumFileSize = newMaximumFileSize; |
||
632 | [self maybeRollLogFileDueToSize]; |
||
633 | |||
634 | }}; |
||
635 | |||
636 | // The design of this method is taken from the DDAbstractLogger implementation. |
||
637 | // For extensive documentation please refer to the DDAbstractLogger implementation. |
||
638 | |||
639 | // Note: The internal implementation MUST access the maximumFileSize variable directly, |
||
640 | // This method is designed explicitly for external access. |
||
641 | // |
||
642 | // Using "self." syntax to go through this method will cause immediate deadlock. |
||
643 | // This is the intended result. Fix it by accessing the ivar directly. |
||
644 | // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. |
||
645 | |||
646 | NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
||
647 | NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); |
||
648 | |||
649 | dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
||
650 | |||
651 | dispatch_async(globalLoggingQueue, ^{ |
||
652 | dispatch_async(loggerQueue, block); |
||
653 | }); |
||
654 | } |
||
655 | |||
656 | - (NSTimeInterval)rollingFrequency |
||
657 | { |
||
658 | __block NSTimeInterval result; |
||
659 | |||
660 | dispatch_block_t block = ^{ |
||
661 | result = rollingFrequency; |
||
662 | }; |
||
663 | |||
664 | // The design of this method is taken from the DDAbstractLogger implementation. |
||
665 | // For extensive documentation please refer to the DDAbstractLogger implementation. |
||
666 | |||
667 | // Note: The internal implementation should access the rollingFrequency variable directly, |
||
668 | // This method is designed explicitly for external access. |
||
669 | // |
||
670 | // Using "self." syntax to go through this method will cause immediate deadlock. |
||
671 | // This is the intended result. Fix it by accessing the ivar directly. |
||
672 | // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. |
||
673 | |||
674 | NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
||
675 | NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); |
||
676 | |||
677 | dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
||
678 | |||
679 | dispatch_sync(globalLoggingQueue, ^{ |
||
680 | dispatch_sync(loggerQueue, block); |
||
681 | }); |
||
682 | |||
683 | return result; |
||
684 | } |
||
685 | |||
686 | - (void)setRollingFrequency:(NSTimeInterval)newRollingFrequency |
||
687 | { |
||
688 | dispatch_block_t block = ^{ @autoreleasepool { |
||
689 | |||
690 | rollingFrequency = newRollingFrequency; |
||
691 | [self maybeRollLogFileDueToAge]; |
||
692 | }}; |
||
693 | |||
694 | // The design of this method is taken from the DDAbstractLogger implementation. |
||
695 | // For extensive documentation please refer to the DDAbstractLogger implementation. |
||
696 | |||
697 | // Note: The internal implementation should access the rollingFrequency variable directly, |
||
698 | // This method is designed explicitly for external access. |
||
699 | // |
||
700 | // Using "self." syntax to go through this method will cause immediate deadlock. |
||
701 | // This is the intended result. Fix it by accessing the ivar directly. |
||
702 | // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. |
||
703 | |||
704 | NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
||
705 | NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); |
||
706 | |||
707 | dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
||
708 | |||
709 | dispatch_async(globalLoggingQueue, ^{ |
||
710 | dispatch_async(loggerQueue, block); |
||
711 | }); |
||
712 | } |
||
713 | |||
714 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
715 | #pragma mark File Rolling |
||
716 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
717 | |||
718 | - (void)scheduleTimerToRollLogFileDueToAge |
||
719 | { |
||
720 | if (rollingTimer) |
||
721 | { |
||
722 | dispatch_source_cancel(rollingTimer); |
||
723 | rollingTimer = NULL; |
||
724 | } |
||
725 | |||
726 | if (currentLogFileInfo == nil || rollingFrequency <= 0.0) |
||
727 | { |
||
728 | return; |
||
729 | } |
||
730 | |||
731 | NSDate *logFileCreationDate = [currentLogFileInfo creationDate]; |
||
732 | |||
733 | NSTimeInterval ti = [logFileCreationDate timeIntervalSinceReferenceDate]; |
||
734 | ti += rollingFrequency; |
||
735 | |||
736 | NSDate *logFileRollingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ti]; |
||
737 | |||
738 | NSLogVerbose(@"DDFileLogger: scheduleTimerToRollLogFileDueToAge"); |
||
739 | |||
740 | NSLogVerbose(@"DDFileLogger: logFileCreationDate: %@", logFileCreationDate); |
||
741 | NSLogVerbose(@"DDFileLogger: logFileRollingDate : %@", logFileRollingDate); |
||
742 | |||
743 | rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); |
||
744 | |||
745 | dispatch_source_set_event_handler(rollingTimer, ^{ @autoreleasepool { |
||
746 | |||
747 | [self maybeRollLogFileDueToAge]; |
||
748 | |||
749 | }}); |
||
750 | |||
751 | #if !OS_OBJECT_USE_OBJC |
||
752 | dispatch_source_t theRollingTimer = rollingTimer; |
||
753 | dispatch_source_set_cancel_handler(rollingTimer, ^{ |
||
754 | dispatch_release(theRollingTimer); |
||
755 | }); |
||
756 | #endif |
||
757 | |||
758 | uint64_t delay = (uint64_t)([logFileRollingDate timeIntervalSinceNow] * NSEC_PER_SEC); |
||
759 | dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, delay); |
||
760 | |||
761 | dispatch_source_set_timer(rollingTimer, fireTime, DISPATCH_TIME_FOREVER, 1.0); |
||
762 | dispatch_resume(rollingTimer); |
||
763 | } |
||
764 | |||
765 | |||
766 | - (void)rollLogFile |
||
767 | { |
||
768 | [self rollLogFileWithCompletionBlock:nil]; |
||
769 | } |
||
770 | |||
771 | - (void)rollLogFileWithCompletionBlock:(void (^)())completionBlock |
||
772 | { |
||
773 | // This method is public. |
||
774 | // We need to execute the rolling on our logging thread/queue. |
||
775 | |||
776 | dispatch_block_t block = ^{ @autoreleasepool { |
||
777 | |||
778 | [self rollLogFileNow]; |
||
779 | |||
780 | if (completionBlock) |
||
781 | { |
||
782 | dispatch_async(dispatch_get_main_queue(), ^{ |
||
783 | completionBlock(); |
||
784 | }); |
||
785 | } |
||
786 | }}; |
||
787 | |||
788 | // The design of this method is taken from the DDAbstractLogger implementation. |
||
789 | // For extensive documentation please refer to the DDAbstractLogger implementation. |
||
790 | |||
791 | if ([self isOnInternalLoggerQueue]) |
||
792 | { |
||
793 | block(); |
||
794 | } |
||
795 | else |
||
796 | { |
||
797 | dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; |
||
798 | NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); |
||
799 | |||
800 | dispatch_async(globalLoggingQueue, ^{ |
||
801 | dispatch_async(loggerQueue, block); |
||
802 | }); |
||
803 | } |
||
804 | } |
||
805 | |||
806 | - (void)rollLogFileNow |
||
807 | { |
||
808 | NSLogVerbose(@"DDFileLogger: rollLogFileNow"); |
||
809 | |||
810 | |||
811 | if (currentLogFileHandle == nil) return; |
||
812 | |||
813 | [currentLogFileHandle synchronizeFile]; |
||
814 | [currentLogFileHandle closeFile]; |
||
815 | currentLogFileHandle = nil; |
||
816 | |||
817 | currentLogFileInfo.isArchived = YES; |
||
818 | |||
819 | if ([logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]) |
||
820 | { |
||
821 | [logFileManager didRollAndArchiveLogFile:(currentLogFileInfo.filePath)]; |
||
822 | } |
||
823 | |||
824 | currentLogFileInfo = nil; |
||
825 | |||
826 | if (currentLogFileVnode) { |
||
827 | dispatch_source_cancel(currentLogFileVnode); |
||
828 | currentLogFileVnode = NULL; |
||
829 | } |
||
830 | |||
831 | if (rollingTimer) |
||
832 | { |
||
833 | dispatch_source_cancel(rollingTimer); |
||
834 | rollingTimer = NULL; |
||
835 | } |
||
836 | } |
||
837 | |||
838 | - (void)maybeRollLogFileDueToAge |
||
839 | { |
||
840 | if (rollingFrequency > 0.0 && currentLogFileInfo.age >= rollingFrequency) |
||
841 | { |
||
842 | NSLogVerbose(@"DDFileLogger: Rolling log file due to age..."); |
||
843 | |||
844 | [self rollLogFileNow]; |
||
845 | } |
||
846 | else |
||
847 | { |
||
848 | [self scheduleTimerToRollLogFileDueToAge]; |
||
849 | } |
||
850 | } |
||
851 | |||
852 | - (void)maybeRollLogFileDueToSize |
||
853 | { |
||
854 | // This method is called from logMessage. |
||
855 | // Keep it FAST. |
||
856 | |||
857 | // Note: Use direct access to maximumFileSize variable. |
||
858 | // We specifically wrote our own getter/setter method to allow us to do this (for performance reasons). |
||
859 | |||
860 | if (maximumFileSize > 0) |
||
861 | { |
||
862 | unsigned long long fileSize = [currentLogFileHandle offsetInFile]; |
||
863 | |||
864 | if (fileSize >= maximumFileSize) |
||
865 | { |
||
866 | NSLogVerbose(@"DDFileLogger: Rolling log file due to size (%qu)...", fileSize); |
||
867 | |||
868 | [self rollLogFileNow]; |
||
869 | } |
||
870 | } |
||
871 | } |
||
872 | |||
873 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
874 | #pragma mark File Logging |
||
875 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
876 | |||
877 | /** |
||
878 | * Returns the log file that should be used. |
||
879 | * If there is an existing log file that is suitable, |
||
880 | * within the constraints of maximumFileSize and rollingFrequency, then it is returned. |
||
881 | * |
||
882 | * Otherwise a new file is created and returned. |
||
883 | **/ |
||
884 | - (DDLogFileInfo *)currentLogFileInfo |
||
885 | { |
||
886 | if (currentLogFileInfo == nil) |
||
887 | { |
||
888 | NSArray *sortedLogFileInfos = [logFileManager sortedLogFileInfos]; |
||
889 | |||
890 | if ([sortedLogFileInfos count] > 0) |
||
891 | { |
||
892 | DDLogFileInfo *mostRecentLogFileInfo = [sortedLogFileInfos objectAtIndex:0]; |
||
893 | |||
894 | BOOL useExistingLogFile = YES; |
||
895 | BOOL shouldArchiveMostRecent = NO; |
||
896 | |||
897 | if (mostRecentLogFileInfo.isArchived) |
||
898 | { |
||
899 | useExistingLogFile = NO; |
||
900 | shouldArchiveMostRecent = NO; |
||
901 | } |
||
902 | else if (maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= maximumFileSize) |
||
903 | { |
||
904 | useExistingLogFile = NO; |
||
905 | shouldArchiveMostRecent = YES; |
||
906 | } |
||
907 | else if (rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= rollingFrequency) |
||
908 | { |
||
909 | useExistingLogFile = NO; |
||
910 | shouldArchiveMostRecent = YES; |
||
911 | } |
||
912 | |||
913 | |||
914 | #if TARGET_OS_IPHONE |
||
915 | // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen. |
||
916 | // |
||
917 | // But in case if app is able to launch from background we need to have an ability to open log file any time we |
||
918 | // want (even if device is locked). Thats why that attribute have to be changed to |
||
919 | // NSFileProtectionCompleteUntilFirstUserAuthentication. |
||
920 | // |
||
921 | // If previous log was created when app wasn't running in background, but now it is - we archive it and create |
||
922 | // a new one. |
||
923 | |||
924 | if (useExistingLogFile && doesAppRunInBackground()) { |
||
925 | NSString *key = mostRecentLogFileInfo.fileAttributes[NSFileProtectionKey]; |
||
926 | |||
927 | if (! [key isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) { |
||
928 | useExistingLogFile = NO; |
||
929 | shouldArchiveMostRecent = YES; |
||
930 | } |
||
931 | } |
||
932 | #endif |
||
933 | |||
934 | if (useExistingLogFile) |
||
935 | { |
||
936 | NSLogVerbose(@"DDFileLogger: Resuming logging with file %@", mostRecentLogFileInfo.fileName); |
||
937 | |||
938 | currentLogFileInfo = mostRecentLogFileInfo; |
||
939 | } |
||
940 | else |
||
941 | { |
||
942 | if (shouldArchiveMostRecent) |
||
943 | { |
||
944 | mostRecentLogFileInfo.isArchived = YES; |
||
945 | |||
946 | if ([logFileManager respondsToSelector:@selector(didArchiveLogFile:)]) |
||
947 | { |
||
948 | [logFileManager didArchiveLogFile:(mostRecentLogFileInfo.filePath)]; |
||
949 | } |
||
950 | } |
||
951 | } |
||
952 | } |
||
953 | |||
954 | if (currentLogFileInfo == nil) |
||
955 | { |
||
956 | NSString *currentLogFilePath = [logFileManager createNewLogFile]; |
||
957 | |||
958 | currentLogFileInfo = [[DDLogFileInfo alloc] initWithFilePath:currentLogFilePath]; |
||
959 | } |
||
960 | } |
||
961 | |||
962 | return currentLogFileInfo; |
||
963 | } |
||
964 | |||
965 | - (NSFileHandle *)currentLogFileHandle |
||
966 | { |
||
967 | if (currentLogFileHandle == nil) |
||
968 | { |
||
969 | NSString *logFilePath = [[self currentLogFileInfo] filePath]; |
||
970 | |||
971 | currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath]; |
||
972 | [currentLogFileHandle seekToEndOfFile]; |
||
973 | |||
974 | if (currentLogFileHandle) |
||
975 | { |
||
976 | [self scheduleTimerToRollLogFileDueToAge]; |
||
977 | |||
978 | // Here we are monitoring the log file. In case if it would be deleted ormoved |
||
979 | // somewhere we want to roll it and use a new one. |
||
980 | currentLogFileVnode = dispatch_source_create( |
||
981 | DISPATCH_SOURCE_TYPE_VNODE, |
||
982 | [currentLogFileHandle fileDescriptor], |
||
983 | DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME, |
||
984 | loggerQueue |
||
985 | ); |
||
986 | |||
987 | dispatch_source_set_event_handler(currentLogFileVnode, ^{ @autoreleasepool { |
||
988 | NSLogInfo(@"DDFileLogger: Current logfile was moved. Rolling it and creating a new one"); |
||
989 | [self rollLogFileNow]; |
||
990 | }}); |
||
991 | |||
992 | #if !OS_OBJECT_USE_OBJC |
||
993 | dispatch_source_t vnode = currentLogFileVnode; |
||
994 | dispatch_source_set_cancel_handler(currentLogFileVnode, ^{ |
||
995 | dispatch_release(vnode); |
||
996 | }); |
||
997 | #endif |
||
998 | |||
999 | dispatch_resume(currentLogFileVnode); |
||
1000 | } |
||
1001 | } |
||
1002 | |||
1003 | return currentLogFileHandle; |
||
1004 | } |
||
1005 | |||
1006 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1007 | #pragma mark DDLogger Protocol |
||
1008 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1009 | |||
1010 | static int exception_count = 0; |
||
1011 | - (void)logMessage:(DDLogMessage *)logMessage |
||
1012 | { |
||
1013 | NSString *logMsg = logMessage->logMsg; |
||
1014 | |||
1015 | if (formatter) |
||
1016 | { |
||
1017 | logMsg = [formatter formatLogMessage:logMessage]; |
||
1018 | } |
||
1019 | |||
1020 | if (logMsg) |
||
1021 | { |
||
1022 | if (![logMsg hasSuffix:@"\n"]) |
||
1023 | { |
||
1024 | logMsg = [logMsg stringByAppendingString:@"\n"]; |
||
1025 | } |
||
1026 | |||
1027 | NSData *logData = [logMsg dataUsingEncoding:NSUTF8StringEncoding]; |
||
1028 | |||
1029 | @try { |
||
1030 | [[self currentLogFileHandle] writeData:logData]; |
||
1031 | |||
1032 | [self maybeRollLogFileDueToSize]; |
||
1033 | } |
||
1034 | @catch (NSException *exception) { |
||
1035 | exception_count++; |
||
1036 | if (exception_count <= 10) { |
||
1037 | NSLogError(@"DDFileLogger.logMessage: %@", exception); |
||
1038 | if (exception_count == 10) |
||
1039 | NSLogError(@"DDFileLogger.logMessage: Too many exceptions -- will not log any more of them."); |
||
1040 | } |
||
1041 | } |
||
1042 | } |
||
1043 | } |
||
1044 | |||
1045 | - (void)willRemoveLogger |
||
1046 | { |
||
1047 | // If you override me be sure to invoke [super willRemoveLogger]; |
||
1048 | |||
1049 | [self rollLogFileNow]; |
||
1050 | } |
||
1051 | |||
1052 | - (NSString *)loggerName |
||
1053 | { |
||
1054 | return @"cocoa.lumberjack.fileLogger"; |
||
1055 | } |
||
1056 | |||
1057 | @end |
||
1058 | |||
1059 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1060 | #pragma mark - |
||
1061 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1062 | |||
1063 | #if TARGET_IPHONE_SIMULATOR |
||
1064 | #define XATTR_ARCHIVED_NAME @"archived" |
||
1065 | #else |
||
1066 | #define XATTR_ARCHIVED_NAME @"lumberjack.log.archived" |
||
1067 | #endif |
||
1068 | |||
1069 | @implementation DDLogFileInfo |
||
1070 | |||
1071 | @synthesize filePath; |
||
1072 | |||
1073 | @dynamic fileName; |
||
1074 | @dynamic fileAttributes; |
||
1075 | @dynamic creationDate; |
||
1076 | @dynamic modificationDate; |
||
1077 | @dynamic fileSize; |
||
1078 | @dynamic age; |
||
1079 | |||
1080 | @dynamic isArchived; |
||
1081 | |||
1082 | |||
1083 | #pragma mark Lifecycle |
||
1084 | |||
1085 | + (instancetype)logFileWithPath:(NSString *)aFilePath |
||
1086 | { |
||
1087 | return [[self alloc] initWithFilePath:aFilePath]; |
||
1088 | } |
||
1089 | |||
1090 | - (instancetype)initWithFilePath:(NSString *)aFilePath |
||
1091 | { |
||
1092 | if ((self = [super init])) |
||
1093 | { |
||
1094 | filePath = [aFilePath copy]; |
||
1095 | } |
||
1096 | return self; |
||
1097 | } |
||
1098 | |||
1099 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1100 | #pragma mark Standard Info |
||
1101 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1102 | |||
1103 | - (NSDictionary *)fileAttributes |
||
1104 | { |
||
1105 | if (fileAttributes == nil) |
||
1106 | { |
||
1107 | fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil]; |
||
1108 | } |
||
1109 | return fileAttributes; |
||
1110 | } |
||
1111 | |||
1112 | - (NSString *)fileName |
||
1113 | { |
||
1114 | if (fileName == nil) |
||
1115 | { |
||
1116 | fileName = [filePath lastPathComponent]; |
||
1117 | } |
||
1118 | return fileName; |
||
1119 | } |
||
1120 | |||
1121 | - (NSDate *)modificationDate |
||
1122 | { |
||
1123 | if (modificationDate == nil) |
||
1124 | { |
||
1125 | modificationDate = [[self fileAttributes] objectForKey:NSFileModificationDate]; |
||
1126 | } |
||
1127 | |||
1128 | return modificationDate; |
||
1129 | } |
||
1130 | |||
1131 | - (NSDate *)creationDate |
||
1132 | { |
||
1133 | if (creationDate == nil) |
||
1134 | { |
||
1135 | creationDate = [[self fileAttributes] objectForKey:NSFileCreationDate]; |
||
1136 | } |
||
1137 | return creationDate; |
||
1138 | } |
||
1139 | |||
1140 | - (unsigned long long)fileSize |
||
1141 | { |
||
1142 | if (fileSize == 0) |
||
1143 | { |
||
1144 | fileSize = [[[self fileAttributes] objectForKey:NSFileSize] unsignedLongLongValue]; |
||
1145 | } |
||
1146 | |||
1147 | return fileSize; |
||
1148 | } |
||
1149 | |||
1150 | - (NSTimeInterval)age |
||
1151 | { |
||
1152 | return [[self creationDate] timeIntervalSinceNow] * -1.0; |
||
1153 | } |
||
1154 | |||
1155 | - (NSString *)description |
||
1156 | { |
||
1157 | return [@{@"filePath": self.filePath ?: @"", |
||
1158 | @"fileName": self.fileName ?: @"", |
||
1159 | @"fileAttributes": self.fileAttributes ?: @"", |
||
1160 | @"creationDate": self.creationDate ?: @"", |
||
1161 | @"modificationDate": self.modificationDate ?: @"", |
||
1162 | @"fileSize": @(self.fileSize), |
||
1163 | @"age": @(self.age), |
||
1164 | @"isArchived": @(self.isArchived)} description]; |
||
1165 | } |
||
1166 | |||
1167 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1168 | #pragma mark Archiving |
||
1169 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1170 | |||
1171 | - (BOOL)isArchived |
||
1172 | { |
||
1173 | |||
1174 | #if TARGET_IPHONE_SIMULATOR |
||
1175 | |||
1176 | // Extended attributes don't work properly on the simulator. |
||
1177 | // So we have to use a less attractive alternative. |
||
1178 | // See full explanation in the header file. |
||
1179 | |||
1180 | return [self hasExtensionAttributeWithName:XATTR_ARCHIVED_NAME]; |
||
1181 | |||
1182 | #else |
||
1183 | |||
1184 | return [self hasExtendedAttributeWithName:XATTR_ARCHIVED_NAME]; |
||
1185 | |||
1186 | #endif |
||
1187 | } |
||
1188 | |||
1189 | - (void)setIsArchived:(BOOL)flag |
||
1190 | { |
||
1191 | |||
1192 | #if TARGET_IPHONE_SIMULATOR |
||
1193 | |||
1194 | // Extended attributes don't work properly on the simulator. |
||
1195 | // So we have to use a less attractive alternative. |
||
1196 | // See full explanation in the header file. |
||
1197 | |||
1198 | if (flag) |
||
1199 | [self addExtensionAttributeWithName:XATTR_ARCHIVED_NAME]; |
||
1200 | else |
||
1201 | [self removeExtensionAttributeWithName:XATTR_ARCHIVED_NAME]; |
||
1202 | |||
1203 | #else |
||
1204 | |||
1205 | if (flag) |
||
1206 | [self addExtendedAttributeWithName:XATTR_ARCHIVED_NAME]; |
||
1207 | else |
||
1208 | [self removeExtendedAttributeWithName:XATTR_ARCHIVED_NAME]; |
||
1209 | |||
1210 | #endif |
||
1211 | } |
||
1212 | |||
1213 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1214 | #pragma mark Changes |
||
1215 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1216 | |||
1217 | - (void)reset |
||
1218 | { |
||
1219 | fileName = nil; |
||
1220 | fileAttributes = nil; |
||
1221 | creationDate = nil; |
||
1222 | modificationDate = nil; |
||
1223 | } |
||
1224 | |||
1225 | - (void)renameFile:(NSString *)newFileName |
||
1226 | { |
||
1227 | // This method is only used on the iPhone simulator, where normal extended attributes are broken. |
||
1228 | // See full explanation in the header file. |
||
1229 | |||
1230 | if (![newFileName isEqualToString:[self fileName]]) |
||
1231 | { |
||
1232 | NSString *fileDir = [filePath stringByDeletingLastPathComponent]; |
||
1233 | |||
1234 | NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName]; |
||
1235 | |||
1236 | NSLogVerbose(@"DDLogFileInfo: Renaming file: '%@' -> '%@'", self.fileName, newFileName); |
||
1237 | |||
1238 | NSError *error = nil; |
||
1239 | if (![[NSFileManager defaultManager] moveItemAtPath:filePath toPath:newFilePath error:&error]) |
||
1240 | { |
||
1241 | NSLogError(@"DDLogFileInfo: Error renaming file (%@): %@", self.fileName, error); |
||
1242 | } |
||
1243 | |||
1244 | filePath = newFilePath; |
||
1245 | [self reset]; |
||
1246 | } |
||
1247 | } |
||
1248 | |||
1249 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1250 | #pragma mark Attribute Management |
||
1251 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1252 | |||
1253 | #if TARGET_IPHONE_SIMULATOR |
||
1254 | |||
1255 | // Extended attributes don't work properly on the simulator. |
||
1256 | // So we have to use a less attractive alternative. |
||
1257 | // See full explanation in the header file. |
||
1258 | |||
1259 | - (BOOL)hasExtensionAttributeWithName:(NSString *)attrName |
||
1260 | { |
||
1261 | // This method is only used on the iPhone simulator, where normal extended attributes are broken. |
||
1262 | // See full explanation in the header file. |
||
1263 | |||
1264 | // Split the file name into components. |
||
1265 | // |
||
1266 | // log-ABC123.archived.uploaded.txt |
||
1267 | // |
||
1268 | // 0. log-ABC123 |
||
1269 | // 1. archived |
||
1270 | // 2. uploaded |
||
1271 | // 3. txt |
||
1272 | // |
||
1273 | // So we want to search for the attrName in the components (ignoring the first and last array indexes). |
||
1274 | |||
1275 | NSArray *components = [[self fileName] componentsSeparatedByString:@"."]; |
||
1276 | |||
1277 | // Watch out for file names without an extension |
||
1278 | |||
1279 | NSUInteger count = [components count]; |
||
1280 | NSUInteger max = (count >= 2) ? count-1 : count; |
||
1281 | |||
1282 | NSUInteger i; |
||
1283 | for (i = 1; i < max; i++) |
||
1284 | { |
||
1285 | NSString *attr = [components objectAtIndex:i]; |
||
1286 | |||
1287 | if ([attrName isEqualToString:attr]) |
||
1288 | { |
||
1289 | return YES; |
||
1290 | } |
||
1291 | } |
||
1292 | |||
1293 | return NO; |
||
1294 | } |
||
1295 | |||
1296 | - (void)addExtensionAttributeWithName:(NSString *)attrName |
||
1297 | { |
||
1298 | // This method is only used on the iPhone simulator, where normal extended attributes are broken. |
||
1299 | // See full explanation in the header file. |
||
1300 | |||
1301 | if ([attrName length] == 0) return; |
||
1302 | |||
1303 | // Example: |
||
1304 | // attrName = "archived" |
||
1305 | // |
||
1306 | // "log-ABC123.txt" -> "log-ABC123.archived.txt" |
||
1307 | |||
1308 | NSArray *components = [[self fileName] componentsSeparatedByString:@"."]; |
||
1309 | |||
1310 | NSUInteger count = [components count]; |
||
1311 | |||
1312 | NSUInteger estimatedNewLength = [[self fileName] length] + [attrName length] + 1; |
||
1313 | NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength]; |
||
1314 | |||
1315 | if (count > 0) |
||
1316 | { |
||
1317 | [newFileName appendString:[components objectAtIndex:0]]; |
||
1318 | } |
||
1319 | |||
1320 | NSString *lastExt = @""; |
||
1321 | |||
1322 | NSUInteger i; |
||
1323 | for (i = 1; i < count; i++) |
||
1324 | { |
||
1325 | NSString *attr = [components objectAtIndex:i]; |
||
1326 | if ([attr length] == 0) |
||
1327 | { |
||
1328 | continue; |
||
1329 | } |
||
1330 | |||
1331 | if ([attrName isEqualToString:attr]) |
||
1332 | { |
||
1333 | // Extension attribute already exists in file name |
||
1334 | return; |
||
1335 | } |
||
1336 | |||
1337 | if ([lastExt length] > 0) |
||
1338 | { |
||
1339 | [newFileName appendFormat:@".%@", lastExt]; |
||
1340 | } |
||
1341 | |||
1342 | lastExt = attr; |
||
1343 | } |
||
1344 | |||
1345 | [newFileName appendFormat:@".%@", attrName]; |
||
1346 | |||
1347 | if ([lastExt length] > 0) |
||
1348 | { |
||
1349 | [newFileName appendFormat:@".%@", lastExt]; |
||
1350 | } |
||
1351 | |||
1352 | [self renameFile:newFileName]; |
||
1353 | } |
||
1354 | |||
1355 | - (void)removeExtensionAttributeWithName:(NSString *)attrName |
||
1356 | { |
||
1357 | // This method is only used on the iPhone simulator, where normal extended attributes are broken. |
||
1358 | // See full explanation in the header file. |
||
1359 | |||
1360 | if ([attrName length] == 0) return; |
||
1361 | |||
1362 | // Example: |
||
1363 | // attrName = "archived" |
||
1364 | // |
||
1365 | // "log-ABC123.archived.txt" -> "log-ABC123.txt" |
||
1366 | |||
1367 | NSArray *components = [[self fileName] componentsSeparatedByString:@"."]; |
||
1368 | |||
1369 | NSUInteger count = [components count]; |
||
1370 | |||
1371 | NSUInteger estimatedNewLength = [[self fileName] length]; |
||
1372 | NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength]; |
||
1373 | |||
1374 | if (count > 0) |
||
1375 | { |
||
1376 | [newFileName appendString:[components objectAtIndex:0]]; |
||
1377 | } |
||
1378 | |||
1379 | BOOL found = NO; |
||
1380 | |||
1381 | NSUInteger i; |
||
1382 | for (i = 1; i < count; i++) |
||
1383 | { |
||
1384 | NSString *attr = [components objectAtIndex:i]; |
||
1385 | |||
1386 | if ([attrName isEqualToString:attr]) |
||
1387 | { |
||
1388 | found = YES; |
||
1389 | } |
||
1390 | else |
||
1391 | { |
||
1392 | [newFileName appendFormat:@".%@", attr]; |
||
1393 | } |
||
1394 | } |
||
1395 | |||
1396 | if (found) |
||
1397 | { |
||
1398 | [self renameFile:newFileName]; |
||
1399 | } |
||
1400 | } |
||
1401 | |||
1402 | #else |
||
1403 | |||
1404 | - (BOOL)hasExtendedAttributeWithName:(NSString *)attrName |
||
1405 | { |
||
1406 | const char *path = [filePath UTF8String]; |
||
1407 | const char *name = [attrName UTF8String]; |
||
1408 | |||
1409 | ssize_t result = getxattr(path, name, NULL, 0, 0, 0); |
||
1410 | |||
1411 | return (result >= 0); |
||
1412 | } |
||
1413 | |||
1414 | - (void)addExtendedAttributeWithName:(NSString *)attrName |
||
1415 | { |
||
1416 | const char *path = [filePath UTF8String]; |
||
1417 | const char *name = [attrName UTF8String]; |
||
1418 | |||
1419 | int result = setxattr(path, name, NULL, 0, 0, 0); |
||
1420 | |||
1421 | if (result < 0) |
||
1422 | { |
||
1423 | NSLogError(@"DDLogFileInfo: setxattr(%@, %@): error = %i", attrName, self.fileName, result); |
||
1424 | } |
||
1425 | } |
||
1426 | |||
1427 | - (void)removeExtendedAttributeWithName:(NSString *)attrName |
||
1428 | { |
||
1429 | const char *path = [filePath UTF8String]; |
||
1430 | const char *name = [attrName UTF8String]; |
||
1431 | |||
1432 | int result = removexattr(path, name, 0); |
||
1433 | |||
1434 | if (result < 0 && errno != ENOATTR) |
||
1435 | { |
||
1436 | NSLogError(@"DDLogFileInfo: removexattr(%@, %@): error = %i", attrName, self.fileName, result); |
||
1437 | } |
||
1438 | } |
||
1439 | |||
1440 | #endif |
||
1441 | |||
1442 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1443 | #pragma mark Comparisons |
||
1444 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
||
1445 | |||
1446 | - (BOOL)isEqual:(id)object |
||
1447 | { |
||
1448 | if ([object isKindOfClass:[self class]]) |
||
1449 | { |
||
1450 | DDLogFileInfo *another = (DDLogFileInfo *)object; |
||
1451 | |||
1452 | return [filePath isEqualToString:[another filePath]]; |
||
1453 | } |
||
1454 | |||
1455 | return NO; |
||
1456 | } |
||
1457 | |||
1458 | - (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another |
||
1459 | { |
||
1460 | NSDate *us = [self creationDate]; |
||
1461 | NSDate *them = [another creationDate]; |
||
1462 | |||
1463 | NSComparisonResult result = [us compare:them]; |
||
1464 | |||
1465 | if (result == NSOrderedAscending) |
||
1466 | return NSOrderedDescending; |
||
1467 | |||
1468 | if (result == NSOrderedDescending) |
||
1469 | return NSOrderedAscending; |
||
1470 | |||
1471 | return NSOrderedSame; |
||
1472 | } |
||
1473 | |||
1474 | - (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another |
||
1475 | { |
||
1476 | NSDate *us = [self modificationDate]; |
||
1477 | NSDate *them = [another modificationDate]; |
||
1478 | |||
1479 | NSComparisonResult result = [us compare:them]; |
||
1480 | |||
1481 | if (result == NSOrderedAscending) |
||
1482 | return NSOrderedDescending; |
||
1483 | |||
1484 | if (result == NSOrderedDescending) |
||
1485 | return NSOrderedAscending; |
||
1486 | |||
1487 | return NSOrderedSame; |
||
1488 | } |
||
1489 | |||
1490 | @end |
||
1491 | |||
1492 | #if TARGET_OS_IPHONE |
||
1493 | /** |
||
1494 | * When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen. |
||
1495 | * |
||
1496 | * But in case if app is able to launch from background we need to have an ability to open log file any time we |
||
1497 | * want (even if device is locked). Thats why that attribute have to be changed to |
||
1498 | * NSFileProtectionCompleteUntilFirstUserAuthentication. |
||
1499 | */ |
||
1500 | BOOL doesAppRunInBackground() |
||
1501 | { |
||
1502 | BOOL answer = NO; |
||
1503 | |||
1504 | NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"]; |
||
1505 | |||
1506 | for (NSString *mode in backgroundModes) { |
||
1507 | if (mode.length > 0) { |
||
1508 | answer = YES; |
||
1509 | break; |
||
1510 | } |
||
1511 | } |
||
1512 | |||
1513 | return answer; |
||
1514 | } |
||
1515 | #endif |
||
1516 |