Last active
November 26, 2020 19:10
-
-
Save allenhumphreys/e235af773a6685f2a872d40c47770796 to your computer and use it in GitHub Desktop.
Intercepting file descriptors such as stderr or stdout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Darwin | |
import Foundation | |
import SystemPackage | |
/// Utilities for intercepting the contents written to a file descriptor, useful for testing | |
/// the output of command line utilities or programs that interact heavily with standard out on POSIX systems | |
extension UnsafeMutablePointer where Pointee == FILE { | |
/// Convenience method for intercepting the contents of a file descriptor as a String with the specified encoding | |
func interceptString(encoding: String.Encoding = .utf8, _ intercepted: () -> Void) throws -> String? { | |
String(data: try intercept(intercepted), encoding: encoding) | |
} | |
/// Intercepts data written to a file descriptor during the `intercepted` closure | |
func intercept(_ intercepted: () -> Void) throws -> Data { | |
let fileDescriptor = FileDescriptor(rawValue: fileNumber) | |
let temporaryFileDescriptor = try fileDescriptor.duplicate() | |
let originalPosition = position | |
let pipe = Pipe() | |
// send our file descriptor to the pipe | |
try fileDescriptor.duplicateOnto(rawValue: pipe.fileHandleForWriting.fileDescriptor) | |
let data: Data = try withLock { | |
intercepted() | |
// if there is no data, `availableData` unhelpfully blocks | |
// so we ensure at least one byte is available and then remove it | |
try putCharacter(CChar(UInt8(ascii: " "))) | |
// If the file is buffered, we need to flush it to ensure it's written out | |
try flush() | |
var data = pipe.fileHandleForReading.availableData | |
data.removeLast() | |
return data | |
} | |
// Put everything back the way it was | |
try flush() | |
try fileDescriptor.duplicateOnto(other: temporaryFileDescriptor) | |
clearError() | |
position = originalPosition | |
try temporaryFileDescriptor.close() | |
// Write the intercepted bytes to the original fd | |
_ = try? fileDescriptor.writeAll(data) | |
return data | |
} | |
var position: fpos_t { | |
get { | |
var fpos: fpos_t = -1 | |
fgetpos(self, &fpos) | |
return fpos | |
} | |
nonmutating set { | |
var pos = newValue | |
fsetpos(self, &pos) | |
} | |
} | |
func flush() throws { | |
if (fflush(self) == -1) { | |
throw Errno(rawValue: errno) | |
} | |
} | |
func lock() { flockfile(self) } | |
func unlock() { funlockfile(self) } | |
public func withLock<R>(_ body: () throws -> R) rethrows -> R { | |
let result: R | |
do { | |
lock() | |
result = try body() | |
} catch { | |
unlock() | |
throw error | |
} | |
return result | |
} | |
func clearError() { clearerr(self) } | |
var fileNumber: CInt { fileno(self) } | |
func putCharacter(_ character: CChar) throws { | |
let character = CInt(character) | |
let result = fputc(character, self) | |
if (result == EOF) { | |
throw PutCharacterError.endOfFile | |
} else if (result != character) { | |
throw PutCharacterError.mismatchedReturnValue | |
} | |
} | |
enum PutCharacterError: Error { | |
case endOfFile, mismatchedReturnValue | |
} | |
} | |
extension FileDescriptor { | |
func duplicate() throws -> FileDescriptor { | |
let result = dup(rawValue) | |
if (result == -1) { | |
throw Errno(rawValue: errno) | |
} | |
return FileDescriptor(rawValue: result) | |
} | |
func duplicateOnto(other: FileDescriptor) throws { | |
try duplicateOnto(rawValue: other.rawValue) | |
} | |
func duplicateOnto(rawValue other: CInt) throws { | |
let result = dup2(other, rawValue) | |
if (result == -1) { | |
throw Errno(rawValue: errno) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment