[Shared] Refactors error handling based on ALTLocalizedError protocol (#1115)

* [Shared] Revises ALTLocalizedError protocol

* Refactors errors to conform to revised ALTLocalizedError protocol

* [Missing Commit] Remaining changes for ALTLocalizedError

* [AltServer] Refactors errors to conform to revised ALTLocalizedError protocol

* [Missing Commit] Declares ALTLocalizedTitleErrorKey + ALTLocalizedDescriptionKey

* Updates Objective-C errors to match revised ALTLocalizedError

* [Missing Commit] Unnecessary ALTLocalizedDescription logic

* [Shared] Refactors NSError.withLocalizedFailure to properly support ALTLocalizedError

* [Shared] Supports adding localized titles to errors via NSError.withLocalizedTitle()

* Revises ErrorResponse logic to support arbitrary errors and user info values

* [Missed Commit] Renames CodableServerError to CodableError

* Merges ConnectionError into OperationError

* [Missed Commit] Doesn’t check ALTWrappedError’s userInfo for localizedDescription

* [Missed] Fixes incorrect errorDomain for ALTErrorEnums

* [Missed] Removes nonexistent ALTWrappedError.h

* Includes source file and line number in OperationError.unknown failureReason

* Adds localizedTitle to AppManager operation errors

* Fixes adding localizedTitle + localizedFailure to ALTWrappedError

* Updates ToastView to use error’s localizedTitle as title

* [Shared] Adds NSError.formattedDetailedDescription(with:)

Returns formatted NSAttributedString containing all user info values intended for displaying to the user.

* [Shared] Updates Error.localizedErrorCode to say “code” instead of “error”

* Conforms ALTLocalizedError to CustomStringConvertible

* Adds “View More Details” option to Error Log context menu to view detailed error description

* [Shared] Fixes NSError.formattedDetailedDescription appearing black in dark mode

* [AltServer] Updates error alert to match revised error logic

Uses error’s localizedTitle as alert title.

* [AltServer] Adds “View More Details” button to error alert to view detailed error info

* [AltServer] Renames InstallError to OperationError and conforms to ALTErrorEnum

* [Shared] Removes CodableError support for Date user info values

Not currently used, and we don’t want to accidentally parse a non-Date as a Date in the meantime.

* [Shared] Includes dynamic UserInfoValueProvider values in NSError.formattedDetailedDescription()

* [Shared] Includes source file + line in NSError.formattedDetailedDescription()

Automatically captures source file + line when throwing ALTErrorEnums.

* [Shared] Captures source file + line for unknown errors

* Removes sourceFunction from OperationError

* Adds localizedTitle to AuthenticationViewController errors

* [Shared] Moves nested ALTWrappedError logic to ALTWrappedError initializer

* [AltServer] Removes now-redundant localized failure from JIT errors

All JIT errors now have a localizedTitle which effectively says the same thing.

* Makes OperationError.Code start at 1000

“Connection errors” subsection starts at 1200.

* [Shared] Updates Error domains to revised [Source].[ErrorType] format

* Updates ALTWrappedError.localizedDescription to prioritize using wrapped NSLocalizedDescription as failure reason

* Makes ALTAppleAPIError codes start at 3000

* [AltServer] Adds relevant localizedFailures to ALTDeviceManager.installApplication() errors

* Revises OperationError failureReasons and recovery suggestions

All failure reasons now read correctly when preceded by a failure reason and “because”.

* Revises ALTServerError error messages
All failure reasons now read correctly when preceded by a failure reason and “because”.

* Most failure reasons now read correctly when preceded by a failure reason and “because”.
* ALTServerErrorUnderlyingError forwards all user info provider calls to underlying error.

* Revises error messages for ALTAppleAPIErrorIncorrectCredentials

* [Missed] Removes NSError+AltStore.swift from AltStore target

* [Shared] Updates AltServerErrorDomain to revised [Source].[ErrorType] format

* [Shared] Removes “code” from Error.localizedErrorCode

* [Shared] Makes ALTServerError codes (appear to) start at 2000

We can’t change the actual error codes without breaking backwards compatibility, so instead we just add 2000 whenever we display ALTServerError codes to the user.

* Moves VerificationError.errorFailure to VerifyAppOperation

* Supports custom failure reason for OperationError.unknown

* [Shared] Changes AltServerErrorDomain to “AltServer.ServerError”

* [Shared] Converts ALTWrappedError to Objective-C class

NSError subclasses must be written in ObjC for Swift.Error <-> NSError bridging to work correctly.

# Conflicts:
#	AltStore.xcodeproj/project.pbxproj

* Fixes decoding CodableError nested user info values
This commit is contained in:
Riley Testut
2023-01-24 13:56:41 -06:00
committed by GitHub
parent 0c4fe98370
commit 3b38d725d7
44 changed files with 1707 additions and 644 deletions

View File

@@ -11,5 +11,6 @@
#import "ALTConstants.h"
#import "ALTConnection.h"
#import "AltXPCProtocol.h"
#import "ALTWrappedError.h"
#import "NSError+ALTServerError.h"
#import "CFNotificationName+AltStore.h"

View File

@@ -56,6 +56,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var isAltPluginUpdateAvailable = false
private var popoverController: NSPopover?
private var popoverError: NSError?
private var errorAlert: NSAlert?
func applicationDidFinishLaunching(_ aNotification: Notification)
{
UserDefaults.standard.registerDefaults()
@@ -159,8 +163,9 @@ private extension AppDelegate
DispatchQueue.main.async {
switch result
{
case .failure(let error):
self.showErrorAlert(error: error, localizedFailure: String(format: NSLocalizedString("JIT compilation could not be enabled for %@.", comment: ""), app.name))
case .failure(let error as NSError):
let localizedTitle = String(format: NSLocalizedString("JIT could not be enabled for %@.", comment: ""), app.name)
self.showErrorAlert(error: error.withLocalizedTitle(localizedTitle))
case .success:
let alert = NSAlert()
@@ -250,13 +255,13 @@ private extension AppDelegate
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
case .failure(InstallError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
case .failure(~OperationErrorCode.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
// Ignore
break
case .failure(let error):
DispatchQueue.main.async {
self.showErrorAlert(error: error, localizedFailure: String(format: NSLocalizedString("Could not install app to %@.", comment: ""), device.name))
self.showErrorAlert(error: error)
}
}
}
@@ -278,7 +283,10 @@ private extension AppDelegate
self.pluginManager.isUpdateAvailable { result in
switch result
{
case .failure(let error): finish(.failure(error))
case .failure(let error):
let error = (error as NSError).withLocalizedTitle(NSLocalizedString("Could not check for Mail plug-in updates.", comment: ""))
finish(.failure(error))
case .success(let isUpdateAvailable):
self.isAltPluginUpdateAvailable = isUpdateAvailable
@@ -302,85 +310,61 @@ private extension AppDelegate
}
}
func showErrorAlert(error: Error, localizedFailure: String)
func showErrorAlert(error: Error)
{
self.popoverError = error as NSError
let nsError = error as NSError
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = localizedFailure
var messageComponents = [String]()
let separator: String
switch error
{
case ALTServerError.maximumFreeAppLimitReached: separator = "\n\n"
default: separator = " "
}
if let errorFailure = nsError.localizedFailure
{
if let debugDescription = nsError.localizedDebugDescription
{
alert.messageText = errorFailure
messageComponents.append(debugDescription)
}
else if let failureReason = nsError.localizedFailureReason
{
if nsError.localizedDescription.starts(with: errorFailure)
{
alert.messageText = errorFailure
messageComponents.append(failureReason)
}
else
{
alert.messageText = errorFailure
messageComponents.append(nsError.localizedDescription)
}
}
else
{
// No failure reason given.
if nsError.localizedDescription.starts(with: errorFailure)
{
// No need to duplicate errorFailure in both title and message.
alert.messageText = localizedFailure
messageComponents.append(nsError.localizedDescription)
}
else
{
alert.messageText = errorFailure
messageComponents.append(nsError.localizedDescription)
}
}
}
else
{
alert.messageText = localizedFailure
if let debugDescription = nsError.localizedDebugDescription
{
messageComponents.append(debugDescription)
}
else
{
messageComponents.append(nsError.localizedDescription)
}
}
var messageComponents = [error.localizedDescription]
if let recoverySuggestion = nsError.localizedRecoverySuggestion
{
messageComponents.append(recoverySuggestion)
}
let informativeText = messageComponents.joined(separator: separator)
alert.informativeText = informativeText
let title = nsError.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
let message = messageComponents.joined(separator: "\n\n")
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = title
alert.informativeText = message
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.addButton(withTitle: NSLocalizedString("View More Details", comment: ""))
if let viewMoreButton = alert.buttons.last
{
viewMoreButton.target = self
viewMoreButton.action = #selector(AppDelegate.showDetailedErrorDescription)
self.errorAlert = alert
}
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
alert.runModal()
self.popoverController = nil
self.errorAlert = nil
self.popoverError = nil
}
@objc func showDetailedErrorDescription()
{
guard let errorAlert, let contentView = errorAlert.window.contentView else { return }
let errorDetailsViewController = NSStoryboard(name: "Main", bundle: .main).instantiateController(withIdentifier: "errorDetailsViewController") as! ErrorDetailsViewController
errorDetailsViewController.error = self.popoverError
let fittingSize = errorDetailsViewController.view.fittingSize
errorDetailsViewController.view.frame.size = fittingSize
let popoverController = NSPopover()
popoverController.contentViewController = errorDetailsViewController
popoverController.contentSize = fittingSize
popoverController.behavior = .transient
popoverController.show(relativeTo: contentView.bounds, of: contentView, preferredEdge: .maxX)
self.popoverController = popoverController
}
@objc func toggleLaunchAtLogin(_ item: NSMenuItem)

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -384,5 +384,59 @@
</objects>
<point key="canvasLocation" x="75" y="0.0"/>
</scene>
<!--Error Details View Controller-->
<scene sceneID="IpC-sd-lPu">
<objects>
<viewController storyboardIdentifier="errorDetailsViewController" id="vPa-1q-slD" customClass="ErrorDetailsViewController" customModule="AltServer" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="umL-zC-zP8">
<rect key="frame" x="0.0" y="0.0" width="450" height="88"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="16" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="aUr-m2-nXm">
<rect key="frame" x="20" y="20" width="410" height="48"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8GZ-nV-XXA">
<rect key="frame" x="-2" y="32" width="40" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" selectable="YES" title="Label" id="V5D-v5-MVX">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="j2o-2b-63k">
<rect key="frame" x="-2" y="0.0" width="92" height="16"/>
<textFieldCell key="cell" selectable="YES" title="Multiline Label" id="3jf-Z7-88l">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="aUr-m2-nXm" secondAttribute="trailing" constant="20" id="RnW-JC-q92"/>
<constraint firstAttribute="bottom" secondItem="aUr-m2-nXm" secondAttribute="bottom" constant="20" id="eGe-BS-TFJ"/>
<constraint firstItem="aUr-m2-nXm" firstAttribute="top" secondItem="umL-zC-zP8" secondAttribute="top" constant="20" id="fel-H8-WFO"/>
<constraint firstItem="aUr-m2-nXm" firstAttribute="leading" secondItem="umL-zC-zP8" secondAttribute="leading" constant="20" id="tcH-p8-4QH"/>
</constraints>
</view>
<connections>
<outlet property="detailedDescriptionLabel" destination="j2o-2b-63k" id="de3-yf-oTt"/>
<outlet property="errorCodeLabel" destination="8GZ-nV-XXA" id="cXZ-11-wrJ"/>
</connections>
</viewController>
<customObject id="wrv-m0-8zg" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="537" y="17"/>
</scene>
</scenes>
</document>

View File

@@ -108,9 +108,6 @@ char *bin2hex(const unsigned char *bin, size_t length)
- (void)_enableUnsignedCodeExecutionForProcessWithName:(nullable NSString *)processName pid:(int32_t)pid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
{
dispatch_async(self.connectionQueue, ^{
NSString *name = processName ?: NSLocalizedString(@"this app", @"");
NSString *localizedFailure = [NSString stringWithFormat:NSLocalizedString(@"JIT could not be enabled for %@.", comment: @""), name];
NSString *attachCommand = nil;
if (processName)
@@ -133,7 +130,6 @@ char *bin2hex(const unsigned char *bin, size_t length)
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
userInfo[ALTAppNameErrorKey] = processName;
userInfo[ALTDeviceNameErrorKey] = self.device.name;
userInfo[NSLocalizedFailureErrorKey] = localizedFailure;
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
return completionHandler(NO, returnError);
@@ -145,7 +141,6 @@ char *bin2hex(const unsigned char *bin, size_t length)
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
userInfo[ALTAppNameErrorKey] = processName;
userInfo[ALTDeviceNameErrorKey] = self.device.name;
userInfo[NSLocalizedFailureErrorKey] = localizedFailure;
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
return completionHandler(NO, returnError);
@@ -249,7 +244,11 @@ char *bin2hex(const unsigned char *bin, size_t length)
{
if (error)
{
*error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{NSLocalizedFailureReasonErrorKey: response}];
*error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
NSLocalizedFailureReasonErrorKey: response,
ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
ALTSourceLineErrorKey: @(__LINE__)
}];
}
return NO;
@@ -292,7 +291,11 @@ char *bin2hex(const unsigned char *bin, size_t length)
break;
default:
*error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{NSLocalizedFailureReasonErrorKey: response}];
*error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
NSLocalizedFailureReasonErrorKey: response,
ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
ALTSourceLineErrorKey: @(__LINE__)
}];
break;
}
}

