Merge branch 'logging'

This commit is contained in:
Riley Testut
2023-10-19 14:16:50 -05:00
26 changed files with 385 additions and 105 deletions

View File

@@ -355,6 +355,7 @@
D51AD27E29356B7B00967AAA /* ALTWrappedError.h in Headers */ = {isa = PBXBuildFile; fileRef = D51AD27C29356B7B00967AAA /* ALTWrappedError.h */; settings = {ATTRIBUTES = (Public, ); }; };
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; };
D51AD28029356B8000967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; };
D52A2F972ACB40F700BDF8E3 /* Logger+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */; };
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */; };
D52DD35E2AAA89A600A7F2B6 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D52DD35D2AAA89A600A7F2B6 /* AltSign-Dynamic */; };
D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52EF2BD2A0594550096C377 /* AppDetailCollectionViewController.swift */; };
@@ -956,6 +957,7 @@
D5189C002A01BC6800F44625 /* UserInfoValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoValue.swift; sourceTree = "<group>"; };
D51AD27C29356B7B00967AAA /* ALTWrappedError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWrappedError.h; sourceTree = "<group>"; };
D51AD27D29356B7B00967AAA /* ALTWrappedError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWrappedError.m; sourceTree = "<group>"; };
D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+AltStore.swift"; sourceTree = "<group>"; };
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = "<group>"; };
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = "<group>"; };
D52EF2BD2A0594550096C377 /* AppDetailCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailCollectionViewController.swift; sourceTree = "<group>"; };
@@ -1580,6 +1582,7 @@
BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */,
D5F48B4729CCF21B002B52A4 /* AltStore+Async.swift */,
D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */,
D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -2999,6 +3002,7 @@
BFAECC572501B0A400528F27 /* ConnectionManager.swift in Sources */,
BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */,
D519AD46292D665B004B12F9 /* Managed.swift in Sources */,
D52A2F972ACB40F700BDF8E3 /* Logger+AltStore.swift in Sources */,
BFC712C42512D5F100AB5EBE /* XPCConnection.swift in Sources */,
D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */,
D5708417292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift in Sources */,

View File

@@ -202,7 +202,11 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
{
guard !self.isFinished else { return }
print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
switch result
{
case .failure(let error): Logger.sideload.error("Failed to authenticate account. \(error.localizedDescription, privacy: .public)")
case .success((let team, _, _)): Logger.sideload.notice("Authenticated account for team \(team.identifier, privacy: .public).")
}
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.perform {
@@ -347,6 +351,8 @@ private extension AuthenticationOperation
if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
{
Logger.sideload.notice("Authenticating Apple ID...")
self.authenticate(appleID: appleID, password: password) { (result) in
switch result
{

View File

@@ -103,7 +103,7 @@ class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledA
}
self.managedObjectContext.perform {
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
Logger.sideload.notice("Refreshing apps in background: \(self.installedApps.map(\.bundleIdentifier), privacy: .public)")
self.startListeningForRunningApps()
@@ -114,7 +114,10 @@ class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledA
self.managedObjectContext.perform {
let filteredApps = self.installedApps.filter { !self.runningApplications.contains($0.bundleIdentifier) }
print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier })
if !self.runningApplications.isEmpty
{
Logger.sideload.notice("Skipping refreshing running apps: \(self.runningApplications, privacy: .public)")
}
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
group.beginInstallationHandler = { (installedApp) in
@@ -225,7 +228,7 @@ private extension BackgroundRefreshAppsOperation
}
catch
{
print("Failed to refresh apps in background.", error)
Logger.sideload.error("Failed to refresh apps in background. \(error.localizedDescription, privacy: .public)")
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription
@@ -269,7 +272,7 @@ private extension BackgroundRefreshAppsOperation
_ = RefreshAttempt(identifier: self.refreshIdentifier, result: result, context: context)
do { try context.save() }
catch { print("Failed to save refresh attempt.", error) }
catch { Logger.sideload.error("Failed to save refresh attempt. \(error.localizedDescription, privacy: .public)") }
}
}

View File

@@ -87,8 +87,8 @@ class BackupAppOperation: ResultOperation<Void>
// Failed too quickly for human to respond to alert, possibly still finalizing installation.
// Try again in a couple seconds.
print("Failed too quickly, retrying after a few seconds...")
Logger.sideload.error("Failed to open app too quickly, retrying after a few seconds...")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
UIApplication.shared.open(openURL, options: [:]) { (success) in
if success

View File

@@ -108,12 +108,12 @@ private extension ClearAppCacheOperation
{
do
{
print("[ALTLog] Removing item from temporary directory:", fileURL.lastPathComponent)
Logger.main.debug("Removing item from temporary directory: \(fileURL.lastPathComponent, privacy: .public)")
try FileManager.default.removeItem(at: fileURL)
}
catch
{
print("[ALTLog] Failed to remove \(fileURL.lastPathComponent) from temporary directory.", error)
Logger.main.error("Failed to remove \(fileURL.lastPathComponent) from temporary directory. \(error.localizedDescription, privacy: .public)")
errors.append(error)
}
}
@@ -171,13 +171,13 @@ private extension ClearAppCacheOperation
if isDirectory && !installedAppBundleIDs.contains(bundleID) && !AppManager.shared.isActivelyManagingApp(withBundleID: bundleID)
{
print("[ALTLog] Removing backup directory for uninstalled app:", bundleID)
Logger.main.debug("Removing backup directory for uninstalled app: \(bundleID, privacy: .public)")
try FileManager.default.removeItem(at: backupDirectory)
}
}
catch
{
print("[ALTLog] Failed to remove app backup directory:", error)
Logger.main.error("Failed to remove app backup directory. \(error.localizedDescription, privacy: .public)")
errors.append(error)
}
}
@@ -194,7 +194,7 @@ private extension ClearAppCacheOperation
}
catch
{
print("[ALTLog] Failed to remove app backup directory:", error)
Logger.main.error("Failed to remove app backup directory. \(error.localizedDescription, privacy: .public)")
completion(.failure(error))
}
}

