我有一个围绕Process
的包装函数来轻松调用一些外部程序(类似于pythonic check_output
):
struct Output {
public var code: Int32
public var stdout: String
public var stderr: String
}
func env(workingDir: String, command: [String]) -> Output {
let stdout = Pipe()
let stderr = Pipe()
let process = Process()
process.launchPath = "/usr/bin/env"
process.arguments = command
process.standardError = stderr
process.standardOutput = stdout
process.currentDirectoryPath = workingDir
var out = Data()
var err = Data()
stdout.fileHandleForReading.readabilityHandler = { fh in
out.append(fh.availableData)
}
stderr.fileHandleForReading.readabilityHandler = { fh in
err.append(fh.availableData)
}
process.launch()
process.waitUntilExit()
let code = process.terminationStatus
let outstr = String(data: out, encoding: .utf8) ?? ""
let errstr = String(data: err, encoding: .utf8) ?? ""
return .init(code: code, stdout: outstr, stderr: errstr)
}
不幸的是,有时会失败。我正在构建一个小程序,运行数千个程序,例如:
env(workingDir: ".", command: ["file", "-b", "--mime-type", file.path])
有时,非常非常罕见,它不会输出退出代码0。
我试图在测试中重现它:
func testEnv() {
let checkEcho: (String) -> () -> () = { mode in {
let speech = "Hello, \(mode) world!"
let output = autoreleasepool {
Process.env(workingDir: ".", command: ["echo", speech])
}
XCTAssertEqual(output.code, 0)
XCTAssertEqual(output.stderr, "")
XCTAssertEqual(output.stdout, speech + "\n")
} }
let performNTimesLoop: (Int, () -> Void) -> Void = {
for _ in 0..<$0 { $1() }
}
let performNTimesConc: (Int, () -> Void) -> Void = { count, code in
DispatchQueue.concurrentPerform(
iterations: count, execute: { _ in code() })
}
performNTimesLoop(1000, checkEcho("serial"))
performNTimesConc(1000, checkEcho("concurrent"))
}
它对于串行循环非常有用,但对于并发循环,它会系统地失败。虽然,我的程序中没有并发(但我想在不久的将来添加一些),我认为失败的原因可能是相似的。我试图在这里和那里添加一些Locks,Semaphores和DispatchGroups,但没有运气。
这非常烦人,所以任何帮助都会非常感激。谢谢!
UPD。据我所知,这是因为有可能最终outstr
创建将在最后一次回调(readabilityHandler
)完成之前执行,因为它在后台线程上运行。今天确认这一点后,我启动了ThreadSanitizer
...
out.append(fh.availableData) // modifying access
...
let outstr = String(data: out, encoding: .utf8) ?? "" //read acces
...
但我无法想办法说“等到每个可读性callbalck完成执行”。它甚至可能吗?可能我需要一些其他api来做到这一点?乍一看,似乎这是一个用高级api解决的微不足道的任务。
在我看来,没有办法用FileHandle
实现这一目标,相反,我已经去了更低的api。
如果有人感兴趣,那么SwiftPM实用程序中的代码实际上非常相似 - https://github.com/apple/swift-package-manager/blob/master/Sources/Basic/Process.swift(参见Process.popen
或Process.checkNonZeroExit
)