Skip to content

Commit c41a888

Browse files
authored
Use Mutex for thread-safe access to structs (#325)
Changes in this PR prevent a race caused by an implicit call to a computed property getter when updating the property value.
1 parent f0d82d2 commit c41a888

File tree

3 files changed

+50
-37
lines changed

3 files changed

+50
-37
lines changed

Sources/TerminalProgress/ProgressBar+Add.swift

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ extension ProgressBar {
6161

6262
/// Performs a check to see if the progress bar should be finished.
6363
public func checkIfFinished() {
64+
let state = self.state.withLock { $0 }
65+
6466
var finished = true
6567
var defined = false
6668
if let totalTasks = state.totalTasks, totalTasks > 0 {
@@ -85,7 +87,7 @@ extension ProgressBar {
8587
/// - Parameter newTasks: The current tasks to set.
8688
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
8789
public func set(tasks newTasks: Int, render: Bool = true) {
88-
state.tasks = newTasks
90+
state.withLock { $0.tasks = newTasks }
8991
if render {
9092
self.render()
9193
}
@@ -96,7 +98,7 @@ extension ProgressBar {
9698
/// - Parameter delta: The tasks to add to the current tasks.
9799
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
98100
public func add(tasks delta: Int, render: Bool = true) {
99-
_state.withLock {
101+
state.withLock {
100102
let newTasks = $0.tasks + delta
101103
$0.tasks = newTasks
102104
}
@@ -109,7 +111,7 @@ extension ProgressBar {
109111
/// - Parameter newTotalTasks: The total tasks to set.
110112
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
111113
public func set(totalTasks newTotalTasks: Int, render: Bool = true) {
112-
state.totalTasks = newTotalTasks
114+
state.withLock { $0.totalTasks = newTotalTasks }
113115
if render {
114116
self.render()
115117
}
@@ -119,7 +121,7 @@ extension ProgressBar {
119121
/// - Parameter delta: The tasks to add to the total tasks.
120122
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
121123
public func add(totalTasks delta: Int, render: Bool = true) {
122-
_state.withLock {
124+
state.withLock {
123125
let totalTasks = $0.totalTasks ?? 0
124126
let newTotalTasks = totalTasks + delta
125127
$0.totalTasks = newTotalTasks
@@ -133,7 +135,7 @@ extension ProgressBar {
133135
/// - Parameter newItemsName: The current items to set.
134136
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
135137
public func set(itemsName newItemsName: String, render: Bool = true) {
136-
state.itemsName = newItemsName
138+
state.withLock { $0.itemsName = newItemsName }
137139
if render {
138140
self.render()
139141
}
@@ -142,7 +144,7 @@ extension ProgressBar {
142144
/// Sets the current items.
143145
/// - Parameter newItems: The current items to set.
144146
public func set(items newItems: Int, render: Bool = true) {
145-
state.items = newItems
147+
state.withLock { $0.items = newItems }
146148
if render {
147149
self.render()
148150
}
@@ -152,7 +154,7 @@ extension ProgressBar {
152154
/// - Parameter delta: The items to add to the current items.
153155
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
154156
public func add(items delta: Int, render: Bool = true) {
155-
_state.withLock {
157+
state.withLock {
156158
let newItems = $0.items + delta
157159
$0.items = newItems
158160
}
@@ -165,7 +167,7 @@ extension ProgressBar {
165167
/// - Parameter newTotalItems: The total items to set.
166168
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
167169
public func set(totalItems newTotalItems: Int, render: Bool = true) {
168-
state.totalItems = newTotalItems
170+
state.withLock { $0.totalItems = newTotalItems }
169171
if render {
170172
self.render()
171173
}
@@ -175,7 +177,7 @@ extension ProgressBar {
175177
/// - Parameter delta: The items to add to the total items.
176178
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
177179
public func add(totalItems delta: Int, render: Bool = true) {
178-
_state.withLock {
180+
state.withLock {
179181
let totalItems = $0.totalItems ?? 0
180182
let newTotalItems = totalItems + delta
181183
$0.totalItems = newTotalItems
@@ -189,7 +191,7 @@ extension ProgressBar {
189191
/// - Parameter newSize: The current size to set.
190192
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
191193
public func set(size newSize: Int64, render: Bool = true) {
192-
state.size = newSize
194+
state.withLock { $0.size = newSize }
193195
if render {
194196
self.render()
195197
}
@@ -199,7 +201,7 @@ extension ProgressBar {
199201
/// - Parameter delta: The size to add to the current size.
200202
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
201203
public func add(size delta: Int64, render: Bool = true) {
202-
_state.withLock {
204+
state.withLock {
203205
let newSize = $0.size + delta
204206
$0.size = newSize
205207
}
@@ -212,7 +214,7 @@ extension ProgressBar {
212214
/// - Parameter newTotalSize: The total size to set.
213215
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
214216
public func set(totalSize newTotalSize: Int64, render: Bool = true) {
215-
state.totalSize = newTotalSize
217+
state.withLock { $0.totalSize = newTotalSize }
216218
if render {
217219
self.render()
218220
}
@@ -222,7 +224,7 @@ extension ProgressBar {
222224
/// - Parameter delta: The size to add to the total size.
223225
/// - Parameter render: The flag indicating whether the progress bar has to render after the update.
224226
public func add(totalSize delta: Int64, render: Bool = true) {
225-
_state.withLock {
227+
state.withLock {
226228
let totalSize = $0.totalSize ?? 0
227229
let newTotalSize = totalSize + delta
228230
$0.totalSize = newTotalSize

Sources/TerminalProgress/ProgressBar+Terminal.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ extension ProgressBar {
6868
// Clears previously printed characters if the new string is shorter.
6969
text += String(repeating: " ", count: max(printedWidth - text.count, 0))
7070
printedWidth = text.count
71-
state.output = text
71+
state.withLock {
72+
$0.output = text
73+
}
7274

7375
// Clears previously printed lines.
7476
var lines = ""

Sources/TerminalProgress/ProgressBar.swift

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@
1616

1717
import Foundation
1818
import SendableProperty
19+
import Synchronization
1920

2021
/// A progress bar that updates itself as tasks are completed.
2122
public final class ProgressBar: Sendable {
2223
let config: ProgressConfig
23-
// `@SendableProperty` adds `_state: Synchronized<State>`, which can be updated inside a lock using `_state.withLock()`.
24-
@SendableProperty
25-
var state = State()
24+
let state: Mutex<State>
2625
@SendableProperty
2726
var printedWidth = 0
2827
let term: FileHandle?
@@ -31,18 +30,19 @@ public final class ProgressBar: Sendable {
3130

3231
/// Returns `true` if the progress bar has finished.
3332
public var isFinished: Bool {
34-
state.finished
33+
state.withLock { $0.finished }
3534
}
3635

3736
/// Creates a new progress bar.
3837
/// - Parameter config: The configuration for the progress bar.
3938
public init(config: ProgressConfig) {
4039
self.config = config
4140
term = isatty(config.terminal.fileDescriptor) == 1 ? config.terminal : nil
42-
state = State(
41+
let state = State(
4342
description: config.initialDescription, itemsName: config.initialItemsName, totalTasks: config.initialTotalTasks,
4443
totalItems: config.initialTotalItems,
4544
totalSize: config.initialTotalSize)
45+
self.state = Mutex(state)
4646
display(EscapeSequence.hideCursor)
4747
}
4848

@@ -52,19 +52,25 @@ public final class ProgressBar: Sendable {
5252

5353
/// Allows resetting the progress state.
5454
public func reset() {
55-
state = State(description: config.initialDescription)
55+
state.withLock {
56+
$0 = State(description: config.initialDescription)
57+
}
5658
}
5759

5860
/// Allows resetting the progress state of the current task.
5961
public func resetCurrentTask() {
60-
state = State(description: state.description, itemsName: state.itemsName, tasks: state.tasks, totalTasks: state.totalTasks, startTime: state.startTime)
62+
state.withLock {
63+
$0 = State(description: $0.description, itemsName: $0.itemsName, tasks: $0.tasks, totalTasks: $0.totalTasks, startTime: $0.startTime)
64+
}
6165
}
6266

6367
private func printFullDescription() {
64-
if state.subDescription != "" {
65-
standardError.write("\(state.description) \(state.subDescription)")
68+
let (description, subDescription) = state.withLock { ($0.description, $0.subDescription) }
69+
70+
if subDescription != "" {
71+
standardError.write("\(description) \(subDescription)")
6672
} else {
67-
standardError.write(state.description)
73+
standardError.write(description)
6874
}
6975
}
7076

@@ -73,35 +79,36 @@ public final class ProgressBar: Sendable {
7379
public func set(description: String) {
7480
resetCurrentTask()
7581

76-
state.description = description
77-
state.subDescription = ""
82+
state.withLock {
83+
$0.description = description
84+
$0.subDescription = ""
85+
$0.tasks += 1
86+
}
7887
if config.disableProgressUpdates {
7988
printFullDescription()
8089
}
81-
82-
state.tasks += 1
8390
}
8491

8592
/// Updates the additional description of the progress bar.
8693
/// - Parameter subDescription: The additional description of the action being performed.
8794
public func set(subDescription: String) {
8895
resetCurrentTask()
8996

90-
state.subDescription = subDescription
97+
state.withLock { $0.subDescription = subDescription }
9198
if config.disableProgressUpdates {
9299
printFullDescription()
93100
}
94101
}
95102

96103
private func start(intervalSeconds: TimeInterval) async {
97-
if config.disableProgressUpdates && !state.description.isEmpty {
104+
if config.disableProgressUpdates && !state.withLock({ $0.description.isEmpty }) {
98105
printFullDescription()
99106
}
100107

101-
while !state.finished {
108+
while !state.withLock({ $0.finished }) {
102109
let intervalNanoseconds = UInt64(intervalSeconds * 1_000_000_000)
103110
render()
104-
state.iteration += 1
111+
state.withLock { $0.iteration += 1 }
105112
if (try? await Task.sleep(nanoseconds: intervalNanoseconds)) == nil {
106113
return
107114
}
@@ -118,17 +125,17 @@ public final class ProgressBar: Sendable {
118125

119126
/// Finishes the progress bar.
120127
public func finish() {
121-
guard !state.finished else {
128+
guard !state.withLock({ $0.finished }) else {
122129
return
123130
}
124131

125-
state.finished = true
132+
state.withLock { $0.finished = true }
126133

127134
// The last render.
128135
render(force: true)
129136

130137
if !config.disableProgressUpdates && !config.clearOnFinish {
131-
displayText(state.output, terminating: "\n")
138+
displayText(state.withLock { $0.output }, terminating: "\n")
132139
}
133140

134141
if config.clearOnFinish {
@@ -143,20 +150,22 @@ public final class ProgressBar: Sendable {
143150

144151
extension ProgressBar {
145152
private func secondsSinceStart() -> Int {
146-
let timeDifferenceNanoseconds = DispatchTime.now().uptimeNanoseconds - state.startTime.uptimeNanoseconds
153+
let timeDifferenceNanoseconds = DispatchTime.now().uptimeNanoseconds - state.withLock { $0.startTime.uptimeNanoseconds }
147154
let timeDifferenceSeconds = Int(floor(Double(timeDifferenceNanoseconds) / 1_000_000_000))
148155
return timeDifferenceSeconds
149156
}
150157

151158
func render(force: Bool = false) {
152-
guard term != nil && !config.disableProgressUpdates && (force || !state.finished) else {
159+
guard term != nil && !config.disableProgressUpdates && (force || !state.withLock { $0.finished }) else {
153160
return
154161
}
155162
let output = draw()
156163
displayText(output)
157164
}
158165

159166
func draw() -> String {
167+
let state = self.state.withLock { $0 }
168+
160169
var components = [String]()
161170
if config.showSpinner && !config.showProgressBar {
162171
if !state.finished {

0 commit comments

Comments
 (0)