damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }