/
CrashReporter.swift
142 lines (116 loc) · 4.81 KB
/
CrashReporter.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//
// CrashReporter.swift
// NetNewsWire
//
// Created by Brent Simmons on 12/17/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import Foundation
import RSWeb
// Based originally on Uli Kusterer's UKCrashReporter: http://www.zathras.de/angelweb/blog-ukcrashreporter-oh-one.htm
// Then based on the crash reporter included in NetNewsWire 3 and NetNewsWire Lite 4.
// Displays a window that shows the crash log — gives the user the chance to add data.
// (Or just decide not to send it.)
// This code is not included in the MAS build.
// At some point this code should probably move into RSCore, so Rainier and any other
// future apps can use it.
struct CrashLog {
let path: String
let modificationDate: Date
let content: String
let contentHash: String
init?(path: String, modificationDate: Date) {
guard let s = try? NSString(contentsOfFile: path, usedEncoding: nil) as String, !s.isEmpty else {
return nil
}
self.content = s
self.contentHash = s.rs_md5Hash()
self.path = path
self.modificationDate = modificationDate
}
}
struct CrashReporter {
struct DefaultsKey {
static let lastSeenCrashLogDateKey = "LastSeenCrashLogDate"
static let hashOfLastSeenCrashLogKey = "LastSeenCrashLogHash"
static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically"
}
private static var crashReportWindowController: CrashReportWindowController?
/// Look in ~/Library/Logs/DiagnosticReports/ for a new crash log for this app.
/// Show a crash log reporter window if found.
static func check(appName: String) {
let folder = ("~/Library/Logs/DiagnosticReports/" as NSString).expandingTildeInPath
guard let filenames = try? FileManager.default.contentsOfDirectory(atPath: folder) else {
return
}
let crashSuffix = ".crash"
let lowerAppName = appName.lowercased()
let lastSeenCrashLogDate: Date = {
let lastSeenCrashLogDouble = UserDefaults.standard.double(forKey: DefaultsKey.lastSeenCrashLogDateKey)
return Date(timeIntervalSince1970: lastSeenCrashLogDouble)
}()
var mostRecentFilePath: String? = nil
var mostRecentFileDate = Date.distantPast
for filename in filenames {
if !filename.lowercased().hasPrefix(lowerAppName) || !filename.hasSuffix(crashSuffix) {
continue
}
let path = (folder as NSString).appendingPathComponent(filename)
let fileAttributes = (try? FileManager.default.attributesOfItem(atPath: path)) ?? [FileAttributeKey: Any]()
if let fileModificationDate = fileAttributes[.modificationDate] as? Date {
if fileModificationDate > lastSeenCrashLogDate && fileModificationDate > mostRecentFileDate { // Ignore if previously seen
mostRecentFileDate = fileModificationDate
mostRecentFilePath = path
}
}
}
guard let crashLogPath = mostRecentFilePath, let crashLog = CrashLog(path: crashLogPath, modificationDate: mostRecentFileDate) else {
return
}
if hasSeen(crashLog) {
return
}
remember(crashLog)
if shouldSendCrashLogsAutomatically() {
sendCrashLogText(crashLog.content)
}
else {
runCrashReporterWindow(crashLog)
}
}
static func sendCrashLogText(_ crashLogText: String) {
var request = URLRequest(url: URL(string: "https://ranchero.com/netnewswire/crashreportcatcher.php")!)
request.httpMethod = HTTPMethod.post
let boundary = "0xKhTmLbOuNdArY"
let contentType = "multipart/form-data; boundary=\(boundary)"
request.setValue(contentType, forHTTPHeaderField:HTTPRequestHeader.contentType)
let formString = "--\(boundary)\r\nContent-Disposition: form-data; name=\"crashlog\"\r\n\r\n\(crashLogText)\r\n--\(boundary)--\r\n"
let formData = formString.data(using: .utf8, allowLossyConversion: true)
request.httpBody = formData
download(request) { (_, _, _) in
// Don’t care about the result.
}
}
static func runCrashReporterWindow(_ crashLog: CrashLog) {
crashReportWindowController = CrashReportWindowController(crashLogText: crashLog.content)
crashReportWindowController!.showWindow(self)
}
}
private extension CrashReporter {
static func hasSeen(_ crashLog: CrashLog) -> Bool {
// No need to compare dates, because that’s done in the file loop.
// Check to see if we’ve already reported this exact crash log.
guard let hashOfLastSeenCrashLog = UserDefaults.standard.string(forKey: DefaultsKey.hashOfLastSeenCrashLogKey) else {
return false
}
return hashOfLastSeenCrashLog == crashLog.contentHash
}
static func remember(_ crashLog: CrashLog) {
// Save the modification date and hash, so we don’t send duplicates.
UserDefaults.standard.set(crashLog.contentHash, forKey: DefaultsKey.hashOfLastSeenCrashLogKey)
UserDefaults.standard.set(crashLog.modificationDate.timeIntervalSince1970, forKey: DefaultsKey.lastSeenCrashLogDateKey)
}
static func shouldSendCrashLogsAutomatically() -> Bool {
return UserDefaults.standard.bool(forKey: DefaultsKey.sendCrashLogsAutomaticallyKey)
}
}