I solved this by writing a utility class that redirects a file descriptor to a file and is able to reset this redirection:
@interface IORedirector : NSObject
- (instancetype)initWithFileDescriptor:(int)fileDescriptor targetPath:(NSURL *)fileURL flags:(int)oflags mode:(mode_t)mode;
- (void)reset;
@end
@implementation IORedirector
{
int _fileDescriptor;
int _savedFileDescriptor;
}
- (instancetype)initWithFileDescriptor:(int)fileDescriptor targetPath:(NSURL *)fileURL flags:(int)oflags mode:(mode_t)mode
{
_savedFileDescriptor = dup(fileDescriptor);
if (_savedFileDescriptor == -1) {
NSLog(@"Could not save file descriptor %d: %s", fileDescriptor, strerror(errno));
return nil;
}
int tempFD = open(fileURL.path.fileSystemRepresentation, oflags, mode);
if (tempFD == -1) {
NSLog(@"Could not open %@: %s", fileURL.path, strerror(errno));
close(_savedFileDescriptor);
return nil;
}
if (close(fileDescriptor) == -1) {
NSLog(@"Closing file descriptor %d failed: %s", fileDescriptor, strerror(errno));
close(_savedFileDescriptor);
close(tempFD);
return nil;
}
if (dup2(tempFD, fileDescriptor) == -1) {
NSLog(@"Could not replace file descriptor %d with new one for %@: %s", fileDescriptor, fileURL.path, strerror(errno));
close(_savedFileDescriptor);
close(tempFD);
return nil;
}
_fileDescriptor = fileDescriptor;
close(tempFD);
return self;
}
- (void)dealloc
{
[self reset];
}
- (void)reset
{
if (_savedFileDescriptor == -1) return;
if (close(_fileDescriptor) == -1) {
NSLog(@"Closing file descriptor %d failed: %s", _fileDescriptor, strerror(errno));
return;
}
if (dup2(_savedFileDescriptor, _fileDescriptor) == -1) {
NSLog(@"Could not restore file descriptor %d: %s", _fileDescriptor, strerror(errno));
return;
}
close(_savedFileDescriptor);
_savedFileDescriptor = -1;
}
@end
The initializer either returns a valid instance (redirection established) or nil
. Call reset
to restore the redirection immediately.
Both the initializer and reset
can fail and "lose" the original file descriptor, but the window is small and unlikely. This is probably only relevant when using this class in a multithreaded app (which I don't), so if you intend to use this in a multithreaded environment you want to make sure that only one initializer or reset
can run at a time using a global mutex or something.
Using this class (which I've got a separate test case for, of course), I can redirect STDERR_FILENO
to a file and thus catch the output of NSLog
. This way I can test the unmodified logging class.
Creating a suitable path to a temporary file is left as an exercise to the reader.