View File

@@ -39,12 +39,14 @@ class DeactivateAppOperation: ResultOperation<InstalledApp>
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
Logger.sideload.notice("Deactivating app \(self.app.bundleIdentifier, privacy: .public)...")
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
print("Sending deactivate app request...")
Logger.sideload.notice("Sending deactivate app request...")
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
@@ -54,21 +56,28 @@ class DeactivateAppOperation: ResultOperation<InstalledApp>
let request = RemoveProvisioningProfilesRequest(udid: udid, bundleIdentifiers: Set(allIdentifiers))
connection.send(request) { (result) in
print("Sent deactive app request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .failure(let error):
Logger.sideload.error("Failed to send deactivate app request. \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
case .success:
print("Waiting for deactivate app response...")
Logger.sideload.debug("Waiting for deactivate app response...")
connection.receiveResponse() { (result) in
print("Receiving deactivate app response:", result)
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(.error(let response)): self.finish(.failure(response.error))
case .failure(let error):
Logger.sideload.error("Failed to receive deactivate app response. \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
case .success(.error(let response)):
Logger.sideload.error("Failed to deactivate app. \(response.error.localizedDescription, privacy: .public)")
self.finish(.failure(response.error))
case .success(.removeProvisioningProfiles):
Logger.sideload.notice("Successfully deactivated app \(self.app.bundleIdentifier, privacy: .public)!")
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
self.progress.completedUnitCount += 1

View File

@@ -50,7 +50,7 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
return
}
print("Downloading App:", self.bundleIdentifier)
Logger.sideload.notice("Downloading app \(self.bundleIdentifier, privacy: .public)...")
// Set _after_ checking self.context.error to prevent overwriting localized failure for previous errors.
self.localizedFailure = String(format: NSLocalizedString("%@ could not be downloaded.", comment: ""), self.appName)
@@ -108,7 +108,7 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
}
catch
{
print("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory).", error)
Logger.sideload.error("Failed to remove DownloadAppOperation temporary directory: \(self.temporaryDirectory, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
super.finish(result)
@@ -172,6 +172,9 @@ private extension DownloadAppOperation
try FileManager.default.copyItem(at: application.fileURL, to: self.destinationURL, shouldReplace: true)
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
Logger.sideload.notice("Downloaded app \(copiedApplication.bundleIdentifier, privacy: .public) from \(sourceURL, privacy: .public)")
self.finish(.success(copiedApplication))
self.progress.completedUnitCount += 1

View File

@@ -43,6 +43,8 @@ class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
guard let server = self.context.server, let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
Logger.altjit.notice("Enabling JIT for app \(installedApp.bundleIdentifier, privacy: .public)...")
installedApp.managedObjectContext?.perform {
guard let bundle = Bundle(url: installedApp.fileURL),
let processName = bundle.executableURL?.lastPathComponent
@@ -56,7 +58,7 @@ class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
print("Sending enable JIT request...")
Logger.altjit.debug("Sending enable JIT request...")
DispatchQueue.main.async {
@@ -69,22 +71,31 @@ class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
let result = Future<Result<Void, Error>, Never> { promise in
let request = EnableUnsignedCodeExecutionRequest(udid: udid, processName: processName)
connection.send(request) { result in
print("Sent enable JIT request!")
Logger.altjit.debug("Sent enable JIT request!")
switch result
{
case .failure(let error): promise(.success(.failure(error)))
case .success:
print("Waiting for enable JIT response...")
Logger.altjit.debug("Waiting for enable JIT response...")
connection.receiveResponse() { result in
print("Received enable JIT response:", result)
switch result
{
case .failure(let error): promise(.success(.failure(error)))
case .success(.error(let response)): promise(.success(.failure(response.error)))
case .success(.enableUnsignedCodeExecution): promise(.success(.success(())))
case .success: promise(.success(.failure(ALTServerError(.unknownResponse))))
case .failure(let error):
Logger.altjit.error("Failed to receive enable JIT response. \(error.localizedDescription, privacy: .public)")
promise(.success(.failure(error)))
case .success(.error(let response)):
Logger.altjit.error("Failed to enable JIT. \(response.error.localizedDescription, privacy: .public)")
promise(.success(.failure(response.error)))
case .success(.enableUnsignedCodeExecution):
Logger.altjit.notice("Successfully enabled JIT!")
promise(.success(.success(())))
case .success:
Logger.altjit.notice("Received unknown enable JIT response.")
promise(.success(.failure(ALTServerError(.unknownResponse))))
}
}
}

View File

@@ -34,32 +34,42 @@ class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
Logger.sideload.notice("Fetching anisette data...")
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error):
self.finish(.failure(error))
case .success(let connection):
print("Sending anisette data request...")
Logger.sideload.debug("Sending anisette data request...")
let request = AnisetteDataRequest()
connection.send(request) { (result) in
print("Sent anisette data request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .failure(let error):
Logger.sideload.error("Failed to send anisette data request. \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
case .success:
print("Waiting for anisette data...")
Logger.sideload.debug("Waiting for anisette data...")
connection.receiveResponse() { (result) in
print("Receiving anisette data:", result.error?.localizedDescription ?? "success")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(.error(let response)): self.finish(.failure(response.error))
case .success(.anisetteData(let response)): self.finish(.success(response.anisetteData))
case .success: self.finish(.failure(ALTServerError(.unknownRequest)))
case .failure(let error):
Logger.sideload.error("Failed to receive anisette data response. \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
case .success(.error(let response)):
Logger.sideload.error("Failed to receive anisette data. \(response.error.localizedDescription, privacy: .public)")
self.finish(.failure(response.error))
case .success(.anisetteData(let response)):
Logger.sideload.info("Successfully received anisette data!")
self.finish(.success(response.anisetteData))
case .success: self.finish(.failure(ALTServerError(.unknownResponse)))
}
}
}

View File

@@ -47,6 +47,8 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
Logger.sideload.notice("Fetching provisioning profiles for app \(self.context.bundleIdentifier, privacy: .public)...")
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
@@ -245,6 +247,8 @@ extension FetchProvisioningProfilesOperation
if let appID = appIDs.first(where: { $0.bundleIdentifier.lowercased() == bundleIdentifier.lowercased() })
{
Logger.sideload.notice("Using existing App ID \(appID.bundleIdentifier, privacy: .public)")
completionHandler(.success(appID))
}
else
@@ -275,6 +279,9 @@ extension FetchProvisioningProfilesOperation
do
{
let appID = try Result(appID, error).get()
Logger.sideload.notice("Registered new App ID \(appID.bundleIdentifier, privacy: .public)")
completionHandler(.success(appID))
}
catch ALTAppleAPIError.maximumAppIDLimitReached
@@ -358,8 +365,15 @@ extension FetchProvisioningProfilesOperation
let appID = appID.copy() as! ALTAppID
appID.features = features
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error))
ALTAppleAPI.shared.update(appID, team: team, session: session) { (updatedAppID, error) in
let result = Result(updatedAppID, error)
switch result
{
case .success(let appID): Logger.sideload.notice("Updated features for App ID \(appID.bundleIdentifier, privacy: .public).")
case .failure(let error): Logger.sideload.error("Failed to update features for App ID \(appID.bundleIdentifier, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
completionHandler(result)
}
}
else
@@ -377,6 +391,7 @@ extension FetchProvisioningProfilesOperation
}
guard var applicationGroups = entitlements[.appGroups] as? [String], !applicationGroups.isEmpty else {
Logger.sideload.notice("App ID \(appID.bundleIdentifier, privacy: .public) has no app groups, skipping assignment.")
// Assigning an App ID to an empty app group array fails,
// so just do nothing if there are no app groups.
return completionHandler(.success(appID))
@@ -414,7 +429,10 @@ extension FetchProvisioningProfilesOperation
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error)
{
case .failure(let error): finish(.failure(error))
case .failure(let error):
Logger.sideload.error("Failed to fetch app groups for team \(team.identifier, privacy: .public). \(error.localizedDescription, privacy: .public)")
finish(.failure(error))
case .success(let fetchedGroups):
let dispatchGroup = DispatchGroup()
@@ -439,8 +457,13 @@ extension FetchProvisioningProfilesOperation
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
switch Result(group, error)
{
case .success(let group): groups.append(group)
case .failure(let error): errors.append(error)
case .success(let group):
Logger.sideload.notice("Created new App Group \(group.groupIdentifier, privacy: .public).")
groups.append(group)
case .failure(let error):
Logger.sideload.notice("Failed to create new App Group \(adjustedGroupIdentifier, privacy: .public). \(error.localizedDescription, privacy: .public)")
errors.append(error)
}
dispatchGroup.leave()
@@ -456,8 +479,17 @@ extension FetchProvisioningProfilesOperation
else
{
ALTAppleAPI.shared.assign(appID, to: Array(groups), team: team, session: session) { (success, error) in
let result = Result(success, error)
finish(result.map { _ in appID })
let groupIDs = groups.map { $0.groupIdentifier }
switch Result(success, error)
{
case .success:
Logger.sideload.notice("Assigned App ID \(appID.bundleIdentifier, privacy: .public) to App Groups \(groupIDs.description, privacy: .public).")
finish(.success(appID))
case .failure(let error):
Logger.sideload.error("Failed to assign App ID \(appID.bundleIdentifier, privacy: .public) to App Groups \(groupIDs.description, privacy: .public). \(error.localizedDescription, privacy: .public)")
finish(.failure(error))
}
}
}
}
@@ -484,6 +516,8 @@ extension FetchProvisioningProfilesOperation
completionHandler(.success(profile))
case .success:
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
// Fetch new provisioning profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))

View File

@@ -48,6 +48,8 @@ class FindServerOperation: ResultOperation<Server>
return
}
Logger.sideload.notice("Discovering AltServers...")
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
@@ -63,22 +65,30 @@ class FindServerOperation: ResultOperation<Server>
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
if let machServiceName = self.localServerMachServiceName
{
Logger.sideload.notice("Found AltDaemon!")
// Prefer background daemon, if it exists and is running.
let server = Server(connectionType: .local, machServiceName: machServiceName)
self.finish(.success(server))
}
else if self.isWiredServerConnectionAvailable
{
Logger.sideload.notice("Found AltServer connected via USB!")
let server = Server(connectionType: .wired)
self.finish(.success(server))
}
else if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred })
{
Logger.sideload.notice("Found preferred AltServer! \(server.localizedName ?? "nil", privacy: .public)")
// Preferred server.
self.finish(.success(server))
}
else if let server = ServerManager.shared.discoveredServers.first
{
Logger.sideload.notice("Found AltServer! \(server.localizedName ?? "nil", privacy: .public)")
// Any available server.
self.finish(.success(server))
}
@@ -113,7 +123,7 @@ fileprivate extension FindServerOperation
connection.connect { (result) in
switch result
{
case .failure(let error): print("Could not connect to AltDaemon XPC service \(machServiceName).", error)
case .failure(let error): Logger.sideload.notice("Could not connect to AltDaemon XPC service \(machServiceName, privacy: .public). \(error.localizedDescription, privacy: .public)")
case .success: self.localServerMachServiceName = machServiceName
}
}

View File

@@ -45,6 +45,8 @@ class InstallAppOperation: ResultOperation<InstalledApp>
let connection = self.context.installationConnection
else { return self.finish(.failure(OperationError.invalidParameters)) }
Logger.sideload.notice("Installing resigned app \(resignedApp.bundleIdentifier, privacy: .public)...")
@Managed var appVersion = self.context.appVersion
let storeBuildVersion = $appVersion.buildVersion
@@ -152,23 +154,32 @@ class InstallAppOperation: ResultOperation<InstalledApp>
})
}
let request = BeginInstallationRequest(activeProfiles: activeProfiles, bundleIdentifier: installedApp.resignedBundleIdentifier)
let resignedBundleID = installedApp.resignedBundleIdentifier
let request = BeginInstallationRequest(activeProfiles: activeProfiles, bundleIdentifier: resignedBundleID)
connection.send(request) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .failure(let error):
Logger.sideload.notice("Failed to send begin installation request for resigned app \(resignedBundleID, privacy: .public). \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
case .success:
Logger.sideload.notice("Sent begin installation request for resigned app \(resignedBundleID, privacy: .public).")
self.receive(from: connection) { (result) in
switch result
{
case .success:
backgroundContext.perform {
Logger.sideload.notice("Successfully installed resigned app \(resignedBundleID, privacy: .public)!")
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
}
case .failure(let error):
Logger.sideload.notice("Failed to install resigned app \(resignedBundleID, privacy: .public). \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
}
}
@@ -192,7 +203,7 @@ class InstallAppOperation: ResultOperation<InstalledApp>
}
catch
{
print("Failed to remove refreshed .ipa:", error)
Logger.sideload.error("Failed to remove refreshed .ipa: \(error.localizedDescription, privacy: .public)")
}
}
@@ -208,11 +219,12 @@ private extension InstallAppOperation
do
{
let response = try result.get()
print(response)
switch response
{
case .installationProgress(let response):
Logger.sideload.debug("Installing \(self.context.resignedApp?.bundleIdentifier ?? self.context.bundleIdentifier, privacy: .public)... \((response.progress * 100).rounded())%")
if response.progress == 1.0
{
self.progress.completedUnitCount = self.progress.totalUnitCount
@@ -249,7 +261,7 @@ private extension InstallAppOperation
}
catch
{
print("Failed to remove temporary directory.", error)
Logger.sideload.error("Failed to remove temporary directory: \(error.localizedDescription, privacy: .public)")
}
}
}

View File

@@ -198,7 +198,7 @@ private extension PatchAppOperation
}
}
print("Downloaded OTA archive.")
Logger.fugu14.notice("Downloaded iOS OTA archive.")
return archiveURL
#endif
@@ -228,7 +228,7 @@ private extension PatchAppOperation
return .ok
}
print("Extracted Spotlight from OTA archive.")
Logger.fugu14.notice("Extracted Spotlight from OTA archive.")
return spotlightFileURL
#endif
@@ -250,7 +250,7 @@ private extension PatchAppOperation
let appBinaryURL = temporaryAppURL.appendingPathComponent(appName, isDirectory: false)
try self.appPatcher.patchAppBinary(at: appBinaryURL, withBinaryAt: patchFileURL)
print("Patched \(app.name).")
Logger.fugu14.notice("Patched \(app.name, privacy: .public)!")
return temporaryAppURL
}
.mapError { ($0 as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), app.name)) }