View File

@@ -10,13 +10,14 @@ import Foundation
import AltSign
enum DeveloperDiskError: LocalizedError
typealias DeveloperDiskError = DeveloperDiskErrorCode.Error
enum DeveloperDiskErrorCode: Int, ALTErrorEnum
{
case unknownDownloadURL
case unsupportedOperatingSystem
case downloadedDiskNotFound
var errorDescription: String? {
var errorFailureReason: String {
switch self
{
case .unknownDownloadURL: return NSLocalizedString("The URL to download the Developer disk image could not be determined.", comment: "")
@@ -88,14 +89,14 @@ class DeveloperDiskManager
{
do
{
guard let osName = device.type.osName else { throw DeveloperDiskError.unsupportedOperatingSystem }
guard let osName = device.type.osName else { throw DeveloperDiskError(.unsupportedOperatingSystem) }
let osKeyPath: KeyPath<FetchURLsResponse.Disks, [String: DeveloperDiskURL]?>
switch device.type
{
case .iphone, .ipad: osKeyPath = \FetchURLsResponse.Disks.iOS
case .appletv: osKeyPath = \FetchURLsResponse.Disks.tvOS
default: throw DeveloperDiskError.unsupportedOperatingSystem
default: throw DeveloperDiskError(.unsupportedOperatingSystem)
}
var osVersion = device.osVersion
@@ -146,7 +147,7 @@ class DeveloperDiskManager
do
{
let developerDiskURLs = try result.get()
guard let diskURL = developerDiskURLs[keyPath: osKeyPath]?[osVersion.stringValue] else { throw DeveloperDiskError.unknownDownloadURL }
guard let diskURL = developerDiskURLs[keyPath: osKeyPath]?[osVersion.stringValue] else { throw DeveloperDiskError(.unknownDownloadURL) }
switch diskURL
{
@@ -201,7 +202,7 @@ private extension DeveloperDiskManager
{
guard let data = data else { throw error! }
let response = try JSONDecoder().decode(FetchURLsResponse.self, from: data)
let response = try Foundation.JSONDecoder().decode(FetchURLsResponse.self, from: data)
completionHandler(.success(response.disks))
}
catch
@@ -244,7 +245,7 @@ private extension DeveloperDiskManager
}
}
guard let diskFileURL = tempDiskFileURL, let signatureFileURL = tempSignatureFileURL else { throw DeveloperDiskError.downloadedDiskNotFound }
guard let diskFileURL = tempDiskFileURL, let signatureFileURL = tempSignatureFileURL else { throw DeveloperDiskError(.downloadedDiskNotFound) }
completionHandler(.success((diskFileURL, signatureFileURL)))
}
@@ -318,7 +319,7 @@ private extension DeveloperDiskManager
}
guard let diskFileURL = diskFileURL, let signatureFileURL = signatureFileURL else {
return completionHandler(.failure(downloadError ?? DeveloperDiskError.downloadedDiskNotFound))
return completionHandler(.failure(downloadError ?? DeveloperDiskError(.downloadedDiskNotFound)))
}
completionHandler(.success((diskFileURL, signatureFileURL)))

View File

@@ -14,34 +14,21 @@ private let appGroupsSemaphore = DispatchSemaphore(value: 1)
private let developerDiskManager = DeveloperDiskManager()
enum InstallError: Int, LocalizedError, _ObjectiveCBridgeableError
typealias OperationError = OperationErrorCode.Error
enum OperationErrorCode: Int, ALTErrorEnum
{
case cancelled
case noTeam
case missingPrivateKey
case missingCertificate
var errorDescription: String? {
var errorFailureReason: String {
switch self
{
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .noTeam: return "You are not a member of any developer teams."
case .missingPrivateKey: return "The developer certificate's private key could not be found."
case .missingCertificate: return "The developer certificate could not be found."
}
}
init?(_bridgedNSError error: NSError)
{
guard error.domain == InstallError.cancelled._domain else { return nil }
if let installError = InstallError(rawValue: error.code)
{
self = installError
}
else
{
return nil
case .noTeam: return NSLocalizedString("You are not a member of any developer teams.", comment: "")
case .missingPrivateKey: return NSLocalizedString("The developer certificate's private key could not be found.", comment: "")
case .missingCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
}
}
}
@@ -54,16 +41,18 @@ extension ALTDeviceManager
var appName = (url.isFileURL) ? url.deletingPathExtension().lastPathComponent : NSLocalizedString("AltStore", comment: "")
func finish(_ result: Result<ALTApplication, Error>, title: String = "")
func finish(_ result: Result<ALTApplication, Error>, failure: String? = nil)
{
DispatchQueue.main.async {
switch result
{
case .success(let app): completion(.success(app))
case .failure(var error as NSError):
if error.localizedFailure == nil
error = error.withLocalizedTitle(String(format: NSLocalizedString("%@ could not be installed onto %@.", comment: ""), appName, altDevice.name))
if let failure, error.localizedFailure == nil
{
error = error.withLocalizedFailure(String(format: NSLocalizedString("Could not install %@ to %@.", comment: ""), appName, altDevice.name))
error = error.withLocalizedFailure(failure)
}
completion(.failure(error))
@@ -144,24 +133,25 @@ extension ALTDeviceManager
let profiles = try result.get()
self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in
finish(result.map { application }, title: "Failed to Install AltStore")
finish(result.map { application })
}
}
catch
{
finish(.failure(error), title: "Failed to Fetch Provisioning Profiles")
finish(.failure(error), failure: NSLocalizedString("AltServer could not fetch new provisioning profiles.", comment: ""))
}
}
}
catch
{
finish(.failure(error), title: "Failed to Refresh Anisette Data")
finish(.failure(error))
}
}
}
catch
{
finish(.failure(error), title: "Failed to Download AltStore")
let failure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), appName)
finish(.failure(error), failure: failure)
}
}
}
@@ -169,31 +159,31 @@ extension ALTDeviceManager
}
catch
{
finish(.failure(error), title: "Failed to Fetch Certificate")
finish(.failure(error), failure: NSLocalizedString("A valid signing certificate could not be created.", comment: ""))
}
}
}
catch
{
finish(.failure(error), title: "Failed to Register Device")
finish(.failure(error), failure: NSLocalizedString("Your device could not be registered with your development team.", comment: ""))
}
}
}
catch
{
finish(.failure(error), title: "Failed to Fetch Team")
finish(.failure(error))
}
}
}
catch
{
finish(.failure(error), title: "Failed to Authenticate")
finish(.failure(error), failure: NSLocalizedString("AltServer could not sign in with your Apple ID.", comment: ""))
}
}
}
catch
{
finish(.failure(error), title: "Failed to Fetch Anisette Data")
finish(.failure(error))
}
}
}
@@ -306,7 +296,7 @@ private extension ALTDeviceManager
}
else
{
completionHandler(.failure(error ?? ALTAppleAPIError(.unknown)))
completionHandler(.failure(error ?? ALTAppleAPIError.unknown()))
}
}
}
@@ -332,7 +322,7 @@ private extension ALTDeviceManager
}
else
{
throw InstallError.noTeam
throw OperationError(.noTeam)
}
}
catch
@@ -384,7 +374,7 @@ private extension ALTDeviceManager
}
}
guard !isCancelled else { return completionHandler(.failure(InstallError.cancelled)) }
guard !isCancelled else { return completionHandler(.failure(OperationError(.cancelled))) }
}
func addCertificate()
@@ -393,7 +383,7 @@ private extension ALTDeviceManager
do
{
let certificate = try Result(certificate, error).get()
guard let privateKey = certificate.privateKey else { throw InstallError.missingPrivateKey }
guard let privateKey = certificate.privateKey else { throw OperationError(.missingPrivateKey) }
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
@@ -401,7 +391,7 @@ private extension ALTDeviceManager
let certificates = try Result(certificates, error).get()
guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
throw InstallError.missingCertificate
throw OperationError(.missingCertificate)
}
certificate.privateKey = privateKey
@@ -454,7 +444,7 @@ private extension ALTDeviceManager
}
}
guard !isCancelled else { return completionHandler(.failure(InstallError.cancelled)) }
guard !isCancelled else { return completionHandler(.failure(OperationError(.cancelled))) }
}
ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in

