FBControlCore/Utility/FBFileWriter.m (196 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBFileWriter.h"
#import "FBControlCoreError.h"
@interface FBFileWriter ()
@property (nonatomic, assign, readonly) int fileDescriptor;
@property (nonatomic, assign, readonly) BOOL closeOnEndOfFile;
@property (nonatomic, strong, readwrite) FBMutableFuture<NSNull *> *finishedConsumingMutable;
- (instancetype)initWithFileDescriptor:(int)fileDescriptor closeOnEndOfFile:(BOOL)closeOnEndOfFile;
@end
@interface FBFileWriter_Null : FBFileWriter <FBDispatchDataConsumer, FBDataConsumerLifecycle>
@end
@interface FBFileWriter_Sync : FBFileWriter <FBDispatchDataConsumer, FBDataConsumerLifecycle>
@end
@interface FBFileWriter_Async : FBFileWriter <FBDispatchDataConsumer, FBDataConsumerLifecycle>
@property (nonatomic, strong, readonly) dispatch_queue_t writeQueue;
@property (nonatomic, strong, readwrite) dispatch_io_t io;
- (instancetype)initWithFileDescriptor:(int)fileDescriptor closeOnEndOfFile:(BOOL)closeOnEndOfFile writeQueue:(dispatch_queue_t)writeQueue;
- (BOOL)startReadingWithError:(NSError **)error;
@end
@implementation FBFileWriter
#pragma mark Initializers
+ (dispatch_queue_t)createWorkQueue
{
return dispatch_queue_create("com.facebook.fbcontrolcore.fbfilewriter", DISPATCH_QUEUE_SERIAL);;
}
+ (id<FBDataConsumer, FBDataConsumerLifecycle>)nullWriter
{
return [FBDataConsumerAdaptor dataConsumerForDispatchDataConsumer:[[FBFileWriter_Null alloc] init]];
}
+ (int)fileDescriptorForPath:(NSString *)filePath error:(NSError **)error
{
int fileDescriptor = open(filePath.UTF8String, O_WRONLY | O_CREAT, 0644);
if (!fileDescriptor) {
return [[FBControlCoreError
describeFormat:@"A file handle for path %@ could not be opened: %s", filePath, strerror(errno)]
failInt:error];
}
return fileDescriptor;
}
+ (FBFuture<id<FBDataConsumer, FBDataConsumerLifecycle>> *)asyncDispatchDataWriterWithFileDescriptor:(int)fileDescriptor closeOnEndOfFile:(BOOL)closeOnEndOfFile
{
NSError *error = nil;
FBFileWriter_Async *writer = [[FBFileWriter_Async alloc] initWithFileDescriptor:fileDescriptor closeOnEndOfFile:closeOnEndOfFile writeQueue:self.createWorkQueue];
if (![writer startReadingWithError:&error]) {
return [FBFuture futureWithError:error];
}
return [FBFuture futureWithResult:writer];
}
+ (id<FBDataConsumer, FBDataConsumerLifecycle>)syncWriterWithFileDescriptor:(int)fileDescriptor closeOnEndOfFile:(BOOL)closeOnEndOfFile
{
return [FBDataConsumerAdaptor dataConsumerForDispatchDataConsumer:[[FBFileWriter_Sync alloc] initWithFileDescriptor:fileDescriptor closeOnEndOfFile:closeOnEndOfFile]];
}
+ (id<FBDataConsumer, FBDataConsumerLifecycle>)asyncWriterWithFileDescriptor:(int)fileDescriptor closeOnEndOfFile:(BOOL)closeOnEndOfFile queue:(dispatch_queue_t)queue error:(NSError **)error
{
FBFileWriter_Async *writer = [[FBFileWriter_Async alloc] initWithFileDescriptor:fileDescriptor closeOnEndOfFile:closeOnEndOfFile writeQueue:queue];
if (![writer startReadingWithError:error]) {
return nil;
}
return [FBDataConsumerAdaptor dataConsumerForDispatchDataConsumer:writer];
}
+ (id<FBDataConsumer, FBDataConsumerLifecycle>)asyncWriterWithFileDescriptor:(int)fileDescriptor closeOnEndOfFile:(BOOL)closeOnEndOfFile error:(NSError **)error
{
dispatch_queue_t queue = self.createWorkQueue;
return [self asyncWriterWithFileDescriptor:fileDescriptor closeOnEndOfFile:closeOnEndOfFile queue:queue error:error];
}
+ (id<FBDataConsumer, FBDataConsumerLifecycle, FBDataConsumerSync>)syncWriterForFilePath:(NSString *)filePath error:(NSError **)error
{
int fileDescriptor = [self fileDescriptorForPath:filePath error:error];
if (!fileDescriptor) {
return nil;
}
return [FBFileWriter syncWriterWithFileDescriptor:fileDescriptor closeOnEndOfFile:YES];
}
+ (FBFuture<id<FBDataConsumer, FBDataConsumerLifecycle>> *)asyncWriterForFilePath:(NSString *)filePath
{
dispatch_queue_t queue = self.createWorkQueue;
return [FBFuture
onQueue:queue resolve:^() {
NSError *error = nil;
int fileDescriptor = [self fileDescriptorForPath:filePath error:&error];
if (!fileDescriptor) {
return [FBFuture futureWithError:error];
}
FBFileWriter_Async *writer = [[FBFileWriter_Async alloc] initWithFileDescriptor:fileDescriptor closeOnEndOfFile:YES writeQueue:queue];
if (![writer startReadingWithError:&error]) {
return [FBFuture futureWithError:error];
}
return [FBFuture futureWithResult:[FBDataConsumerAdaptor dataConsumerForDispatchDataConsumer:writer]];
}];
}
- (instancetype)initWithFileDescriptor:(int)fileDescriptor closeOnEndOfFile:(BOOL)closeOnEndOfFile
{
self = [super init];
if (!self) {
return nil;
}
_fileDescriptor = fileDescriptor;
_closeOnEndOfFile = closeOnEndOfFile;
_finishedConsumingMutable = [FBMutableFuture futureWithName:@"EOF Received"];
return self;
}
@end
@implementation FBFileWriter_Null
#pragma mark FBDataConsumer
- (void)consumeData:(dispatch_data_t)data
{
// do nothing
}
- (void)consumeEndOfFile
{
[self.finishedConsumingMutable resolveWithResult:NSNull.null];
}
- (FBFuture<NSNull *> *)finishedConsuming
{
return self.finishedConsumingMutable;
}
@end
@implementation FBFileWriter_Sync
#pragma mark FBDataConsumer
- (void)consumeData:(dispatch_data_t)data
{
dispatch_data_apply(data, ^ bool (dispatch_data_t region, size_t offset, const void *buffer, size_t size) {
write(self.fileDescriptor, buffer, size);
return true;
});
}
- (void)consumeEndOfFile
{
[self.finishedConsumingMutable resolveWithResult:NSNull.null];
if (self.closeOnEndOfFile) {
close(self.fileDescriptor);
}
}
- (FBFuture<NSNull *> *)finishedConsuming
{
return self.finishedConsumingMutable;
}
@end
@implementation FBFileWriter_Async
#pragma mark Initializers
- (instancetype)initWithFileDescriptor:(int)fileDescriptor closeOnEndOfFile:(BOOL)closeOnEndOfFile writeQueue:(dispatch_queue_t)writeQueue
{
self = [super initWithFileDescriptor:fileDescriptor closeOnEndOfFile:closeOnEndOfFile];
if (!self) {
return nil;
}
_writeQueue = writeQueue;
return self;
}
#pragma mark FBDataConsumer
- (void)consumeData:(dispatch_data_t)data
{
dispatch_io_t io = self.io;
if (!io) {
return;
}
dispatch_io_write(io, 0, data, self.writeQueue, ^(bool done, dispatch_data_t remainder, int error) {});
}
- (void)consumeEndOfFile
{
dispatch_io_t io = self.io;
if (!io) {
return;
}
// We can't close the file handle right now since there may still be pending IO operations on the channel.
// The safe place to do this is within the dispatch_io_create cleanup_handler callback.
// Until the cleanup_handler is called, libdispatch takes over control of the file descriptor.
// We also want to ensure that there are no pending write operations on the channel, otherwise it's easy to miss data.
// The barrier ensures that there are no pending writes before we attempt to interrupt the channel.
dispatch_io_barrier(io, ^{
dispatch_io_close(io, DISPATCH_IO_STOP);
});
}
- (FBFuture<NSNull *> *)finishedConsuming
{
return self.finishedConsumingMutable;
}
#pragma mark Private
- (BOOL)startReadingWithError:(NSError **)error
{
NSParameterAssert(!self.io);
FBMutableFuture<NSNull *> *finishedConsuming = self.finishedConsumingMutable;
// If there is an error creating the IO Object, the errorCode will be delivered asynchronously.
// Having a self -> IO -> self cycle shouldn't be a problem in theory, since the cleanup handler should get when IO is done.
// However, it appears that having the cycle in place here means that the cleanup handler is *never* called in the following circumstance:
// 1) Pipe of FD14 is created.
// 2) A writer is created for this pipe
// 3) Data is written to this writer
// 4) `consumeEndOfFile` is called and subsequently dispatch_io_close.
// 5) The cleanup handler is called and subsequently the FD closed and IO channel disposed of via nil-ification.
// 6) Pipe FD14 is torn down.
// 7) A new Pipe resolving to FD14 is created.
// 8) Data is written to this writer
// 9) `consumeEndOfFile` is called and subsequently dispatch_io_close.
// 10) The cleanup handler is *never* called and the FD is therefore never closed.
// This isn't a problem in practice if different FDs are splayed, but repeating FDs representing different dispatch channels will cause this problem.
__weak typeof(self) weakSelf = self;
self.io = dispatch_io_create(DISPATCH_IO_STREAM, self.fileDescriptor, self.writeQueue, ^(int errorCode) {
[weakSelf ioChannelDidCloseWithError:errorCode];
// Since writing is asynchronous, we don't want to vend futures that show that all work on a file descriptor has finished.
// Instead we should wait until the io channel is fully closed, this only occurs in this callback.
[finishedConsuming resolveWithResult:NSNull.null];
});
if (!self.io) {
return [[FBControlCoreError
describeFormat:@"A IO Channel could not be created for fd %d", self.fileDescriptor]
failBool:error];
}
// Report partial results with as little as 1 byte read.
dispatch_io_set_low_water(self.io, 1);
return YES;
}
- (void)ioChannelDidCloseWithError:(int)errorCode
{
self.io = nil;
if (self.closeOnEndOfFile) {
close(self.fileDescriptor);
}
}
@end