import Foundation enum AppLogStore { private static let queue = DispatchQueue(label: "BusyMirror.log.store") private static let maxLogSizeBytes: UInt64 = 1_000_000 static let logDirectoryURL: URL = { let base = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library", isDirectory: true) return base.appendingPathComponent("Logs/BusyMirror", isDirectory: true) }() static let logFileURL = logDirectoryURL.appendingPathComponent("BusyMirror.log", isDirectory: false) private static let archivedLogFileURL = logDirectoryURL.appendingPathComponent("BusyMirror.previous.log", isDirectory: false) static let launchdStdoutURL = logDirectoryURL.appendingPathComponent("launchd.stdout.log", isDirectory: false) static let launchdStderrURL = logDirectoryURL.appendingPathComponent("launchd.stderr.log", isDirectory: false) private static let timestampFormatter: ISO8601DateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return f }() static func append(_ message: String) { let line = "[\(timestampFormatter.string(from: Date()))] \(message)\n" queue.async { let fm = FileManager.default do { try fm.createDirectory(at: logDirectoryURL, withIntermediateDirectories: true) if let attrs = try? fm.attributesOfItem(atPath: logFileURL.path), let size = attrs[.size] as? NSNumber, size.uint64Value >= maxLogSizeBytes { try? fm.removeItem(at: archivedLogFileURL) try? fm.moveItem(at: logFileURL, to: archivedLogFileURL) } if !fm.fileExists(atPath: logFileURL.path) { fm.createFile(atPath: logFileURL.path, contents: nil) } guard let data = line.data(using: .utf8) else { return } // Use the throwing initialiser so we don't silently swallow // an inaccessible file — the outer catch handles it. let handle = try FileHandle(forWritingTo: logFileURL) defer { try? handle.close() } try handle.seekToEnd() try handle.write(contentsOf: data) } catch { // Logging must never break the app's main behavior. } } } }