HTTPAsyncFileResponse.m 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. #import "HTTPAsyncFileResponse.h"
  2. #import "HTTPConnection.h"
  3. #import "HTTPLogging.h"
  4. #import <unistd.h>
  5. #import <fcntl.h>
  6. #if ! __has_feature(objc_arc)
  7. #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
  8. #endif
  9. // Log levels : off, error, warn, info, verbose
  10. // Other flags: trace
  11. static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE;
  12. #define NULL_FD -1
  13. /**
  14. * Architecure overview:
  15. *
  16. * HTTPConnection will invoke our readDataOfLength: method to fetch data.
  17. * We will return nil, and then proceed to read the data via our readSource on our readQueue.
  18. * Once the requested amount of data has been read, we then pause our readSource,
  19. * and inform the connection of the available data.
  20. *
  21. * While our read is in progress, we don't have to worry about the connection calling any other methods,
  22. * except the connectionDidClose method, which would be invoked if the remote end closed the socket connection.
  23. * To safely handle this, we do a synchronous dispatch on the readQueue,
  24. * and nilify the connection as well as cancel our readSource.
  25. *
  26. * In order to minimize resource consumption during a HEAD request,
  27. * we don't open the file until we have to (until the connection starts requesting data).
  28. **/
  29. @implementation HTTPAsyncFileResponse
  30. - (id)initWithFilePath:(NSString *)fpath forConnection:(HTTPConnection *)parent
  31. {
  32. if ((self = [super init]))
  33. {
  34. HTTPLogTrace();
  35. connection = parent; // Parents retain children, children do NOT retain parents
  36. fileFD = NULL_FD;
  37. filePath = [fpath copy];
  38. if (filePath == nil)
  39. {
  40. HTTPLogWarn(@"%@: Init failed - Nil filePath", THIS_FILE);
  41. return nil;
  42. }
  43. NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL];
  44. if (fileAttributes == nil)
  45. {
  46. HTTPLogWarn(@"%@: Init failed - Unable to get file attributes. filePath: %@", THIS_FILE, filePath);
  47. return nil;
  48. }
  49. fileLength = (UInt64)[[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue];
  50. fileOffset = 0;
  51. aborted = NO;
  52. // We don't bother opening the file here.
  53. // If this is a HEAD request we only need to know the fileLength.
  54. }
  55. return self;
  56. }
  57. - (void)abort
  58. {
  59. HTTPLogTrace();
  60. [connection responseDidAbort:self];
  61. aborted = YES;
  62. }
  63. - (void)processReadBuffer
  64. {
  65. // This method is here to allow superclasses to perform post-processing of the data.
  66. // For an example, see the HTTPDynamicFileResponse class.
  67. //
  68. // At this point, the readBuffer has readBufferOffset bytes available.
  69. // This method is in charge of updating the readBufferOffset.
  70. // Failure to do so will cause the readBuffer to grow to fileLength. (Imagine a 1 GB file...)
  71. // Copy the data out of the temporary readBuffer.
  72. data = [[NSData alloc] initWithBytes:readBuffer length:readBufferOffset];
  73. // Reset the read buffer.
  74. readBufferOffset = 0;
  75. // Notify the connection that we have data available for it.
  76. [connection responseHasAvailableData:self];
  77. }
  78. - (void)pauseReadSource
  79. {
  80. if (!readSourceSuspended)
  81. {
  82. HTTPLogVerbose(@"%@[%p]: Suspending readSource", THIS_FILE, self);
  83. readSourceSuspended = YES;
  84. dispatch_suspend(readSource);
  85. }
  86. }
  87. - (void)resumeReadSource
  88. {
  89. if (readSourceSuspended)
  90. {
  91. HTTPLogVerbose(@"%@[%p]: Resuming readSource", THIS_FILE, self);
  92. readSourceSuspended = NO;
  93. dispatch_resume(readSource);
  94. }
  95. }
  96. - (void)cancelReadSource
  97. {
  98. HTTPLogVerbose(@"%@[%p]: Canceling readSource", THIS_FILE, self);
  99. dispatch_source_cancel(readSource);
  100. // Cancelling a dispatch source doesn't
  101. // invoke the cancel handler if the dispatch source is paused.
  102. if (readSourceSuspended)
  103. {
  104. readSourceSuspended = NO;
  105. dispatch_resume(readSource);
  106. }
  107. }
  108. - (BOOL)openFileAndSetupReadSource
  109. {
  110. HTTPLogTrace();
  111. fileFD = open([filePath UTF8String], (O_RDONLY | O_NONBLOCK));
  112. if (fileFD == NULL_FD)
  113. {
  114. HTTPLogError(@"%@: Unable to open file. filePath: %@", THIS_FILE, filePath);
  115. return NO;
  116. }
  117. HTTPLogVerbose(@"%@[%p]: Open fd[%i] -> %@", THIS_FILE, self, fileFD, filePath);
  118. readQueue = dispatch_queue_create("HTTPAsyncFileResponse", NULL);
  119. readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fileFD, 0, readQueue);
  120. dispatch_source_set_event_handler(readSource, ^{
  121. HTTPLogTrace2(@"%@: eventBlock - fd[%i]", THIS_FILE, fileFD);
  122. // Determine how much data we should read.
  123. //
  124. // It is OK if we ask to read more bytes than exist in the file.
  125. // It is NOT OK to over-allocate the buffer.
  126. unsigned long long _bytesAvailableOnFD = dispatch_source_get_data(readSource);
  127. UInt64 _bytesLeftInFile = fileLength - readOffset;
  128. NSUInteger bytesAvailableOnFD;
  129. NSUInteger bytesLeftInFile;
  130. bytesAvailableOnFD = (_bytesAvailableOnFD > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesAvailableOnFD;
  131. bytesLeftInFile = (_bytesLeftInFile > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesLeftInFile;
  132. NSUInteger bytesLeftInRequest = readRequestLength - readBufferOffset;
  133. NSUInteger bytesLeft = MIN(bytesLeftInRequest, bytesLeftInFile);
  134. NSUInteger bytesToRead = MIN(bytesAvailableOnFD, bytesLeft);
  135. // Make sure buffer is big enough for read request.
  136. // Do not over-allocate.
  137. if (readBuffer == NULL || bytesToRead > (readBufferSize - readBufferOffset))
  138. {
  139. readBufferSize = bytesToRead;
  140. readBuffer = reallocf(readBuffer, (size_t)bytesToRead);
  141. if (readBuffer == NULL)
  142. {
  143. HTTPLogError(@"%@[%p]: Unable to allocate buffer", THIS_FILE, self);
  144. [self pauseReadSource];
  145. [self abort];
  146. return;
  147. }
  148. }
  149. // Perform the read
  150. HTTPLogVerbose(@"%@[%p]: Attempting to read %lu bytes from file", THIS_FILE, self, (unsigned long)bytesToRead);
  151. ssize_t result = read(fileFD, readBuffer + readBufferOffset, (size_t)bytesToRead);
  152. // Check the results
  153. if (result < 0)
  154. {
  155. HTTPLogError(@"%@: Error(%i) reading file(%@)", THIS_FILE, errno, filePath);
  156. [self pauseReadSource];
  157. [self abort];
  158. }
  159. else if (result == 0)
  160. {
  161. HTTPLogError(@"%@: Read EOF on file(%@)", THIS_FILE, filePath);
  162. [self pauseReadSource];
  163. [self abort];
  164. }
  165. else // (result > 0)
  166. {
  167. HTTPLogVerbose(@"%@[%p]: Read %lu bytes from file", THIS_FILE, self, (unsigned long)result);
  168. readOffset += result;
  169. readBufferOffset += result;
  170. [self pauseReadSource];
  171. [self processReadBuffer];
  172. }
  173. });
  174. int theFileFD = fileFD;
  175. #if !OS_OBJECT_USE_OBJC
  176. dispatch_source_t theReadSource = readSource;
  177. #endif
  178. dispatch_source_set_cancel_handler(readSource, ^{
  179. // Do not access self from within this block in any way, shape or form.
  180. //
  181. // Note: You access self if you reference an iVar.
  182. HTTPLogTrace2(@"%@: cancelBlock - Close fd[%i]", THIS_FILE, theFileFD);
  183. #if !OS_OBJECT_USE_OBJC
  184. dispatch_release(theReadSource);
  185. #endif
  186. close(theFileFD);
  187. });
  188. readSourceSuspended = YES;
  189. return YES;
  190. }
  191. - (BOOL)openFileIfNeeded
  192. {
  193. if (aborted)
  194. {
  195. // The file operation has been aborted.
  196. // This could be because we failed to open the file,
  197. // or the reading process failed.
  198. return NO;
  199. }
  200. if (fileFD != NULL_FD)
  201. {
  202. // File has already been opened.
  203. return YES;
  204. }
  205. return [self openFileAndSetupReadSource];
  206. }
  207. - (UInt64)contentLength
  208. {
  209. HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, fileLength);
  210. return fileLength;
  211. }
  212. - (UInt64)offset
  213. {
  214. HTTPLogTrace();
  215. return fileOffset;
  216. }
  217. - (void)setOffset:(UInt64)offset
  218. {
  219. HTTPLogTrace2(@"%@[%p]: setOffset:%llu", THIS_FILE, self, offset);
  220. if (![self openFileIfNeeded])
  221. {
  222. // File opening failed,
  223. // or response has been aborted due to another error.
  224. return;
  225. }
  226. fileOffset = offset;
  227. readOffset = offset;
  228. off_t result = lseek(fileFD, (off_t)offset, SEEK_SET);
  229. if (result == -1)
  230. {
  231. HTTPLogError(@"%@[%p]: lseek failed - errno(%i) filePath(%@)", THIS_FILE, self, errno, filePath);
  232. [self abort];
  233. }
  234. }
  235. - (NSData *)readDataOfLength:(NSUInteger)length
  236. {
  237. HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)length);
  238. if (data)
  239. {
  240. NSUInteger dataLength = [data length];
  241. HTTPLogVerbose(@"%@[%p]: Returning data of length %lu", THIS_FILE, self, (unsigned long)dataLength);
  242. fileOffset += dataLength;
  243. NSData *result = data;
  244. data = nil;
  245. return result;
  246. }
  247. else
  248. {
  249. if (![self openFileIfNeeded])
  250. {
  251. // File opening failed,
  252. // or response has been aborted due to another error.
  253. return nil;
  254. }
  255. dispatch_sync(readQueue, ^{
  256. NSAssert(readSourceSuspended, @"Invalid logic - perhaps HTTPConnection has changed.");
  257. readRequestLength = length;
  258. [self resumeReadSource];
  259. });
  260. return nil;
  261. }
  262. }
  263. - (BOOL)isDone
  264. {
  265. BOOL result = (fileOffset == fileLength);
  266. HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO"));
  267. return result;
  268. }
  269. - (NSString *)filePath
  270. {
  271. return filePath;
  272. }
  273. - (BOOL)isAsynchronous
  274. {
  275. HTTPLogTrace();
  276. return YES;
  277. }
  278. - (void)connectionDidClose
  279. {
  280. HTTPLogTrace();
  281. if (fileFD != NULL_FD)
  282. {
  283. dispatch_sync(readQueue, ^{
  284. // Prevent any further calls to the connection
  285. connection = nil;
  286. // Cancel the readSource.
  287. // We do this here because the readSource's eventBlock has retained self.
  288. // In other words, if we don't cancel the readSource, we will never get deallocated.
  289. [self cancelReadSource];
  290. });
  291. }
  292. }
  293. - (void)dealloc
  294. {
  295. HTTPLogTrace();
  296. #if !OS_OBJECT_USE_OBJC
  297. if (readQueue) dispatch_release(readQueue);
  298. #endif
  299. if (readBuffer)
  300. free(readBuffer);
  301. }
  302. @end