WatchLogger.swift 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  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 = 7.5 * 60
  14. private let flushSizeThreshold = 75
  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. private var dateFormatter: DateFormatter {
  23. let dateFormatter = DateFormatter()
  24. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
  25. return dateFormatter
  26. }
  27. func log(_ message: String, force: Bool = false, function: String = #function, file: String = #fileID, line: Int = #line) {
  28. let shortFile = (file as NSString).lastPathComponent
  29. let timestamp = dateFormatter.string(from: Date())
  30. let entry = "[\(timestamp)] [\(shortFile):\(line)] \(function) → \(message)"
  31. logs.append(entry)
  32. if logs.count > maxEntries {
  33. logs.removeFirst()
  34. }
  35. print(entry)
  36. flushIfNeeded(force: force)
  37. }
  38. func flushIfNeeded(force: Bool = false) {
  39. let now = Date()
  40. let shouldFlush = force || now.timeIntervalSince(lastFlush) >= flushInterval || logs.count >= flushSizeThreshold
  41. if shouldFlush {
  42. flushToPhone()
  43. }
  44. }
  45. private func flushToPhone() {
  46. guard !logs.isEmpty else { return }
  47. let payload: [String: Any] = [
  48. "watchLogs": logs.joined(separator: "\n")
  49. ]
  50. if session.activationState != .activated {
  51. session.activate()
  52. }
  53. if session.isReachable {
  54. session.sendMessage(payload, replyHandler: nil) { error in
  55. print("⌚️ Failed to flush logs to phone via sendMessage: \(error.localizedDescription)")
  56. self.persistLogsLocally()
  57. }
  58. }
  59. lastFlush = Date()
  60. logs.removeAll()
  61. }
  62. func persistLogsLocally() {
  63. let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
  64. .appendingPathComponent("logs", isDirectory: true)
  65. try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true)
  66. let logFile = logDir.appendingPathComponent("watch_log.txt")
  67. let previousLogFile = logDir.appendingPathComponent("watch_log_prev.txt")
  68. let startOfDay = Calendar.current.startOfDay(for: Date())
  69. // Rotate if necessary
  70. if let attributes = try? FileManager.default.attributesOfItem(atPath: logFile.path),
  71. let creationDate = attributes[.creationDate] as? Date,
  72. creationDate < startOfDay
  73. {
  74. try? FileManager.default.removeItem(at: previousLogFile)
  75. try? FileManager.default.moveItem(at: logFile, to: previousLogFile)
  76. FileManager.default.createFile(atPath: logFile.path, contents: nil, attributes: [.creationDate: startOfDay])
  77. }
  78. let fullLog = logs.joined(separator: "\n") + "\n"
  79. if let data = fullLog.data(using: .utf8) {
  80. if let handle = try? FileHandle(forWritingTo: logFile) {
  81. _ = try? handle.seekToEnd()
  82. handle.write(data)
  83. try? handle.close()
  84. } else {
  85. try? data.write(to: logFile)
  86. }
  87. }
  88. }
  89. /// Optional recovery mechanism:
  90. /// Use this on startup to attempt flushing local file logs to phone
  91. func flushPersistedLogs() {
  92. let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
  93. .appendingPathComponent("logs", isDirectory: true)
  94. let logFile = logDir.appendingPathComponent("watch_log.txt")
  95. guard let data = try? Data(contentsOf: logFile),
  96. let logString = String(data: data, encoding: .utf8),
  97. !logString.isEmpty
  98. else { return }
  99. let payload: [String: Any] = [
  100. "watchLogs": logString
  101. ]
  102. if session.activationState != .activated {
  103. session.activate()
  104. }
  105. if session.isReachable {
  106. session.sendMessage(payload, replyHandler: nil) { error in
  107. print("⌚️ Failed to resend persisted logs: \(error.localizedDescription)")
  108. }
  109. try? FileManager.default.removeItem(at: logFile)
  110. } else {
  111. _ = session.transferUserInfo(payload)
  112. try? FileManager.default.removeItem(at: logFile)
  113. }
  114. }
  115. }