💚 Fix stdout interception

This commit is contained in:
Ben Chatelain 2019-01-08 15:34:23 -07:00
parent 66563b8083
commit 7636343540
2 changed files with 55 additions and 39 deletions

View file

@ -60,11 +60,11 @@ class InfoCommandSpec: QuickSpec {
let result = cmd.run(InfoCommand.Options(appId: result.trackId.description))
expect(result).to(beSuccess())
waitUntil { done in
print(output.contents)
expect(output.contents) == expectedOutput
done()
}
// output is async so need to wait for contents to be updated
expect(output.contents).toNotEventually(beEmpty())
expect(output.contents) == expectedOutput
output.closeConsolePipe()
}
}
}

View file

@ -6,60 +6,76 @@
// Copyright © 2019 mas-cli. All rights reserved.
//
import XCTest
import Foundation
/// Test helper for monitoring strings written to stdout. Modified from:
/// https://medium.com/@thesaadismail/eavesdropping-on-swifts-print-statements-57f0215efb42
class OutputListener {
// open a new Pipe to consume the messages on STDOUT and STDERR
/// consumes the messages on STDOUT
let inputPipe = Pipe()
// open another Pipe to output messages back to STDOUT
/// outputs messages back to STDOUT
let outputPipe = Pipe()
/// Buffers strings written to stdout
var contents = ""
/// Sets up the "tee" of piped output, intercepting stdout then passing it through.
///
/// ## [dup2 documentation](https://linux.die.net/man/2/dup2)
/// `int dup2(int oldfd, int newfd);`
/// `dup2()` makes `newfd` be the copy of `oldfd`, closing `newfd` first if necessary.
func openConsolePipe() {
let pipeReadHandle = inputPipe.fileHandleForReading
// Set up a read handler which fires when data is written to our inputPipe
inputPipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
guard let strongSelf = self else { return }
//from documentation
//dup2() makes newfd (new file descriptor) be the copy of oldfd (old file descriptor), closing newfd first if necessary.
let data = fileHandle.availableData
if let string = String(data: data, encoding: String.Encoding.utf8) {
strongSelf.contents += string
}
//here we are copying the STDOUT file descriptor into our output pipe's file descriptor
//this is so we can write the strings back to STDOUT, so it can show up on the xcode console
dup2(STDOUT_FILENO, outputPipe.fileHandleForWriting.fileDescriptor)
// Write input back to stdout
strongSelf.outputPipe.fileHandleForWriting.write(data)
}
//In this case, the newFileDescriptor is the pipe's file descriptor and the old file descriptor is STDOUT_FILENO and STDERR_FILENO
var dupStatus: Int32
dup2(inputPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)
dup2(inputPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO)
// Copy STDOUT file descriptor to outputPipe for writing strings back to STDOUT
dupStatus = dup2(stdoutFileDescriptor, outputPipe.fileHandleForWriting.fileDescriptor)
// Status should equal newfd
assert(dupStatus == outputPipe.fileHandleForWriting.fileDescriptor)
//listen in to the readHandle notification
NotificationCenter.default.addObserver(self, selector: #selector(self.handlePipeNotification), name: FileHandle.readCompletionNotification, object: pipeReadHandle)
// Intercept STDOUT with inputPipe
// newFileDescriptor is the pipe's file descriptor and the old file descriptor is STDOUT_FILENO and STDERR_FILENO
dupStatus = dup2(inputPipe.fileHandleForWriting.fileDescriptor, stdoutFileDescriptor)
// Status should equal newfd
assert(dupStatus == stdoutFileDescriptor)
//state that you want to be notified of any data coming across the pipe
pipeReadHandle.readInBackgroundAndNotify()
// Don't have any tests on stderr yet
// dup2(inputPipe.fileHandleForWriting.fileDescriptor, stderr)
}
@objc func handlePipeNotification(notification: Notification) {
//note you have to continuously call this when you get a message
//see this from documentation:
//Note that this method does not cause a continuous stream of notifications to be sent. If you wish to keep getting notified, youll also need to call readInBackgroundAndNotify() in your observer method.
inputPipe.fileHandleForReading.readInBackgroundAndNotify()
/// Tears down the "tee" of piped output.
func closeConsolePipe() {
// Restore stdout
freopen("/dev/stdout", "a", stdout)
if let userInfo = notification.userInfo,
let data = userInfo[NSFileHandleNotificationDataItem] as? Data,
let str = String(data: data, encoding: String.Encoding.ascii) {
contents += str
//write the data back into the output pipe. the output pipe's write file descriptor points to STDOUT. this allows the logs to show up on the xcode console
outputPipe.fileHandleForWriting.write(data)
// `str` here is the log/contents of the print statement
//if you would like to route your print statements to the UI: make
//sure to subscribe to this notification in your VC and update the UITextView.
//Or if you wanted to send your print statements to the server, then
//you could do this in your notification handler in the app delegate.
[inputPipe.fileHandleForReading, outputPipe.fileHandleForWriting].forEach { file in
file.closeFile()
}
}
}
extension OutputListener {
/// File descriptor for stdout (aka STDOUT_FILENO)
var stdoutFileDescriptor: Int32 {
return FileHandle.standardOutput.fileDescriptor
}
/// File descriptor for stderr (aka STDERR_FILENO)
var stderrFileDescriptor: Int32 {
return FileHandle.standardError.fileDescriptor
}
}