WatchLogger.swift 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. //
  2. // WatchLogger.swift
  3. // Trio
  4. //
  5. // Created by Cengiz Deniz on 18.04.25.
  6. //
  7. import Foundation
  8. import WatchConnectivity
  9. final class WatchLogger {
  10. static let shared = WatchLogger()
  11. private var logs: [String] = []
  12. private let maxEntries = 300
  13. private let flushInterval: TimeInterval = 15 * 60
  14. private let flushSizeThreshold = 150
  15. private var lastFlush = Date()
  16. private let session = WCSession.default
  17. private init() {
  18. Timer.scheduledTimer(withTimeInterval: flushInterval, repeats: true) { _ in
  19. self.flushIfNeeded(force: false)
  20. }
  21. }
  22. func log(_ message: String) {
  23. let timestamp = ISO8601DateFormatter().string(from: Date())
  24. let entry = "[\(timestamp)] \(message)"
  25. logs.append(entry)
  26. if logs.count > maxEntries {
  27. logs.removeFirst()
  28. }
  29. print(entry)
  30. flushIfNeeded(force: false)
  31. }
  32. func flushIfNeeded(force: Bool = false) {
  33. let now = Date()
  34. let shouldFlush = force || logs.count >= flushSizeThreshold || now.timeIntervalSince(lastFlush) >= flushInterval
  35. if shouldFlush {
  36. flushToPhone()
  37. }
  38. }
  39. private func flushToPhone() {
  40. guard !logs.isEmpty else { return }
  41. let payload: [String: Any] = [
  42. "watchLogs": logs.joined(separator: "\n")
  43. ]
  44. if session.activationState != .activated {
  45. session.activate()
  46. }
  47. if session.isReachable {
  48. session.sendMessage(payload, replyHandler: nil) { error in
  49. print("⌚️ Failed to flush logs to phone via sendMessage: \(error.localizedDescription)")
  50. self.persistLogsLocally()
  51. }
  52. }
  53. lastFlush = Date()
  54. logs.removeAll()
  55. }
  56. func persistLogsLocally() {
  57. let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
  58. .appendingPathComponent("logs", isDirectory: true)
  59. try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true)
  60. let logFile = logDir.appendingPathComponent("watch_log.txt")
  61. let previousLogFile = logDir.appendingPathComponent("watch_log_prev.txt")
  62. let startOfDay = Calendar.current.startOfDay(for: Date())
  63. // Rotate if necessary
  64. if let attributes = try? FileManager.default.attributesOfItem(atPath: logFile.path),
  65. let creationDate = attributes[.creationDate] as? Date,
  66. creationDate < startOfDay
  67. {
  68. try? FileManager.default.removeItem(at: previousLogFile)
  69. try? FileManager.default.moveItem(at: logFile, to: previousLogFile)
  70. FileManager.default.createFile(atPath: logFile.path, contents: nil, attributes: [.creationDate: startOfDay])
  71. }
  72. let fullLog = logs.joined(separator: "\n") + "\n"
  73. if let data = fullLog.data(using: .utf8) {
  74. if let handle = try? FileHandle(forWritingTo: logFile) {
  75. _ = try? handle.seekToEnd()
  76. handle.write(data)
  77. try? handle.close()
  78. } else {
  79. try? data.write(to: logFile)
  80. }
  81. }
  82. }
  83. /// Optional recovery mechanism:
  84. /// Use this on startup to attempt flushing local file logs to phone
  85. func flushPersistedLogs() {
  86. let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
  87. .appendingPathComponent("logs", isDirectory: true)
  88. let logFile = logDir.appendingPathComponent("watch_log.txt")
  89. guard let data = try? Data(contentsOf: logFile),
  90. let logString = String(data: data, encoding: .utf8),
  91. !logString.isEmpty
  92. else { return }
  93. let payload: [String: Any] = [
  94. "watchLogs": logString
  95. ]
  96. if session.activationState != .activated {
  97. session.activate()
  98. }
  99. if session.isReachable {
  100. session.sendMessage(payload, replyHandler: nil) { error in
  101. print("⌚️ Failed to resend persisted logs: \(error.localizedDescription)")
  102. }
  103. try? FileManager.default.removeItem(at: logFile)
  104. } else {
  105. _ = session.transferUserInfo(payload)
  106. try? FileManager.default.removeItem(at: logFile)
  107. }
  108. }
  109. }