2018-12-18 07:44:06 +01:00
|
|
|
|
//
|
|
|
|
|
// CrashReporter.swift
|
|
|
|
|
// NetNewsWire
|
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 12/17/18.
|
|
|
|
|
// Copyright © 2018 Ranchero Software. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
2019-01-10 08:02:01 +01:00
|
|
|
|
import RSWeb
|
2018-12-18 07:44:06 +01:00
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
|
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
struct CrashLog {
|
2018-12-21 07:21:42 +01:00
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
let path: String
|
|
|
|
|
let modificationDate: Date
|
|
|
|
|
let content: String
|
|
|
|
|
let contentHash: String
|
2018-12-21 07:21:42 +01:00
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
init?(path: String, modificationDate: Date) {
|
|
|
|
|
guard let s = try? NSString(contentsOfFile: path, usedEncoding: nil) as String, !s.isEmpty else {
|
|
|
|
|
return nil
|
2018-12-21 07:21:42 +01:00
|
|
|
|
}
|
2018-12-22 21:01:31 +01:00
|
|
|
|
self.content = s
|
2020-01-18 08:00:56 +01:00
|
|
|
|
self.contentHash = s.md5String
|
2018-12-22 21:01:31 +01:00
|
|
|
|
self.path = path
|
|
|
|
|
self.modificationDate = modificationDate
|
2018-12-21 07:21:42 +01:00
|
|
|
|
}
|
2018-12-22 21:01:31 +01:00
|
|
|
|
}
|
2018-12-21 07:21:42 +01:00
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
struct CrashReporter {
|
2018-12-21 07:21:42 +01:00
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
struct DefaultsKey {
|
|
|
|
|
static let lastSeenCrashLogDateKey = "LastSeenCrashLogDate"
|
|
|
|
|
static let hashOfLastSeenCrashLogKey = "LastSeenCrashLogHash"
|
|
|
|
|
static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically"
|
2018-12-21 07:21:42 +01:00
|
|
|
|
}
|
2018-12-18 07:44:06 +01:00
|
|
|
|
|
2019-01-12 07:50:36 +01:00
|
|
|
|
private static var crashReportWindowController: CrashReportWindowController?
|
|
|
|
|
|
2018-12-18 07:44:06 +01:00
|
|
|
|
/// Look in ~/Library/Logs/DiagnosticReports/ for a new crash log for this app.
|
2018-12-22 21:01:31 +01:00
|
|
|
|
/// Show a crash log reporter window if found.
|
|
|
|
|
static func check(appName: String) {
|
2018-12-18 07:44:06 +01:00
|
|
|
|
let folder = ("~/Library/Logs/DiagnosticReports/" as NSString).expandingTildeInPath
|
2018-12-22 21:01:31 +01:00
|
|
|
|
guard let filenames = try? FileManager.default.contentsOfDirectory(atPath: folder) else {
|
2018-12-21 07:21:42 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
2018-12-18 07:44:06 +01:00
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
let crashSuffix = ".crash"
|
|
|
|
|
let lowerAppName = appName.lowercased()
|
|
|
|
|
let lastSeenCrashLogDate: Date = {
|
|
|
|
|
let lastSeenCrashLogDouble = UserDefaults.standard.double(forKey: DefaultsKey.lastSeenCrashLogDateKey)
|
|
|
|
|
return Date(timeIntervalSince1970: lastSeenCrashLogDouble)
|
|
|
|
|
}()
|
|
|
|
|
|
2018-12-18 07:44:06 +01:00
|
|
|
|
var mostRecentFilePath: String? = nil
|
2018-12-21 07:21:42 +01:00
|
|
|
|
var mostRecentFileDate = Date.distantPast
|
2018-12-18 07:44:06 +01:00
|
|
|
|
for filename in filenames {
|
2018-12-22 21:01:31 +01:00
|
|
|
|
if !filename.lowercased().hasPrefix(lowerAppName) || !filename.hasSuffix(crashSuffix) {
|
2018-12-18 07:44:06 +01:00
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let path = (folder as NSString).appendingPathComponent(filename)
|
2018-12-22 21:01:31 +01:00
|
|
|
|
let fileAttributes = (try? FileManager.default.attributesOfItem(atPath: path)) ?? [FileAttributeKey: Any]()
|
2018-12-18 07:44:06 +01:00
|
|
|
|
if let fileModificationDate = fileAttributes[.modificationDate] as? Date {
|
2018-12-22 21:01:31 +01:00
|
|
|
|
if fileModificationDate > lastSeenCrashLogDate && fileModificationDate > mostRecentFileDate { // Ignore if previously seen
|
2018-12-18 07:44:06 +01:00
|
|
|
|
mostRecentFileDate = fileModificationDate
|
|
|
|
|
mostRecentFilePath = path
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
guard let crashLogPath = mostRecentFilePath, let crashLog = CrashLog(path: crashLogPath, modificationDate: mostRecentFileDate) else {
|
2018-12-18 07:44:06 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
2018-12-21 07:21:42 +01:00
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
if hasSeen(crashLog) {
|
2018-12-18 07:44:06 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
2018-12-22 21:01:31 +01:00
|
|
|
|
remember(crashLog)
|
2018-12-18 07:44:06 +01:00
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
if shouldSendCrashLogsAutomatically() {
|
2018-12-29 21:31:27 +01:00
|
|
|
|
sendCrashLogText(crashLog.content)
|
2018-12-18 07:44:06 +01:00
|
|
|
|
}
|
2018-12-22 21:01:31 +01:00
|
|
|
|
else {
|
|
|
|
|
runCrashReporterWindow(crashLog)
|
2018-12-21 07:21:42 +01:00
|
|
|
|
}
|
2018-12-22 21:01:31 +01:00
|
|
|
|
}
|
2018-12-18 07:44:06 +01:00
|
|
|
|
|
2018-12-29 21:31:27 +01:00
|
|
|
|
static func sendCrashLogText(_ crashLogText: String) {
|
2019-01-12 06:59:27 +01:00
|
|
|
|
var request = URLRequest(url: URL(string: "https://ranchero.com/netnewswire/crashreportcatcher.php")!)
|
2019-01-10 08:02:01 +01:00
|
|
|
|
request.httpMethod = HTTPMethod.post
|
|
|
|
|
|
|
|
|
|
let boundary = "0xKhTmLbOuNdArY"
|
|
|
|
|
|
2019-01-12 07:11:46 +01:00
|
|
|
|
let contentType = "multipart/form-data; boundary=\(boundary)"
|
2019-01-10 08:02:01 +01:00
|
|
|
|
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
|
|
|
|
|
|
2019-01-12 06:59:27 +01:00
|
|
|
|
download(request) { (_, _, _) in
|
|
|
|
|
// Don’t care about the result.
|
|
|
|
|
}
|
2018-12-22 21:01:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func runCrashReporterWindow(_ crashLog: CrashLog) {
|
2019-01-12 07:50:36 +01:00
|
|
|
|
crashReportWindowController = CrashReportWindowController(crashLogText: crashLog.content)
|
|
|
|
|
crashReportWindowController!.showWindow(self)
|
2018-12-18 07:44:06 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-21 07:21:42 +01:00
|
|
|
|
private extension CrashReporter {
|
2018-12-18 07:44:06 +01:00
|
|
|
|
|
2018-12-22 21:01:31 +01:00
|
|
|
|
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)
|
2018-12-21 07:21:42 +01:00
|
|
|
|
}
|
|
|
|
|
}
|