mirror of
https://github.com/apple/container.git
synced 2026-07-17 23:16:59 +00:00
Allow printed output to flush to prevent a race between progress updates and other output (#23)
This PR also improves clearing the last progress update after moving to a new task using `setDescription()`.
This commit is contained in:
@@ -140,7 +140,10 @@ struct Application: AsyncParsableCommand {
|
||||
signal(SIGTERM, signalHandler)
|
||||
// Normal and explicit exit.
|
||||
atexit {
|
||||
ProgressBar.resetCursor()
|
||||
if let progressConfig = try? ProgressConfig() {
|
||||
let progressBar = ProgressBar(config: progressConfig)
|
||||
progressBar.resetCursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ enum EscapeSequence {
|
||||
}
|
||||
|
||||
extension ProgressBar {
|
||||
static var terminalWidth: Int {
|
||||
private var terminalWidth: Int {
|
||||
guard
|
||||
let termimalHandle = ProgressBar.term,
|
||||
let termimalHandle = term,
|
||||
let terminal = try? Terminal(descriptor: termimalHandle.fileDescriptor)
|
||||
else {
|
||||
return 0
|
||||
@@ -39,28 +39,20 @@ extension ProgressBar {
|
||||
/// Clears the progress bar and resets the cursor.
|
||||
public func clearAndResetCursor() {
|
||||
clear()
|
||||
ProgressBar.resetCursor()
|
||||
resetCursor()
|
||||
}
|
||||
|
||||
/// Clears the progress bar.
|
||||
public func clear() {
|
||||
// We can't use "\u{001B}[2K" for clearing the line because this may lead to a race with `stdout` when using `stderr` for progress updates.
|
||||
displayText("")
|
||||
}
|
||||
|
||||
/// Resets the cursor.
|
||||
static public func resetCursor() {
|
||||
ProgressBar.display(EscapeSequence.showCursor)
|
||||
public func resetCursor() {
|
||||
display(EscapeSequence.showCursor)
|
||||
}
|
||||
|
||||
static func getTerminal() -> FileHandle? {
|
||||
let standardError = FileHandle.standardError
|
||||
let fd = standardError.fileDescriptor
|
||||
let isATTY = isatty(fd)
|
||||
return isATTY == 1 ? standardError : nil
|
||||
}
|
||||
|
||||
static func display(_ text: String) {
|
||||
func display(_ text: String) {
|
||||
guard let term else {
|
||||
return
|
||||
}
|
||||
@@ -74,19 +66,20 @@ extension ProgressBar {
|
||||
var text = text
|
||||
|
||||
// Clears previously printed characters if the new string is shorter.
|
||||
text += String(repeating: " ", count: max(state.output.count - text.count, 0))
|
||||
text += String(repeating: " ", count: max(printedWidth - text.count, 0))
|
||||
printedWidth = text.count
|
||||
state.output = text
|
||||
|
||||
// Clears previously printed lines.
|
||||
var lines = ""
|
||||
if ProgressBar.terminalWidth > 0 {
|
||||
let lineCount = (text.count - 1) / ProgressBar.terminalWidth
|
||||
if terminating.hasSuffix("\r") && terminalWidth > 0 {
|
||||
let lineCount = (text.count - 1) / terminalWidth
|
||||
for _ in 0..<lineCount {
|
||||
lines += EscapeSequence.moveUp
|
||||
}
|
||||
}
|
||||
|
||||
text = "\(text)\(terminating)\(lines)"
|
||||
ProgressBar.display(text)
|
||||
display(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,19 +22,27 @@ public final class ProgressBar: Sendable {
|
||||
let config: ProgressConfig
|
||||
@SendableProperty
|
||||
var state: State
|
||||
static let term: FileHandle? = getTerminal()
|
||||
static let termQueue = DispatchQueue(label: "com.apple.container.ProgressBar")
|
||||
@SendableProperty
|
||||
var printedWidth = 0
|
||||
let term: FileHandle?
|
||||
let termQueue = DispatchQueue(label: "com.apple.container.ProgressBar")
|
||||
private let standardError = StandardError()
|
||||
|
||||
/// Returns `true` if the progress bar has finished.
|
||||
public var isFinished: Bool {
|
||||
state.finished
|
||||
}
|
||||
|
||||
/// Creates a new progress bar.
|
||||
/// - Parameter config: The configuration for the progress bar.
|
||||
public init(config: ProgressConfig) {
|
||||
self.config = config
|
||||
term = isatty(config.terminal.fileDescriptor) == 1 ? config.terminal : nil
|
||||
state = State(
|
||||
description: config.initialDescription, itemsName: config.initialItemsName, totalTasks: config.initialTotalTasks,
|
||||
totalItems: config.initialTotalItems,
|
||||
totalSize: config.initialTotalSize)
|
||||
ProgressBar.display(EscapeSequence.hideCursor)
|
||||
display(EscapeSequence.hideCursor)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -48,15 +56,9 @@ public final class ProgressBar: Sendable {
|
||||
|
||||
/// Allows resetting the progress state of the current task.
|
||||
public func resetCurrentTask() {
|
||||
clear()
|
||||
state = State(description: state.description, itemsName: state.itemsName, tasks: state.tasks, totalTasks: state.totalTasks, startTime: state.startTime)
|
||||
}
|
||||
|
||||
/// Returns `true` if the progress bar has finished.
|
||||
public func isFinished() -> Bool {
|
||||
state.finished
|
||||
}
|
||||
|
||||
private func printFullDescription() {
|
||||
if state.subDescription != "" {
|
||||
standardError.write("\(state.description) \(state.subDescription)")
|
||||
@@ -95,7 +97,7 @@ public final class ProgressBar: Sendable {
|
||||
printFullDescription()
|
||||
}
|
||||
|
||||
while !self.isFinished() {
|
||||
while !isFinished {
|
||||
let intervalNanoseconds = UInt64(intervalSeconds * 1_000_000_000)
|
||||
render()
|
||||
state.iteration += 1
|
||||
@@ -115,7 +117,7 @@ public final class ProgressBar: Sendable {
|
||||
|
||||
/// Finishes the progress bar.
|
||||
public func finish() {
|
||||
guard !self.isFinished() else {
|
||||
guard !isFinished else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,8 +129,10 @@ public final class ProgressBar: Sendable {
|
||||
if config.clearOnFinish {
|
||||
clearAndResetCursor()
|
||||
} else {
|
||||
ProgressBar.resetCursor()
|
||||
resetCursor()
|
||||
}
|
||||
// Allow printed output to flush.
|
||||
usleep(100_000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +144,7 @@ extension ProgressBar {
|
||||
}
|
||||
|
||||
func render() {
|
||||
guard ProgressBar.term != nil && !config.disableProgressUpdates else {
|
||||
guard term != nil && !config.disableProgressUpdates && !isFinished else {
|
||||
return
|
||||
}
|
||||
let output = draw()
|
||||
|
||||
@@ -18,6 +18,8 @@ import Foundation
|
||||
|
||||
/// A configuration for displaying a progress bar.
|
||||
public struct ProgressConfig: Sendable {
|
||||
/// The file handle for progress updates.
|
||||
let terminal: FileHandle
|
||||
/// The initial description of the progress bar.
|
||||
let initialDescription: String
|
||||
/// The initial additional description of the progress bar.
|
||||
@@ -65,6 +67,7 @@ public struct ProgressConfig: Sendable {
|
||||
public let disableProgressUpdates: Bool
|
||||
/// Creates a new instance of `ProgressConfig`.
|
||||
/// - Parameters:
|
||||
/// - terminal: The file handle for progress updates. The default value is `FileHandle.standardError`.
|
||||
/// - description: The initial description of the progress bar. The default value is `""`.
|
||||
/// - subDescription: The initial additional description of the progress bar. The default value is `""`.
|
||||
/// - itemsName: The initial items name. The default value is `"it"`.
|
||||
@@ -86,6 +89,7 @@ public struct ProgressConfig: Sendable {
|
||||
/// - clearOnFinish: The flag indicating whether to clear the progress bar before reseting the cursor. The default is `true`.
|
||||
/// - disableProgressUpdates: The flag indicating whether to update the progress bar. The default is `false`.
|
||||
public init(
|
||||
terminal: FileHandle = .standardError,
|
||||
description: String = "",
|
||||
subDescription: String = "",
|
||||
itemsName: String = "it",
|
||||
@@ -123,6 +127,7 @@ public struct ProgressConfig: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
self.terminal = terminal
|
||||
self.initialDescription = description
|
||||
self.initialSubDescription = subDescription
|
||||
self.initialItemsName = itemsName
|
||||
|
||||
Reference in New Issue
Block a user