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:
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,11 +257,64 @@ private struct UpdatesPreferencesView: View {
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 4)
|
||||
|
||||
Divider()
|
||||
.padding(.vertical, 8)
|
||||
|
||||
Button(action: { sparkleUpdater.showLogs.toggle() }) {
|
||||
Label(sparkleUpdater.showLogs ? "Hide Logs" : "Show Logs", systemImage: "terminal.fill")
|
||||
}
|
||||
|
||||
if sparkleUpdater.showLogs {
|
||||
logsView
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.toggleStyle(.switch)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var logsView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Update Logs")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if sparkleUpdater.logMessages.isEmpty {
|
||||
Text("No logs yet. Check for updates to see activity.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
} else {
|
||||
ForEach(sparkleUpdater.logMessages, id: \.self) { message in
|
||||
Text(message)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(3)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(8)
|
||||
}
|
||||
.frame(height: 200)
|
||||
.background(Color(nsColor: .controlBackgroundColor))
|
||||
.cornerRadius(6)
|
||||
.border(.separator, width: 1)
|
||||
|
||||
Button(action: {
|
||||
sparkleUpdater.logMessages.removeAll()
|
||||
}) {
|
||||
Label("Clear Logs", systemImage: "trash")
|
||||
.font(.caption)
|
||||
}
|
||||
.disabled(sparkleUpdater.logMessages.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NotificationsPreferencesView: View {
|
||||
|
||||
Reference in New Issue
Block a user