1import Foundation
2
3/// Diagnostics that must survive without a console attached. NSLog only
4/// reaches the unified log (invisible for `open`-launched apps unless you go
5/// digging in Console.app), so the lines worth keeping — every [miss] spinner
6/// event, rescue/build failures, admission blocks — are ALSO appended to
7/// `~/Library/Logs/Sequencer.log`. Plain text, timestamped, rotated at 5 MB
8/// (one `.old` generation kept).
9enum SeqLog {
10 private static let queue = DispatchQueue(label: "sequencer.log", qos: .utility)
11 private static let url: URL = {
12 let dir = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
13 .appendingPathComponent("Logs", isDirectory: true)
14 try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
15 return dir.appendingPathComponent("Sequencer.log")
16 }()
17 private static let stamp: DateFormatter = {
18 let f = DateFormatter()
19 f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
20 return f
21 }()
22
23 /// Log to the unified log (NSLog) AND the plain-text file.
24 static func log(_ format: String, _ args: CVarArg...) {
25 let line = String(format: format, arguments: args)
26 NSLog("%@", line)
27 let dated = "\(stamp.string(from: Date())) \(line)\n"
28 queue.async {
29 if let size = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size]
30 as? NSNumber)?.int64Value, size > 5_000_000 {
31 let old = url.deletingPathExtension().appendingPathExtension("old.log")
32 try? FileManager.default.removeItem(at: old)
33 try? FileManager.default.moveItem(at: url, to: old)
34 }
35 if let h = FileHandle(forWritingAtPath: url.path) {
36 defer { try? h.close() }
37 _ = try? h.seekToEnd()
38 try? h.write(contentsOf: Data(dated.utf8))
39 } else {
40 try? Data(dated.utf8).write(to: url)
41 }
42 }
43 }
44}