1

In Swift, you can create a Pipe and assign it to process1.standardOutput and process2.standardInput for process1: Process, process2: Process to pipe the output of process1 into the input of process2. I would like to send the output of process1 into two different Pipes/FileHandles, similar to the functionality of tee.

Is there a simple way to do this with Foundation, or do I need to implement this behavior myself?

deaton.dg
  • 1,282
  • 9
  • 21
  • 1
    Heh, I've asked this before, never figured it out. https://stackoverflow.com/questions/38852518/how-to-tee-nspipe-in-swift – Alexander May 22 '21 at 00:53
  • 1
    If nobody gives an answer for this in a couple days, I will write some code for this. Is this something you'd be interested in as a SwiftPM package? If so, I will probably do that, if not, I will probably just paste the code into an answer. – deaton.dg May 22 '21 at 01:30
  • Doesn't hurt! Though that was for a project I haven't touched in a whilleeee – Alexander May 22 '21 at 03:16
  • 1
    Just put my package as an answer. Hope it ends up being useful if you ever decide to go back to that project! – deaton.dg May 23 '21 at 20:13

1 Answers1

1

As far as I have been able to find, there is no built-in way to do this, but maybe somebody will come by and prove me wrong. If sockets/file handles get a Combine-like interface or are redesigned with async in mind, a feature like this could end up being added.

In the meanwhile, I have created a Swift package for this functionality and made it available on on GitHub. The idea is to use the readabilityHandler and writeabilityHandler properties on FileHandle to read from one handle and write it back to many. In the implementation of FileHandle, this is implemented with Dispatch, and I also synchronize writes with a DispatchGroup. This is simple enough that it is a stone's throw away from being built-in.

If you need this functionality in a Mac app, you may already have a run loop, in which case you might wish to pursue implementing this behavior with the readInBackgroundAndNotify() method of FileHandle so that you can have more control over where the reads and writes occur.

Here is the relevant code for implementing the Dispatch solution yourself:

/**
 Duplicates the data from `input` into each of the `outputs`.
 Following the precedent of `standardInput`/`standardOutput`/`standardError` in `Process` from `Foundation`,
    we accept the type `Any`, but throw a precondition failure if the arguments are not of type `Pipe` or `FileHandle`.
 https://github.com/apple/swift-corelibs-foundation/blob/eec4b26deee34edb7664ddd9c1222492a399d122/Sources/Foundation/Process.swift
 When `input` sends an EOF (write of length 0), the `outputs` file handles are closed, so only output to handles you own.
 This function sets the `readabilityHandler` of inputs and the `writeabilityHandler` of outputs,
    so you should not set these yourself after calling `tee`.
 The one exception to this guidance is that you can set the `readabilityHandler` of `input` to `nil` to stop `tee`ing.
 After doing so, the `writeabilityHandler`s of the `output`s will be set to `nil` automatically after all in-progress writes complete,
    but if desired, you could set them to `nil` manually to cancel these writes. However, this may result in some outputs recieving less of the data than others.
 This implementation waits for all outputs to consume a piece of input before more input is read.
 This means that the speed at which your processes read data may be bottlenecked by the speed at which the slowest process reads data,
    but this method also comes with very little memory overhead and is easy to cancel.
 If this is unacceptable for your use case. you may wish to rewrite this with a data deque for each output.
 */
public func tee(from input: Any, into outputs: Any...) {
    tee(from: input, into: outputs)
}
public func tee(from input: Any, into outputs: [Any]) {
    /// Get reading and writing handles from the input and outputs respectively.
    guard let input = fileHandleForReading(input) else {
        preconditionFailure(incorrectTypeMessage)
    }
    let outputs: [FileHandle] = outputs.map({
        guard let output = fileHandleForWriting($0) else {
            preconditionFailure(incorrectTypeMessage)
        }
        return output
    })
    
    let writeGroup = DispatchGroup()
    
    input.readabilityHandler = { input in
        let data = input.availableData
        
        /// If the data is empty, EOF reached
        guard !data.isEmpty else {
            /// Close all the outputs
            for output in outputs {
                output.closeFile()
            }
            /// Stop reading and return
            input.readabilityHandler = nil
            return
        }
        
        for output in outputs {
            /// Tell `writeGroup` to wait on this output.
            writeGroup.enter()
            output.writeabilityHandler = { output in
                /// Synchronously write the data
                output.write(data)
                /// Signal that we do not need to write anymore
                output.writeabilityHandler = nil
                /// Inform `writeGroup` that we are done.
                writeGroup.leave()
            }
        }
        
        /// Wait until all outputs have recieved the data
        writeGroup.wait()
    }
}

/// The message that is passed to `preconditionFailure` when an incorrect type is passed to `tee`.
let incorrectTypeMessage = "Arguments of tee must be either Pipe or FileHandle."

/// Get a file handle for reading from a `Pipe` or the handle itself from a `FileHandle`, or `nil` otherwise.
func fileHandleForReading(_ handle: Any) -> FileHandle? {
    switch handle {
    case let pipe as Pipe:
        return pipe.fileHandleForReading
    case let file as FileHandle:
        return file
    default:
        return nil
    }
}
/// Get a file handle for writing from a `Pipe` or the handle itself from a `FileHandle`, or `nil` otherwise.
func fileHandleForWriting(_ handle: Any) -> FileHandle? {
    switch handle {
    case let pipe as Pipe:
        return pipe.fileHandleForWriting
    case let file as FileHandle:
        return file
    default:
        return nil
    }
}

For usage examples, see the package on GitHub.

deaton.dg
  • 1,282
  • 9
  • 21
  • Note that you aren't supposed to close `FileHandle.standardOutput` and friends, so you will need to modify this code to use them. I have added functionality to control when handles are released to the git repo. – deaton.dg May 27 '21 at 05:27