feat: smooth cryptex DDI migration

This commit is contained in:
CelloSerenity 2026-09-23 17:09:26 -06:00
parent f76672838b
commit e85e0b3d47
5 changed files with 123 additions and 57 deletions

View file

@ -233,15 +233,47 @@ private enum IdeviceBridge {
} }
extension JITEnableContext { extension JITEnableContext {
func isCryptexDDIInstalled() throws -> Bool { func isDeveloperDiskImageMounted() throws -> Bool {
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
var installed: UnsafeMutablePointer<InstalledCryptexC>? var installed: UnsafeMutablePointer<InstalledCryptexC>?
if let ffiError = cryptexd_installed_ddi(adapter, handshake, &installed) { let ffiError = cryptexd_installed_ddi(adapter, handshake, &installed)
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query installed DDI cryptex") let queryError = ffiError.map {
IdeviceBridge.consumeFFIError($0, fallback: "Failed to query installed DDI cryptex")
}
defer { cryptexd_free_installed_cryptex(installed) }
if installed != nil {
return true
} }
defer { cryptexd_free_installed_cryptex(installed) } let legacyMounted = (try? IdeviceBridge.withConnectedClient(
return installed != nil fallback: "Failed to connect to image mounter",
missingClientMessage: "Image mounter client was not created",
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
cleanup: { image_mounter_free($0) }
) { client in
var devices: UnsafeMutablePointer<plist_t?>?
var deviceCount = 0
if let error = image_mounter_copy_devices(client, &devices, &deviceCount) {
throw IdeviceBridge.consumeFFIError(error, fallback: "Failed to fetch mounted devices")
}
if let devices {
for index in 0..<deviceCount {
plist_free(devices[index])
}
idevice_data_free(
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
)
}
return deviceCount > 0
}) ?? false
if legacyMounted {
return true
}
if let queryError {
throw queryError
}
return false
} }
} }

View file

@ -14,6 +14,8 @@ final class MountingProgress: ObservableObject {
private let mountCheckLock = NSLock() private let mountCheckLock = NSLock()
private var mountCheckInProgress = false private var mountCheckInProgress = false
private let mountLock = NSLock()
private var mountInProgress = false
private init() {} private init() {}
@ -29,66 +31,86 @@ final class MountingProgress: ObservableObject {
mountCheckLock.unlock() mountCheckLock.unlock()
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
let mounted = isMounted() let status = checkMountStatus()
self.mountCheckLock.lock() self.mountCheckLock.lock()
self.mountCheckInProgress = false self.mountCheckInProgress = false
self.mountCheckLock.unlock() self.mountCheckLock.unlock()
DispatchQueue.main.async { DispatchQueue.main.async {
self.coolisMounted = mounted if status != .unreachable {
self.coolisMounted = status == .mounted
}
} }
} }
} }
func pubMount() { func pubMount() {
guard TunnelManager.shared.isConnected else { return } guard TunnelManager.shared.isConnected, DeveloperDiskImageService.filesAreReady else { return }
DispatchQueue.global(qos: .utility).async { [weak self] in mountLock.lock()
guard !mountInProgress else {
mountLock.unlock()
return
}
mountInProgress = true
mountLock.unlock()
let thread = Thread { [weak self] in
self?.mount() self?.mount()
} }
thread.qualityOfService = .background
thread.name = "mounting"
DispatchQueue.main.async {
self.mountingThread = thread
}
thread.start()
} }
private func mount() { private func mount() {
let currentlyMounted = isMounted() switch checkMountStatus() {
DispatchQueue.main.async { case .mounted:
self.coolisMounted = currentlyMounted finishMount {
self.coolisMounted = true
}
return
case .unreachable:
finishMount()
return
case .notMounted:
break
} }
guard isPairing(), !currentlyMounted else { guard isPairing() else {
finishMount()
return return
} }
if let mountingThread { let mountError = mountDeveloperDiskImage(from: DeveloperDiskImageService.directoryURL.path)
mountingThread.cancel() let mounted = mountError == nil || checkMountStatus() == .mounted
self.mountingThread = nil finishMount {
} if mounted {
self.coolisMounted = true
let thread = Thread { [weak self] in self.checkforMounted()
guard let self else { return } } else if let mountError {
let mountError = installCryptexDDI( LogManager.shared.addErrorLog("Failed to install DDI cryptex: \(mountError)")
from: URL.documentsDirectory.appendingPathComponent("DDI_Cryptex").path
)
DispatchQueue.main.async {
if let mountError {
showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
if shouldTryAgain { if shouldTryAgain {
self.pubMount() self.pubMount()
} }
} }
} else {
self.coolisMounted = true
self.checkforMounted()
} }
self.mountingThread = nil
} }
} }
thread.qualityOfService = .background private func finishMount(_ completion: @escaping () -> Void = {}) {
thread.name = "mounting" DispatchQueue.main.async {
thread.start() self.mountingThread = nil
mountingThread = thread self.mountLock.lock()
self.mountInProgress = false
self.mountLock.unlock()
completion()
}
} }
} }

View file

