From e85e0b3d47ffd5503053e90fd358b9a6827dd66c Mon Sep 17 00:00:00 2001 From: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:09:26 -0600 Subject: [PATCH] feat: smooth cryptex DDI migration --- StikDebug/Device/IdeviceFFIBridge.swift | 42 +++++++-- StikDebug/Device/MountingProgress.swift | 88 ++++++++++++------- StikDebug/Device/mountDDI.swift | 5 +- .../Services/DeveloperDiskImageService.swift | 38 +++++--- StikDebug/Services/TunnelManager.swift | 7 +- 5 files changed, 123 insertions(+), 57 deletions(-) diff --git a/StikDebug/Device/IdeviceFFIBridge.swift b/StikDebug/Device/IdeviceFFIBridge.swift index 372c277..ef034ea 100644 --- a/StikDebug/Device/IdeviceFFIBridge.swift +++ b/StikDebug/Device/IdeviceFFIBridge.swift @@ -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? - 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? + 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...stride) + ) + } + return deviceCount > 0 + }) ?? false + if legacyMounted { + return true + } + if let queryError { + throw queryError + } + return false } } diff --git a/StikDebug/Device/MountingProgress.swift b/StikDebug/Device/MountingProgress.swift index 8915367..0ef26fb 100644 --- a/StikDebug/Device/MountingProgress.swift +++ b/StikDebug/Device/MountingProgress.swift @@ -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() + } } } diff --git a/StikDebug/Device/mountDDI.swift b/StikDebug/Device/mountDDI.swift index 98247fc..1b18623 100644 --- a/StikDebug/Device/mountDDI.swift +++ b/StikDebug/Device/mountDDI.swift @@ -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 diff --git a/StikDebug/Services/DeveloperDiskImageService.swift b/StikDebug/Services/DeveloperDiskImageService.swift index 4b5cc42..cc8b066 100644 --- a/StikDebug/Services/DeveloperDiskImageService.swift +++ b/StikDebug/Services/DeveloperDiskImageService.swift @@ -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 } diff --git a/StikDebug/Services/TunnelManager.swift b/StikDebug/Services/TunnelManager.swift index 67ac636..444b88f 100644 --- a/StikDebug/Services/TunnelManager.swift +++ b/StikDebug/Services/TunnelManager.swift @@ -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() }