From 6adcad63b3686c6718098ef72e5db5492fe1fadd Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 20 Jun 2020 23:11:35 -0400 Subject: [PATCH] Add crash report helper --- Tusker.xcodeproj/project.pbxproj | 33 +++++ .../xcshareddata/swiftpm/Package.resolved | 10 +- Tusker/AppDelegate.swift | 24 +++- Tusker/SceneDelegate.swift | 50 +++++-- .../CrashReporterViewController.swift | 133 ++++++++++++++++++ .../CrashReporterViewController.xib | 86 +++++++++++ 6 files changed, 321 insertions(+), 15 deletions(-) create mode 100644 Tusker/Screens/Crash Reporter/CrashReporterViewController.swift create mode 100644 Tusker/Screens/Crash Reporter/CrashReporterViewController.xib diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 2c4d5997..f46a99e7 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -185,6 +185,7 @@ D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; }; D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; }; D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */; }; + D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = D69CCBBE249E6EFD000AF167 /* CrashReporter */; }; D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; }; D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */; }; D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; }; @@ -248,6 +249,8 @@ D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; }; D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; }; D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; }; + D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; }; + D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; }; D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; /* End PBXBuildFile section */ @@ -554,6 +557,8 @@ D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = ""; }; D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = ""; }; D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; + D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = ""; }; + D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = ""; }; D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = ""; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = ""; }; D6F98BD523AE951F008A4DAC /* Swifter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Swifter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -582,6 +587,7 @@ D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */, D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */, D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */, + D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */, D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */, 0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */, ); @@ -851,6 +857,7 @@ isa = PBXGroup; children = ( D6C693FA2162FE5D007D6A6D /* Utilities */, + D6F2E960249E772F005846BB /* Crash Reporter */, D641C782213DD7F0004B4513 /* Main */, D641C783213DD7FE004B4513 /* Onboarding */, D641C781213DD7DD004B4513 /* Timeline */, @@ -1333,6 +1340,15 @@ path = Caching; sourceTree = ""; }; + D6F2E960249E772F005846BB /* Crash Reporter */ = { + isa = PBXGroup; + children = ( + D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */, + D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */, + ); + path = "Crash Reporter"; + sourceTree = ""; + }; D6F953F121251A2F00CF0F2B /* Controllers */ = { isa = PBXGroup; children = ( @@ -1410,6 +1426,7 @@ name = Tusker; packageProductDependencies = ( D6B0539E23BD2BA300A066FA /* SheetController */, + D69CCBBE249E6EFD000AF167 /* CrashReporter */, ); productName = Tusker; productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */; @@ -1505,6 +1522,7 @@ mainGroup = D6D4DDC3212518A000E1C4BB; packageReferences = ( D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */, + D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */, ); productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */; projectDirPath = ""; @@ -1563,6 +1581,7 @@ D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */, D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */, D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */, + D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1705,6 +1724,7 @@ D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */, + D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */, D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */, D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */, @@ -2298,6 +2318,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/microsoft/plcrashreporter"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.7.0; + }; + }; D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://git.shadowfacts.net/shadowfacts/SheetController.git"; @@ -2309,6 +2337,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + D69CCBBE249E6EFD000AF167 /* CrashReporter */ = { + isa = XCSwiftPackageProductDependency; + package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */; + productName = CrashReporter; + }; D6B0539E23BD2BA300A066FA /* SheetController */ = { isa = XCSwiftPackageProductDependency; package = D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */; diff --git a/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved index 30987bbb..7cd77ddd 100644 --- a/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Tusker.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -2,12 +2,12 @@ "object": { "pins": [ { - "package": "SheetController", - "repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git", + "package": "PLCrashReporter", + "repositoryURL": "https://github.com/microsoft/plcrashreporter", "state": { - "branch": "master", - "revision": "6926446c4e15eb7f4513c4c00df9279553b330be", - "version": null + "branch": null, + "revision": "4637a7854de2cc5c354d46fb931d74bdbc2c043e", + "version": "1.7.0" } } ] diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 1854af62..9d8ecee9 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -7,13 +7,35 @@ // import UIKit +import CrashReporter +import MessageUI @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { + + static private(set) var crashReporter: PLCrashReporter! + static var pendingCrashReport: PLCrashReport? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + #if !DEBUG + setupCrashReporter() + #endif AppShortcutItem.createItems(for: application) return true } - + + private func setupCrashReporter() { + let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all) + AppDelegate.crashReporter = PLCrashReporter(configuration: config) + + if AppDelegate.crashReporter.hasPendingCrashReport() { + let data = try! AppDelegate.crashReporter.loadPendingCrashReportDataAndReturnError() + AppDelegate.crashReporter.purgePendingCrashReport() + let report = try! PLCrashReport(data: data) + + AppDelegate.pendingCrashReport = report + } + + AppDelegate.crashReporter.enable() + } } diff --git a/Tusker/SceneDelegate.swift b/Tusker/SceneDelegate.swift index 10eba605..9626e0ee 100644 --- a/Tusker/SceneDelegate.swift +++ b/Tusker/SceneDelegate.swift @@ -8,6 +8,8 @@ import UIKit import Pachyderm +import CrashReporter +import MessageUI class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -21,17 +23,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) - if LocalData.shared.onboardingComplete { - if session.mastodonController == nil { - let account = LocalData.shared.getMostRecentAccount()! - session.mastodonController = MastodonController.getForAccount(account) - } - - showAppUI() + if let report = AppDelegate.pendingCrashReport { + AppDelegate.pendingCrashReport = nil + handlePendingCrashReport(report, session: session) } else { - showOnboardingUI() + showAppOrOnboardingUI(session: session) } - + window!.makeKeyAndVisible() NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil) @@ -113,6 +111,32 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { try! scene.session.mastodonController?.persistentContainer.viewContext.save() } + private func handlePendingCrashReport(_ report: PLCrashReport, session: UISceneSession) { + #if !DEBUG + guard MFMailComposeViewController.canSendMail() else { + print("Cannot send email") + showAppOrOnboardingUI(session: session) + return + } + + window!.rootViewController = CrashReporterViewController.create(report: report) + #endif + } + + func showAppOrOnboardingUI(session: UISceneSession? = nil) { + let session = session ?? window!.windowScene!.session + if LocalData.shared.onboardingComplete { + if session.mastodonController == nil { + let account = LocalData.shared.getMostRecentAccount()! + session.mastodonController = MastodonController.getForAccount(account) + } + + showAppUI() + } else { + showOnboardingUI() + } + } + func activateAccount(_ account: LocalData.UserAccountInfo) { LocalData.shared.setMostRecentAccount(account) window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account) @@ -154,3 +178,11 @@ extension SceneDelegate: OnboardingViewControllerDelegate { activateAccount(account) } } + +extension SceneDelegate: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + controller.dismiss(animated: true) { + self.showAppOrOnboardingUI() + } + } +} diff --git a/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift b/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift new file mode 100644 index 00000000..25f9f658 --- /dev/null +++ b/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift @@ -0,0 +1,133 @@ +// +// CrashReporterViewController.swift +// Tusker +// +// Created by Shadowfacts on 6/20/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import CrashReporter +import MessageUI + +class CrashReporterViewController: UIViewController { + + private let report: PLCrashReport + private var reportText: String! + + private var reportFilename: String { + let timestamp = ISO8601DateFormatter().string(from: report.systemInfo.timestamp) + return "Tusker-crash-\(timestamp).crash" + } + + @IBOutlet weak var crashReportTextView: UITextView! + @IBOutlet weak var sendReportButton: UIButton! + + static func create(report: PLCrashReport) -> UINavigationController { + let nav = UINavigationController(rootViewController: CrashReporterViewController(report: report)) + nav.navigationBar.prefersLargeTitles = true + return nav + } + + private init(report: PLCrashReport){ + self.report = report + + super.init(nibName: "CrashReporterViewController", bundle: .main) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = NSLocalizedString("Crash Detected", comment: "crash reporter title") + navigationItem.largeTitleDisplayMode = .always + + crashReportTextView.font = .monospacedSystemFont(ofSize: 14, weight: .regular) + + reportText = PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)! + let info = "Tusker has detected that it crashed the last time it was running. You can email the report to the developer or skip sending and continue to the app. You may review the report below before sending.\n\nIf you choose to send the report, please include any additional details about what you were doing prior to the crash that may be pertinent.\n\n" + let attributed = NSMutableAttributedString() + attributed.append(NSAttributedString(string: info, attributes: [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17), + NSAttributedString.Key.foregroundColor: UIColor.label + ])) + attributed.append(NSAttributedString(string: reportText, attributes: [ + NSAttributedString.Key.font: UIFont.monospacedSystemFont(ofSize: 14, weight: .regular), + NSAttributedString.Key.foregroundColor: UIColor.label + ])) + crashReportTextView.attributedText = attributed + + sendReportButton.layer.cornerRadius = 12.5 + sendReportButton.layer.masksToBounds = true + + sendReportButton.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(sendReportButtonLongPressed))) + } + + private func updateSendReportButtonColor(lightened: Bool, animate: Bool) { + let color: UIColor + if lightened { + var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0 + UIColor.systemBlue.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + color = UIColor(hue: hue, saturation: 0.85 * saturation, brightness: brightness, alpha: alpha) + } else { + color = .systemBlue + } + if animate { + UIView.animate(withDuration: 0.25) { + self.sendReportButton.backgroundColor = color + } + } else { + sendReportButton.backgroundColor = color + } + } + + @IBAction func sendReportTouchDown(_ sender: Any) { + updateSendReportButtonColor(lightened: true, animate: false) + } + + @IBAction func sendReportButtonTouchDragExit(_ sender: Any) { + updateSendReportButtonColor(lightened: false, animate: true) + } + + @IBAction func sendReportButtonTouchDragEnter(_ sender: Any) { + updateSendReportButtonColor(lightened: true, animate: true) + } + + @IBAction func sendReportTouchUpInside(_ sender: Any) { + updateSendReportButtonColor(lightened: false, animate: true) + + let composeVC = MFMailComposeViewController() + composeVC.mailComposeDelegate = self + composeVC.setToRecipients(["me@shadowfacts.net"]) + composeVC.setSubject("Tusker Crash Report") + + let data = reportText.data(using: .utf8)! + composeVC.addAttachmentData(data, mimeType: "text/plain", fileName: reportFilename) + + self.present(composeVC, animated: true) + } + + @objc func sendReportButtonLongPressed() { + let dir = FileManager.default.temporaryDirectory + let url = dir.appendingPathComponent(reportFilename) + try! reportText.data(using: .utf8)!.write(to: url) + let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil) + present(activityController, animated: true) + } + + @IBAction func cancelPressed(_ sender: Any) { + (view.window!.windowScene!.delegate as! SceneDelegate).showAppOrOnboardingUI() + } + +} + +extension CrashReporterViewController: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + controller.dismiss(animated: true) { + (self.view.window!.windowScene!.delegate as! SceneDelegate).showAppOrOnboardingUI() + } + } +} diff --git a/Tusker/Screens/Crash Reporter/CrashReporterViewController.xib b/Tusker/Screens/Crash Reporter/CrashReporterViewController.xib new file mode 100644 index 00000000..76dfe814 --- /dev/null +++ b/Tusker/Screens/Crash Reporter/CrashReporterViewController.xib @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +