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() try process.run()
} catch { } catch {
self.log("ERROR: \(error)\n") self.log("ERROR: \(error)\n")
completion("") DispatchQueue.main.async { completion("") }
return return
} }
@ -204,7 +204,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let output = String(data: data, encoding: .utf8) ?? "" let output = String(data: data, encoding: .utf8) ?? ""
self.log(output) self.log(output)
completion(output) // completion always on main: callers update AppKit UI, which is main-thread only
DispatchQueue.main.async { completion(output) }
} }
} }
@ -249,14 +250,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.cachedOutdated = lines self.cachedOutdated = lines
self.lastOutdatedFetch = Date() self.lastOutdatedFetch = Date()
DispatchQueue.main.async { self.updateOutdatedMenu(with: lines)
self.updateOutdatedMenu(with: lines) self.updateStatus(count: lines.count)
self.updateStatus(count: lines.count) if lines.isEmpty {
if lines.isEmpty { self.setMenuBarIcon("brewbar-uptodate")
self.setMenuBarIcon("brewbar-uptodate") } else {
} else { self.setMenuBarIcon("brewbar-outdated")
self.setMenuBarIcon("brewbar-outdated")
}
} }
} }
} }
@ -305,10 +304,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func refreshBrewVersion() { func refreshBrewVersion() {
runBrew("--version") { output in runBrew("--version") { output in
let firstLine = BrewParser.parseVersion(output) let firstLine = BrewParser.parseVersion(output)
self.versionItem.title = "🏷 \(firstLine)"
DispatchQueue.main.async {
self.versionItem.title = "🏷 \(firstLine)"
}
} }
} }