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 {
func isCryptexDDIInstalled() throws -> Bool {
func isDeveloperDiskImageMounted() throws -> Bool {
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
var installed: UnsafeMutablePointer<InstalledCryptexC>?
if let ffiError = cryptexd_installed_ddi(adapter, handshake, &installed) {
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query installed DDI cryptex")
let ffiError = cryptexd_installed_ddi(adapter, handshake, &installed)
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) }
return installed != nil
let legacyMounted = (try? IdeviceBridge.withConnectedClient(
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 var mountCheckInProgress = false
private let mountLock = NSLock()
private var mountInProgress = false
private init() {}
@ -29,66 +31,86 @@ final class MountingProgress: ObservableObject {
mountCheckLock.unlock()
DispatchQueue.global(qos: .utility).async {
let mounted = isMounted()
let status = checkMountStatus()
self.mountCheckLock.lock()
self.mountCheckInProgress = false
self.mountCheckLock.unlock()
DispatchQueue.main.async {
self.coolisMounted = mounted
if status != .unreachable {
self.coolisMounted = status == .mounted
}
}
}
}
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()
}
thread.qualityOfService = .background
thread.name = "mounting"
DispatchQueue.main.async {
self.mountingThread = thread
}
thread.start()
}
private func mount() {
let currentlyMounted = isMounted()
DispatchQueue.main.async {
self.coolisMounted = currentlyMounted
switch checkMountStatus() {
case .mounted:
finishMount {
self.coolisMounted = true
}
return
case .unreachable:
finishMount()
return
case .notMounted:
break
}
guard isPairing(), !currentlyMounted else {
guard isPairing() else {
finishMount()
return
}
if let mountingThread {
mountingThread.cancel()
self.mountingThread = nil
}
let thread = Thread { [weak self] in
guard let self else { return }
let mountError = installCryptexDDI(
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
if shouldTryAgain {
self.pubMount()
}
let mountError = mountDeveloperDiskImage(from: DeveloperDiskImageService.directoryURL.path)
let mounted = mountError == nil || checkMountStatus() == .mounted
finishMount {
if mounted {
self.coolisMounted = true
self.checkforMounted()
} else if let mountError {
LogManager.shared.addErrorLog("Failed to install DDI cryptex: \(mountError)")
showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
if shouldTryAgain {
self.pubMount()
}
} else {
self.coolisMounted = true
self.checkforMounted()
}
self.mountingThread = nil
}
}
}
thread.qualityOfService = .background
thread.name = "mounting"
thread.start()
mountingThread = thread
private func finishMount(_ completion: @escaping () -> Void = {}) {
DispatchQueue.main.async {
self.mountingThread = nil
self.mountLock.lock()
self.mountInProgress = false
self.mountLock.unlock()
completion()
}
}
}

View file

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

View file

@ -7,6 +7,13 @@ import Foundation
final class 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 session: URLSession
@ -17,9 +24,16 @@ final class DeveloperDiskImageService {
}
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 {
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
guard !fileManager.fileExists(atPath: destinationURL.path) else {
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
guard replaceLegacyFiles || !fileManager.fileExists(atPath: destinationURL.path) else {
continue
}
try await downloadFile(from: item.urlString, to: destinationURL)
@ -56,8 +70,12 @@ final class DeveloperDiskImageService {
var completedStages = 0.0
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 {
let fileURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
let fileURL = Self.directoryURL.appendingPathComponent(item.fileName)
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
}
@ -68,7 +86,7 @@ final class DeveloperDiskImageService {
for item in Self.downloadItems {
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)
completedStages += 1.0
progressHandler?(completedStages / totalStages, "\(item.name) ready")
@ -80,27 +98,27 @@ final class DeveloperDiskImageService {
private static let downloadItems: [DDIDownloadItem] = [
.init(
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"
),
.init(
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"
),
.init(
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"
),
.init(
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"
),
.init(
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"
)
]
@ -108,7 +126,7 @@ final class DeveloperDiskImageService {
private struct DDIDownloadItem {
let name: String
let relativePath: String
let fileName: String
let urlString: String
}

View file

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