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 logger = Logger(subsystem: "net.24unix.iKeyMon", category: "Sparkle")
|
||||||
private let verboseLogging: Bool
|
private let verboseLogging: Bool
|
||||||
|
@Published var logMessages: [String] = []
|
||||||
|
@Published var showLogs: Bool = false
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.verboseLogging = ProcessInfo.processInfo.environment["SPARKLE_VERBOSE_LOGGING"] == "1"
|
self.verboseLogging = ProcessInfo.processInfo.environment["SPARKLE_VERBOSE_LOGGING"] == "1"
|
||||||
@@ -34,6 +36,7 @@ final class SparkleUpdater: NSObject, ObservableObject {
|
|||||||
|
|
||||||
private func log(_ message: String) {
|
private func log(_ message: String) {
|
||||||
logger.log("\(message, privacy: .public)")
|
logger.log("\(message, privacy: .public)")
|
||||||
|
addLogMessage("[INFO] \(message)")
|
||||||
if verboseLogging {
|
if verboseLogging {
|
||||||
print("[Sparkle] \(message)")
|
print("[Sparkle] \(message)")
|
||||||
}
|
}
|
||||||
@@ -41,11 +44,21 @@ final class SparkleUpdater: NSObject, ObservableObject {
|
|||||||
|
|
||||||
private func logError(_ message: String) {
|
private func logError(_ message: String) {
|
||||||
logger.error("\(message, privacy: .public)")
|
logger.error("\(message, privacy: .public)")
|
||||||
|
addLogMessage("[ERROR] \(message)")
|
||||||
if verboseLogging {
|
if verboseLogging {
|
||||||
fputs("[Sparkle][error] \(message)\n", stderr)
|
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 {
|
private func describe(update item: SUAppcastItem) -> String {
|
||||||
let short = item.displayVersionString
|
let short = item.displayVersionString
|
||||||
let build = item.versionString
|
let build = item.versionString
|
||||||
@@ -53,41 +66,58 @@ final class SparkleUpdater: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
extension SparkleUpdater: SPUUpdaterDelegate {
|
extension SparkleUpdater: SPUUpdaterDelegate {
|
||||||
func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
|
nonisolated func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
|
||||||
log("Loaded Sparkle appcast containing \(appcast.items.count) item(s).")
|
Task { @MainActor in
|
||||||
|
log("Loaded Sparkle appcast containing \(appcast.items.count) item(s).")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
nonisolated func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
||||||
log("Found valid update \(describe(update: item))")
|
Task { @MainActor in
|
||||||
|
log("Found valid update \(describe(update: item))")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
|
nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
|
||||||
log("No updates available.")
|
Task { @MainActor in
|
||||||
|
log("No updates available.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updater(_ updater: SPUUpdater, willDownloadUpdate item: SUAppcastItem, with request: NSMutableURLRequest) {
|
nonisolated func updater(_ updater: SPUUpdater, willDownloadUpdate item: SUAppcastItem, with request: NSMutableURLRequest) {
|
||||||
log("Downloading \(describe(update: item)) from \(request.url?.absoluteString ?? "unknown URL")")
|
Task { @MainActor in
|
||||||
|
log("Downloading \(describe(update: item)) from \(request.url?.absoluteString ?? "unknown URL")")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
|
nonisolated func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
|
||||||
log("Finished downloading \(describe(update: item))")
|
Task { @MainActor in
|
||||||
|
log("Finished downloading \(describe(update: item))")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
|
nonisolated func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
|
||||||
logError("Failed to download \(describe(update: item)): \(error.localizedDescription)")
|
Task { @MainActor in
|
||||||
|
logError("Failed to download \(describe(update: item)): \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func userDidCancelDownload(_ updater: SPUUpdater) {
|
nonisolated func userDidCancelDownload(_ updater: SPUUpdater) {
|
||||||
log("User cancelled Sparkle download.")
|
Task { @MainActor in
|
||||||
|
log("User cancelled Sparkle download.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
|
nonisolated func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
|
||||||
log("Will install update \(describe(update: item))")
|
Task { @MainActor in
|
||||||
|
log("Will install update \(describe(update: item))")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
||||||
logError("Sparkle aborted: \(error.localizedDescription)")
|
Task { @MainActor in
|
||||||
|
logError("Sparkle aborted: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,11 +257,64 @@ private struct UpdatesPreferencesView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding(.top, 4)
|
.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()
|
Spacer()
|
||||||
}
|
}
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.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 {
|
private struct NotificationsPreferencesView: View {
|
||||||
|
|||||||
Reference in New Issue
Block a user