如果存在管道,通过 NSTask 的 cURL 不会终止

cURL through NSTask not terminating if a pipe is present

我正在尝试为 Swift 中的简单命令行批处理脚本同步读取 URL 的内容。为了简单起见,我使用 cURL - 我知道如果必须的话我可以使用 NSURLSession 。我还在 swift build 上使用 OSX 上的 Swift 的开源版本构建它。

问题是在某些 URL 上,如果 stdout 已被重定向到管道,NSTask 永远不会终止。

// This will hang, and when terminated with Ctrl-C reports "(23) Failed writing body"
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.standardOutput = pipe
task.launch()
task.waitUntilExit()

但是,如果您移除管道,或更改 URL,任务会成功。

// This will succeed - no pipe
import Foundation
let task = NSTask()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.launch()
task.waitUntilExit()

// This will succeed - different URL
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704646"]
task.standardOutput = pipe
task.launch()
task2.waitUntilExit()

运行 任何直接从终端使用 curl 的例子都成功了,所以当从那个特定的 URL(和其他一些)检索时,与 NSTask 的交互有一些东西,当存在管道,导致 cURL 失败。

curl 和 NSPipe 都缓冲数据。根据您在 ctrl-c out 时遇到的错误(这表明 curl 无法写入预期的数据量),您在它们之间的交互很糟糕。

尝试将 -N 选项添加到 curl 以防止它缓冲其输出。

curl也可以输出进度。我认为这不会造成问题,但您可以添加 -s 以仅获取数据以防万一。

扩展一下@Hod 的回答:启动的标准输出 进程被重定向到管道,但您的程序从未从 其他管端。管道具有 有限缓冲区, 参见示例 How big is the pipe buffer? 这解释了 macOS 上的管道缓冲区大小(最多)64KB。

如果管道缓冲区已满,则启动的进程无法写入 了。如果进程使用阻塞 I/O,那么到管道的 write() 将阻塞,直到至少可以写入一个字节。那确实 在你的情况下永远不会发生,所以进程挂起并且不会终止。

只有写入标准输出的数量才会出现这个问题 超过管道缓冲区大小,这解释了为什么它只发生在某些 URL 而不是其他 URL。

作为解决方案,您可以从管道中读取,例如与

let data = pipe.fileHandleForReading.readDataToEndOfFile()

before等待进程终止。另一种选择是 使用异步读取,例如使用 :

中的代码
pipe.fileHandleForReading.readabilityHandler = { fh in
    let data = fh.availableData
    // process data ...
}

这也将允许读取标准输出和标准错误 通过管道从进程无阻塞。