CompressingLogFileManager.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. //
  2. // CompressingLogFileManager.m
  3. // LogFileCompressor
  4. //
  5. // CocoaLumberjack Demos
  6. //
  7. #import "CompressingLogFileManager.h"
  8. #import <zlib.h>
  9. // We probably shouldn't be using DDLog() statements within the DDLog implementation.
  10. // But we still want to leave our log statements for any future debugging,
  11. // and to allow other developers to trace the implementation (which is a great learning tool).
  12. //
  13. // So we use primitive logging macros around NSLog.
  14. // We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog.
  15. #define LOG_LEVEL 4
  16. #define NSLogError(frmt, ...) do{ if(LOG_LEVEL >= 1) NSLog(frmt, ##__VA_ARGS__); } while(0)
  17. #define NSLogWarn(frmt, ...) do{ if(LOG_LEVEL >= 2) NSLog(frmt, ##__VA_ARGS__); } while(0)
  18. #define NSLogInfo(frmt, ...) do{ if(LOG_LEVEL >= 3) NSLog(frmt, ##__VA_ARGS__); } while(0)
  19. #define NSLogVerbose(frmt, ...) do{ if(LOG_LEVEL >= 4) NSLog(frmt, ##__VA_ARGS__); } while(0)
  20. @interface CompressingLogFileManager (/* Must be nameless for properties */)
  21. @property (readwrite) BOOL isCompressing;
  22. @end
  23. @interface DDLogFileInfo (Compressor)
  24. @property (nonatomic, readonly) BOOL isCompressed;
  25. - (NSString *)tempFilePathByAppendingPathExtension:(NSString *)newExt;
  26. - (NSString *)fileNameByAppendingPathExtension:(NSString *)newExt;
  27. @end
  28. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  29. #pragma mark -
  30. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  31. @implementation CompressingLogFileManager
  32. @synthesize isCompressing;
  33. - (id)init
  34. {
  35. return [self initWithLogsDirectory:nil];
  36. }
  37. - (id)initWithLogsDirectory:(NSString *)aLogsDirectory
  38. {
  39. if ((self = [super initWithLogsDirectory:aLogsDirectory]))
  40. {
  41. upToDate = NO;
  42. // Check for any files that need to be compressed.
  43. // But don't start right away.
  44. // Wait for the app startup process to finish.
  45. [self performSelector:@selector(compressNextLogFile) withObject:nil afterDelay:5.0];
  46. }
  47. return self;
  48. }
  49. - (void)dealloc
  50. {
  51. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(compressNextLogFile) object:nil];
  52. }
  53. - (void)compressLogFile:(DDLogFileInfo *)logFile
  54. {
  55. self.isCompressing = YES;
  56. CompressingLogFileManager* __weak weakSelf = self;
  57. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
  58. [weakSelf backgroundThread_CompressLogFile:logFile];
  59. });
  60. }
  61. - (void)compressNextLogFile
  62. {
  63. if (self.isCompressing)
  64. {
  65. // We're already compressing a file.
  66. // Wait until it's done to move onto the next file.
  67. return;
  68. }
  69. NSLogVerbose(@"CompressingLogFileManager: compressNextLogFile");
  70. upToDate = NO;
  71. NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
  72. NSUInteger count = [sortedLogFileInfos count];
  73. if (count == 0)
  74. {
  75. // Nothing to compress
  76. upToDate = YES;
  77. return;
  78. }
  79. NSUInteger i = count;
  80. while (i > 0)
  81. {
  82. DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:(i - 1)];
  83. if (logFileInfo.isArchived && !logFileInfo.isCompressed)
  84. {
  85. [self compressLogFile:logFileInfo];
  86. break;
  87. }
  88. i--;
  89. }
  90. upToDate = YES;
  91. }
  92. - (void)compressionDidSucceed:(DDLogFileInfo *)logFile
  93. {
  94. NSLogVerbose(@"CompressingLogFileManager: compressionDidSucceed: %@", logFile.fileName);
  95. self.isCompressing = NO;
  96. [self compressNextLogFile];
  97. }
  98. - (void)compressionDidFail:(DDLogFileInfo *)logFile
  99. {
  100. NSLogWarn(@"CompressingLogFileManager: compressionDidFail: %@", logFile.fileName);
  101. self.isCompressing = NO;
  102. // We should try the compression again, but after a short delay.
  103. //
  104. // If the compression failed there is probably some filesystem issue,
  105. // so flooding it with compression attempts is only going to make things worse.
  106. NSTimeInterval delay = (60 * 15); // 15 minutes
  107. [self performSelector:@selector(compressNextLogFile) withObject:nil afterDelay:delay];
  108. }
  109. - (void)didArchiveLogFile:(NSString *)logFilePath
  110. {
  111. NSLogVerbose(@"CompressingLogFileManager: didArchiveLogFile: %@", [logFilePath lastPathComponent]);
  112. // If all other log files have been compressed,
  113. // then we can get started right away.
  114. // Otherwise we should just wait for the current compression process to finish.
  115. if (upToDate)
  116. {
  117. [self compressLogFile:[DDLogFileInfo logFileWithPath:logFilePath]];
  118. }
  119. }
  120. - (void)didRollAndArchiveLogFile:(NSString *)logFilePath
  121. {
  122. NSLogVerbose(@"CompressingLogFileManager: didRollAndArchiveLogFile: %@", [logFilePath lastPathComponent]);
  123. // If all other log files have been compressed,
  124. // then we can get started right away.
  125. // Otherwise we should just wait for the current compression process to finish.
  126. if (upToDate)
  127. {
  128. [self compressLogFile:[DDLogFileInfo logFileWithPath:logFilePath]];
  129. }
  130. }
  131. - (void)backgroundThread_CompressLogFile:(DDLogFileInfo *)logFile
  132. {
  133. @autoreleasepool {
  134. NSLogInfo(@"CompressingLogFileManager: Compressing log file: %@", logFile.fileName);
  135. // Steps:
  136. // 1. Create a new file with the same fileName, but added "gzip" extension
  137. // 2. Open the new file for writing (output file)
  138. // 3. Open the given file for reading (input file)
  139. // 4. Setup zlib for gzip compression
  140. // 5. Read a chunk of the given file
  141. // 6. Compress the chunk
  142. // 7. Write the compressed chunk to the output file
  143. // 8. Repeat steps 5 - 7 until the input file is exhausted
  144. // 9. Close input and output file
  145. // 10. Teardown zlib
  146. // STEP 1
  147. NSString *inputFilePath = logFile.filePath;
  148. NSString *tempOutputFilePath = [logFile tempFilePathByAppendingPathExtension:@"gz"];
  149. #if TARGET_OS_IPHONE
  150. // We use the same protection as the original file. This means that it has the same security characteristics.
  151. // Also, if the app can run in the background, this means that it gets
  152. // NSFileProtectionCompleteUntilFirstUserAuthentication so that we can do this compression even with the
  153. // device locked. c.f. DDFileLogger.doesAppRunInBackground.
  154. NSString* protection = logFile.fileAttributes[NSFileProtectionKey];
  155. NSDictionary* attributes = protection == nil ? nil : @{NSFileProtectionKey: protection};
  156. [[NSFileManager defaultManager] createFileAtPath:tempOutputFilePath contents:nil attributes:attributes];
  157. #endif
  158. // STEP 2 & 3
  159. NSInputStream *inputStream = [NSInputStream inputStreamWithFileAtPath:inputFilePath];
  160. NSOutputStream *outputStream = [NSOutputStream outputStreamToFileAtPath:tempOutputFilePath append:NO];
  161. [inputStream open];
  162. [outputStream open];
  163. // STEP 4
  164. z_stream strm;
  165. // Zero out the structure before (to be safe) before we start using it
  166. bzero(&strm, sizeof(strm));
  167. strm.zalloc = Z_NULL;
  168. strm.zfree = Z_NULL;
  169. strm.opaque = Z_NULL;
  170. strm.total_out = 0;
  171. // Compresssion Levels:
  172. // Z_NO_COMPRESSION
  173. // Z_BEST_SPEED
  174. // Z_BEST_COMPRESSION
  175. // Z_DEFAULT_COMPRESSION
  176. deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY);
  177. // Prepare our variables for steps 5-7
  178. //
  179. // inputDataLength : Total length of buffer that we will read file data into
  180. // outputDataLength : Total length of buffer that zlib will output compressed bytes into
  181. //
  182. // Note: The output buffer can be smaller than the input buffer because the
  183. // compressed/output data is smaller than the file/input data (obviously).
  184. //
  185. // inputDataSize : The number of bytes in the input buffer that have valid data to be compressed.
  186. //
  187. // Imagine compressing a tiny file that is actually smaller than our inputDataLength.
  188. // In this case only a portion of the input buffer would have valid file data.
  189. // The inputDataSize helps represent the portion of the buffer that is valid.
  190. //
  191. // Imagine compressing a huge file, but consider what happens when we get to the very end of the file.
  192. // The last read will likely only fill a portion of the input buffer.
  193. // The inputDataSize helps represent the portion of the buffer that is valid.
  194. NSUInteger inputDataLength = (1024 * 2); // 2 KB
  195. NSUInteger outputDataLength = (1024 * 1); // 1 KB
  196. NSMutableData *inputData = [NSMutableData dataWithLength:inputDataLength];
  197. NSMutableData *outputData = [NSMutableData dataWithLength:outputDataLength];
  198. NSUInteger inputDataSize = 0;
  199. BOOL done = YES;
  200. NSError* error = nil;
  201. do
  202. {
  203. @autoreleasepool {
  204. // STEP 5
  205. // Read data from the input stream into our input buffer.
  206. //
  207. // inputBuffer : pointer to where we want the input stream to copy bytes into
  208. // inputBufferLength : max number of bytes the input stream should read
  209. //
  210. // Recall that inputDataSize is the number of valid bytes that already exist in the
  211. // input buffer that still need to be compressed.
  212. // This value is usually zero, but may be larger if a previous iteration of the loop
  213. // was unable to compress all the bytes in the input buffer.
  214. //
  215. // For example, imagine that we ready 2K worth of data from the file in the last loop iteration,
  216. // but when we asked zlib to compress it all, zlib was only able to compress 1.5K of it.
  217. // We would still have 0.5K leftover that still needs to be compressed.
  218. // We want to make sure not to skip this important data.
  219. //
  220. // The [inputData mutableBytes] gives us a pointer to the beginning of the underlying buffer.
  221. // When we add inputDataSize we get to the proper offset within the buffer
  222. // at which our input stream can start copying bytes into without overwriting anything it shouldn't.
  223. const void *inputBuffer = [inputData mutableBytes] + inputDataSize;
  224. NSUInteger inputBufferLength = inputDataLength - inputDataSize;
  225. NSInteger readLength = [inputStream read:(uint8_t *)inputBuffer maxLength:inputBufferLength];
  226. if (readLength < 0) {
  227. error = [inputStream streamError];
  228. break;
  229. }
  230. NSLogVerbose(@"CompressingLogFileManager: Read %li bytes from file", (long)readLength);
  231. inputDataSize += readLength;
  232. // STEP 6
  233. // Ask zlib to compress our input buffer.
  234. // Tell it to put the compressed bytes into our output buffer.
  235. strm.next_in = (Bytef *)[inputData mutableBytes]; // Read from input buffer
  236. strm.avail_in = (uInt)inputDataSize; // as much as was read from file (plus leftovers).
  237. strm.next_out = (Bytef *)[outputData mutableBytes]; // Write data to output buffer
  238. strm.avail_out = (uInt)outputDataLength; // as much space as is available in the buffer.
  239. // When we tell zlib to compress our data,
  240. // it won't directly tell us how much data was processed.
  241. // Instead it keeps a running total of the number of bytes it has processed.
  242. // In other words, every iteration from the loop it increments its total values.
  243. // So to figure out how much data was processed in this iteration,
  244. // we fetch the totals before we ask it to compress data,
  245. // and then afterwards we subtract from the new totals.
  246. NSInteger prevTotalIn = strm.total_in;
  247. NSInteger prevTotalOut = strm.total_out;
  248. int flush = [inputStream hasBytesAvailable] ? Z_SYNC_FLUSH : Z_FINISH;
  249. deflate(&strm, flush);
  250. NSInteger inputProcessed = strm.total_in - prevTotalIn;
  251. NSInteger outputProcessed = strm.total_out - prevTotalOut;
  252. NSLogVerbose(@"CompressingLogFileManager: Total bytes uncompressed: %lu", (unsigned long)strm.total_in);
  253. NSLogVerbose(@"CompressingLogFileManager: Total bytes compressed: %lu", (unsigned long)strm.total_out);
  254. NSLogVerbose(@"CompressingLogFileManager: Compression ratio: %.1f%%",
  255. (1.0F - (float)(strm.total_out) / (float)(strm.total_in)) * 100);
  256. // STEP 7
  257. // Now write all compressed bytes to our output stream.
  258. //
  259. // It is theoretically possible that the write operation doesn't write everything we ask it to.
  260. // Although this is highly unlikely, we take precautions.
  261. // Also, we watch out for any errors (maybe the disk is full).
  262. NSUInteger totalWriteLength = 0;
  263. NSInteger writeLength = 0;
  264. do
  265. {
  266. const void *outputBuffer = [outputData mutableBytes] + totalWriteLength;
  267. NSUInteger outputBufferLength = outputProcessed - totalWriteLength;
  268. writeLength = [outputStream write:(const uint8_t *)outputBuffer maxLength:outputBufferLength];
  269. if (writeLength < 0)
  270. {
  271. error = [outputStream streamError];
  272. }
  273. else
  274. {
  275. totalWriteLength += writeLength;
  276. }
  277. } while((totalWriteLength < outputProcessed) && !error);
  278. // STEP 7.5
  279. //
  280. // We now have data in our input buffer that has already been compressed.
  281. // We want to remove all the processed data from the input buffer,
  282. // and we want to move any unprocessed data to the beginning of the buffer.
  283. //
  284. // If the amount processed is less than the valid buffer size, we have leftovers.
  285. NSUInteger inputRemaining = inputDataSize - inputProcessed;
  286. if (inputRemaining > 0)
  287. {
  288. void *inputDst = [inputData mutableBytes];
  289. void *inputSrc = [inputData mutableBytes] + inputProcessed;
  290. memmove(inputDst, inputSrc, inputRemaining);
  291. }
  292. inputDataSize = inputRemaining;
  293. // Are we done yet?
  294. done = ((flush == Z_FINISH) && (inputDataSize == 0));
  295. // STEP 8
  296. // Loop repeats until end of data (or unlikely error)
  297. } // end @autoreleasepool
  298. } while (!done && error == nil);
  299. // STEP 9
  300. [inputStream close];
  301. [outputStream close];
  302. // STEP 10
  303. deflateEnd(&strm);
  304. // We're done!
  305. // Report success or failure back to the logging thread/queue.
  306. if (error)
  307. {
  308. // Remove output file.
  309. // Our compression attempt failed.
  310. NSLogError(@"Compression of %@ failed: %@", inputFilePath, error);
  311. error = nil;
  312. BOOL ok = [[NSFileManager defaultManager] removeItemAtPath:tempOutputFilePath error:&error];
  313. if (!ok)
  314. NSLogError(@"Failed to clean up %@ after failed compression: %@", tempOutputFilePath, error);
  315. // Report failure to class via logging thread/queue
  316. dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool {
  317. [self compressionDidFail:logFile];
  318. }});
  319. }
  320. else
  321. {
  322. // Remove original input file.
  323. // It will be replaced with the new compressed version.
  324. error = nil;
  325. BOOL ok = [[NSFileManager defaultManager] removeItemAtPath:inputFilePath error:&error];
  326. if (!ok)
  327. NSLogWarn(@"Warning: failed to remove original file %@ after compression: %@", inputFilePath, error);
  328. // Mark the compressed file as archived,
  329. // and then move it into its final destination.
  330. //
  331. // temp-log-ABC123.txt.gz -> log-ABC123.txt.gz
  332. //
  333. // The reason we were using the "temp-" prefix was so the file would not be
  334. // considered a log file while it was only partially complete.
  335. // Only files that begin with "log-" are considered log files.
  336. DDLogFileInfo *compressedLogFile = [DDLogFileInfo logFileWithPath:tempOutputFilePath];
  337. compressedLogFile.isArchived = YES;
  338. NSString *outputFileName = [logFile fileNameByAppendingPathExtension:@"gz"];
  339. [compressedLogFile renameFile:outputFileName];
  340. // Report success to class via logging thread/queue
  341. dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool {
  342. [self compressionDidSucceed:compressedLogFile];
  343. }});
  344. }
  345. } // end @autoreleasepool
  346. }
  347. @end
  348. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  349. #pragma mark -
  350. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  351. @implementation DDLogFileInfo (Compressor)
  352. @dynamic isCompressed;
  353. - (BOOL)isCompressed
  354. {
  355. return [[[self fileName] pathExtension] isEqualToString:@"gz"];
  356. }
  357. - (NSString *)tempFilePathByAppendingPathExtension:(NSString *)newExt
  358. {
  359. // Example:
  360. //
  361. // Current File Name: "/full/path/to/log-ABC123.txt"
  362. //
  363. // newExt: "gzip"
  364. // result: "/full/path/to/temp-log-ABC123.txt.gzip"
  365. NSString *tempFileName = [NSString stringWithFormat:@"temp-%@", [self fileName]];
  366. NSString *newFileName = [tempFileName stringByAppendingPathExtension:newExt];
  367. NSString *fileDir = [[self filePath] stringByDeletingLastPathComponent];
  368. NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName];
  369. return newFilePath;
  370. }
  371. - (NSString *)fileNameByAppendingPathExtension:(NSString *)newExt
  372. {
  373. // Example:
  374. //
  375. // Current File Name: "log-ABC123.txt"
  376. //
  377. // newExt: "gzip"
  378. // result: "log-ABC123.txt.gzip"
  379. NSString *fileNameExtension = [[self fileName] pathExtension];
  380. if ([fileNameExtension isEqualToString:newExt])
  381. {
  382. return [self fileName];
  383. }
  384. return [[self fileName] stringByAppendingPathExtension:newExt];
  385. }
  386. @end