fix: deliver runBrew completion on the main thread

Completions were invoked on a background queue. Most call sites wrapped
their UI work in DispatchQueue.main.async, but upgradeSingle's completion
calls refreshAll(), which sets statusMenuItem.title directly — an AppKit
mutation off the main thread. It also raced on cachedOutdated /
lastOutdatedFetch (written from background, read from main).

runBrew now always dispatches its completion to the main queue, so every
caller can safely touch UI, and the now-redundant inner main.async hops
are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
maxsoch 2026-07-06 21:26:14 +02:00
parent 4ab3cc1e70
commit 7390d68a33

View file

@ -194,7 +194,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
try process.run()
} catch {
self.log("ERROR: \(error)\n")
completion("")
DispatchQueue.main.async { completion("") }
return
}
@ -204,7 +204,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let output = String(data: data, encoding: .utf8) ?? ""
self.log(output)
completion(output)
// completion always on main: callers update AppKit UI, which is main-thread only
DispatchQueue.main.async { completion(output) }
}
}
@ -249,7 +250,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.cachedOutdated = lines
self.lastOutdatedFetch = Date()
DispatchQueue.main.async {
self.updateOutdatedMenu(with: lines)
self.updateStatus(count: lines.count)
if lines.isEmpty {
@ -259,7 +259,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
}
}
func updateOutdatedMenu(with lines: [String]) {
outdatedSubmenu.removeAllItems()
@ -305,12 +304,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func refreshBrewVersion() {
runBrew("--version") { output in
let firstLine = BrewParser.parseVersion(output)
DispatchQueue.main.async {
self.versionItem.title = "🏷 \(firstLine)"
}
}
}
// MARK: - Logs