mirror of
https://github.com/xtool-org/xtool.git
synced 2026-02-04 11:53:30 +01:00
Installer refactoring and bug fixes
- Create new Mac CLI with better keychain support - Fix auto-created member center device name - Misc refactoring
This commit is contained in:
committed by
Kabir Oberai
parent
bf2729321e
commit
e05edc63bd
@@ -45,7 +45,7 @@ public struct DeveloperServicesFetchCertificateOperation: DeveloperServicesOpera
|
||||
platform: context.platform,
|
||||
teamID: context.teamID,
|
||||
csr: csr,
|
||||
machineName: "Supercharge: \(context.deviceName)",
|
||||
machineName: "Supercharge: \(SigningContext.hostName)",
|
||||
machineID: context.client.deviceInfo.deviceID
|
||||
)
|
||||
context.client.send(request) { result in
|
||||
|
||||
@@ -61,14 +61,14 @@ public final class DeveloperServicesClient {
|
||||
public init(
|
||||
loginToken: DeveloperServicesLoginToken,
|
||||
deviceInfo: DeviceInfo,
|
||||
httpFactory: HTTPClientFactory.Type = defaultHTTPClientFactory,
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory,
|
||||
customAnisetteDataProvider: AnisetteDataProvider? = nil
|
||||
) {
|
||||
self.loginToken = loginToken
|
||||
self.deviceInfo = deviceInfo
|
||||
self.anisetteDataProvider = customAnisetteDataProvider
|
||||
?? SupersetteDataProvider(deviceInfo: deviceInfo)
|
||||
self.httpClient = httpFactory.shared.makeClient()
|
||||
self.httpClient = httpFactory.makeClient()
|
||||
}
|
||||
|
||||
private func send<R: DeveloperServicesRequest>(
|
||||
|
||||
@@ -23,9 +23,9 @@ public class SupersetteDataProvider: AnisetteDataProvider {
|
||||
public let deviceInfo: DeviceInfo
|
||||
private let httpClient: HTTPClientProtocol
|
||||
|
||||
public init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory.Type = defaultHTTPClientFactory) {
|
||||
public init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory = defaultHTTPClientFactory) {
|
||||
self.deviceInfo = deviceInfo
|
||||
self.httpClient = httpFactory.shared.makeClient()
|
||||
self.httpClient = httpFactory.makeClient()
|
||||
}
|
||||
|
||||
private struct AnisetteRequestBody: Encodable {
|
||||
|
||||
@@ -19,14 +19,14 @@ class GrandSlamClient {
|
||||
private let httpClient: HTTPClientProtocol
|
||||
init(
|
||||
deviceInfo: DeviceInfo,
|
||||
httpFactory: HTTPClientFactory.Type = defaultHTTPClientFactory,
|
||||
httpFactory: HTTPClientFactory = defaultHTTPClientFactory,
|
||||
customAnisetteDataProvider: AnisetteDataProvider? = nil
|
||||
) {
|
||||
self.deviceInfo = deviceInfo
|
||||
self.anisetteDataProvider = customAnisetteDataProvider
|
||||
?? SupersetteDataProvider(deviceInfo: deviceInfo)
|
||||
self.lookupManager = .init(deviceInfo: deviceInfo, httpFactory: httpFactory)
|
||||
self.httpClient = httpFactory.shared.makeClient()
|
||||
self.httpClient = httpFactory.makeClient()
|
||||
}
|
||||
|
||||
private func send<R: GrandSlamRequest>(
|
||||
|
||||
@@ -21,9 +21,9 @@ class GrandSlamLookupManager {
|
||||
|
||||
let httpClient: HTTPClientProtocol
|
||||
let deviceInfo: DeviceInfo
|
||||
init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory.Type = defaultHTTPClientFactory) {
|
||||
init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory = defaultHTTPClientFactory) {
|
||||
self.deviceInfo = deviceInfo
|
||||
self.httpClient = httpFactory.shared.makeClient()
|
||||
self.httpClient = httpFactory.makeClient()
|
||||
}
|
||||
|
||||
private func performLookup(completion: @escaping (Result<GrandSlamEndpoints, Error>) -> Void) {
|
||||
|
||||
@@ -89,7 +89,7 @@ extension HTTPClient: HTTPClientProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public final class AsyncHTTPClientFactory: HTTPClientFactory {
|
||||
final class AsyncHTTPClientFactory: HTTPClientFactory {
|
||||
private static let appleRootPEM = """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET
|
||||
@@ -131,12 +131,12 @@ public final class AsyncHTTPClientFactory: HTTPClientFactory {
|
||||
)
|
||||
client = HTTPClient(eventLoopGroupProvider: .createNew, configuration: config)
|
||||
}
|
||||
public static let shared = AsyncHTTPClientFactory()
|
||||
static let shared = AsyncHTTPClientFactory()
|
||||
|
||||
public func shutdown() {
|
||||
func shutdown() {
|
||||
try! client.syncShutdown()
|
||||
}
|
||||
|
||||
public func makeClient() -> HTTPClientProtocol { client }
|
||||
func makeClient() -> HTTPClientProtocol { client }
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -66,15 +66,14 @@ public protocol HTTPClientProtocol {
|
||||
}
|
||||
|
||||
public protocol HTTPClientFactory {
|
||||
static var shared: Self { get }
|
||||
func shutdown() // client is invalidated after this
|
||||
func makeClient() -> HTTPClientProtocol
|
||||
}
|
||||
|
||||
public let defaultHTTPClientFactory: HTTPClientFactory.Type = {
|
||||
public let defaultHTTPClientFactory: HTTPClientFactory = {
|
||||
#if os(Linux)
|
||||
return AsyncHTTPClientFactory.self
|
||||
return AsyncHTTPClientFactory.shared
|
||||
#else
|
||||
return URLHTTPClientFactory.self
|
||||
return URLHTTPClientFactory.shared
|
||||
#endif
|
||||
}()
|
||||
|
||||
@@ -57,8 +57,8 @@ extension URLSession: HTTPClientProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
public final class URLHTTPClientFactory: HTTPClientFactory {
|
||||
public static let shared = URLHTTPClientFactory()
|
||||
final class URLHTTPClientFactory: HTTPClientFactory {
|
||||
static let shared = URLHTTPClientFactory()
|
||||
|
||||
// no need for cert handling since the Apple Root CA is already
|
||||
// installed on any devices which support the Security APIs which
|
||||
@@ -66,7 +66,7 @@ public final class URLHTTPClientFactory: HTTPClientFactory {
|
||||
private let session = URLSession(configuration: .ephemeral)
|
||||
private init() {}
|
||||
|
||||
public func shutdown() {}
|
||||
public func makeClient() -> HTTPClientProtocol { session }
|
||||
func shutdown() {}
|
||||
func makeClient() -> HTTPClientProtocol { session }
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -82,6 +82,7 @@ public final class SuperchargeInstaller {
|
||||
let udid: String
|
||||
let appleID: String
|
||||
let password: String
|
||||
let signingInfoManager: SigningInfoManager
|
||||
public weak var delegate: SuperchargeInstallerDelegate?
|
||||
|
||||
private var appInstaller: AppInstaller?
|
||||
@@ -144,45 +145,46 @@ public final class SuperchargeInstaller {
|
||||
udid: String,
|
||||
appleID: String,
|
||||
password: String,
|
||||
signingInfoManager: SigningInfoManager,
|
||||
delegate: SuperchargeInstallerDelegate
|
||||
) {
|
||||
self.udid = udid
|
||||
self.appleID = appleID
|
||||
self.password = password
|
||||
self.signingInfoManager = signingInfoManager
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
private func performWithRecovery<T>(block: () throws -> T) throws -> T {
|
||||
var presentedMessage: SuperchargeInstaller.Message?
|
||||
|
||||
func present(message: SuperchargeInstaller.Message) throws {
|
||||
if presentedMessage != message {
|
||||
delegate?.setPresentedMessage(message)
|
||||
}
|
||||
presentedMessage = message
|
||||
// allow some time before looping
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
private func performWithRecovery<T>(repeatAfter: TimeInterval = 0.1, block: () throws -> T) throws -> T {
|
||||
var currMessage: SuperchargeInstaller.Message?
|
||||
|
||||
defer {
|
||||
if presentedMessage != nil {
|
||||
if currMessage != nil {
|
||||
delegate?.setPresentedMessage(nil)
|
||||
}
|
||||
}
|
||||
|
||||
while true {
|
||||
guard shouldContinue() else { throw Error.userCancelled }
|
||||
let nextMessage: SuperchargeInstaller.Message
|
||||
do {
|
||||
return try block()
|
||||
} catch let error as LockdownClient.Error where error == .pairingDialogResponsePending {
|
||||
try present(message: .pairDevice)
|
||||
nextMessage = .pairDevice
|
||||
} catch let error as LockdownClient.Error where error == .passwordProtected {
|
||||
try present(message: .unlockDevice)
|
||||
nextMessage = .unlockDevice
|
||||
}
|
||||
if currMessage != nextMessage {
|
||||
delegate?.setPresentedMessage(nextMessage)
|
||||
currMessage = nextMessage
|
||||
}
|
||||
if repeatAfter > 0 {
|
||||
Thread.sleep(forTimeInterval: repeatAfter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchPairingKeys() throws -> Data? {
|
||||
private func prepareDevice() throws -> (deviceName: String, pairingKeys: Data)? {
|
||||
// TODO: Maybe use `Connection` here instead of creating the lockdown
|
||||
// client manually?
|
||||
|
||||
@@ -199,6 +201,8 @@ public final class SuperchargeInstaller {
|
||||
)
|
||||
}
|
||||
|
||||
let deviceName = try lockdownClient.deviceName()
|
||||
|
||||
try lockdownClient.setValue(udid, forDomain: "com.apple.mobile.wireless_lockdown", key: "WirelessBuddyID")
|
||||
try lockdownClient.setValue(true, forDomain: "com.apple.mobile.wireless_lockdown", key: "EnableWifiConnections")
|
||||
|
||||
@@ -251,7 +255,7 @@ public final class SuperchargeInstaller {
|
||||
|
||||
guard updateProgress(to: 1) else { return nil }
|
||||
|
||||
return data
|
||||
return (deviceName, data)
|
||||
}
|
||||
|
||||
private func install(
|
||||
@@ -314,30 +318,22 @@ public final class SuperchargeInstaller {
|
||||
) {
|
||||
guard self.updateStage(to: "Preparing device") else { return }
|
||||
|
||||
let deviceName: String
|
||||
let pairingKeys: Data
|
||||
|
||||
do {
|
||||
guard let _pairingKeys = try fetchPairingKeys()
|
||||
guard let prepareResult = try prepareDevice()
|
||||
else { return } // nil: user cancelled
|
||||
pairingKeys = _pairingKeys
|
||||
(deviceName, pairingKeys) = prepareResult
|
||||
} catch {
|
||||
return completion(.failure(error))
|
||||
}
|
||||
|
||||
#warning("Persist manager on Windows/Linux")
|
||||
let signingInfoManager: SigningInfoManager
|
||||
#if os(macOS)
|
||||
signingInfoManager = KeyValueSigningInfoManager(
|
||||
storage: KeychainStorage(service: "com.kabiroberai.Supercharge-Installer.credentials")
|
||||
)
|
||||
#else
|
||||
signingInfoManager = MemoryBackedSigningInfoManager()
|
||||
#endif
|
||||
|
||||
let context: SigningContext
|
||||
do {
|
||||
context = try SigningContext(
|
||||
udid: udid,
|
||||
deviceName: deviceName,
|
||||
teamID: team.id,
|
||||
client: client,
|
||||
signingInfoManager: signingInfoManager,
|
||||
@@ -461,7 +457,11 @@ public final class SuperchargeInstaller {
|
||||
|
||||
extension SuperchargeInstaller: TwoFactorAuthDelegate {
|
||||
public func fetchCode(completion: @escaping (String?) -> Void) {
|
||||
delegate?.fetchCode { code in
|
||||
guard let delegate = delegate else {
|
||||
self.installQueue.async { completion(nil) }
|
||||
return
|
||||
}
|
||||
delegate.fetchCode { code in
|
||||
self.installQueue.async {
|
||||
completion(code)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import Foundation
|
||||
public struct SigningContext {
|
||||
|
||||
public let udid: String
|
||||
public let deviceName: String
|
||||
public let teamID: DeveloperServicesTeam.ID
|
||||
public let client: DeveloperServicesClient
|
||||
public let signingInfoManager: SigningInfoManager
|
||||
@@ -19,6 +20,7 @@ public struct SigningContext {
|
||||
|
||||
public init(
|
||||
udid: String,
|
||||
deviceName: String,
|
||||
teamID: DeveloperServicesTeam.ID,
|
||||
client: DeveloperServicesClient,
|
||||
signingInfoManager: SigningInfoManager,
|
||||
@@ -26,6 +28,7 @@ public struct SigningContext {
|
||||
signerImpl: SignerImpl? = nil
|
||||
) throws {
|
||||
self.udid = udid
|
||||
self.deviceName = deviceName
|
||||
self.teamID = teamID
|
||||
self.client = client
|
||||
self.signingInfoManager = signingInfoManager
|
||||
@@ -39,7 +42,7 @@ public struct SigningContext {
|
||||
import UIKit
|
||||
#endif
|
||||
extension SigningContext {
|
||||
public var deviceName: String {
|
||||
public static var hostName: String {
|
||||
#if targetEnvironment(simulator)
|
||||
return "Simulator"
|
||||
#elseif canImport(UIKit)
|
||||
|
||||
@@ -73,6 +73,9 @@ public struct KeychainStorage: KeyValueStorage {
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrAccount: key
|
||||
]
|
||||
if #available(macOS 10.15, *) {
|
||||
query[kSecUseDataProtectionKeychain] = true
|
||||
}
|
||||
query[kSecAttrService] = service
|
||||
query.merge(parameters) { _, b in b }
|
||||
return query as CFDictionary
|
||||
|
||||
@@ -44,14 +44,8 @@ public class MemoryBackedSigningInfoManager: SigningInfoManager {
|
||||
}
|
||||
|
||||
public struct KeyValueSigningInfoManager: SigningInfoManager {
|
||||
private enum KeyType: String {
|
||||
case certificate
|
||||
case privateKey
|
||||
}
|
||||
|
||||
private func key(_ teamID: DeveloperServicesTeam.ID, _ keyType: KeyType) -> String {
|
||||
"\(teamID.rawValue).\(keyType.rawValue)"
|
||||
}
|
||||
private let encoder = PropertyListEncoder()
|
||||
private let decoder = PropertyListDecoder()
|
||||
|
||||
public let storage: KeyValueStorage
|
||||
public init(storage: KeyValueStorage) {
|
||||
@@ -59,22 +53,13 @@ public struct KeyValueSigningInfoManager: SigningInfoManager {
|
||||
}
|
||||
|
||||
public func info(forTeamID teamID: DeveloperServicesTeam.ID) throws -> SigningInfo? {
|
||||
guard let privKeyData = try storage.data(forKey: key(teamID, .privateKey)),
|
||||
let certData = try storage.data(forKey: key(teamID, .certificate))
|
||||
guard let data = try storage.data(forKey: teamID.rawValue)
|
||||
else { return nil }
|
||||
let cert = try Certificate(data: certData)
|
||||
let privKey = PrivateKey(data: privKeyData)
|
||||
return SigningInfo(privateKey: privKey, certificate: cert)
|
||||
return try decoder.decode(SigningInfo.self, from: data)
|
||||
}
|
||||
|
||||
public func setInfo(_ info: SigningInfo?, forTeamID teamID: DeveloperServicesTeam.ID) throws {
|
||||
try storage.setData(
|
||||
info?.certificate.data(),
|
||||
forKey: key(teamID, .certificate)
|
||||
)
|
||||
try storage.setData(
|
||||
info?.privateKey.data,
|
||||
forKey: key(teamID, .privateKey)
|
||||
)
|
||||
let data = try encoder.encode(info)
|
||||
try storage.setData(data, forKey: teamID.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,244 +1,27 @@
|
||||
import Foundation
|
||||
import Supersign
|
||||
|
||||
defer { defaultHTTPClientFactory.shared.shutdown() }
|
||||
let moduleBundle: Bundle
|
||||
#if swift(>=5.5) || os(macOS)
|
||||
moduleBundle = Bundle.module
|
||||
#else
|
||||
moduleBundle = Bundle(url: Bundle.main.url(forResource: "Supersign_SupersignCLI", withExtension: "resources")!)!
|
||||
#endif
|
||||
let app = moduleBundle.url(forResource: "Supercharge", withExtension: "ipa")!
|
||||
|
||||
// nil = EOF
|
||||
func prompt(_ message: String) -> String? {
|
||||
if !message.isEmpty {
|
||||
print(message, terminator: "")
|
||||
}
|
||||
return readLine()
|
||||
}
|
||||
#warning("Persist manager on Windows/Linux")
|
||||
|
||||
func getPassword(_ message: String) -> String? {
|
||||
if !message.isEmpty {
|
||||
print(message, terminator: "")
|
||||
}
|
||||
// for Windows, we could use dpapi.h or wincred.h.
|
||||
// for Linux, maybe use libsecret?
|
||||
// see https://github.com/atom/node-keytar
|
||||
|
||||
var origAttr = termios()
|
||||
tcgetattr(STDIN_FILENO, &origAttr)
|
||||
let signingInfoManager: SigningInfoManager
|
||||
#if os(macOS)
|
||||
signingInfoManager = KeyValueSigningInfoManager(
|
||||
storage: KeychainStorage(service: "com.kabiroberai.Supercharge-Keychain.credentials")
|
||||
)
|
||||
#else
|
||||
signingInfoManager = MemoryBackedSigningInfoManager()
|
||||
#endif
|
||||
|
||||
var newAttr = origAttr
|
||||
newAttr.c_lflag = newAttr.c_lflag & ~tcflag_t(ECHO)
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &newAttr)
|
||||
|
||||
let password = prompt("")
|
||||
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &origAttr)
|
||||
print()
|
||||
|
||||
return password
|
||||
}
|
||||
|
||||
func chooseNumber(in range: Range<Int>) -> Int {
|
||||
print("Choice (\(range.lowerBound)-\(range.upperBound - 1)): ", terminator: "")
|
||||
guard let choice = readLine().flatMap(Int.init), range.contains(choice)
|
||||
else { return chooseNumber(in: range) }
|
||||
return choice
|
||||
}
|
||||
|
||||
func choose<T>(
|
||||
from elements: [T],
|
||||
onNoElement: () throws -> T,
|
||||
multiPrompt: @autoclosure () -> String,
|
||||
formatter: (T) throws -> String
|
||||
) rethrows -> T {
|
||||
switch elements.count {
|
||||
case 0:
|
||||
return try onNoElement()
|
||||
case 1:
|
||||
return elements[0]
|
||||
default:
|
||||
print(multiPrompt())
|
||||
try elements.enumerated().forEach { index, element in
|
||||
try print("\(index): \(formatter(element))")
|
||||
}
|
||||
let choice = chooseNumber(in: elements.indices)
|
||||
return elements[choice]
|
||||
}
|
||||
}
|
||||
|
||||
class SupersignCLIDelegate: SuperchargeInstallerDelegate {
|
||||
let completion: () -> Void
|
||||
init(completion: @escaping () -> Void) {
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func fetchCode(completion: @escaping (String?) -> Void) {
|
||||
let code = prompt("Code: ")
|
||||
completion(code)
|
||||
}
|
||||
|
||||
func fetchTeam(fromTeams teams: [DeveloperServicesTeam], completion: @escaping (DeveloperServicesTeam?) -> Void) {
|
||||
let selected = choose(
|
||||
from: teams,
|
||||
onNoElement: { fatalError() },
|
||||
multiPrompt: "Select a team",
|
||||
formatter: {
|
||||
"\($0.name) (\($0.id))"
|
||||
}
|
||||
)
|
||||
completion(selected)
|
||||
}
|
||||
|
||||
func setPresentedMessage(_ message: SuperchargeInstaller.Message?) {
|
||||
let text: String
|
||||
switch message {
|
||||
case .pairDevice:
|
||||
text = "Please tap 'trust' on your device..."
|
||||
case .unlockDevice:
|
||||
text = "Please unlock your device..."
|
||||
case nil:
|
||||
text = "Continuing..."
|
||||
}
|
||||
print("\n\(text)", terminator: "")
|
||||
fflush(stdout)
|
||||
}
|
||||
|
||||
var prevStage: String?
|
||||
var prevProgress: String?
|
||||
|
||||
func installerDidUpdate(toStage stage: String, progress: Double?) {
|
||||
let progString: String?
|
||||
if let progress = progress {
|
||||
let progInt = Int(progress * 100)
|
||||
if progInt < 10 {
|
||||
progString = " \(progInt)%"
|
||||
} else if progInt < 100 {
|
||||
progString = " \(progInt)%"
|
||||
} else {
|
||||
progString = "\(progInt)%"
|
||||
}
|
||||
} else {
|
||||
progString = nil
|
||||
}
|
||||
|
||||
defer {
|
||||
prevStage = stage
|
||||
prevProgress = progString
|
||||
}
|
||||
|
||||
if stage != prevStage {
|
||||
if let progString = progString {
|
||||
print("\n[\(stage)] \(progString)", terminator: "")
|
||||
fflush(stdout)
|
||||
} else {
|
||||
print("\n[\(stage)] ...", terminator: "")
|
||||
fflush(stdout)
|
||||
}
|
||||
} else if progString != prevProgress {
|
||||
if let progString = progString {
|
||||
print("\r[\(stage)] \(progString)", terminator: "")
|
||||
fflush(stdout)
|
||||
} else {
|
||||
print("\r[\(stage)]", terminator: "")
|
||||
fflush(stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installerDidComplete(withResult result: Result<(), Error>) {
|
||||
print("\n")
|
||||
switch result {
|
||||
case .success:
|
||||
print("Complete!")
|
||||
case .failure(let error):
|
||||
print("Failed :(")
|
||||
print("Error: \(error)")
|
||||
}
|
||||
completion()
|
||||
}
|
||||
|
||||
func decompress(
|
||||
ipa: URL,
|
||||
in directory: URL,
|
||||
progress: @escaping (Double?) -> Void,
|
||||
completion: @escaping (_ success: Bool) -> Void
|
||||
) {
|
||||
progress(nil)
|
||||
|
||||
let unzip = Process()
|
||||
unzip.launchPath = "/usr/bin/unzip"
|
||||
unzip.arguments = ["-q", ipa.path, "-d", directory.path]
|
||||
unzip.launch()
|
||||
unzip.waitUntilExit()
|
||||
guard unzip.terminationStatus == 0 else { return completion(false) }
|
||||
|
||||
completion(true)
|
||||
}
|
||||
|
||||
func compress(payloadDir: URL, progress: @escaping (Double?) -> Void, completion: @escaping (URL?) -> Void) {
|
||||
progress(nil)
|
||||
|
||||
let dest = payloadDir.deletingLastPathComponent().appendingPathComponent("app.ipa")
|
||||
|
||||
let zip = Process()
|
||||
zip.launchPath = "/usr/bin/zip"
|
||||
zip.currentDirectoryPath = payloadDir.deletingLastPathComponent().path
|
||||
zip.arguments = ["-yqru0", dest.path, "Payload"]
|
||||
zip.launch()
|
||||
zip.waitUntilExit()
|
||||
guard zip.terminationStatus == 0 else { return completion(nil) }
|
||||
|
||||
completion(dest)
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionDelegate: ConnectionManagerDelegate {
|
||||
var onConnect: (([ConnectionManager.Client]) -> Void)?
|
||||
init(onConnect: @escaping ([ConnectionManager.Client]) -> Void) {
|
||||
self.onConnect = onConnect
|
||||
}
|
||||
|
||||
func connectionManager(_ manager: ConnectionManager, clientsDidChangeFrom oldValue: [ConnectionManager.Client]) {
|
||||
let usableClients = manager.clients
|
||||
guard !usableClients.isEmpty else { return }
|
||||
onConnect?(usableClients)
|
||||
onConnect = nil
|
||||
}
|
||||
}
|
||||
|
||||
func main() throws {
|
||||
let moduleBundle: Bundle
|
||||
#if swift(>=5.5) || os(macOS)
|
||||
moduleBundle = Bundle.module
|
||||
#else
|
||||
moduleBundle = Bundle(url: Bundle.main.url(forResource: "Supersign_SupersignCLI", withExtension: "resources")!)!
|
||||
#endif
|
||||
let app = moduleBundle.url(forResource: "Supercharge", withExtension: "ipa")!
|
||||
|
||||
guard let username = prompt("Apple ID: "),
|
||||
let password = getPassword("Password: ")
|
||||
else { return }
|
||||
|
||||
print("Waiting for device to be connected...")
|
||||
var clients: [ConnectionManager.Client]!
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let connDelegate = ConnectionDelegate { currClients in
|
||||
clients = currClients
|
||||
semaphore.signal()
|
||||
}
|
||||
try withExtendedLifetime(ConnectionManager(usbOnly: true, delegate: connDelegate)) {
|
||||
semaphore.wait()
|
||||
}
|
||||
|
||||
let client = choose(
|
||||
from: clients,
|
||||
onNoElement: { fatalError() },
|
||||
multiPrompt: "Choose device",
|
||||
formatter: { "\($0.deviceName) (udid: \($0.udid))" }
|
||||
)
|
||||
|
||||
print("Installing to device: \(client.deviceName) (udid: \(client.udid))")
|
||||
|
||||
let installDelegate = SupersignCLIDelegate {
|
||||
semaphore.signal()
|
||||
}
|
||||
let installer = SuperchargeInstaller(udid: client.udid, appleID: username, password: password, delegate: installDelegate)
|
||||
installer.install(ipa: app)
|
||||
semaphore.wait()
|
||||
_ = installer
|
||||
}
|
||||
|
||||
try main()
|
||||
try SupersignCLI(app: app, signingInfoManager: signingInfoManager).run()
|
||||
|
||||
Reference in New Issue
Block a user