Group Group Group Group Group Group Group Group Group

Displaying console output in a text field

I finally had a breakthrough and got my ffmpeg GUI app working. My next challenge is getting the console output to print to a text field in the app instead of to the console so that the user can monitor ffmpeg’s progress and possible errors.

In my research, I see lots of how-tos about redirecting console output to a log file, but not about redirecting it to a text field.

EDIT: I actually managed to get a version of this working using this approach. Unfortunately each new line overwrites the last so it’s not a scrolling text, but it’s a step in that direction.

Here’s my code if anyone is interested:

		convertTask.arguments = arguments
		convertTask.standardInput = FileHandle.nullDevice
		
		let pipe = Pipe()
		convertTask.standardError = pipe
		let outHandle = pipe.fileHandleForReading
		outHandle.waitForDataInBackgroundAndNotify()
		
		var obs1 : NSObjectProtocol!
		obs1 = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable,
																									object: outHandle, queue: nil) {  notification -> Void in
																										let data = outHandle.availableData
																										if data.count > 0 {
																											if let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
																												self.ffmpegLogOutput.string = ("\(str)")
																											}
																											outHandle.waitForDataInBackgroundAndNotify()
																										} else {
																											print("EOF on stderr from process")
																											NotificationCenter.default.removeObserver(obs1!)
																										}
		}
		
		var obs2 : NSObjectProtocol!
		obs2 = NotificationCenter.default.addObserver(forName: Process.didTerminateNotification,
																									object: convertTask, queue: nil) { notification -> Void in
																										print("terminated")
																										NotificationCenter.default.removeObserver(obs2!)
		}
		convertTask.launch()
		convertTask.waitUntilExit()
	}

@ncrusher Do you still have issues with this?

Someone from the Swift forums helped me build a better what. Here is what I ended up with:

    /// The pipe that will receive standard out and standard error.
    let pipe = Pipe()
    // Hook up the pipe.
    process.standardOutput = pipe
    process.standardError = pipe

    /// Starts the external process.
    // (This does the same as ”launch()“, but it is a more modern API.)
    try process.run()

    /// A variable to store the output as we catch it.
    var output = String()

    /// A buffer to store partial lines of data before we can turn them into strings.
    ///
    /// This is important, because we might get only the first or the second half of a multi‐byte character. Converting it before getting the rest of the character would result in corrupt replacements (�).
    var stream = Data()

    /// The POSIX newline character. (Windows line endings are more complicated, but they still contain it.)
    let newline = "\n"
    /// The newline as a single byte of data instead of a string (since we will be looking for it in data, not strings.
    let newlineData = newline.data(using: String.Encoding.utf8)!

    /// Reads whatever data the pipe has ready for us so far, returning `nil` when the pipe finished.
    func read() -> Data? {
        /// The data in the pipe.
        ///
        /// This will only be empty if the pipe is finished. Otherwise the pipe will stall until it has more. (See the documentation for `availableData`.)
        let newData = pipe.fileHandleForReading.availableData
        // If the new data is empty, the pipe was indicating that it is finished. `nil` is a better indicator of that, so we return `nil`.
        // If there actually is data, we return it.
        return newData.isEmpty ? nil : newData
    }

    // The purpose in the following loop‐based design is two‐fold:
    // 1. Each line of output can be handled in real time as it arrives.
    // 2. The entire thing is encapsulated in a single synchronous function. (It is much easier to send a synchronous task to another queue than it is to stall a queue to wait for the result of an asynchronous task.)

    /// Whether the loop should end. We will set it to `true` when we want it to stop.
    var shouldEnd = false
    // Loop as long as “shouldEnd” is “false”.
    while !shouldEnd {
        // “autoreleasepool” cleans up after some Objective C stuff Foundation will be doing.
        autoreleasepool {
            guard let newData = read() else {
                // “read()” returned `nil`, so we reached the end.
                shouldEnd = true
                // This returns from the autorelease pool.
                return
            }
            // Put the new data into the buffer.
            stream.append(newData)

            // Loop as long as we can find a line break in what’s left.
            while let lineEnd = stream.range(of: newlineData) {
                /// The data up to the newline.
                let lineData = stream.subdata(in: stream.startIndex ..< lineEnd.lowerBound)
                // Clear the part of the buffer we’ve already dealt with.
                stream.removeSubrange(..<lineEnd.upperBound)

                /// The line converted back to a string.
                let line = String(decoding: lineData, as: UTF8.self)

                output += line+newline
            }
        }
    }

    /// Stall until the external process is done, even if it closed its pipes early.
    while process.isRunning {}

    /// Check whether it ended with success or an error.
    if process.terminationStatus == 0 {
        // It succeeded.
        // Return the output.
        return output
    } else {
        // It failed.
        throw ExternalProcessError.processErrored(
            exitCode: Int(process.terminationStatus),
            output: output)
    }
}