feat: add in-app Sparkle update logging

- Add published logMessages array to SparkleUpdater to track all update events
- Display logs in the Updates preferences tab with Show/Hide toggle
- Each log entry is timestamped and shows both info and error messages
- Logs persist during session with max 100 entries
- Users can clear logs manually
- Helps diagnose update failures directly in the app UI
This commit is contained in:
Micha
2025-12-30 13:12:27 +01:00
parent 10683ebc73
commit 25723b7f07
2 changed files with 102 additions and 19 deletions

View File

@@ -9,6 +9,8 @@ final class SparkleUpdater: NSObject, ObservableObject {
}()
private let logger = Logger(subsystem: "net.24unix.iKeyMon", category: "Sparkle")
private let verboseLogging: Bool
@Published var logMessages: [String] = []
@Published var showLogs: Bool = false
override init() {
self.verboseLogging = ProcessInfo.processInfo.environment["SPARKLE_VERBOSE_LOGGING"] == "1"
@@ -34,6 +36,7 @@ final class SparkleUpdater: NSObject, ObservableObject {
private func log(_ message: String) {
logger.log("\(message, privacy: .public)")
addLogMessage("[INFO] \(message)")
if verboseLogging {
print("[Sparkle] \(message)")
}
@@ -41,11 +44,21 @@ final class SparkleUpdater: NSObject, ObservableObject {
private func logError(_ message: String) {
logger.error("\(message, privacy: .public)")
addLogMessage("[ERROR] \(message)")
if verboseLogging {
fputs("[Sparkle][error] \(message)\n", stderr)
}
}
private func addLogMessage(_ message: String) {
let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
let timestampedMessage = "[\(timestamp)] \(message)"
logMessages.append(timestampedMessage)
if logMessages.count > 100 {
logMessages.removeFirst()
}
}
private func describe(update item: SUAppcastItem) -> String {
let short = item.displayVersionString
let build = item.versionString
@@ -53,41 +66,58 @@ final class SparkleUpdater: NSObject, ObservableObject {
}
}
@MainActor
extension SparkleUpdater: SPUUpdaterDelegate {
func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
log("Loaded Sparkle appcast containing \(appcast.items.count) item(s).")
nonisolated func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
Task { @MainActor in
log("Loaded Sparkle appcast containing \(appcast.items.count) item(s).")
}
}
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
log("Found valid update \(describe(update: item))")
nonisolated func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
Task { @MainActor in
log("Found valid update \(describe(update: item))")
}
}
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
log("No updates available.")
nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
Task { @MainActor in
log("No updates available.")
}
}
func updater(_ updater: SPUUpdater, willDownloadUpdate item: SUAppcastItem, with request: NSMutableURLRequest) {
log("Downloading \(describe(update: item)) from \(request.url?.absoluteString ?? "unknown URL")")
nonisolated func updater(_ updater: SPUUpdater, willDownloadUpdate item: SUAppcastItem, with request: NSMutableURLRequest) {
Task { @MainActor in
log("Downloading \(describe(update: item)) from \(request.url?.absoluteString ?? "unknown URL")")
}
}
func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
log("Finished downloading \(describe(update: item))")
nonisolated func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
Task { @MainActor in
log("Finished downloading \(describe(update: item))")
}
}
func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
logError("Failed to download \(describe(update: item)): \(error.localizedDescription)")
nonisolated func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
Task { @MainActor in
logError("Failed to download \(describe(update: item)): \(error.localizedDescription)")
}
}
func userDidCancelDownload(_ updater: SPUUpdater) {
log("User cancelled Sparkle download.")
nonisolated func userDidCancelDownload(_ updater: SPUUpdater) {
Task { @MainActor in
log("User cancelled Sparkle download.")
}
}
func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
log("Will install update \(describe(update: item))")
nonisolated func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
Task { @MainActor in
log("Will install update \(describe(update: item))")
}
}
func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
logError("Sparkle aborted: \(error.localizedDescription)")
nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
Task { @MainActor in
logError("Sparkle aborted: \(error.localizedDescription)")
}
}
}