CompressingLogFileManager.m 16 KB

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