@ -25,17 +25,16 @@ func isMounted() -> Bool {
func checkMountStatus() -> MountCheckResult { func checkMountStatus() -> MountCheckResult {
do { do {
return try JITEnableContext.shared.isCryptexDDIInstalled() ? .mounted : .notMounted return try JITEnableContext.shared.isDeveloperDiskImageMounted() ? .mounted : .notMounted
} catch { } catch {
return .unreachable return .unreachable
} }
} }
func installCryptexDDI(from directoryPath: String) -> String? { func mountDeveloperDiskImage(from directoryPath: String) -> String? {
do { do {
try JITEnableContext.shared.installCryptexDDI(from: directoryPath) try JITEnableContext.shared.installCryptexDDI(from: directoryPath)
} catch { } catch {
LogManager.shared.addErrorLog("Failed to install DDI cryptex: \(error.localizedDescription)")
return error.localizedDescription return error.localizedDescription
} }
return nil return nil

View file

@ -7,6 +7,13 @@ import Foundation
final class DeveloperDiskImageService { final class DeveloperDiskImageService {
static let shared = DeveloperDiskImageService() static let shared = DeveloperDiskImageService()
static let directoryURL = URL.documentsDirectory.appendingPathComponent("DDI", isDirectory: true)
static var filesAreReady: Bool {
downloadItems.allSatisfy {
FileManager.default.fileExists(atPath: directoryURL.appendingPathComponent($0.fileName).path)
}
}
private let fileManager: FileManager private let fileManager: FileManager
private let session: URLSession private let session: URLSession
@ -17,9 +24,16 @@ final class DeveloperDiskImageService {
} }
func downloadMissingFiles() async throws { func downloadMissingFiles() async throws {
let hasCryptexMarker = ["Image.dmg.cryptex_info", "Image.dmg.root_hash"].contains {
fileManager.fileExists(atPath: Self.directoryURL.appendingPathComponent($0).path)
}
let replaceLegacyFiles = !hasCryptexMarker && Self.downloadItems.contains {
fileManager.fileExists(atPath: Self.directoryURL.appendingPathComponent($0.fileName).path)
}
for item in Self.downloadItems { for item in Self.downloadItems {
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath) let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
guard !fileManager.fileExists(atPath: destinationURL.path) else { guard replaceLegacyFiles || !fileManager.fileExists(atPath: destinationURL.path) else {
continue continue
} }
try await downloadFile(from: item.urlString, to: destinationURL) try await downloadFile(from: item.urlString, to: destinationURL)
@ -56,8 +70,12 @@ final class DeveloperDiskImageService {
var completedStages = 0.0 var completedStages = 0.0
progressHandler?(0.0, "Removing existing DDI files...") progressHandler?(0.0, "Removing existing DDI files...")
let completionMarker = Self.directoryURL.appendingPathComponent("Image.dmg.root_hash")
if fileManager.fileExists(atPath: completionMarker.path) {
try fileManager.removeItem(at: completionMarker)
}
for item in Self.downloadItems { for item in Self.downloadItems {
let fileURL = URL.documentsDirectory.appendingPathComponent(item.relativePath) let fileURL = Self.directoryURL.appendingPathComponent(item.fileName)
if fileManager.fileExists(atPath: fileURL.path) { if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL) try fileManager.removeItem(at: fileURL)
} }
@ -68,7 +86,7 @@ final class DeveloperDiskImageService {
for item in Self.downloadItems { for item in Self.downloadItems {
progressHandler?(completedStages / totalStages, "Downloading \(item.name)...") progressHandler?(completedStages / totalStages, "Downloading \(item.name)...")
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath) let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
try await downloadFile(from: item.urlString, to: destinationURL) try await downloadFile(from: item.urlString, to: destinationURL)
completedStages += 1.0 completedStages += 1.0
progressHandler?(completedStages / totalStages, "\(item.name) ready") progressHandler?(completedStages / totalStages, "\(item.name) ready")
@ -80,27 +98,27 @@ final class DeveloperDiskImageService {
private static let downloadItems: [DDIDownloadItem] = [ private static let downloadItems: [DDIDownloadItem] = [
.init( .init(
name: "Build Manifest", name: "Build Manifest",
relativePath: "DDI_Cryptex/BuildManifest.plist", fileName: "BuildManifest.plist",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/BuildManifest.plist" urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/BuildManifest.plist"
), ),
.init( .init(
name: "Image", name: "Image",
relativePath: "DDI_Cryptex/Image.dmg", fileName: "Image.dmg",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg" urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg"
), ),
.init( .init(
name: "TrustCache", name: "TrustCache",
relativePath: "DDI_Cryptex/Image.dmg.trustcache", fileName: "Image.dmg.trustcache",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.trustcache" urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.trustcache"
), ),
.init( .init(
name: "Cryptex Info", name: "Cryptex Info",
relativePath: "DDI_Cryptex/Image.dmg.cryptex_info", fileName: "Image.dmg.cryptex_info",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.cryptex_info" urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.cryptex_info"
), ),
.init( .init(
name: "Root Hash", name: "Root Hash",
relativePath: "DDI_Cryptex/Image.dmg.root_hash", fileName: "Image.dmg.root_hash",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.root_hash" urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.root_hash"
) )
] ]
@ -108,7 +126,7 @@ final class DeveloperDiskImageService {
private struct DDIDownloadItem { private struct DDIDownloadItem {
let name: String let name: String
let relativePath: String let fileName: String
let urlString: String let urlString: String
} }

View file

@ -70,12 +70,7 @@ final class TunnelManager: ObservableObject {
} }
private func mountDeveloperDiskImageIfNeeded() { private func mountDeveloperDiskImageIfNeeded() {
let manifestPath = URL.documentsDirectory.appendingPathComponent("DDI_Cryptex/BuildManifest.plist").path guard DeveloperDiskImageService.filesAreReady else { return }
guard FileManager.default.fileExists(atPath: manifestPath),
!MountingProgress.shared.coolisMounted,
MountingProgress.shared.mountingThread == nil else {
return
}
MountingProgress.shared.pubMount() MountingProgress.shared.pubMount()
} }