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 *
+}