iOS: AltKit source
This commit is contained in:
parent
12f302d356
commit
bb217e5c49
|
@ -0,0 +1,5 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
|
@ -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}
|
||||
)
|
|
@ -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"]),
|
||||
]
|
||||
)
|
|
@ -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:
|
||||
|
||||
```
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_altserver._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>[Your app] uses the local network to find and communicate with AltServer.</string>
|
||||
```
|
||||
|
||||
## 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];
|
||||
}];
|
||||
}];
|
||||
```
|
|
@ -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<E: Error>(_ 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<E: Error>(_ code: ALTServerError.Code, underlyingError: E)
|
||||
{
|
||||
self = ALTServerError(code, userInfo: [NSUnderlyingErrorKey: underlyingError])
|
||||
}
|
||||
}
|
|
@ -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<T, U>(_ 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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, ALTServerError>) -> Void)
|
||||
func receiveData(expectedSize: Int, completionHandler: @escaping (Result<Data, ALTServerError>) -> Void)
|
||||
|
||||
func disconnect()
|
||||
}
|
|
@ -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, ALTServerError>) -> 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<Data, ALTServerError>) -> 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)"
|
||||
}
|
||||
}
|
|
@ -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).
|
||||
}
|
||||
}
|
|
@ -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, Error>) -> 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, Error>) -> Void)
|
||||
{
|
||||
func finish(_ result: Result<Void, Error>)
|
||||
{
|
||||
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<T: Encodable>(_ payload: T, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let data: Data
|
||||
|
||||
if let payload = payload as? Data
|
||||
{
|
||||
data = payload
|
||||
}
|
||||
else
|
||||
{
|
||||
data = try JSONEncoder().encode(payload)
|
||||
}
|
||||
|
||||
func process<T>(_ result: Result<T, ALTServerError>) -> 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<ServerResponse, Error>) -> Void)
|
||||
{
|
||||
let size = MemoryLayout<Int32>.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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<NetService>()
|
||||
|
||||
private var autoconnectGroup: DispatchGroup?
|
||||
private var ignoredServers = Set<Server>()
|
||||
|
||||
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<ServerConnection, Error>) -> Void)
|
||||
{
|
||||
var didFinish = false
|
||||
|
||||
func finish(_ result: Result<ServerConnection, Error>)
|
||||
{
|
||||
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<ServerConnection, Error>) -> 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// NSError+ALTServerError.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
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
|
|
@ -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
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// NSError+ALTServerError.h
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
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
|
|
@ -0,0 +1,4 @@
|
|||
module CAltKit {
|
||||
umbrella "./include"
|
||||
export *
|
||||
}
|
Loading…
Reference in New Issue