From b3126f5d8868d3df0e39d95facf2da63fc976e8f Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:05:43 -0400 Subject: [PATCH] Add developer mode status check and indicator (#264) --- StikJIT/Utilities/mountDDI.swift | 74 ++++++++++++ StikJIT/Views/HomeView.swift | 187 ++++++++++++++++++++++++++++++- 2 files changed, 260 insertions(+), 1 deletion(-) diff --git a/StikJIT/Utilities/mountDDI.swift b/StikJIT/Utilities/mountDDI.swift index 7ecd95f..eb07bb1 100644 --- a/StikJIT/Utilities/mountDDI.swift +++ b/StikJIT/Utilities/mountDDI.swift @@ -14,6 +14,12 @@ typealias AdapterHandle = OpaquePointer typealias ImageMounterHandle = OpaquePointer typealias LockdowndClientHandle = OpaquePointer +enum DeveloperModeQueryResult { + case enabled + case disabled + case unavailable +} + func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) { MountingProgress.shared.progressCallback(progress: progress, total: total, context: context) } @@ -113,6 +119,74 @@ func isMounted() -> Bool { } } +func queryDeveloperModeStatus() -> DeveloperModeQueryResult { + let pairingFilePath = URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path + + guard FileManager.default.fileExists(atPath: pairingFilePath) else { + return .unavailable + } + + var addr = sockaddr_in() + memset(&addr, 0, MemoryLayout.size) + addr.sin_family = sa_family_t(AF_INET) + addr.sin_port = htons(UInt16(LOCKDOWN_PORT)) + + guard inet_pton(AF_INET, "10.7.0.1", &addr.sin_addr) == 1 else { + print("Invalid IP address") + return .unavailable + } + + let sockaddrPointer = UnsafeRawPointer(&addr).bindMemory(to: sockaddr.self, capacity: 1) + + var pairingFile: IdevicePairingFile? + if let err = idevice_pairing_file_read(pairingFilePath, &pairingFile) { + print("Failed to read pairing file: \(err.pointee.code)") + idevice_error_free(err) + return .unavailable + } + + guard let pairingFile else { + return .unavailable + } + + defer { idevice_pairing_file_free(pairingFile) } + + var provider: TcpProviderHandle? + if let providerError = idevice_tcp_provider_new(sockaddrPointer, pairingFile, "DeveloperModeStatus", &provider) { + print("Failed to create TCP provider: \(providerError.pointee.code)") + idevice_error_free(providerError) + return .unavailable + } + + guard let provider else { + return .unavailable + } + + defer { idevice_provider_free(provider) } + + var client: ImageMounterHandle? + if let connectError = image_mounter_connect(provider, &client) { + print("Failed to connect to image mounter: \(connectError.pointee.code)") + idevice_error_free(connectError) + return .unavailable + } + + guard let client else { + return .unavailable + } + + defer { image_mounter_free(client) } + + var status: Int32 = 0 + if let statusError = image_mounter_query_developer_mode_status(client, &status) { + print("Failed to query developer mode status: \(statusError.pointee.code)") + idevice_error_free(statusError) + return .unavailable + } + + return status != 0 ? .enabled : .disabled +} + func mountPersonalDDI(deviceIP: String = "10.7.0.1", imagePath: String, trustcachePath: String, manifestPath: String, pairingFilePath: String) -> Int { idevice_init_logger(Debug, Disabled, nil) diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index f26748b..8829901 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -65,6 +65,11 @@ struct HomeView: View { @State private var launchingSystemApps: Set = [] @State private var systemLaunchMessage: String? = nil + @State private var developerModeStatus: DeveloperModeStatus = .unknown + @State private var isCheckingDeveloperMode = false + @State private var lastDeveloperModeCheck: Date? = nil + @State private var developerModeCheckToken = UUID() + @AppStorage("showiOS26Disclaimer") private var showiOS26Disclaimer: Bool = true @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue @@ -159,8 +164,15 @@ struct HomeView: View { .onChange(of: pairingFileExists) { _, newValue in if newValue { loadAppListIfNeeded(force: cachedAppNames.isEmpty) + developerModeStatus = .unknown + lastDeveloperModeCheck = nil + refreshBackground() } else { cachedAppNames = [:] + developerModeStatus = .unknown + lastDeveloperModeCheck = nil + developerModeCheckToken = UUID() + isCheckingDeveloperMode = false } } .onChange(of: favoriteApps) { _, _ in @@ -439,6 +451,31 @@ struct HomeView: View { tint: .orange ) } + switch developerModeStatus { + case .unknown, .checking: + return .init( + title: "Checking Developer Mode", + subtitle: "Keep your device unlocked while we verify Developer Mode is enabled.", + icon: "gearshape", + tint: .orange + ) + case .disabled: + return .init( + title: "Enable Developer Mode", + subtitle: "Turn on Developer Mode in Settings > Privacy & Security, then restart if iOS asks you to.", + icon: "gearshape.fill", + tint: .orange + ) + case .error: + return .init( + title: "Can’t confirm Developer Mode", + subtitle: "Unlock your device and make sure it’s connected. We’ll try again automatically.", + icon: "questionmark.app.fill", + tint: .yellow + ) + case .enabled: + break + } if !ddiMounted { return .init( title: "Mount the Developer Disk Image", @@ -474,6 +511,16 @@ struct HomeView: View { private var primaryActionTitle: String { if isValidatingPairingFile { return "Validating…" } if !pairingFileExists { return pairingFilePresentOnDisk ? "Re-import Pairing File" : "Import Pairing File" } + switch developerModeStatus { + case .unknown, .checking: + return "Checking Developer Mode…" + case .disabled: + return "Enable Developer Mode" + case .error: + return "Retry Developer Mode Check" + case .enabled: + break + } if !ddiMounted { return "Mount Developer Disk Image" } return "Connect by App" } @@ -481,6 +528,16 @@ struct HomeView: View { private var primaryActionIcon: String { if isValidatingPairingFile { return "hourglass" } if !pairingFileExists { return pairingFilePresentOnDisk ? "arrow.clockwise" : "doc.badge.plus" } + switch developerModeStatus { + case .unknown, .checking: + return "hourglass" + case .disabled: + return "exclamationmark.triangle" + case .error: + return "questionmark.app.fill" + case .enabled: + break + } if !ddiMounted { return "externaldrive" } return "cable.connector.horizontal" } @@ -523,8 +580,55 @@ struct HomeView: View { ) } + let developerModeItem: ChecklistItem = { + if !pairingFileExists { + return ChecklistItem( + title: "Developer Mode", + subtitle: "Import your pairing file to check the Developer Mode status.", + status: .waiting, + actionTitle: nil, + action: nil + ) + } + switch developerModeStatus { + case .unknown, .checking: + return ChecklistItem( + title: "Developer Mode", + subtitle: "Checking developer mode status…", + status: .waiting, + actionTitle: nil, + action: nil + ) + case .enabled: + return ChecklistItem( + title: "Developer Mode", + subtitle: "Enabled.", + status: .ready, + actionTitle: nil, + action: nil + ) + case .disabled: + return ChecklistItem( + title: "Developer Mode", + subtitle: "Turn on Developer Mode in Settings > Privacy & Security.", + status: .actionRequired, + actionTitle: nil, + action: nil + ) + case .error: + return ChecklistItem( + title: "Developer Mode", + subtitle: "We couldn’t determine the status. Unlock your device and keep it connected.", + status: .attention, + actionTitle: nil, + action: nil + ) + } + }() + return [ pairingItem, + developerModeItem, ChecklistItem( title: "Developer Disk Image", subtitle: ddiMounted ? "Mounted successfully." : "", @@ -911,6 +1015,32 @@ struct HomeView: View { private func primaryActionTapped() { guard !isValidatingPairingFile else { return } if pairingFileExists { + switch developerModeStatus { + case .unknown, .checking: + lastDeveloperModeCheck = nil + refreshBackground() + return + case .disabled: + lastDeveloperModeCheck = nil + refreshBackground() + showAlert( + title: "Developer Mode Required".localized, + message: "Turn on Developer Mode in Settings > Privacy & Security, then restart if prompted.".localized, + showOk: true + ) + return + case .error: + lastDeveloperModeCheck = nil + refreshBackground() + showAlert( + title: "Unable to Verify Developer Mode".localized, + message: "Unlock your device and make sure it’s connected to the network, then try again.".localized, + showOk: true + ) + return + case .enabled: + break + } if !ddiMounted { showAlert(title: "Device Not Mounted".localized, message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.".localized, showOk: true) { _ in } return @@ -983,7 +1113,54 @@ struct HomeView: View { let sizeValue = (attributes[.size] as? NSNumber)?.uint64Value ?? 0 return PairingFileSignature(modificationDate: modificationDate, fileSize: sizeValue) } - private func refreshBackground() { } + private func refreshBackground() { + guard pairingFileExists else { + developerModeStatus = .unknown + lastDeveloperModeCheck = nil + developerModeCheckToken = UUID() + isCheckingDeveloperMode = false + return + } + + if isCheckingDeveloperMode { + return + } + + let minimumInterval: TimeInterval = developerModeStatus == .enabled ? 15 : 5 + if let last = lastDeveloperModeCheck, Date().timeIntervalSince(last) < minimumInterval { + return + } + + isCheckingDeveloperMode = true + let checkToken = UUID() + developerModeCheckToken = checkToken + + if developerModeStatus != .enabled { + developerModeStatus = .checking + } + + DispatchQueue.global(qos: .utility).async { + let result = queryDeveloperModeStatus() + DispatchQueue.main.async { + guard developerModeCheckToken == checkToken else { + isCheckingDeveloperMode = false + return + } + + switch result { + case .enabled: + developerModeStatus = .enabled + case .disabled: + developerModeStatus = .disabled + case .unavailable: + developerModeStatus = .error + } + + lastDeveloperModeCheck = Date() + isCheckingDeveloperMode = false + } + } + } private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback { return { pid, debugProxyHandle, semaphore in @@ -1404,6 +1581,14 @@ private struct PairingFileSignature: Equatable { let fileSize: UInt64 } +private enum DeveloperModeStatus { + case unknown + case checking + case enabled + case disabled + case error +} + private enum ChecklistStatus { case ready case waiting