Swift:如何在子进程中读取标准输出而不等待进程完成

问题描述 投票:0回答:3

我有一个外部控制台应用程序(在 OS X 上),它向标准输出发出一系列从 1 到 100 的整数,大约每秒一次。

我是 Swift,我需要使用该数字流来更新进度指示器。

这是我到目前为止的代码:

class MasterViewController: NSViewController {

@IBOutlet weak var progressIndicator: NSProgressIndicator!

override func viewDidLoad() {
    super.viewDidLoad()
    
    let task = Process()
    task.launchPath = "/bin/sh"
    task.arguments = ["-c", "sleep 1; echo 10 ; sleep 1 ; echo 20 ; sleep 1 ; echo 30 ; sleep 1 ; echo 40; sleep 1; echo 50; sleep 1; echo 60; sleep 1"]  
  
    let pipe = Pipe()
    task.standardOutput = pipe

    task.launch()

    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    if let string = String(data: data, encoding: String.Encoding.utf8) {
        print(string)
    }
}

代码可以工作——也就是说,它从命令行实用程序读取输出并相应地修改进度指示器——但它在实用程序退出后进行所有更改(并让我的 UI 同时等待)。

我该如何设置它才能读取后台应用程序的输出并实时更新进度指示器?

swift cocoa
3个回答
8
投票
您正在主线程上同步读取,因此在函数返回主循环之前,UI 不会更新。

(至少)有两种可能的方法来解决问题:

    在后台线程上从管道中读取数据(例如,将其分派到后台队列 - 但不要忘记 再次将 UI 更新分派到主线程)。
  • 使用通知从管道异步读取(请参阅
  • 使用 Swift 实时 NSTask 输出到 NSTextView 为例)。

1
投票
(从问题中提取答案)


为了将来的参考,以下是我如何让它最终工作(现已更新为 Swift 3):

class ViewController: NSViewController { @IBOutlet weak var progressIndicator: NSProgressIndicator! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let task = Process() task.launchPath = "/bin/sh" task.arguments = ["-c", "sleep 1; echo 10 ; sleep 1 ; echo 20 ; sleep 1 ; echo 30 ; sleep 1 ; echo 40; sleep 1; echo 50; sleep 1; echo 60; sleep 1"] let pipe = Pipe() task.standardOutput = pipe let outHandle = pipe.fileHandleForReading outHandle.waitForDataInBackgroundAndNotify() var progressObserver : NSObjectProtocol! progressObserver = NotificationCenter.default.addObserver( forName: NSNotification.Name.NSFileHandleDataAvailable, object: outHandle, queue: nil) { notification -> Void in let data = outHandle.availableData if data.count > 0 { if let str = String(data: data, encoding: String.Encoding.utf8) { if let newValue = Double(str.trimEverything) { self.progressIndicator.doubleValue = newValue } } outHandle.waitForDataInBackgroundAndNotify() } else { // That means we've reached the end of the input. NotificationCenter.default.removeObserver(progressObserver) } } var terminationObserver : NSObjectProtocol! terminationObserver = NotificationCenter.default.addObserver( forName: Process.didTerminateNotification, object: task, queue: nil) { notification -> Void in // Process was terminated. Hence, progress should be 100% self.progressIndicator.doubleValue = 100 NotificationCenter.default.removeObserver(terminationObserver) } task.launch() } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } } // This is just a convenience extension so that I can trim // off the extra newlines and white spaces before converting // the input to a Double. fileprivate extension String { var trimEverything: String { return self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } }
现在,一旦子进程完成,进度条将前进至 60%,然后跳至 100%。


0
投票
这是我们为 Lambda Runtime 项目编写的解决方案。它适用于 Swift 6 并发。

https://github.com/swift-server/swift-aws-lambda-runtime/blob/main/Plugins/AWSLambdaPackager/PluginUtils.swift

复杂性来自于对输出缓冲区的并发访问。 它由您启动的进程写入,并由您的应用程序读取。 所以我们必须确保两个线程之间的数据访问是隔离的。

它需要 macOS 15 (Sequoia),因为该解决方案利用同步包中的 Mutex。

当代码可用时,我们将用

Subprocess

 重写此代码。
https://github.com/swiftlang/swift-foundation/blob/main/Proposals/0007-swift-subprocess.md

import Dispatch import Foundation import PackagePlugin import Synchronization @available(macOS 15.0, *) struct Utils { @discardableResult static func execute( executable: URL, arguments: [String], customWorkingDirectory: URL? = .none, logLevel: ProcessLogLevel ) throws -> String { if logLevel >= .debug { print("\(executable.path()) \(arguments.joined(separator: " "))") if let customWorkingDirectory { print("Working directory: \(customWorkingDirectory.path())") } } let fd = dup(1) let stdout = fdopen(fd, "rw") defer { if let so = stdout { fclose(so) } } // We need to use an unsafe transfer here to get the fd into our Sendable closure. // This transfer is fine, because we write to the variable from a single SerialDispatchQueue here. // We wait until the process is run below process.waitUntilExit(). // This means no further writes to output will happen. // This makes it save for us to read the output struct UnsafeTransfer<Value>: @unchecked Sendable { let value: Value } let outputMutex = Mutex("") let outputSync = DispatchGroup() let outputQueue = DispatchQueue(label: "AWSLambdaPackager.output") let unsafeTransfer = UnsafeTransfer(value: stdout) let outputHandler = { @Sendable (data: Data?) in dispatchPrecondition(condition: .onQueue(outputQueue)) outputSync.enter() defer { outputSync.leave() } guard let _output = data.flatMap({ String(data: $0, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(["\n"])) }), !_output.isEmpty else { return } outputMutex.withLock { output in output += _output + "\n" } switch logLevel { case .silent: break case .debug(let outputIndent), .output(let outputIndent): print(String(repeating: " ", count: outputIndent), terminator: "") print(_output) fflush(unsafeTransfer.value) } } let pipe = Pipe() pipe.fileHandleForReading.readabilityHandler = { fileHandle in outputQueue.async { outputHandler(fileHandle.availableData) } } let process = Process() process.standardOutput = pipe process.standardError = pipe process.executableURL = executable process.arguments = arguments if let customWorkingDirectory { process.currentDirectoryURL = URL(fileURLWithPath: customWorkingDirectory.path()) } process.terminationHandler = { _ in outputQueue.async { outputHandler(try? pipe.fileHandleForReading.readToEnd()) } } try process.run() process.waitUntilExit() // wait for output to be full processed outputSync.wait() let output = outputMutex.withLock { $0 } if process.terminationStatus != 0 { // print output on failure and if not already printed if logLevel < .output { print(output) fflush(stdout) } throw ProcessError.processFailed([executable.path()] + arguments, process.terminationStatus) } return output } enum ProcessError: Error, CustomStringConvertible { case processFailed([String], Int32) var description: String { switch self { case .processFailed(let arguments, let code): return "\(arguments.joined(separator: " ")) failed with code \(code)" } } } enum ProcessLogLevel: Comparable { case silent case output(outputIndent: Int) case debug(outputIndent: Int) var naturalOrder: Int { switch self { case .silent: return 0 case .output: return 1 case .debug: return 2 } } static var output: Self { .output(outputIndent: 2) } static var debug: Self { .debug(outputIndent: 2) } static func < (lhs: ProcessLogLevel, rhs: ProcessLogLevel) -> Bool { lhs.naturalOrder < rhs.naturalOrder } } }
    
© www.soinside.com 2019 - 2024. All rights reserved.