是否有内置方法可以在 Swift 中为 `FileHandle`s 或 `Pipe`s 实现类似 `tee` 的功能?

问题描述

在 Swift 中,您可以创建一个 Pipe 并将其分配给 process1.standardOutputprocess2.standardInput,用于 process1: Processprocess2: Process 以管道化 {{1} 的输出}} 进入 process1 的输入。我想将 process2输出发送到两个不同的 process1s/Pipes,类似于 FileHandle功能

是否有一种简单的方法可以使用 tee 执行此操作,还是我需要自己实现此行为?

解决方法

据我所知,没有内置的方法可以做到这一点,但也许有人会过来证明我错了。如果套接字/文件句柄获得类似 Combine 的界面或重新设计时考虑到 async,最终可能会添加这样的功能。

与此同时,我为此功能创建了一个 Swift 包,并使其在 on GitHub 上可用。 这个想法是使用 readabilityHandler 上的 writeabilityHandlerFileHandle 属性从一个句柄读取并将其写回多个句柄。在 the implementation of FileHandle 中,这是使用 Dispatch 实现的,我还使用 DispatchGroup 同步写入。这很简单,距离内置仅一箭之遥。

如果您在 Mac 应用程序中需要此功能,您可能已经有一个运行循环,在这种情况下,您可能希望使用 readInBackgroundAndNotify()FileHandle 方法实现此行为,以便您可以可以更好地控制读取和写入发生的位置。

以下是自己实现 Dispatch 解决方案的相关代码:

/**
 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
    }
}

有关使用示例,请参阅包 on GitHub