WatchLogger.swift 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import Foundation
  2. import WatchConnectivity
  3. actor WatchLogger {
  4. static let shared = WatchLogger()
  5. private var logs: [String] = []
  6. private let maxEntries = 500
  7. private let flushInterval: TimeInterval = 3 * 60
  8. private let flushSizeThreshold = 100
  9. private var lastFlush = Date()
  10. private let session = WCSession.default
  11. private var timerTask: Task<Void, Never>?
  12. private init() {
  13. Task {
  14. await startFlushTimer()
  15. }
  16. }
  17. private var dateFormatter: DateFormatter {
  18. let formatter = DateFormatter()
  19. formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
  20. return formatter
  21. }
  22. private func startFlushTimer() async {
  23. timerTask = Task {
  24. while true {
  25. try? await Task.sleep(nanoseconds: UInt64(flushInterval * 1_000_000_000))
  26. await flushIfNeeded(force: false)
  27. }
  28. }
  29. }
  30. func log(
  31. _ message: String,
  32. force: Bool = false,
  33. function: String = #function,
  34. file: String = #fileID,
  35. line: Int = #line
  36. ) async {
  37. let shortFile = (file as NSString).lastPathComponent
  38. let timestamp = dateFormatter.string(from: Date())
  39. let entry = "[\(timestamp)] [\(shortFile):\(line)] \(function) → \(message)"
  40. logs.append(entry)
  41. if logs.count > maxEntries {
  42. logs.removeFirst(logs.count - maxEntries)
  43. }
  44. print(entry)
  45. await flushIfNeeded(force: force)
  46. }
  47. func flushIfNeeded(force: Bool = false) async {
  48. let now = Date()
  49. let shouldFlush = force || now.timeIntervalSince(lastFlush) >= flushInterval || logs.count >= flushSizeThreshold
  50. if shouldFlush {
  51. await flushToPhone()
  52. }
  53. }
  54. private func flushToPhone() async {
  55. guard !logs.isEmpty else {
  56. return
  57. }
  58. let payload: [String: Any] = ["watchLogs": logs.joined(separator: "\n")]
  59. if session.activationState != .activated {
  60. session.activate()
  61. }
  62. if session.isReachable {
  63. session.sendMessage(payload, replyHandler: nil) { _ in
  64. Task {
  65. await self.persistLogsLocally()
  66. }
  67. }
  68. } else {
  69. await persistLogsLocally()
  70. }
  71. lastFlush = Date()
  72. logs.removeAll()
  73. }
  74. func persistLogsLocally() async {
  75. let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
  76. .appendingPathComponent("logs", isDirectory: true)
  77. try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true)
  78. let logFile = logDir.appendingPathComponent("watch_log.txt")
  79. let previousLogFile = logDir.appendingPathComponent("watch_log_prev.txt")
  80. let startOfDay = Calendar.current.startOfDay(for: Date())
  81. if let attributes = try? FileManager.default.attributesOfItem(atPath: logFile.path),
  82. let creationDate = attributes[.creationDate] as? Date,
  83. creationDate < startOfDay
  84. {
  85. try? FileManager.default.removeItem(at: previousLogFile)
  86. try? FileManager.default.moveItem(at: logFile, to: previousLogFile)
  87. FileManager.default.createFile(atPath: logFile.path, contents: nil, attributes: [.creationDate: startOfDay])
  88. }
  89. let fullLog = logs.joined(separator: "\n") + "\n"
  90. if let data = fullLog.data(using: .utf8) {
  91. if let handle = try? FileHandle(forWritingTo: logFile) {
  92. try? handle.seekToEnd()
  93. handle.write(data)
  94. try? handle.close()
  95. } else {
  96. try? data.write(to: logFile)
  97. }
  98. }
  99. }
  100. func flushPersistedLogs() async {
  101. let logDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
  102. .appendingPathComponent("logs", isDirectory: true)
  103. let logFile = logDir.appendingPathComponent("watch_log.txt")
  104. guard let data = try? Data(contentsOf: logFile),
  105. let logString = String(data: data, encoding: .utf8),
  106. !logString.isEmpty
  107. else { return }
  108. let payload: [String: Any] = ["watchLogs": logString]
  109. if session.activationState != .activated {
  110. session.activate()
  111. }
  112. if session.isReachable {
  113. session.sendMessage(payload, replyHandler: nil) { _ in
  114. Task {
  115. await self.persistLogsLocally()
  116. }
  117. }
  118. try? FileManager.default.removeItem(at: logFile)
  119. } else {
  120. _ = session.transferUserInfo(payload)
  121. try? FileManager.default.removeItem(at: logFile)
  122. }
  123. }
  124. }