从Process(NSTask)获取标准输出有时会失败

时间:2019-02-19 23:04:55

标签: process foundation nstask

我在check_output周围有一个包装函数,可以轻松地调用某些外部过程(类似于pythonic 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])

不幸的是,有时它会失败。我正在构建一个运行数千个这样的小程序,例如:

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"))
}

有时候,很少有退出代码为0的输出。

我试图在测试中重现它:

outstr

对于串行循环来说非常顺利,但是对于并发循环它会系统地失败。尽管我的程序没有并发性(但是我想在不久的将来添加一些并发性),但我认为失败的原因可能相似。我试图在这里和那里添加一些Lock,Semaphores和DispatchGroups,但是没有运气。

这很烦人,因此非常感谢您的帮助。谢谢!

UPD。据我了解,这是因为最终readabilityHandler的创建可能会在最后一个回调(... out.append(fh.availableData) // modifying access ... let outstr = String(data: out, encoding: .utf8) ?? "" //read acces ... )完成之前执行,因为它在后台线程上运行。为了证实这一点,今天我触发了ThreadSanitizer

float

但是我想不出一种方法来说“等到每个可读性调用程序将完成执行”。可能吗可能我需要其他一些api来做到这一点吗?乍一看,这似乎是使用高级api可以解决的琐碎任务。

1 个答案:

答案 0 :(得分:0)

在我看来,FileHandle无法实现这一目标,相反,我使用了较低的api。

如果有人感兴趣,SwiftPM实用工具中实际上有非常相似的代码– https://github.com/apple/swift-package-manager/blob/master/Sources/Basic/Process.swift(请参阅Process.popenProcess.checkNonZeroExit