From bb217e5c4942ad599a56eea2de6cbb785f368015 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 10 Aug 2021 11:34:11 +0200 Subject: [PATCH] iOS: AltKit source --- shell/apple/emulator-ios/AltKit/.gitignore | 5 + .../apple/emulator-ios/AltKit/CMakeLists.txt | 41 +++ shell/apple/emulator-ios/AltKit/Package.swift | 31 ++ shell/apple/emulator-ios/AltKit/README.md | 84 +++++ .../ALTServerError+Conveniences.swift | 38 ++ .../Extensions/Result+Conveniences.swift | 76 ++++ .../Sources/AltKit/Server/Connection.swift | 18 + .../AltKit/Server/NetworkConnection.swift | 62 ++++ .../AltKit/Sources/AltKit/Server/Server.swift | 44 +++ .../AltKit/Server/ServerConnection.swift | 180 +++++++++ .../Sources/AltKit/Server/ServerManager.swift | 341 ++++++++++++++++++ .../AltKit/Server/ServerProtocol.swift | 163 +++++++++ .../AltKit/Types/CodableServerError.swift | 126 +++++++ .../Sources/CAltKit/NSError+ALTServerError.h | 69 ++++ .../Sources/CAltKit/NSError+ALTServerError.m | 305 ++++++++++++++++ .../CAltKit/include/NSError+ALTServerError.h | 69 ++++ .../AltKit/Sources/CAltKit/module.modulemap | 4 + 17 files changed, 1656 insertions(+) create mode 100644 shell/apple/emulator-ios/AltKit/.gitignore create mode 100644 shell/apple/emulator-ios/AltKit/CMakeLists.txt create mode 100644 shell/apple/emulator-ios/AltKit/Package.swift create mode 100644 shell/apple/emulator-ios/AltKit/README.md create mode 100644 shell/apple/emulator-ios/AltKit/Sources/AltKit/Extensions/ALTServerError+Conveniences.swift create mode 100644 shell/apple/emulator-ios/AltKit/Sources/AltKit/Extensions/Result+Conveniences.swift create mode 100644 shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/Connection.swift create mode 100644 shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/NetworkConnection.swift create mode 100644 shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/Server.swift create mode 100644 shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerConnection.swift create mode 100644 shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerManager.swift create mode 100644 shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerProtocol.swift create mode 100644 shell/apple/emulator-ios/AltKit/Sources/AltKit/Types/CodableServerError.swift create mode 100644 shell/apple/emulator-ios/AltKit/Sources/CAltKit/NSError+ALTServerError.h create mode 100644 shell/apple/emulator-ios/AltKit/Sources/CAltKit/NSError+ALTServerError.m create mode 100644 shell/apple/emulator-ios/AltKit/Sources/CAltKit/include/NSError+ALTServerError.h create mode 100644 shell/apple/emulator-ios/AltKit/Sources/CAltKit/module.modulemap diff --git a/shell/apple/emulator-ios/AltKit/.gitignore b/shell/apple/emulator-ios/AltKit/.gitignore new file mode 100644 index 000000000..95c432091 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/shell/apple/emulator-ios/AltKit/CMakeLists.txt b/shell/apple/emulator-ios/AltKit/CMakeLists.txt new file mode 100644 index 000000000..c7d8e0374 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.15.1) + +project(AltKit LANGUAGES C Swift) + +add_library(CAltKit + Sources/CAltKit/NSError+ALTServerError.h + Sources/CAltKit/NSError+ALTServerError.m +) + +add_library(AltKit + Sources/AltKit/Extensions/ALTServerError+Conveniences.swift + Sources/AltKit/Extensions/Result+Conveniences.swift + + Sources/AltKit/Server/Connection.swift + Sources/AltKit/Server/NetworkConnection.swift + Sources/AltKit/Server/Server.swift + Sources/AltKit/Server/ServerConnection.swift + Sources/AltKit/Server/ServerManager.swift + Sources/AltKit/Server/ServerProtocol.swift + + Sources/AltKit/Types/CodableServerError.swift +) + +target_link_libraries(AltKit PUBLIC CAltKit) + +set_property(TARGET AltKit PROPERTY XCODE_ATTRIBUTE_SWIFT_VERSION "5.0") + +# Make CAltKit's modulemap available to AltKit +set_property(TARGET AltKit PROPERTY XCODE_ATTRIBUTE_SWIFT_INCLUDE_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/Sources/CAltKit") + +set_property(TARGET AltKit PROPERTY XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET "12.0") + +# Add binary dir to interface include path to make Swift header accessible to targets using AltKit +# FIXME not working? +target_include_directories(AltKit PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/..") + +# Copy generated Swift header to binary dir +add_custom_command(TARGET AltKit + POST_BUILD + COMMAND cp -v $DERIVED_SOURCES_DIR/AltKit-Swift.h ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/shell/apple/emulator-ios/AltKit/Package.swift b/shell/apple/emulator-ios/AltKit/Package.swift new file mode 100644 index 000000000..0ad992c86 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. +import PackageDescription + +let package = Package( + name: "AltKit", + platforms: [ + .iOS(.v12), + .tvOS(.v12) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "AltKit", + targets: ["AltKit"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "CAltKit", + dependencies: []), + .target( + name: "AltKit", + dependencies: ["CAltKit"]), + ] +) diff --git a/shell/apple/emulator-ios/AltKit/README.md b/shell/apple/emulator-ios/AltKit/README.md new file mode 100644 index 000000000..a94c2c295 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/README.md @@ -0,0 +1,84 @@ +# AltKit + +AltKit allows apps to communicate with AltServers on the same WiFi network and enable features such as JIT compilation. + +## Installation + +To use AltKit in your app, add the following to your `Package.swift` file's dependencies: + +``` +.package(url: "https://github.com/rileytestut/AltKit.git", .upToNextMajor(from: "0.0.1")), +``` + +Next, add the AltKit package as a dependency for your target: + +``` +.product(name: "AltKit", package: "AltKit"), +``` + +Finally, right-click on your app's `Info.plist`, select "Open As > Source Code", then add the following entries: + +``` +NSBonjourServices + + _altserver._tcp + +NSLocalNetworkUsageDescription +[Your app] uses the local network to find and communicate with AltServer. +``` + +## Usage + +### Swift +``` +import AltKit + +ServerManager.shared.startDiscovering() + +ServerManager.shared.autoconnect { result in + switch result + { + case .failure(let error): print("Could not auto-connect to server.", error) + case .success(let connection): + connection.enableUnsignedCodeExecution { result in + switch result + { + case .failure(let error): print("Could not enable JIT compilation.", error) + case .success: + print("Successfully enabled JIT compilation!") + ServerManager.shared.stopDiscovering() + } + + connection.disconnect() + } + } +} +``` + +### Objective-C +``` +@import AltKit; + +[[ALTServerManager sharedManager] startDiscovering]; + +[[ALTServerManager sharedManager] autoconnectWithCompletionHandler:^(ALTServerConnection *connection, NSError *error) { + if (error) + { + return NSLog(@"Could not auto-connect to server. %@", error); + } + + [connection enableUnsignedCodeExecutionWithCompletionHandler:^(BOOL success, NSError *error) { + if (success) + { + NSLog(@"Successfully enabled JIT compilation!"); + [[ALTServerManager sharedManager] stopDiscovering]; + } + else + { + NSLog(@"Could not enable JIT compilation. %@", error); + } + + [connection disconnect]; + }]; +}]; +``` diff --git a/shell/apple/emulator-ios/AltKit/Sources/AltKit/Extensions/ALTServerError+Conveniences.swift b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Extensions/ALTServerError+Conveniences.swift new file mode 100644 index 000000000..eba88b818 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Extensions/ALTServerError+Conveniences.swift @@ -0,0 +1,38 @@ +// +// ALTServerError+Conveniences.swift +// AltKit +// +// Created by Riley Testut on 6/4/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +public extension ALTServerError +{ + init(_ error: E) + { + switch error + { + case let error as ALTServerError: self = error + case let error as ALTServerConnectionError: + self = ALTServerError(.connectionFailed, underlyingError: error) + case is DecodingError: self = ALTServerError(.invalidRequest, underlyingError: error) + case is EncodingError: self = ALTServerError(.invalidResponse, underlyingError: error) + case let error as NSError: + var userInfo = error.userInfo + if !userInfo.keys.contains(NSUnderlyingErrorKey) + { + // Assign underlying error (if there isn't already one). + userInfo[NSUnderlyingErrorKey] = error + } + + self = ALTServerError(.underlyingError, userInfo: userInfo) + } + } + + init(_ code: ALTServerError.Code, underlyingError: E) + { + self = ALTServerError(code, userInfo: [NSUnderlyingErrorKey: underlyingError]) + } +} diff --git a/shell/apple/emulator-ios/AltKit/Sources/AltKit/Extensions/Result+Conveniences.swift b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Extensions/Result+Conveniences.swift new file mode 100644 index 000000000..eef679f40 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Extensions/Result+Conveniences.swift @@ -0,0 +1,76 @@ +// +// Result+Conveniences.swift +// AltStore +// +// Created by Riley Testut on 5/22/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation + +extension Result +{ + var value: Success? { + switch self + { + case .success(let value): return value + case .failure: return nil + } + } + + var error: Failure? { + switch self + { + case .success: return nil + case .failure(let error): return error + } + } + + init(_ value: Success?, _ error: Failure?) + { + switch (value, error) + { + case (let value?, _): self = .success(value) + case (_, let error?): self = .failure(error) + case (nil, nil): preconditionFailure("Either value or error must be non-nil") + } + } +} + +extension Result where Success == Void +{ + init(_ success: Bool, _ error: Failure?) + { + if success + { + self = .success(()) + } + else if let error = error + { + self = .failure(error) + } + else + { + preconditionFailure("Error must be non-nil if success is false") + } + } +} + +extension Result +{ + init(_ values: (T?, U?), _ error: Failure?) where Success == (T, U) + { + if let value1 = values.0, let value2 = values.1 + { + self = .success((value1, value2)) + } + else if let error = error + { + self = .failure(error) + } + else + { + preconditionFailure("Error must be non-nil if either provided values are nil") + } + } +} diff --git a/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/Connection.swift b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/Connection.swift new file mode 100644 index 000000000..bdb4a60e8 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/Connection.swift @@ -0,0 +1,18 @@ +// +// Connection.swift +// AltKit +// +// Created by Riley Testut on 6/1/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation +import Network + +public protocol Connection +{ + func send(_ data: Data, completionHandler: @escaping (Result) -> Void) + func receiveData(expectedSize: Int, completionHandler: @escaping (Result) -> Void) + + func disconnect() +} diff --git a/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/NetworkConnection.swift b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/NetworkConnection.swift new file mode 100644 index 000000000..4660ec07d --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/NetworkConnection.swift @@ -0,0 +1,62 @@ +// +// NetworkConnection.swift +// AltKit +// +// Created by Riley Testut on 6/1/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation +import Network + +public class NetworkConnection: NSObject, Connection +{ + public let nwConnection: NWConnection + + public init(_ nwConnection: NWConnection) + { + self.nwConnection = nwConnection + } + + public func send(_ data: Data, completionHandler: @escaping (Result) -> Void) + { + self.nwConnection.send(content: data, completion: .contentProcessed { (error) in + if let error = error + { + completionHandler(.failure(.init(.lostConnection, underlyingError: error))) + } + else + { + completionHandler(.success(())) + } + }) + } + + public func receiveData(expectedSize: Int, completionHandler: @escaping (Result) -> Void) + { + self.nwConnection.receive(minimumIncompleteLength: expectedSize, maximumLength: expectedSize) { (data, context, isComplete, error) in + switch (data, error) + { + case (let data?, _): completionHandler(.success(data)) + case (_, let error?): completionHandler(.failure(.init(.lostConnection, underlyingError: error))) + case (nil, nil): completionHandler(.failure(ALTServerError(.lostConnection))) + } + } + } + + public func disconnect() + { + switch self.nwConnection.state + { + case .cancelled, .failed: break + default: self.nwConnection.cancel() + } + } +} + +extension NetworkConnection +{ + override public var description: String { + return "\(self.nwConnection.endpoint) (Network)" + } +} diff --git a/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/Server.swift b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/Server.swift new file mode 100644 index 000000000..3a15238c3 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/Server.swift @@ -0,0 +1,44 @@ +// +// Server.swift +// AltStore +// +// Created by Riley Testut on 6/20/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation + +@objc(ALTServer) +public class Server: NSObject, Identifiable +{ + public let id: String + public let service: NetService + + public var name: String? { + return self.service.hostName + } + + public internal(set) var isPreferred = false + + public override var hash: Int { + return self.id.hashValue ^ self.service.name.hashValue + } + + init?(service: NetService, txtData: Data) + { + let txtDictionary = NetService.dictionary(fromTXTRecord: txtData) + guard let identifierData = txtDictionary["serverID"], let identifier = String(data: identifierData, encoding: .utf8) else { return nil } + + self.id = identifier + self.service = service + + super.init() + } + + public override func isEqual(_ object: Any?) -> Bool + { + guard let server = object as? Server else { return false } + + return self.id == server.id && self.service.name == server.service.name // service.name is consistent, and is not the human readable name (hostName). + } +} diff --git a/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerConnection.swift b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerConnection.swift new file mode 100644 index 000000000..0562339d7 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerConnection.swift @@ -0,0 +1,180 @@ +// +// ServerConnection.swift +// AltStore +// +// Created by Riley Testut on 1/7/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +@objc(ALTServerConnection) @objcMembers +public class ServerConnection: NSObject +{ + public let server: Server + public let connection: Connection + + init(server: Server, connection: Connection) + { + self.server = server + self.connection = connection + } + + deinit + { + self.connection.disconnect() + } + + @objc + public func disconnect() + { + self.connection.disconnect() + } +} + +public extension ServerConnection +{ + func enableUnsignedCodeExecution(completion: @escaping (Result) -> Void) + { + guard let udid = Bundle.main.object(forInfoDictionaryKey: "ALTDeviceID") as? String else { + return ServerManager.shared.callbackQueue.async { + completion(.failure(ConnectionError.unknownUDID)) + } + } + + self.enableUnsignedCodeExecution(udid: udid, completion: completion) + } + + func enableUnsignedCodeExecution(udid: String, completion: @escaping (Result) -> Void) + { + func finish(_ result: Result) + { + ServerManager.shared.callbackQueue.async { + completion(result) + } + } + + let request = EnableUnsignedCodeExecutionRequest(udid: udid, processID: ProcessInfo.processInfo.processIdentifier) + + self.send(request) { (result) in + switch result + { + case .failure(let error): finish(.failure(error)) + case .success: + self.receiveResponse() { (result) in + switch result + { + case .failure(let error): finish(.failure(error)) + case .success(.error(let response)): finish(.failure(response.error)) + case .success(.enableUnsignedCodeExecution): finish(.success(())) + case .success: finish(.failure(ALTServerError(.unknownResponse))) + } + } + } + } + } +} + +public extension ServerConnection +{ + @objc(enableUnsignedCodeExecutionWithCompletionHandler:) + func __enableUnsignedCodeExecution(completion: @escaping (Bool, Error?) -> Void) + { + self.enableUnsignedCodeExecution { result in + switch result { + case .failure(let error): completion(false, error) + case .success: completion(true, nil) + } + } + } + + @objc(enableUnsignedCodeExecutionWithUDID:completionHandler:) + func __enableUnsignedCodeExecution(udid: String, completion: @escaping (Bool, Error?) -> Void) + { + self.enableUnsignedCodeExecution(udid: udid) { result in + switch result { + case .failure(let error): completion(false, error) + case .success: completion(true, nil) + } + } + } +} + +private extension ServerConnection +{ + func send(_ payload: T, completionHandler: @escaping (Result) -> Void) + { + do + { + let data: Data + + if let payload = payload as? Data + { + data = payload + } + else + { + data = try JSONEncoder().encode(payload) + } + + func process(_ result: Result) -> Bool + { + switch result + { + case .success: return true + case .failure(let error): + completionHandler(.failure(error)) + return false + } + } + + let requestSize = Int32(data.count) + let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) } + + self.connection.send(requestSizeData) { (result) in + guard process(result) else { return } + + self.connection.send(data) { (result) in + guard process(result) else { return } + completionHandler(.success(())) + } + } + } + catch + { + print("Invalid request.", error) + completionHandler(.failure(ALTServerError(.invalidRequest))) + } + } + + func receiveResponse(completionHandler: @escaping (Result) -> Void) + { + let size = MemoryLayout.size + + self.connection.receiveData(expectedSize: size) { (result) in + do + { + let data = try result.get() + + let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) }) + self.connection.receiveData(expectedSize: expectedBytes) { (result) in + do + { + let data = try result.get() + + let response = try JSONDecoder().decode(ServerResponse.self, from: data) + completionHandler(.success(response)) + } + catch + { + completionHandler(.failure(ALTServerError(error))) + } + } + } + catch + { + completionHandler(.failure(ALTServerError(error))) + } + } + } +} diff --git a/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerManager.swift b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerManager.swift new file mode 100644 index 000000000..dc22896db --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerManager.swift @@ -0,0 +1,341 @@ +// +// ServerManager.swift +// AltStore +// +// Created by Riley Testut on 5/30/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation +import Network + +import UIKit + +@_exported import CAltKit + +public enum ConnectionError: LocalizedError +{ + case serverNotFound + case connectionFailed(Server) + case connectionDropped(Server) + case unknownUDID + + public var errorDescription: String? { + switch self + { + case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "") + case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "") + case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "") + case .unknownUDID: return NSLocalizedString("This device's UDID could not be determined.", comment: "") + } + } +} + +@objc(ALTServerManager) @objcMembers +public class ServerManager: NSObject +{ + public static let shared = ServerManager() + + private(set) var isDiscovering = false + private(set) var discoveredServers = [Server]() + + public var discoveredServerHandler: ((Server) -> Void)? + public var lostServerHandler: ((Server) -> Void)? + + public var callbackQueue: DispatchQueue = .main + + // Allow other AltKit queues to target this one. + internal let dispatchQueue = DispatchQueue(label: "io.altstore.altkit.ServerManager", qos: .utility, autoreleaseFrequency: .workItem) + + private let serviceBrowser = NetServiceBrowser() + private var resolvingServices = Set() + + private var autoconnectGroup: DispatchGroup? + private var ignoredServers = Set() + + private override init() + { + super.init() + + self.serviceBrowser.delegate = self + self.serviceBrowser.includesPeerToPeer = false + + NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) + } +} + +public extension ServerManager +{ + @objc + func startDiscovering() + { + guard !self.isDiscovering else { return } + self.isDiscovering = true + + self.serviceBrowser.searchForServices(ofType: ALTServerServiceType, inDomain: "") + } + + @objc + func stopDiscovering() + { + guard self.isDiscovering else { return } + self.isDiscovering = false + + self.discoveredServers.removeAll() + self.ignoredServers.removeAll() + self.resolvingServices.removeAll() + + self.serviceBrowser.stop() + } + + func connect(to server: Server, completion: @escaping (Result) -> Void) + { + var didFinish = false + + func finish(_ result: Result) + { + guard !didFinish else { return } + didFinish = true + + self.ignoredServers.insert(server) + + self.callbackQueue.async { + completion(result) + } + } + + self.dispatchQueue.async { + + print("Connecting to service:", server.service) + + let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp) + connection.stateUpdateHandler = { [unowned connection] (state) in + switch state + { + case .failed(let error): + print("Failed to connect to service \(server.service.name).", error) + finish(.failure(ConnectionError.connectionFailed(server))) + + case .cancelled: finish(.failure(CocoaError(.userCancelled))) + + case .ready: + let networkConnection = NetworkConnection(connection) + let serverConnection = ServerConnection(server: server, connection: networkConnection) + finish(.success(serverConnection)) + + case .waiting: break + case .setup: break + case .preparing: break + @unknown default: break + } + } + + connection.start(queue: self.dispatchQueue) + } + } + + func autoconnect(completion: @escaping (Result) -> Void) + { + self.dispatchQueue.async { + if case let availableServers = self.discoveredServers.filter({ !self.ignoredServers.contains($0) }), + let server = availableServers.first(where: { $0.isPreferred }) ?? availableServers.first + { + return self.connect(to: server, completion: completion) + } + + self.autoconnectGroup = DispatchGroup() + self.autoconnectGroup?.enter() + self.autoconnectGroup?.notify(queue: self.dispatchQueue) { + self.autoconnectGroup = nil + + guard + case let availableServers = self.discoveredServers.filter({ !self.ignoredServers.contains($0) }), + let server = availableServers.first(where: { $0.isPreferred }) ?? availableServers.first + else { return self.autoconnect(completion: completion) } + + self.connect(to: server, completion: completion) + } + } + } +} + +public extension ServerManager +{ + @objc(sharedManager) + class var __shared: ServerManager { + return ServerManager.shared + } + + @objc(connectToServer:completionHandler:) + func __connect(to server: Server, completion: @escaping (ServerConnection?, Error?) -> Void) + { + self.connect(to: server) { result in + completion(result.value, result.error) + } + } + + @objc(autoconnectWithCompletionHandler:) + func __autoconnect(completion: @escaping (ServerConnection?, Error?) -> Void) + { + self.autoconnect { result in + completion(result.value, result.error) + } + } +} + +private extension ServerManager +{ + func addDiscoveredServer(_ server: Server) + { + self.dispatchQueue.async { + let serverID = Bundle.main.object(forInfoDictionaryKey: "ALTServerID") as? String + server.isPreferred = (server.id == serverID) + + guard !self.discoveredServers.contains(server) else { return } + + self.discoveredServers.append(server) + + if let callback = self.discoveredServerHandler + { + self.callbackQueue.async { + callback(server) + } + } + } + } + + func removeDiscoveredServer(_ server: Server) + { + self.dispatchQueue.async { + guard let index = self.discoveredServers.firstIndex(of: server) else { return } + + self.discoveredServers.remove(at: index) + + if let callback = self.lostServerHandler + { + self.callbackQueue.async { + callback(server) + } + } + } + } +} + +@objc +private extension ServerManager +{ + @objc + func didEnterBackground(_ notification: Notification) + { + guard self.isDiscovering else { return } + + self.resolvingServices.removeAll() + self.discoveredServers.removeAll() + self.serviceBrowser.stop() + } + + @objc + func willEnterForeground(_ notification: Notification) + { + guard self.isDiscovering else { return } + + self.serviceBrowser.searchForServices(ofType: ALTServerServiceType, inDomain: "") + } +} + +extension ServerManager: NetServiceBrowserDelegate +{ + public func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) + { + print("Discovering servers...") + } + + public func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) + { + print("Stopped discovering servers.") + } + + public func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) + { + print("Failed to discover servers.", errorDict) + } + + public func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) + { + self.dispatchQueue.async { + service.delegate = self + + if let txtData = service.txtRecordData(), let server = Server(service: service, txtData: txtData) + { + self.addDiscoveredServer(server) + } + else + { + service.resolve(withTimeout: 3.0) + self.resolvingServices.insert(service) + } + + self.autoconnectGroup?.enter() + + if !moreComing + { + self.autoconnectGroup?.leave() + } + } + } + + public func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) + { + if let server = self.discoveredServers.first(where: { $0.service == service }) + { + self.removeDiscoveredServer(server) + } + } +} + +extension ServerManager: NetServiceDelegate +{ + public func netServiceDidResolveAddress(_ service: NetService) + { + defer { + self.dispatchQueue.async { + guard self.resolvingServices.contains(service) else { return } + self.resolvingServices.remove(service) + + self.autoconnectGroup?.leave() + } + } + + guard let data = service.txtRecordData(), let server = Server(service: service, txtData: data) else { return } + self.addDiscoveredServer(server) + } + + public func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) + { + print("Error resolving net service \(sender).", errorDict) + + self.dispatchQueue.async { + guard self.resolvingServices.contains(sender) else { return } + self.resolvingServices.remove(sender) + + self.autoconnectGroup?.leave() + } + } + + public func netService(_ sender: NetService, didUpdateTXTRecord data: Data) + { + let txtDict = NetService.dictionary(fromTXTRecord: data) + print("Service \(sender) updated TXT Record:", txtDict) + } + + public func netServiceDidStop(_ sender: NetService) + { + self.dispatchQueue.async { + guard self.resolvingServices.contains(sender) else { return } + self.resolvingServices.remove(sender) + + self.autoconnectGroup?.leave() + } + } +} diff --git a/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerProtocol.swift b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerProtocol.swift new file mode 100644 index 000000000..43e644047 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Server/ServerProtocol.swift @@ -0,0 +1,163 @@ +// +// ServerProtocol.swift +// AltServer +// +// Created by Riley Testut on 5/24/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation + +public let ALTServerServiceType = "_altserver._tcp" + +protocol ServerMessageProtocol: Codable +{ + var version: Int { get } + var identifier: String { get } +} + +public enum ServerRequest: Decodable +{ + case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionRequest) + case unknown(identifier: String, version: Int) + + var identifier: String { + switch self + { + case .enableUnsignedCodeExecution(let request): return request.identifier + case .unknown(let identifier, _): return identifier + } + } + + var version: Int { + switch self + { + case .enableUnsignedCodeExecution(let request): return request.version + case .unknown(_, let version): return version + } + } + + private enum CodingKeys: String, CodingKey + { + case identifier + case version + } + + public init(from decoder: Decoder) throws + { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let version = try container.decode(Int.self, forKey: .version) + + let identifier = try container.decode(String.self, forKey: .identifier) + switch identifier + { + case "EnableUnsignedCodeExecutionRequest": + let request = try EnableUnsignedCodeExecutionRequest(from: decoder) + self = .enableUnsignedCodeExecution(request) + + default: + self = .unknown(identifier: identifier, version: version) + } + } +} + +public enum ServerResponse: Decodable +{ + case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionResponse) + case error(ErrorResponse) + case unknown(identifier: String, version: Int) + + var identifier: String { + switch self + { + case .enableUnsignedCodeExecution(let response): return response.identifier + case .error(let response): return response.identifier + case .unknown(let identifier, _): return identifier + } + } + + var version: Int { + switch self + { + case .enableUnsignedCodeExecution(let response): return response.version + case .error(let response): return response.version + case .unknown(_, let version): return version + } + } + + private enum CodingKeys: String, CodingKey + { + case identifier + case version + } + + public init(from decoder: Decoder) throws + { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let version = try container.decode(Int.self, forKey: .version) + + let identifier = try container.decode(String.self, forKey: .identifier) + switch identifier + { + case "EnableUnsignedCodeExecutionResponse": + let response = try EnableUnsignedCodeExecutionResponse(from: decoder) + self = .enableUnsignedCodeExecution(response) + + case "ErrorResponse": + let response = try ErrorResponse(from: decoder) + self = .error(response) + + default: + self = .unknown(identifier: identifier, version: version) + } + } +} + +// _Don't_ provide generic SuccessResponse, as that would prevent us +// from easily changing response format for a request in the future. +public struct ErrorResponse: ServerMessageProtocol +{ + public var version = 2 + public var identifier = "ErrorResponse" + + public var error: ALTServerError { + return self.serverError?.error ?? ALTServerError(self.errorCode) + } + private var serverError: CodableServerError? + + // Legacy (v1) + private var errorCode: ALTServerError.Code + + public init(error: ALTServerError) + { + self.serverError = CodableServerError(error: error) + self.errorCode = error.code + } +} + +public struct EnableUnsignedCodeExecutionRequest: ServerMessageProtocol +{ + public var version = 1 + public var identifier = "EnableUnsignedCodeExecutionRequest" + + public var udid: String + public var processID: Int32 + + public init(udid: String, processID: Int32) + { + self.udid = udid + self.processID = processID + } +} + +public struct EnableUnsignedCodeExecutionResponse: ServerMessageProtocol +{ + public var version = 1 + public var identifier = "EnableUnsignedCodeExecutionResponse" + + public init() + { + } +} diff --git a/shell/apple/emulator-ios/AltKit/Sources/AltKit/Types/CodableServerError.swift b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Types/CodableServerError.swift new file mode 100644 index 000000000..a9980dcb2 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/AltKit/Types/CodableServerError.swift @@ -0,0 +1,126 @@ +// +// CodableServerError.swift +// AltKit +// +// Created by Riley Testut on 3/5/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself +extension ALTServerError.Code: Codable {} + +extension CodableServerError +{ + enum UserInfoValue: Codable + { + case string(String) + case error(NSError) + + public init(from decoder: Decoder) throws + { + let container = try decoder.singleValueContainer() + + if + let data = try? container.decode(Data.self), + let error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: data) + { + self = .error(error) + } + else if let string = try? container.decode(String.self) + { + self = .string(string) + } + else + { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "UserInfoValue value cannot be decoded") + } + } + + func encode(to encoder: Encoder) throws + { + var container = encoder.singleValueContainer() + + switch self + { + case .string(let string): try container.encode(string) + case .error(let error): + guard let data = try? NSKeyedArchiver.archivedData(withRootObject: error, requiringSecureCoding: true) else { + let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "UserInfoValue value \(self) cannot be encoded") + throw EncodingError.invalidValue(self, context) + } + + try container.encode(data) + } + } + } +} + +struct CodableServerError: Codable +{ + var error: ALTServerError { + return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:]) + } + + private var errorCode: ALTServerError.Code + private var userInfo: [String: Any]? + + private enum CodingKeys: String, CodingKey + { + case errorCode + case userInfo + } + + init(error: ALTServerError) + { + self.errorCode = error.code + + var userInfo = error.userInfo + if let localizedRecoverySuggestion = (error as NSError).localizedRecoverySuggestion + { + userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion + } + + if !userInfo.isEmpty + { + self.userInfo = userInfo + } + } + + init(from decoder: Decoder) throws + { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let errorCode = try container.decode(Int.self, forKey: .errorCode) + self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown + + let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .userInfo) + + let userInfo = rawUserInfo?.mapValues { (value) -> Any in + switch value + { + case .string(let string): return string + case .error(let error): return error + } + } + self.userInfo = userInfo + } + + func encode(to encoder: Encoder) throws + { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.error.code.rawValue, forKey: .errorCode) + + let rawUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in + switch value + { + case let string as String: return .string(string) + case let error as NSError: return .error(error) + default: return nil + } + } + try container.encodeIfPresent(rawUserInfo, forKey: .userInfo) + } +} + diff --git a/shell/apple/emulator-ios/AltKit/Sources/CAltKit/NSError+ALTServerError.h b/shell/apple/emulator-ios/AltKit/Sources/CAltKit/NSError+ALTServerError.h new file mode 100644 index 000000000..60c77c5a2 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/CAltKit/NSError+ALTServerError.h @@ -0,0 +1,69 @@ +// +// NSError+ALTServerError.h +// AltStore +// +// Created by Riley Testut on 5/30/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +#import + +extern NSErrorDomain const AltServerErrorDomain; +extern NSErrorDomain const AltServerInstallationErrorDomain; +extern NSErrorDomain const AltServerConnectionErrorDomain; + +extern NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey; +extern NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey; +extern NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey; +extern NSErrorUserInfoKey const ALTAppNameErrorKey; +extern NSErrorUserInfoKey const ALTDeviceNameErrorKey; + +typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError) +{ + ALTServerErrorUnderlyingError = -1, + + ALTServerErrorUnknown = 0, + ALTServerErrorConnectionFailed = 1, + ALTServerErrorLostConnection = 2, + + ALTServerErrorDeviceNotFound = 3, + ALTServerErrorDeviceWriteFailed = 4, + + ALTServerErrorInvalidRequest = 5, + ALTServerErrorInvalidResponse = 6, + + ALTServerErrorInvalidApp = 7, + ALTServerErrorInstallationFailed = 8, + ALTServerErrorMaximumFreeAppLimitReached = 9, + ALTServerErrorUnsupportediOSVersion = 10, + + ALTServerErrorUnknownRequest = 11, + ALTServerErrorUnknownResponse = 12, + + ALTServerErrorInvalidAnisetteData = 13, + ALTServerErrorPluginNotFound = 14, + + ALTServerErrorProfileNotFound = 15, + + ALTServerErrorAppDeletionFailed = 16, + + ALTServerErrorRequestedAppNotRunning = 100, +}; + +typedef NS_ERROR_ENUM(AltServerConnectionErrorDomain, ALTServerConnectionError) +{ + ALTServerConnectionErrorUnknown, + ALTServerConnectionErrorDeviceLocked, + ALTServerConnectionErrorInvalidRequest, + ALTServerConnectionErrorInvalidResponse, + ALTServerConnectionErrorUSBMUXD, + ALTServerConnectionErrorSSL, + ALTServerConnectionErrorTimedOut, +}; + +NS_ASSUME_NONNULL_BEGIN + +@interface NSError (ALTServerError) +@end + +NS_ASSUME_NONNULL_END diff --git a/shell/apple/emulator-ios/AltKit/Sources/CAltKit/NSError+ALTServerError.m b/shell/apple/emulator-ios/AltKit/Sources/CAltKit/NSError+ALTServerError.m new file mode 100644 index 000000000..ac79d829b --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/CAltKit/NSError+ALTServerError.m @@ -0,0 +1,305 @@ +// +// NSError+ALTServerError.m +// AltStore +// +// Created by Riley Testut on 5/30/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +#import "NSError+ALTServerError.h" + +NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer"; +NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation"; +NSErrorDomain const AltServerConnectionErrorDomain = @"com.rileytestut.AltServer.Connection"; + +NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain"; +NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode"; +NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdentifier"; +NSErrorUserInfoKey const ALTAppNameErrorKey = @"appName"; +NSErrorUserInfoKey const ALTDeviceNameErrorKey = @"deviceName"; + +@implementation NSError (ALTServerError) + ++ (void)load +{ + [NSError setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) { + if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey]) + { + return [error altserver_localizedFailureReason]; + } + else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey]) + { + return [error altserver_localizedRecoverySuggestion]; + } + + return nil; + }]; + + [NSError setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) { + if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey]) + { + return [error altserver_connection_localizedDescription]; + } + else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey]) + { + return [error altserver_connection_localizedRecoverySuggestion]; + } +// else if ([userInfoKey isEqualToString:NSLocalizedFailureErrorKey]) +// { +// return @""; +// } + + return nil; + }]; +} + +//- (nullable NSString *)altserver_localizedDescription +//{ +// switch ((ALTServerError)self.code) +// { +// case ALTServerErrorUnderlyingError: +// { +// NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey]; +// return [underlyingError localizedDescription]; +// } +// +// default: +// { +// return [self altserver_localizedFailureReason]; +// } +// } +//} + +- (nullable NSString *)altserver_localizedFailureReason +{ + switch ((ALTServerError)self.code) + { + case ALTServerErrorUnderlyingError: + { + NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey]; + if (underlyingError.localizedFailureReason != nil) + { + return underlyingError.localizedFailureReason; + } + + NSString *underlyingErrorCode = self.userInfo[ALTUnderlyingErrorCodeErrorKey]; + if (underlyingErrorCode != nil) + { + return [NSString stringWithFormat:NSLocalizedString(@"Error code: %@", @""), underlyingErrorCode]; + } + + return nil; + } + + case ALTServerErrorUnknown: + return NSLocalizedString(@"An unknown error occured.", @""); + + case ALTServerErrorConnectionFailed: +#if TARGET_OS_OSX + return NSLocalizedString(@"There was an error connecting to the device.", @""); +#else + return NSLocalizedString(@"Could not connect to AltServer.", @""); +#endif + + case ALTServerErrorLostConnection: + return NSLocalizedString(@"Lost connection to AltServer.", @""); + + case ALTServerErrorDeviceNotFound: + return NSLocalizedString(@"AltServer could not find this device.", @""); + + case ALTServerErrorDeviceWriteFailed: + return NSLocalizedString(@"Failed to write app data to device.", @""); + + case ALTServerErrorInvalidRequest: + return NSLocalizedString(@"AltServer received an invalid request.", @""); + + case ALTServerErrorInvalidResponse: + return NSLocalizedString(@"AltServer sent an invalid response.", @""); + + case ALTServerErrorInvalidApp: + return NSLocalizedString(@"The app is invalid.", @""); + + case ALTServerErrorInstallationFailed: + return NSLocalizedString(@"An error occured while installing the app.", @""); + + case ALTServerErrorMaximumFreeAppLimitReached: + return NSLocalizedString(@"Cannot activate more than 3 apps and app extensions.", @""); + + case ALTServerErrorUnsupportediOSVersion: + return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @""); + + case ALTServerErrorUnknownRequest: + return NSLocalizedString(@"AltServer does not support this request.", @""); + + case ALTServerErrorUnknownResponse: + return NSLocalizedString(@"Received an unknown response from AltServer.", @""); + + case ALTServerErrorInvalidAnisetteData: + return NSLocalizedString(@"The provided anisette data is invalid.", @""); + + case ALTServerErrorPluginNotFound: + return NSLocalizedString(@"AltServer could not connect to Mail plug-in.", @""); + + case ALTServerErrorProfileNotFound: + return [self profileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not find profile", "")]; + + case ALTServerErrorAppDeletionFailed: + return NSLocalizedString(@"An error occured while removing the app.", @""); + + case ALTServerErrorRequestedAppNotRunning: + { + NSString *appName = self.userInfo[ALTAppNameErrorKey] ?: NSLocalizedString(@"The requested app", @""); + NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @""); + return [NSString stringWithFormat:NSLocalizedString(@"%@ is not currently running on %@.", ""), appName, deviceName]; + } + } +} + +- (nullable NSString *)altserver_localizedRecoverySuggestion +{ + switch ((ALTServerError)self.code) + { + case ALTServerErrorConnectionFailed: + case ALTServerErrorDeviceNotFound: + return NSLocalizedString(@"Make sure you have trusted this device with your computer and WiFi sync is enabled.", @""); + + case ALTServerErrorPluginNotFound: + return NSLocalizedString(@"Make sure Mail is running and the plug-in is enabled in Mail's preferences.", @""); + + case ALTServerErrorMaximumFreeAppLimitReached: + return NSLocalizedString(@"Make sure “Offload Unused Apps” is disabled in Settings > iTunes & App Stores, then install or delete all offloaded apps.", @""); + + case ALTServerErrorRequestedAppNotRunning: + { + NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"your device", @""); + return [NSString stringWithFormat:NSLocalizedString(@"Make sure the app is running in the foreground on %@ then try again.", @""), deviceName]; + } + + default: + return nil; + } +} + +- (NSString *)profileErrorLocalizedDescriptionWithBaseDescription:(NSString *)baseDescription +{ + NSString *localizedDescription = nil; + + NSString *bundleID = self.userInfo[ALTProvisioningProfileBundleIDErrorKey]; + if (bundleID) + { + localizedDescription = [NSString stringWithFormat:@"%@ “%@”", baseDescription, bundleID]; + } + else + { + localizedDescription = [NSString stringWithFormat:@"%@.", baseDescription]; + } + + return localizedDescription; +} + +#pragma mark - AltServerConnectionErrorDomain - + +- (nullable NSString *)altserver_connection_localizedDescription +{ + switch ((ALTServerConnectionError)self.code) + { + case ALTServerConnectionErrorUnknown: + { + NSString *underlyingErrorDomain = self.userInfo[ALTUnderlyingErrorDomainErrorKey]; + NSString *underlyingErrorCode = self.userInfo[ALTUnderlyingErrorCodeErrorKey]; + + if (underlyingErrorDomain != nil && underlyingErrorCode != nil) + { + return [NSString stringWithFormat:NSLocalizedString(@"%@ error %@.", @""), underlyingErrorDomain, underlyingErrorCode]; + } + else if (underlyingErrorCode != nil) + { + return [NSString stringWithFormat:NSLocalizedString(@"Connection error code: %@", @""), underlyingErrorCode]; + } + + return nil; + } + + case ALTServerConnectionErrorDeviceLocked: + { + NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"The device", @""); + return [NSString stringWithFormat:NSLocalizedString(@"%@ is currently locked.", @""), deviceName]; + } + + case ALTServerConnectionErrorInvalidRequest: + { + NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"The device", @""); + return [NSString stringWithFormat:NSLocalizedString(@"%@ received an invalid request from AltServer.", @""), deviceName]; + } + + case ALTServerConnectionErrorInvalidResponse: + { + NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @""); + return [NSString stringWithFormat:NSLocalizedString(@"AltServer received an invalid response from %@.", @""), deviceName]; + } + + case ALTServerConnectionErrorUSBMUXD: + { + return NSLocalizedString(@"A connection to usbmuxd could not be established.", @""); + } + + case ALTServerConnectionErrorSSL: + { + NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @""); + return [NSString stringWithFormat:NSLocalizedString(@"A secure connection between AltServer and %@ could not be established.", @""), deviceName]; + } + + case ALTServerConnectionErrorTimedOut: + { + NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @""); + return [NSString stringWithFormat:NSLocalizedString(@"The connection to %@ timed out.", @""), deviceName]; + } + } + + return nil; +} + +- (nullable NSString *)altserver_connection_localizedRecoverySuggestion +{ + switch ((ALTServerConnectionError)self.code) + { + case ALTServerConnectionErrorUnknown: + { + return nil; + } + + case ALTServerConnectionErrorDeviceLocked: + { + return NSLocalizedString(@"Please unlock the device with your passcode and try again.", @""); + } + + case ALTServerConnectionErrorInvalidRequest: + { + break; + } + + case ALTServerConnectionErrorInvalidResponse: + { + break; + } + + case ALTServerConnectionErrorUSBMUXD: + { + break; + } + + case ALTServerConnectionErrorSSL: + { + break; + } + + case ALTServerConnectionErrorTimedOut: + { + break; + } + } + + return nil; +} + +@end diff --git a/shell/apple/emulator-ios/AltKit/Sources/CAltKit/include/NSError+ALTServerError.h b/shell/apple/emulator-ios/AltKit/Sources/CAltKit/include/NSError+ALTServerError.h new file mode 100644 index 000000000..60c77c5a2 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/CAltKit/include/NSError+ALTServerError.h @@ -0,0 +1,69 @@ +// +// NSError+ALTServerError.h +// AltStore +// +// Created by Riley Testut on 5/30/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +#import + +extern NSErrorDomain const AltServerErrorDomain; +extern NSErrorDomain const AltServerInstallationErrorDomain; +extern NSErrorDomain const AltServerConnectionErrorDomain; + +extern NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey; +extern NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey; +extern NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey; +extern NSErrorUserInfoKey const ALTAppNameErrorKey; +extern NSErrorUserInfoKey const ALTDeviceNameErrorKey; + +typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError) +{ + ALTServerErrorUnderlyingError = -1, + + ALTServerErrorUnknown = 0, + ALTServerErrorConnectionFailed = 1, + ALTServerErrorLostConnection = 2, + + ALTServerErrorDeviceNotFound = 3, + ALTServerErrorDeviceWriteFailed = 4, + + ALTServerErrorInvalidRequest = 5, + ALTServerErrorInvalidResponse = 6, + + ALTServerErrorInvalidApp = 7, + ALTServerErrorInstallationFailed = 8, + ALTServerErrorMaximumFreeAppLimitReached = 9, + ALTServerErrorUnsupportediOSVersion = 10, + + ALTServerErrorUnknownRequest = 11, + ALTServerErrorUnknownResponse = 12, + + ALTServerErrorInvalidAnisetteData = 13, + ALTServerErrorPluginNotFound = 14, + + ALTServerErrorProfileNotFound = 15, + + ALTServerErrorAppDeletionFailed = 16, + + ALTServerErrorRequestedAppNotRunning = 100, +}; + +typedef NS_ERROR_ENUM(AltServerConnectionErrorDomain, ALTServerConnectionError) +{ + ALTServerConnectionErrorUnknown, + ALTServerConnectionErrorDeviceLocked, + ALTServerConnectionErrorInvalidRequest, + ALTServerConnectionErrorInvalidResponse, + ALTServerConnectionErrorUSBMUXD, + ALTServerConnectionErrorSSL, + ALTServerConnectionErrorTimedOut, +}; + +NS_ASSUME_NONNULL_BEGIN + +@interface NSError (ALTServerError) +@end + +NS_ASSUME_NONNULL_END diff --git a/shell/apple/emulator-ios/AltKit/Sources/CAltKit/module.modulemap b/shell/apple/emulator-ios/AltKit/Sources/CAltKit/module.modulemap new file mode 100644 index 000000000..e52c08486 --- /dev/null +++ b/shell/apple/emulator-ios/AltKit/Sources/CAltKit/module.modulemap @@ -0,0 +1,4 @@ +module CAltKit { + umbrella "./include" + export * +}