View File

@@ -1217,7 +1217,11 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
// ALTServerErrorUnderlyingError uses its underlying error's failure reason as its error description (if one exists),
// so assign our recovery suggestion to NSLocalizedFailureReasonErrorKey to make sure it's always displayed on client.
NSError *underlyingError = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{NSLocalizedFailureReasonErrorKey: recoverySuggestion}];
NSError *underlyingError = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
NSLocalizedFailureReasonErrorKey: recoverySuggestion,
ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
ALTSourceLineErrorKey: @(__LINE__)
}];
returnError = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnderlyingError userInfo:@{NSUnderlyingErrorKey: underlyingError}];
}

View File

@@ -0,0 +1,48 @@
//
// ErrorDetailsViewController.swift
// AltServer
//
// Created by Riley Testut on 10/4/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import AppKit
class ErrorDetailsViewController: NSViewController
{
var error: NSError? {
didSet {
self.update()
}
}
@IBOutlet private var errorCodeLabel: NSTextField!
@IBOutlet private var detailedDescriptionLabel: NSTextField!
override func viewDidLoad()
{
super.viewDidLoad()
self.detailedDescriptionLabel.preferredMaxLayoutWidth = 800
}
}
private extension ErrorDetailsViewController
{
func update()
{
if !self.isViewLoaded
{
self.loadView()
}
guard let error = self.error else { return }
self.errorCodeLabel.stringValue = error.localizedErrorCode
let font = self.detailedDescriptionLabel.font ?? NSFont.systemFont(ofSize: 12)
let detailedDescription = error.formattedDetailedDescription(with: font)
self.detailedDescriptionLabel.attributedStringValue = detailedDescription
}
}

