我有一个外部控制台应用程序(在 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 同时等待)。
我该如何设置它才能读取后台应用程序的输出并实时更新进度指示器?
(至少)有两种可能的方法来解决问题:
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%。
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
}
}
}