RelayLog.swift (4798B)
1 // 2 // RelayLog.swift 3 // damus 4 // 5 // Created by Bryan Montz on 6/1/23. 6 // 7 8 import Combine 9 import Foundation 10 import UIKit 11 12 /// Stores a running list of events and state changes related to a relay, so that users 13 /// will have information to help developers debug issues. 14 final class RelayLog: ObservableObject { 15 private static let line_limit = 250 16 private let relay_url: RelayURL? 17 private lazy var formatter: DateFormatter = { 18 let formatter = DateFormatter() 19 formatter.dateStyle = .short 20 formatter.timeStyle = .medium 21 return formatter 22 }() 23 24 private(set) var lines = [String]() 25 26 private var notification_token: AnyCancellable? 27 28 /// Creates a RelayLog 29 /// - Parameter relay_url: the relay url the log represents. Pass nil for the url to create 30 /// a RelayLog that does nothing. This is required to allow RelayLog to be used as a StateObject, 31 /// because they cannot be Optional. 32 init(_ relay_url: RelayURL? = nil) { 33 self.relay_url = relay_url 34 35 setUp() 36 } 37 38 private var log_files_directory: URL { 39 FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("RelayLogs", isDirectory: true) 40 } 41 42 private var log_file_url: URL? { 43 guard let file_name = relay_url?.absoluteString.data(using: .utf8) else { 44 return nil 45 } 46 return log_files_directory.appendingPathComponent(file_name.base64EncodedString()) 47 } 48 49 /// Sets up the log file and prepares to listen to app state changes 50 private func setUp() { 51 guard let log_file_url else { 52 return 53 } 54 55 try? FileManager.default.createDirectory(at: log_files_directory, withIntermediateDirectories: false) 56 57 if !FileManager.default.fileExists(atPath: log_file_url.path) { 58 // create the log file if it doesn't exist yet 59 FileManager.default.createFile(atPath: log_file_url.path, contents: nil) 60 } else { 61 // otherwise load it into memory 62 readFromDisk() 63 } 64 65 let willResignPublisher = NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification) 66 let willTerminatePublisher = NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification) 67 notification_token = Publishers.Merge(willResignPublisher, willTerminatePublisher) 68 .sink { [weak self] _ in 69 self?.writeToDisk() 70 } 71 } 72 73 /// The current contents of the log 74 var contents: String? { 75 guard !lines.isEmpty else { 76 return nil 77 } 78 return lines.joined(separator: "\n") 79 } 80 81 /// Adds content to the log 82 /// - Parameter content: what to add to the log. The date and time are prepended to the content. 83 func add(_ content: String) { 84 Task { 85 await addLine(content) 86 await publishChanges() 87 } 88 } 89 90 @MainActor private func addLine(_ line: String) { 91 let line = "\(formatter.string(from: .now)) - \(line)" 92 lines.insert(line, at: 0) 93 truncateLines() 94 } 95 96 /// Tells views that our log has been updated 97 @MainActor private func publishChanges() { 98 objectWillChange.send() 99 } 100 101 private func truncateLines() { 102 lines = Array(lines.prefix(RelayLog.line_limit)) 103 } 104 105 /// Reads the contents of the log file from disk into memory 106 private func readFromDisk() { 107 guard let log_file_url else { 108 return 109 } 110 111 do { 112 let handle = try FileHandle(forReadingFrom: log_file_url) 113 let data = try handle.readToEnd() 114 try handle.close() 115 116 guard let data, let content = String(data: data, encoding: .utf8) else { 117 return 118 } 119 120 lines = content.components(separatedBy: "\n") 121 122 truncateLines() 123 } catch { 124 print("⚠️ Warning: RelayLog failed to read from \(log_file_url)") 125 } 126 } 127 128 /// Writes the contents of the lines in memory to disk 129 private func writeToDisk() { 130 guard let log_file_url, let relay_url, 131 !lines.isEmpty, 132 let content = lines.joined(separator: "\n").data(using: .utf8) else { 133 return 134 } 135 136 do { 137 let handle = try FileHandle(forWritingTo: log_file_url) 138 139 try handle.truncate(atOffset: 0) 140 try handle.write(contentsOf: content) 141 try handle.close() 142 } catch { 143 print("⚠️ Warning: RelayLog(\(relay_url)) failed to write to file: \(error)") 144 } 145 } 146 }