View File

@@ -80,7 +80,7 @@ class PatchViewController: UIViewController
}
catch
{
print("Failed to create temporary directory:", error)
Logger.fugu14.error("Failed to create temporary directory \(self.temporaryDirectory.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
self.update()
@@ -196,7 +196,7 @@ private extension PatchViewController
}
catch
{
print("Failed to remove temporary directory:", error)
Logger.fugu14.error("Failed to remove temporary directory \(self.temporaryDirectory.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
if let observation = self.didEnterBackgroundObservation
@@ -312,7 +312,7 @@ private extension PatchViewController
}
catch
{
print("Error unzipping app bundle:", error)
Logger.fugu14.error("Error unzipping app bundle: \(error.localizedDescription, privacy: .public)")
unzippingError = error
}
}

View File

@@ -44,13 +44,15 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
guard let app = self.context.app else { throw OperationError.appNotFound(name: nil) }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
Logger.sideload.notice("Refreshing provisioning profiles for app \(self.context.bundleIdentifier, privacy: .public)...")
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
print("Sending refresh app request...")
Logger.sideload.debug("Sending refresh app request...")
var activeProfiles: Set<String>?
if UserDefaults.standard.activeAppsLimit != nil
@@ -65,20 +67,24 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
let request = InstallProvisioningProfilesRequest(udid: udid, provisioningProfiles: Set(profiles.values), activeProfiles: activeProfiles)
connection.send(request) { (result) in
print("Sent refresh app request!")
Logger.sideload.debug("Sent refresh app request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
print("Waiting for refresh app response...")
Logger.sideload.debug("Waiting for refresh app response...")
connection.receiveResponse() { (result) in
print("Receiving refresh app response:", result)
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(.error(let response)): self.finish(.failure(response.error))
case .failure(let error):
Logger.sideload.error("Failed to receive refresh app response. \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
case .success(.error(let response)):
Logger.sideload.debug("Failed to refresh app \(self.context.bundleIdentifier, privacy: .public). \(response.error.localizedDescription, privacy: .public)")
self.finish(.failure(response.error))
case .success(.installProvisioningProfiles):
self.managedObjectContext.perform {
@@ -100,10 +106,13 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
installedExtension.update(provisioningProfile: provisioningProfile)
}
Logger.sideload.notice("Refreshed provisioning profiles for app \(self.context.bundleIdentifier, privacy: .public)")
self.finish(.success(installedApp))
}
case .success: self.finish(.failure(ALTServerError(.unknownRequest)))
case .success:
Logger.sideload.notice("Received unknown refresh app response for app \(self.context.bundleIdentifier, privacy: .public)")
self.finish(.failure(ALTServerError(.unknownResponse)))
}
}
}

View File

@@ -8,6 +8,8 @@
import Foundation
import AltStoreCore
@objc(RemoveAppBackupOperation)
class RemoveAppBackupOperation: ResultOperation<Void>
{
@@ -36,6 +38,9 @@ class RemoveAppBackupOperation: ResultOperation<Void>
}
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
Logger.sideload.notice("Removing backup for app \(self.context.bundleIdentifier, privacy: .public)...")
installedApp.managedObjectContext?.perform {
guard let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return self.finish(.failure(OperationError.missingAppGroup)) }
@@ -61,14 +66,14 @@ class RemoveAppBackupOperation: ResultOperation<Void>
#else
print("Failed to remove app backup directory:", error)
Logger.sideload.error("Failed to remove app backup directory \(backupDirectoryURL.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
#endif
}
catch
{
print("Failed to remove app backup directory:", error)
Logger.sideload.error("Failed to remove app backup directory \(backupDirectoryURL.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
}
}

View File

@@ -35,6 +35,8 @@ class RemoveAppOperation: ResultOperation<InstalledApp>
guard let server = self.context.server, let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
Logger.sideload.notice("Removing app \(self.context.bundleIdentifier, privacy: .public)...")
installedApp.managedObjectContext?.perform {
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
@@ -43,19 +45,24 @@ class RemoveAppOperation: ResultOperation<InstalledApp>
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
print("Sending remove app request...")
Logger.sideload.debug("Sending remove app request...")
let request = RemoveAppRequest(udid: udid, bundleIdentifier: resignedBundleIdentifier)
connection.send(request) { (result) in
print("Sent remove app request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .failure(let error):
Logger.sideload.error("Failed to send remove app request. \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
case .success:
print("Waiting for remove app response...")
Logger.sideload.debug("Waiting for remove app response...")
connection.receiveResponse() { (result) in
print("Receiving remove app response:", result)
switch result
{
case .failure(let error): Logger.sideload.error("Failed to remove app. \(error.localizedDescription, privacy: .public)")
case .success: Logger.sideload.info("Successfully removed app \(self.context.bundleIdentifier, privacy: .public)!")
}
switch result
{

View File

@@ -43,6 +43,8 @@ class ResignAppOperation: ResultOperation<ALTApplication>
let certificate = self.context.certificate
else { return self.finish(.failure(OperationError.invalidParameters)) }
Logger.sideload.notice("Resigning app \(self.context.bundleIdentifier, privacy: .public)...")
// Prepare app bundle
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
@@ -50,8 +52,6 @@ class ResignAppOperation: ResultOperation<ALTApplication>
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
guard let appBundleURL = self.process(result) else { return }
print("Resigning App:", self.context.bundleIdentifier)
// Resign app bundle
let resignProgress = self.resignAppBundle(at: appBundleURL, team: team, certificate: certificate, profiles: Array(profiles.values)) { (result) in
guard let resignedURL = self.process(result) else { return }
@@ -64,6 +64,9 @@ class ResignAppOperation: ResultOperation<ALTApplication>
// Use appBundleURL since we need an app bundle, not .ipa.
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
Logger.sideload.notice("Resigned app \(self.context.bundleIdentifier, privacy: .public) to \(resignedApplication.bundleIdentifier, privacy: .public).")
self.finish(.success(resignedApplication))
}
catch

View File

@@ -41,6 +41,8 @@ class SendAppOperation: ResultOperation<ServerConnection>
guard let resignedApp = self.context.resignedApp, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
Logger.sideload.notice("Sending app \(self.context.bundleIdentifier, privacy: .public) to AltServer \(server.localizedName ?? "nil", privacy: .public)...")
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
let fileURL = InstalledApp.refreshedIPAURL(for: app)
@@ -99,17 +101,17 @@ private extension SendAppOperation
}
else
{
print("Sending app data (\(appData.count) bytes)...")
Logger.sideload.debug("Sending app data (\(appData.count) bytes)...")
connection.send(appData, prependSize: false) { (result) in
switch result
{
case .failure(let error):
print("Failed to send app data (\(appData.count) bytes)")
Logger.sideload.error("Failed to send app to AltServer \(connection.server.localizedName ?? "nil", privacy: .public). \(error.localizedDescription, privacy: .public)")
completionHandler(.failure(error))
case .success:
print("Successfully sent app data (\(appData.count) bytes)")
Logger.sideload.notice("Finished sending app to AltServer \(connection.server.localizedName ?? "nil", privacy: .public)!")
completionHandler(.success(()))
}
}

View File

@@ -87,10 +87,11 @@ class UpdatePatronsOperation: ResultOperation<Void>
self.finish(.success(()))
print("Updated Friend Zone Patrons!")
Logger.main.notice("Updated Friend Zone Patrons! Refresh ID: \(response.refreshID, privacy: .public)")
}
catch
{
Logger.main.error("Failed to update Friend Zone Patrons. \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
}
}

View File

@@ -63,6 +63,8 @@ class VerifyAppOperation: ResultOperation<Void>
guard let app = self.context.app else { throw OperationError.invalidParameters }
Logger.sideload.notice("Verifying app \(self.context.bundleIdentifier, privacy: .public)...")
guard app.bundleIdentifier == self.context.bundleIdentifier else {
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
}
@@ -149,9 +151,9 @@ private extension VerifyAppOperation
let data = try Data(contentsOf: ipaURL)
let sha256Hash = SHA256.hash(data: data)
let hashString = sha256Hash.compactMap { String(format: "%02x", $0) }.joined()
print("[ALTLog] Comparing app hash (\(hashString)) against expected hash (\(expectedHash))...")
Logger.sideload.debug("Comparing app hash (\(hashString, privacy: .public)) against expected hash (\(expectedHash, privacy: .public))...")
guard hashString == expectedHash else { throw VerificationError.mismatchedHash(hashString, expectedHash: expectedHash, app: app) }
}

View File

@@ -31,6 +31,10 @@ struct Server: Equatable
extension Server
{
var localizedName: String? {
return self.service?.name ?? self.identifier
}
// Defined in extension so we can still use the automatically synthesized initializer.
init?(service: NetService, txtData: Data)
{

View File

@@ -80,7 +80,7 @@ extension ServerManager
case .wired:
guard let incomingConnectionsSemaphore = self.incomingConnectionsSemaphore else { return finish(.failure(ALTServerError(.connectionFailed))) }
print("Waiting for incoming connection...")
Logger.sideload.debug("Waiting for incoming connection...")
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
@@ -104,7 +104,7 @@ extension ServerManager
case .wireless:
guard let service = server.service else { return finish(.failure(ALTServerError(.connectionFailed))) }
print("Connecting to service:", service)
Logger.sideload.debug("Connecting to AltServer: \(service.name, privacy: .public)")
let connection = NWConnection(to: .service(name: service.name, type: service.type, domain: service.domain, interface: nil), using: .tcp)
self.connectToRemoteServer(server, connection: connection, completion: finish(_:))
@@ -166,17 +166,32 @@ private extension ServerManager
func connectToRemoteServer(_ server: Server, connection: NWConnection, completion: @escaping (Result<Connection, Error>) -> Void)
{
let serverName: String
if let localizedName = server.localizedName
{
serverName = String(format: NSLocalizedString("remote AltServer %@", comment: ""), localizedName)
}
else if server.connectionType == .wired
{
serverName = NSLocalizedString("wired AltServer", comment: "")
}
else
{
serverName = NSLocalizedString("AltServer", comment: "")
}
connection.stateUpdateHandler = { [unowned connection] (state) in
switch state
{
case .failed(let error):
print("Failed to connect to service \(server.service?.name ?? "").", error)
Logger.sideload.error("Failed to connect to \(serverName, privacy: .public). \(error.localizedDescription, privacy: .public)")
completion(.failure(OperationError.connectionFailed))
case .cancelled:
completion(.failure(OperationError.cancelled))
case .ready:
Logger.sideload.notice("Connected to \(serverName, privacy: .public)!")
let connection = NetworkConnection(connection)
completion(.success(connection))
@@ -201,10 +216,12 @@ private extension ServerManager
switch result
{
case .failure(let error):
print("Could not connect to AltDaemon XPC service \(machServiceName).", error)
Logger.sideload.error("Could not connect to AltDaemon XPC service \(machServiceName, privacy: .public). \(error.localizedDescription, privacy: .public)")
completion(.failure(error))
case .success: completion(.success(connection))
case .success:
Logger.sideload.notice("Connected to AltDaemon XPC service \(machServiceName, privacy: .public)!")
completion(.success(connection))
}
}
}
@@ -214,17 +231,17 @@ extension ServerManager: NetServiceBrowserDelegate
{
func netServiceBrowserWillSearch(_ browser: NetServiceBrowser)
{
print("Discovering servers...")
Logger.main.notice("Discovering AltServers...")
}
func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser)
{
print("Stopped discovering servers.")
Logger.main.notice("Stopped discovering AltServers.")
}
func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber])
{
print("Failed to discovering servers.", errorDict)
Logger.main.error("Failed to discover AltServers. \(errorDict, privacy: .public)")
}
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool)
@@ -263,12 +280,12 @@ extension ServerManager: NetServiceDelegate
func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber])
{
print("Error resolving net service \(sender).", errorDict)
Logger.main.error("Failed to resolve Bonjour service \(sender.name, privacy: .public). \(errorDict, privacy: .public)")
}
func netService(_ sender: NetService, didUpdateTXTRecord data: Data)
{
let txtDict = NetService.dictionary(fromTXTRecord: data)
print("Service \(sender) updated TXT Record:", txtDict)
Logger.main.debug("Bonjour service \(sender.name, privacy: .public) updated TXT Record: \(txtDict, privacy: .public)")
}
}

View File

@@ -33,6 +33,9 @@ class ErrorLogViewController: UITableViewController
return dateFormatter
}()
@IBOutlet private var exportLogButton: UIBarButtonItem!
@IBOutlet private var clearLogButton: UIBarButtonItem!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
@@ -43,6 +46,14 @@ class ErrorLogViewController: UITableViewController
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
self.exportLogButton.activityIndicatorView.color = .white
if #unavailable(iOS 15)
{
// Assign just clearLogButton to hide export button.
self.navigationItem.rightBarButtonItems = [self.clearLogButton]
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
@@ -260,6 +271,77 @@ private extension ErrorLogViewController
{
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
}
@available(iOS 15, *)
@IBAction func exportDetailedLog(_ sender: UIBarButtonItem)
{
self.exportLogButton.isIndicatingActivity = true
Task<Void, Never>.detached(priority: .userInitiated) {
do
{
let store = try OSLogStore(scope: .currentProcessIdentifier)
// All logs since the app launched.
let position = store.position(timeIntervalSinceLatestBoot: 0)
let entries = try store.getEntries(at: position)
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem.contains(Logger.altstoreSubsystem) }
.map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
let outputText = entries.joined(separator: "\n")
let outputDirectory = FileManager.default.uniqueTemporaryURL()
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
defer {
do
{
try FileManager.default.removeItem(at: outputDirectory)
}
catch
{
Logger.main.error("Failed to remove temporary log directory \(outputDirectory.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
}
let outputURL = outputDirectory.appendingPathComponent("altlog.txt")
try outputText.write(to: outputURL, atomically: true, encoding: .utf8)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
Task<Void, Never> { @MainActor in
let activityViewController = UIActivityViewController(activityItems: [outputURL], applicationActivities: nil)
activityViewController.completionWithItemsHandler = { (activityType, completed, _, error) in
if let error
{
continuation.resume(throwing: error)
}
else
{
continuation.resume()
}
}
self.present(activityViewController, animated: true)
}
}
}
catch
{
Logger.main.error("Failed to export OSLog entries. \(error.localizedDescription, privacy: .public)")
await MainActor.run {
let alertController = UIAlertController(title: NSLocalizedString("Unable to Export Detailed Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
alertController.addAction(.ok)
self.present(alertController, animated: true)
}
}
await MainActor.run {
self.exportLogButton.isIndicatingActivity = false
}
}
}
}
extension ErrorLogViewController

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -1095,13 +1095,22 @@ Settings by i cons from the Noun Project</string>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Error Log" largeTitleDisplayMode="never" id="a1p-3W-bSi">
<barButtonItem key="rightBarButtonItem" systemItem="trash" id="BnQ-Eh-1gC">
<connections>
<action selector="clearLoggedErrors:" destination="g8a-Rf-zWa" id="faq-89-H5j"/>
</connections>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem systemItem="trash" id="BnQ-Eh-1gC">
<connections>
<action selector="clearLoggedErrors:" destination="g8a-Rf-zWa" id="faq-89-H5j"/>
</connections>
</barButtonItem>
<barButtonItem systemItem="action" id="BNj-HE-KHr">
<connections>
<action selector="exportDetailedLog:" destination="g8a-Rf-zWa" id="Kbw-Q5-9WO"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="clearLogButton" destination="BnQ-Eh-1gC" id="MHl-Kh-ick"/>
<outlet property="exportLogButton" destination="BNj-HE-KHr" id="w9g-yA-0Yx"/>
<segue destination="7gm-d1-zWK" kind="presentation" identifier="showErrorDetails" id="9vz-y6-evp"/>
</connections>
</tableViewController>

View File

@@ -0,0 +1,37 @@
//
// Logger+AltStore.swift
// AltStoreCore
//
// Created by Riley Testut on 10/2/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
@_exported import OSLog
public extension Logger
{
static let altstoreSubsystem = Bundle.main.bundleIdentifier!
static let main = Logger(subsystem: altstoreSubsystem, category: "Main")
static let sideload = Logger(subsystem: altstoreSubsystem, category: "Sideload")
static let altjit = Logger(subsystem: altstoreSubsystem, category: "AltJIT")
static let fugu14 = Logger(subsystem: altstoreSubsystem, category: "Fugu14")
}
@available(iOS 15, *)
public extension OSLogEntryLog.Level
{
var localizedName: String {
switch self
{
case .undefined: return NSLocalizedString("Undefined", comment: "")
case .debug: return NSLocalizedString("Debug", comment: "")
case .info: return NSLocalizedString("Info", comment: "")
case .notice: return NSLocalizedString("Notice", comment: "")
case .error: return NSLocalizedString("Error", comment: "")
case .fault: return NSLocalizedString("Fault", comment: "")
@unknown default: return NSLocalizedString("Unknown", comment: "")
}
}
}