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,7 +250,6 @@ 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 {
@ -259,7 +259,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
} }
}
func updateOutdatedMenu(with lines: [String]) { func updateOutdatedMenu(with lines: [String]) {
outdatedSubmenu.removeAllItems() outdatedSubmenu.removeAllItems()
@ -305,12 +304,9 @@ 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)
DispatchQueue.main.async {
self.versionItem.title = "🏷 \(firstLine)" self.versionItem.title = "🏷 \(firstLine)"
} }
} }
}
// MARK: - Logs // MARK: - Logs