Skip to content

Instantly share code, notes, and snippets.

@allenhumphreys
Last active November 26, 2020 19:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save allenhumphreys/e235af773a6685f2a872d40c47770796 to your computer and use it in GitHub Desktop.
Save allenhumphreys/e235af773a6685f2a872d40c47770796 to your computer and use it in GitHub Desktop.
Intercepting file descriptors such as stderr or stdout
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