View File

@@ -15,24 +15,71 @@ import STPrivilegedTask
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
enum PluginError: LocalizedError
extension PluginError
{
case cancelled
case unknown
case notFound
case mismatchedHash(hash: String, expectedHash: String)
case taskError(String)
case taskErrorCode(Int)
enum Code: Int, ALTErrorCode
{
typealias Error = PluginError
case cancelled
case unknown
case notFound
case mismatchedHash
case taskError
case taskErrorCode
}
var errorDescription: String? {
switch self
static let cancelled = PluginError(code: .cancelled)
static let notFound = PluginError(code: .notFound)
static func unknown(file: String = #fileID, line: UInt = #line) -> PluginError { PluginError(code: .unknown, sourceFile: file, sourceLine: line) }
static func mismatchedHash(hash: String, expectedHash: String) -> PluginError { PluginError(code: .mismatchedHash, hash: hash, expectedHash: expectedHash) }
static func taskError(output: String) -> PluginError { PluginError(code: .taskError, taskErrorOutput: output) }
static func taskErrorCode(_ code: Int) -> PluginError { PluginError(code: .taskErrorCode, taskErrorCode: code) }
}
struct PluginError: ALTLocalizedError
{
let code: Code
var errorTitle: String?
var errorFailure: String?
var sourceFile: String?
var sourceLine: UInt?
var hash: String?
var expectedHash: String?
var taskErrorOutput: String?
var taskErrorCode: Int?
var errorFailureReason: String {
switch self.code
{
case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "")
case .notFound: return NSLocalizedString("The Mail plug-in does not exist at the requested URL.", comment: "")
case .mismatchedHash(let hash, let expectedHash): return String(format: NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.\n\nHash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash)
case .taskError(let output): return output
case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode))
case .mismatchedHash:
let baseMessage = NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.", comment: "")
guard let hash = self.hash, let expectedHash = self.expectedHash else { return baseMessage }
let additionalInfo = String(format: NSLocalizedString("Hash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash)
return baseMessage + "\n\n" + additionalInfo
case .taskError:
if let output = self.taskErrorOutput
{
return output
}
// Use .taskErrorCode base message as fallback.
fallthrough
case .taskErrorCode:
let baseMessage = NSLocalizedString("There was an error installing the Mail plug-in.", comment: "")
guard let errorCode = self.taskErrorCode else { return baseMessage }
let additionalInfo = String(format: NSLocalizedString("(Error Code: %@)", comment: ""), NSNumber(value: errorCode))
return baseMessage + " " + additionalInfo
}
}
}
@@ -160,7 +207,7 @@ extension PluginManager
let unzippedPluginURL = temporaryDirectoryURL.appendingPathComponent(pluginURL.lastPathComponent)
try self.runAndKeepAuthorization("cp", arguments: ["-R", unzippedPluginURL.path, pluginDirectoryURL.path], authorization: authorization)
guard self.isMailPluginInstalled else { throw PluginError.unknown }
guard self.isMailPluginInstalled else { throw PluginError.unknown() }
// Enable Mail plug-in preferences.
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization)
@@ -360,13 +407,13 @@ private extension PluginManager
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
{
throw PluginError.taskError(outputString)
throw PluginError.taskError(output: outputString)
}
throw PluginError.taskErrorCode(Int(task.terminationStatus))
}
guard let authorization = task.authorization else { throw PluginError.unknown }
guard let authorization = task.authorization else { throw PluginError.unknown() }
return authorization
}
}