#import "HTTPAsyncFileResponse.h" #import "HTTPConnection.h" #import "HTTPLogging.h" #import #import #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif // Log levels : off, error, warn, info, verbose // Other flags: trace static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; #define NULL_FD -1 /** * Architecure overview: * * HTTPConnection will invoke our readDataOfLength: method to fetch data. * We will return nil, and then proceed to read the data via our readSource on our readQueue. * Once the requested amount of data has been read, we then pause our readSource, * and inform the connection of the available data. * * While our read is in progress, we don't have to worry about the connection calling any other methods, * except the connectionDidClose method, which would be invoked if the remote end closed the socket connection. * To safely handle this, we do a synchronous dispatch on the readQueue, * and nilify the connection as well as cancel our readSource. * * In order to minimize resource consumption during a HEAD request, * we don't open the file until we have to (until the connection starts requesting data). **/ @implementation HTTPAsyncFileResponse - (id)initWithFilePath:(NSString *)fpath forConnection:(HTTPConnection *)parent { if ((self = [super init])) { HTTPLogTrace(); connection = parent; // Parents retain children, children do NOT retain parents fileFD = NULL_FD; filePath = [fpath copy]; if (filePath == nil) { HTTPLogWarn(@"%@: Init failed - Nil filePath", THIS_FILE); return nil; } NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL]; if (fileAttributes == nil) { HTTPLogWarn(@"%@: Init failed - Unable to get file attributes. filePath: %@", THIS_FILE, filePath); return nil; } fileLength = (UInt64)[[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue]; fileOffset = 0; aborted = NO; // We don't bother opening the file here. // If this is a HEAD request we only need to know the fileLength. } return self; } - (void)abort { HTTPLogTrace(); [connection responseDidAbort:self]; aborted = YES; } - (void)processReadBuffer { // This method is here to allow superclasses to perform post-processing of the data. // For an example, see the HTTPDynamicFileResponse class. // // At this point, the readBuffer has readBufferOffset bytes available. // This method is in charge of updating the readBufferOffset. // Failure to do so will cause the readBuffer to grow to fileLength. (Imagine a 1 GB file...) // Copy the data out of the temporary readBuffer. data = [[NSData alloc] initWithBytes:readBuffer length:readBufferOffset]; // Reset the read buffer. readBufferOffset = 0; // Notify the connection that we have data available for it. [connection responseHasAvailableData:self]; } - (void)pauseReadSource { if (!readSourceSuspended) { HTTPLogVerbose(@"%@[%p]: Suspending readSource", THIS_FILE, self); readSourceSuspended = YES; dispatch_suspend(readSource); } } - (void)resumeReadSource { if (readSourceSuspended) { HTTPLogVerbose(@"%@[%p]: Resuming readSource", THIS_FILE, self); readSourceSuspended = NO; dispatch_resume(readSource); } } - (void)cancelReadSource { HTTPLogVerbose(@"%@[%p]: Canceling readSource", THIS_FILE, self); dispatch_source_cancel(readSource); // Cancelling a dispatch source doesn't // invoke the cancel handler if the dispatch source is paused. if (readSourceSuspended) { readSourceSuspended = NO; dispatch_resume(readSource); } } - (BOOL)openFileAndSetupReadSource { HTTPLogTrace(); fileFD = open([filePath UTF8String], (O_RDONLY | O_NONBLOCK)); if (fileFD == NULL_FD) { HTTPLogError(@"%@: Unable to open file. filePath: %@", THIS_FILE, filePath); return NO; } HTTPLogVerbose(@"%@[%p]: Open fd[%i] -> %@", THIS_FILE, self, fileFD, filePath); readQueue = dispatch_queue_create("HTTPAsyncFileResponse", NULL); readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fileFD, 0, readQueue); dispatch_source_set_event_handler(readSource, ^{ HTTPLogTrace2(@"%@: eventBlock - fd[%i]", THIS_FILE, fileFD); // Determine how much data we should read. // // It is OK if we ask to read more bytes than exist in the file. // It is NOT OK to over-allocate the buffer. unsigned long long _bytesAvailableOnFD = dispatch_source_get_data(readSource); UInt64 _bytesLeftInFile = fileLength - readOffset; NSUInteger bytesAvailableOnFD; NSUInteger bytesLeftInFile; bytesAvailableOnFD = (_bytesAvailableOnFD > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesAvailableOnFD; bytesLeftInFile = (_bytesLeftInFile > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesLeftInFile; NSUInteger bytesLeftInRequest = readRequestLength - readBufferOffset; NSUInteger bytesLeft = MIN(bytesLeftInRequest, bytesLeftInFile); NSUInteger bytesToRead = MIN(bytesAvailableOnFD, bytesLeft); // Make sure buffer is big enough for read request. // Do not over-allocate. if (readBuffer == NULL || bytesToRead > (readBufferSize - readBufferOffset)) { readBufferSize = bytesToRead; readBuffer = reallocf(readBuffer, (size_t)bytesToRead); if (readBuffer == NULL) { HTTPLogError(@"%@[%p]: Unable to allocate buffer", THIS_FILE, self); [self pauseReadSource]; [self abort]; return; } } // Perform the read HTTPLogVerbose(@"%@[%p]: Attempting to read %lu bytes from file", THIS_FILE, self, (unsigned long)bytesToRead); ssize_t result = read(fileFD, readBuffer + readBufferOffset, (size_t)bytesToRead); // Check the results if (result < 0) { HTTPLogError(@"%@: Error(%i) reading file(%@)", THIS_FILE, errno, filePath); [self pauseReadSource]; [self abort]; } else if (result == 0) { HTTPLogError(@"%@: Read EOF on file(%@)", THIS_FILE, filePath); [self pauseReadSource]; [self abort]; } else // (result > 0) { HTTPLogVerbose(@"%@[%p]: Read %lu bytes from file", THIS_FILE, self, (unsigned long)result); readOffset += result; readBufferOffset += result; [self pauseReadSource]; [self processReadBuffer]; } }); int theFileFD = fileFD; #if !OS_OBJECT_USE_OBJC dispatch_source_t theReadSource = readSource; #endif dispatch_source_set_cancel_handler(readSource, ^{ // Do not access self from within this block in any way, shape or form. // // Note: You access self if you reference an iVar. HTTPLogTrace2(@"%@: cancelBlock - Close fd[%i]", THIS_FILE, theFileFD); #if !OS_OBJECT_USE_OBJC dispatch_release(theReadSource); #endif close(theFileFD); }); readSourceSuspended = YES; return YES; } - (BOOL)openFileIfNeeded { if (aborted) { // The file operation has been aborted. // This could be because we failed to open the file, // or the reading process failed. return NO; } if (fileFD != NULL_FD) { // File has already been opened. return YES; } return [self openFileAndSetupReadSource]; } - (UInt64)contentLength { HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, fileLength); return fileLength; } - (UInt64)offset { HTTPLogTrace(); return fileOffset; } - (void)setOffset:(UInt64)offset { HTTPLogTrace2(@"%@[%p]: setOffset:%llu", THIS_FILE, self, offset); if (![self openFileIfNeeded]) { // File opening failed, // or response has been aborted due to another error. return; } fileOffset = offset; readOffset = offset; off_t result = lseek(fileFD, (off_t)offset, SEEK_SET); if (result == -1) { HTTPLogError(@"%@[%p]: lseek failed - errno(%i) filePath(%@)", THIS_FILE, self, errno, filePath); [self abort]; } } - (NSData *)readDataOfLength:(NSUInteger)length { HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)length); if (data) { NSUInteger dataLength = [data length]; HTTPLogVerbose(@"%@[%p]: Returning data of length %lu", THIS_FILE, self, (unsigned long)dataLength); fileOffset += dataLength; NSData *result = data; data = nil; return result; } else { if (![self openFileIfNeeded]) { // File opening failed, // or response has been aborted due to another error. return nil; } dispatch_sync(readQueue, ^{ NSAssert(readSourceSuspended, @"Invalid logic - perhaps HTTPConnection has changed."); readRequestLength = length; [self resumeReadSource]; }); return nil; } } - (BOOL)isDone { BOOL result = (fileOffset == fileLength); HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO")); return result; } - (NSString *)filePath { return filePath; } - (BOOL)isAsynchronous { HTTPLogTrace(); return YES; } - (void)connectionDidClose { HTTPLogTrace(); if (fileFD != NULL_FD) { dispatch_sync(readQueue, ^{ // Prevent any further calls to the connection connection = nil; // Cancel the readSource. // We do this here because the readSource's eventBlock has retained self. // In other words, if we don't cancel the readSource, we will never get deallocated. [self cancelReadSource]; }); } } - (void)dealloc { HTTPLogTrace(); #if !OS_OBJECT_USE_OBJC if (readQueue) dispatch_release(readQueue); #endif if (readBuffer) free(readBuffer); } @end