Add developer mode status check and indicator (#264)

This commit is contained in:
Stephen B 2025-09-25 15:05:43 -04:00 • committed by GitHub
parent b0ab8c1d6f
commit b3126f5d88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 260 additions and 1 deletions

View file

@ -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<sockaddr_in>.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)

View file

@ -65,6 +65,11 @@ struct HomeView: View {
@State private var launchingSystemApps: Set<String> = []
@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