Получение стандартного вывода из процесса (NSTask) иногда не удается - PullRequest
0 голосов
/ 20 февраля 2019

У меня есть функция-оболочка около 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"))
}

Это очень хорошо подходит для последовательного цикла, но для параллельного цикла он терпит неудачу систематически.Хотя в моей программе нет параллелизма (но я хочу добавить его в ближайшем будущем), я думаю, что причины неудачи могут быть аналогичными.Я попытался добавить несколько замков, семафоров и диспетчерских групп здесь и там, но безуспешно.

Это очень раздражает, поэтому любая помощь будет высоко оценена.Спасибо!

UPD. Как я понимаю, это происходит потому, что вполне возможно, что окончательное создание outstr будет выполнено до того, как завершится последний обратный вызов (readabilityHandler), так как оно выполняется в фоновом потоке.,В подтверждение этого сегодня я получил ThreadSanitizer, запущенный на

...
    out.append(fh.availableData) // modifying access
...
  let outstr = String(data: out, encoding: .utf8) ?? "" //read acces
...

Но я не могу придумать, как сказать: «дождитесь завершения выполнения каждого читабельного callbalck».Это вообще возможно?Может быть, мне нужны другие API для этого?На первый взгляд кажется, что это тривиальная задача, решаемая с помощью API высокого уровня.

1 Ответ

0 голосов
/ 22 февраля 2019

Мне кажется, что нет способа достичь этого с помощью FileHandle, вместо этого я пошел к снижению API.

Если кому-то интересно, в утилите SwiftPM действительно очень похожий код - https://github.com/apple/swift-package-manager/blob/master/Sources/Basic/Process.swift (см. Process.popen или Process.checkNonZeroExit)

...