From 6ec47bbe0cf2f959f4655225f2802d8e342b50bd Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 9 Sep 2022 17:44:15 -0500 Subject: [PATCH] Adds Error Log screen Allows users to view a history of all errors that occured when performing app operations. --- AltStore.xcodeproj/project.pbxproj | 16 + AltStore/Managing Apps/AppManager.swift | 39 +++ AltStore/Operations/OperationError.swift | 2 + .../Error Log/ErrorLogTableViewCell.swift | 52 ++++ .../Error Log/ErrorLogViewController.swift | 294 ++++++++++++++++++ AltStore/Settings/Settings.storyboard | 167 +++++++++- .../Settings/SettingsViewController.swift | 3 +- AltStoreCore/Model/DatabaseManager.swift | 26 +- 8 files changed, 591 insertions(+), 8 deletions(-) create mode 100644 AltStore/Settings/Error Log/ErrorLogTableViewCell.swift create mode 100644 AltStore/Settings/Error Log/ErrorLogViewController.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 50c555083..1231f494c 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -352,11 +352,13 @@ D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8B62727841800A9B5DD /* libAppleArchive.tbd */; settings = {ATTRIBUTES = (Weak, ); }; }; D533E8BC2727BBEE00A9B5DD /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */; }; D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.a */; }; + D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */; }; D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55E163528776CB000A627A1 /* ComplicationView.swift */; }; D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; }; D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D57DF63E271E51E400677701 /* ALTAppPatcher.m */; }; D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */; }; D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */; }; + D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */; }; D58916FE28C7C55C00E39C8B /* LoggedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58916FD28C7C55C00E39C8B /* LoggedError.swift */; }; D58D5F2E26DFE68E00E55E38 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D58D5F2D26DFE68E00E55E38 /* LaunchAtLogin */; }; D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; }; @@ -819,12 +821,14 @@ D533E8B82727B61400A9B5DD /* fragmentzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fragmentzip.h; sourceTree = ""; }; D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libfragmentzip.a; path = Dependencies/fragmentzip/libfragmentzip.a; sourceTree = SOURCE_ROOT; }; D533E8BD2727BBF800A9B5DD /* libcurl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcurl.a; path = Dependencies/libcurl/libcurl.a; sourceTree = SOURCE_ROOT; }; + D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = ""; }; D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = ""; }; D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = ""; }; D57DF63D271E51E400677701 /* ALTAppPatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPatcher.h; sourceTree = ""; }; D57DF63E271E51E400677701 /* ALTAppPatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPatcher.m; sourceTree = ""; }; D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableJITOperation.swift; sourceTree = ""; }; D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Vibration.swift"; sourceTree = ""; }; + D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogViewController.swift; sourceTree = ""; }; D58916FD28C7C55C00E39C8B /* LoggedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedError.swift; sourceTree = ""; }; D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = ""; }; D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = ""; }; @@ -1666,6 +1670,7 @@ BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */, BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */, BFF0B695232242D3007A79E1 /* LicensesViewController.swift */, + D589170128C7D93500E39C8B /* Error Log */, ); path = Settings; sourceTree = ""; @@ -1787,6 +1792,15 @@ path = XPC; sourceTree = ""; }; + D589170128C7D93500E39C8B /* Error Log */ = { + isa = PBXGroup; + children = ( + D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */, + D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */, + ); + path = "Error Log"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2576,6 +2590,7 @@ BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */, D5E1E7C128077DE90016FC96 /* FetchTrustedSourcesOperation.swift in Sources */, BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */, + D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */, BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */, BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */, BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */, @@ -2621,6 +2636,7 @@ BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */, BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */, BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */, + D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */, BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */, BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */, D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */, diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 9271037b5..93b78561c 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -1724,6 +1724,8 @@ private extension AppManager catch { group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier) + + self.log(error, for: operation) } } @@ -1748,6 +1750,43 @@ private extension AppManager UNUserNotificationCenter.current().add(request) } + func log(_ error: Error, for operation: AppOperation) + { + // Sanitize NSError on same thread before performing background task. + let sanitizedError = (error as NSError).sanitizedForCoreData() + + let loggedErrorOperation: LoggedError.Operation = { + switch operation + { + case .install: return .install + case .update: return .update + case .refresh: return .refresh + case .activate: return .activate + case .deactivate: return .deactivate + case .backup: return .backup + case .restore: return .restore + } + }() + + DatabaseManager.shared.persistentContainer.performBackgroundTask { context in + var app = operation.app + if let managedApp = app as? NSManagedObject, let tempApp = context.object(with: managedApp.objectID) as? AppProtocol + { + app = tempApp + } + + do + { + _ = LoggedError(error: sanitizedError, app: app, operation: loggedErrorOperation, context: context) + try context.save() + } + catch let saveError + { + print("[ALTLog] Failed to log error \(sanitizedError.domain) code \(sanitizedError.code) for \(app.bundleIdentifier):", saveError) + } + } + } + func run(_ operations: [Foundation.Operation], context: OperationContext?, requiresSerialQueue: Bool = false) { // Find "Install AltStore" operation if it already exists in `context` diff --git a/AltStore/Operations/OperationError.swift b/AltStore/Operations/OperationError.swift index 497129d50..ded928f52 100644 --- a/AltStore/Operations/OperationError.swift +++ b/AltStore/Operations/OperationError.swift @@ -11,6 +11,8 @@ import AltSign enum OperationError: LocalizedError { + static let domain = OperationError.unknown._domain + case unknown case unknownResult case cancelled diff --git a/AltStore/Settings/Error Log/ErrorLogTableViewCell.swift b/AltStore/Settings/Error Log/ErrorLogTableViewCell.swift new file mode 100644 index 000000000..5fa635513 --- /dev/null +++ b/AltStore/Settings/Error Log/ErrorLogTableViewCell.swift @@ -0,0 +1,52 @@ +// +// ErrorLogTableViewCell.swift +// AltStore +// +// Created by Riley Testut on 9/9/22. +// Copyright © 2022 Riley Testut. All rights reserved. +// + +import UIKit + +@objc(ErrorLogTableViewCell) +class ErrorLogTableViewCell: UITableViewCell +{ + @IBOutlet var appIconImageView: AppIconImageView! + + @IBOutlet var dateLabel: UILabel! + @IBOutlet var errorFailureLabel: UILabel! + @IBOutlet var errorCodeLabel: UILabel! + @IBOutlet var errorDescriptionTextView: CollapsingTextView! + + @IBOutlet var menuButton: UIButton! + + private var didLayoutSubviews = false + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? + { + let moreButtonFrame = self.convert(self.errorDescriptionTextView.moreButton.frame, from: self.errorDescriptionTextView) + guard moreButtonFrame.contains(point) else { return super.hitTest(point, with: event) } + + // Pass touches through menuButton so user can press moreButton. + return self.errorDescriptionTextView.moreButton + } + + override func layoutSubviews() + { + super.layoutSubviews() + + self.didLayoutSubviews = true + } + + override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize + { + if !self.didLayoutSubviews + { + // Ensure cell is laid out so it will report correct size. + self.layoutIfNeeded() + } + + let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority) + return size + } +} diff --git a/AltStore/Settings/Error Log/ErrorLogViewController.swift b/AltStore/Settings/Error Log/ErrorLogViewController.swift new file mode 100644 index 000000000..a2c34a36d --- /dev/null +++ b/AltStore/Settings/Error Log/ErrorLogViewController.swift @@ -0,0 +1,294 @@ +// +// ErrorLogViewController.swift +// AltStore +// +// Created by Riley Testut on 9/6/22. +// Copyright © 2022 Riley Testut. All rights reserved. +// + +import UIKit +import SafariServices + +import AltStoreCore +import Roxas + +import Nuke + +class ErrorLogViewController: UITableViewController +{ + private lazy var dataSource = self.makeDataSource() + private var expandedErrorIDs = Set() + + private lazy var timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .short + return dateFormatter + }() + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + override func viewDidLoad() + { + super.viewDidLoad() + + self.tableView.dataSource = self.dataSource + self.tableView.prefetchDataSource = self.dataSource + } +} + +private extension ErrorLogViewController +{ + func makeDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource + { + let fetchRequest = LoggedError.fetchRequest() as NSFetchRequest + fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \LoggedError.date, ascending: false)] + fetchRequest.returnsObjectsAsFaults = false + + let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(LoggedError.localizedDateString), cacheName: nil) + + let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource(fetchedResultsController: fetchedResultsController) + dataSource.proxy = self + dataSource.rowAnimation = .fade + dataSource.cellConfigurationHandler = { [weak self] (cell, loggedError, indexPath) in + guard let self else { return } + + let cell = cell as! ErrorLogTableViewCell + cell.dateLabel.text = self.timeFormatter.string(from: loggedError.date) + cell.errorFailureLabel.text = loggedError.localizedFailure ?? NSLocalizedString("Operation Failed", comment: "") + + switch loggedError.domain + { + case AltServerErrorDomain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltServer Error %@", comment: ""), NSNumber(value: loggedError.code)) + case OperationError.domain: cell.errorCodeLabel?.text = String(format: NSLocalizedString("AltStore Error %@", comment: ""), NSNumber(value: loggedError.code)) + default: cell.errorCodeLabel?.text = loggedError.error.localizedErrorCode + } + + let nsError = loggedError.error as NSError + let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n") + cell.errorDescriptionTextView.text = errorDescription + cell.errorDescriptionTextView.maximumNumberOfLines = 5 + cell.errorDescriptionTextView.isCollapsed = !self.expandedErrorIDs.contains(loggedError.objectID) + cell.errorDescriptionTextView.moreButton.addTarget(self, action: #selector(ErrorLogViewController.toggleCollapsingCell(_:)), for: .primaryActionTriggered) + + cell.appIconImageView.image = nil + cell.appIconImageView.isIndicatingActivity = true + cell.appIconImageView.layer.borderColor = UIColor.gray.cgColor + + let displayScale = (self.traitCollection.displayScale == 0.0) ? 1.0 : self.traitCollection.displayScale // 0.0 == "unspecified" + cell.appIconImageView.layer.borderWidth = 1.0 / displayScale + + if #available(iOS 14, *) + { + let menu = UIMenu(title: "", children: [ + UIAction(title: NSLocalizedString("Copy Error Message", comment: ""), image: UIImage(systemName: "doc.on.doc")) { [weak self] _ in + self?.copyErrorMessage(for: loggedError) + }, + UIAction(title: NSLocalizedString("Copy Error Code", comment: ""), image: UIImage(systemName: "doc.on.doc")) { [weak self] _ in + self?.copyErrorCode(for: loggedError) + }, + UIAction(title: NSLocalizedString("Search FAQ", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in + self?.searchFAQ(for: loggedError) + } + ]) + + cell.menuButton.menu = menu + } + + // Include errorDescriptionTextView's text in cell summary. + cell.accessibilityLabel = [cell.errorFailureLabel.text, cell.dateLabel.text, cell.errorCodeLabel.text, cell.errorDescriptionTextView.text].compactMap { $0 }.joined(separator: ". ") + + // Group all paragraphs together into single accessibility element (otherwise, each paragraph is independently selectable). + cell.errorDescriptionTextView.accessibilityLabel = cell.errorDescriptionTextView.text + } + dataSource.prefetchHandler = { (loggedError, indexPath, completion) in + RSTAsyncBlockOperation { (operation) in + loggedError.managedObjectContext?.perform { + if let installedApp = loggedError.installedApp + { + installedApp.loadIcon { (result) in + switch result + { + case .failure(let error): completion(nil, error) + case .success(let image): completion(image, nil) + } + } + } + else if let storeApp = loggedError.storeApp + { + ImagePipeline.shared.loadImage(with: storeApp.iconURL, progress: nil) { (response, error) in + guard !operation.isCancelled else { return operation.finish() } + + if let image = response?.image + { + completion(image, nil) + } + else + { + completion(nil, error) + } + } + } + else + { + completion(nil, nil) + } + } + } + } + dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in + let cell = cell as! ErrorLogTableViewCell + cell.appIconImageView.image = image + cell.appIconImageView.isIndicatingActivity = false + } + + let placeholderView = RSTPlaceholderView() + placeholderView.textLabel.text = NSLocalizedString("No Errors", comment: "") + placeholderView.detailTextLabel.text = NSLocalizedString("Errors that occur when sideloading or refreshing apps will appear here.", comment: "") + dataSource.placeholderView = placeholderView + + return dataSource + } +} + +private extension ErrorLogViewController +{ + @IBAction func toggleCollapsingCell(_ sender: UIButton) + { + let point = self.tableView.convert(sender.center, from: sender.superview) + guard let indexPath = self.tableView.indexPathForRow(at: point), let cell = self.tableView.cellForRow(at: indexPath) as? ErrorLogTableViewCell else { return } + + let loggedError = self.dataSource.item(at: indexPath) + + if cell.errorDescriptionTextView.isCollapsed + { + self.expandedErrorIDs.remove(loggedError.objectID) + } + else + { + self.expandedErrorIDs.insert(loggedError.objectID) + } + + self.tableView.performBatchUpdates { + cell.layoutIfNeeded() + } + } + + @IBAction func clearLoggedErrors(_ sender: UIBarButtonItem) + { + let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear the error log?", comment: ""), message: nil, preferredStyle: .actionSheet) + alertController.popoverPresentationController?.barButtonItem = sender + alertController.addAction(.cancel) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Clear Error Log", comment: ""), style: .destructive) { _ in + self.clearLoggedErrors() + }) + self.present(alertController, animated: true) + } + + func clearLoggedErrors() + { + DatabaseManager.shared.purgeLoggedErrors { result in + do + { + try result.get() + } + catch + { + DispatchQueue.main.async { + let alertController = UIAlertController(title: NSLocalizedString("Failed to Clear Error Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert) + alertController.addAction(.ok) + self.present(alertController, animated: true) + } + } + } + } + + func copyErrorMessage(for loggedError: LoggedError) + { + let nsError = loggedError.error as NSError + let errorMessage = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n") + + UIPasteboard.general.string = errorMessage + } + + func copyErrorCode(for loggedError: LoggedError) + { + let errorCode = loggedError.error.localizedErrorCode + UIPasteboard.general.string = errorCode + } + + func searchFAQ(for loggedError: LoggedError) + { + let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")! + var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! + + let query = [loggedError.domain, "\(loggedError.code)"].joined(separator: "+") + components.queryItems = [URLQueryItem(name: "q", value: query)] + + let safariViewController = SFSafariViewController(url: components.url ?? baseURL) + safariViewController.preferredControlTintColor = .altPrimary + self.present(safariViewController, animated: true) + } +} + +extension ErrorLogViewController +{ + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) + { + let loggedError = self.dataSource.item(at: indexPath) + + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in + tableView.deselectRow(at: indexPath, animated: true) + }) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Copy Error Message", comment: ""), style: .default) { [weak self] _ in + self?.copyErrorMessage(for: loggedError) + tableView.deselectRow(at: indexPath, animated: true) + }) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Copy Error Code", comment: ""), style: .default) { [weak self] _ in + self?.copyErrorCode(for: loggedError) + tableView.deselectRow(at: indexPath, animated: true) + }) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Search FAQ", comment: ""), style: .default) { [weak self] _ in + self?.searchFAQ(for: loggedError) + tableView.deselectRow(at: indexPath, animated: true) + }) + self.present(alertController, animated: true) + } + + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? + { + let deleteAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Delete", comment: "")) { _, _, completion in + let loggedError = self.dataSource.item(at: indexPath) + DatabaseManager.shared.persistentContainer.performBackgroundTask { context in + do + { + let loggedError = context.object(with: loggedError.objectID) as! LoggedError + context.delete(loggedError) + + try context.save() + completion(true) + } + catch + { + print("[ALTLog] Failed to delete LoggedError \(loggedError.objectID):", error) + completion(false) + } + } + } + + let configuration = UISwipeActionsConfiguration(actions: [deleteAction]) + configuration.performsFirstActionWithFullSwipe = false + return configuration + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? + { + let indexPath = IndexPath(row: 0, section: section) + + let loggedError = self.dataSource.item(at: indexPath) + return loggedError.localizedDateString + } +} diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 192297ca3..7ac2ee689 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -1,11 +1,12 @@ - + - + + @@ -540,7 +541,7 @@ - + @@ -548,6 +549,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -842,6 +879,124 @@ Settings by i cons from the Noun Project + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -852,5 +1007,11 @@ Settings by i cons from the Noun Project + + + + + + diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift index ed0344716..41711393b 100644 --- a/AltStore/Settings/SettingsViewController.swift +++ b/AltStore/Settings/SettingsViewController.swift @@ -52,6 +52,7 @@ extension SettingsViewController { case sendFeedback case refreshAttempts + case errorLog } } @@ -502,7 +503,7 @@ extension SettingsViewController toastView.show(in: self) } - case .refreshAttempts: break + case .refreshAttempts, .errorLog: break } default: break diff --git a/AltStoreCore/Model/DatabaseManager.swift b/AltStoreCore/Model/DatabaseManager.swift index 44006365c..72d617d7c 100644 --- a/AltStoreCore/Model/DatabaseManager.swift +++ b/AltStoreCore/Model/DatabaseManager.swift @@ -122,6 +122,27 @@ public extension DatabaseManager } } } + + func purgeLoggedErrors(before date: Date? = nil, completion: @escaping (Result) -> Void) + { + self.persistentContainer.performBackgroundTask { context in + do + { + let predicate = date.map { NSPredicate(format: "%K <= %@", #keyPath(LoggedError.date), $0 as NSDate) } + + let loggedErrors = LoggedError.all(satisfying: predicate, in: context, requestProperties: [\.returnsObjectsAsFaults: true]) + loggedErrors.forEach { context.delete($0) } + + try context.save() + + completion(.success(())) + } + catch + { + completion(.failure(error)) + } + } + } } public extension DatabaseManager @@ -129,10 +150,7 @@ public extension DatabaseManager var viewContext: NSManagedObjectContext { return self.persistentContainer.viewContext } -} - -public extension DatabaseManager -{ + func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account? { let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))