Skip to content

Commit b3126f5

Browse files
authored
Add developer mode status check and indicator (#264)
1 parent b0ab8c1 commit b3126f5

2 files changed

Lines changed: 260 additions & 1 deletion

File tree

StikJIT/Utilities/mountDDI.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ typealias AdapterHandle = OpaquePointer
1414
typealias ImageMounterHandle = OpaquePointer
1515
typealias LockdowndClientHandle = OpaquePointer
1616

17+
enum DeveloperModeQueryResult {
18+
case enabled
19+
case disabled
20+
case unavailable
21+
}
22+
1723
func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
1824
MountingProgress.shared.progressCallback(progress: progress, total: total, context: context)
1925
}
@@ -113,6 +119,74 @@ func isMounted() -> Bool {
113119
}
114120
}
115121

122+
func queryDeveloperModeStatus() -> DeveloperModeQueryResult {
123+
let pairingFilePath = URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path
124+
125+
guard FileManager.default.fileExists(atPath: pairingFilePath) else {
126+
return .unavailable
127+
}
128+
129+
var addr = sockaddr_in()
130+
memset(&addr, 0, MemoryLayout<sockaddr_in>.size)
131+
addr.sin_family = sa_family_t(AF_INET)
132+
addr.sin_port = htons(UInt16(LOCKDOWN_PORT))
133+
134+
guard inet_pton(AF_INET, "10.7.0.1", &addr.sin_addr) == 1 else {
135+
print("Invalid IP address")
136+
return .unavailable
137+
}
138+
139+
let sockaddrPointer = UnsafeRawPointer(&addr).bindMemory(to: sockaddr.self, capacity: 1)
140+
141+
var pairingFile: IdevicePairingFile?
142+
if let err = idevice_pairing_file_read(pairingFilePath, &pairingFile) {
143+
print("Failed to read pairing file: \(err.pointee.code)")
144+
idevice_error_free(err)
145+
return .unavailable
146+
}
147+
148+
guard let pairingFile else {
149+
return .unavailable
150+
}
151+
152+
defer { idevice_pairing_file_free(pairingFile) }
153+
154+
var provider: TcpProviderHandle?
155+
if let providerError = idevice_tcp_provider_new(sockaddrPointer, pairingFile, "DeveloperModeStatus", &provider) {
156+
print("Failed to create TCP provider: \(providerError.pointee.code)")
157+
idevice_error_free(providerError)
158+
return .unavailable
159+
}
160+
161+
guard let provider else {
162+
return .unavailable
163+
}
164+
165+
defer { idevice_provider_free(provider) }
166+
167+
var client: ImageMounterHandle?
168+
if let connectError = image_mounter_connect(provider, &client) {
169+
print("Failed to connect to image mounter: \(connectError.pointee.code)")
170+
idevice_error_free(connectError)
171+
return .unavailable
172+
}
173+
174+
guard let client else {
175+
return .unavailable
176+
}
177+
178+
defer { image_mounter_free(client) }
179+
180+
var status: Int32 = 0
181+
if let statusError = image_mounter_query_developer_mode_status(client, &status) {
182+
print("Failed to query developer mode status: \(statusError.pointee.code)")
183+
idevice_error_free(statusError)
184+
return .unavailable
185+
}
186+
187+
return status != 0 ? .enabled : .disabled
188+
}
189+
116190
func mountPersonalDDI(deviceIP: String = "10.7.0.1", imagePath: String, trustcachePath: String, manifestPath: String, pairingFilePath: String) -> Int {
117191
idevice_init_logger(Debug, Disabled, nil)
118192

StikJIT/Views/HomeView.swift

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ struct HomeView: View {
6565
@State private var launchingSystemApps: Set<String> = []
6666
@State private var systemLaunchMessage: String? = nil
6767

68+
@State private var developerModeStatus: DeveloperModeStatus = .unknown
69+
@State private var isCheckingDeveloperMode = false
70+
@State private var lastDeveloperModeCheck: Date? = nil
71+
@State private var developerModeCheckToken = UUID()
72+
6873
@AppStorage("showiOS26Disclaimer") private var showiOS26Disclaimer: Bool = true
6974

7075
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
@@ -159,8 +164,15 @@ struct HomeView: View {
159164
.onChange(of: pairingFileExists) { _, newValue in
160165
if newValue {
161166
loadAppListIfNeeded(force: cachedAppNames.isEmpty)
167+
developerModeStatus = .unknown
168+
lastDeveloperModeCheck = nil
169+
refreshBackground()
162170
} else {
163171
cachedAppNames = [:]
172+
developerModeStatus = .unknown
173+
lastDeveloperModeCheck = nil
174+
developerModeCheckToken = UUID()
175+
isCheckingDeveloperMode = false
164176
}
165177
}
166178
.onChange(of: favoriteApps) { _, _ in
@@ -439,6 +451,31 @@ struct HomeView: View {
439451
tint: .orange
440452
)
441453
}
454+
switch developerModeStatus {
455+
case .unknown, .checking:
456+
return .init(
457+
title: "Checking Developer Mode",
458+
subtitle: "Keep your device unlocked while we verify Developer Mode is enabled.",
459+
icon: "gearshape",
460+
tint: .orange
461+
)
462+
case .disabled:
463+
return .init(
464+
title: "Enable Developer Mode",
465+
subtitle: "Turn on Developer Mode in Settings > Privacy & Security, then restart if iOS asks you to.",
466+
icon: "gearshape.fill",
467+
tint: .orange
468+
)
469+
case .error:
470+
return .init(
471+
title: "Can’t confirm Developer Mode",
472+
subtitle: "Unlock your device and make sure it’s connected. We’ll try again automatically.",
473+
icon: "questionmark.app.fill",
474+
tint: .yellow
475+
)
476+
case .enabled:
477+
break
478+
}
442479
if !ddiMounted {
443480
return .init(
444481
title: "Mount the Developer Disk Image",
@@ -474,13 +511,33 @@ struct HomeView: View {
474511
private var primaryActionTitle: String {
475512
if isValidatingPairingFile { return "Validating…" }
476513
if !pairingFileExists { return pairingFilePresentOnDisk ? "Re-import Pairing File" : "Import Pairing File" }
514+
switch developerModeStatus {
515+
case .unknown, .checking:
516+
return "Checking Developer Mode…"
517+
case .disabled:
518+
return "Enable Developer Mode"
519+
case .error:
520+
return "Retry Developer Mode Check"
521+
case .enabled:
522+
break
523+
}
477524
if !ddiMounted { return "Mount Developer Disk Image" }
478525
return "Connect by App"
479526
}
480527

481528
private var primaryActionIcon: String {
482529
if isValidatingPairingFile { return "hourglass" }
483530
if !pairingFileExists { return pairingFilePresentOnDisk ? "arrow.clockwise" : "doc.badge.plus" }
531+
switch developerModeStatus {
532+
case .unknown, .checking:
533+
return "hourglass"
534+
case .disabled:
535+
return "exclamationmark.triangle"
536+
case .error:
537+
return "questionmark.app.fill"
538+
case .enabled:
539+
break
540+
}
484541
if !ddiMounted { return "externaldrive" }
485542
return "cable.connector.horizontal"
486543
}
@@ -523,8 +580,55 @@ struct HomeView: View {
523580
)
524581
}
525582

583+
let developerModeItem: ChecklistItem = {
584+
if !pairingFileExists {
585+
return ChecklistItem(
586+
title: "Developer Mode",
587+
subtitle: "Import your pairing file to check the Developer Mode status.",
588+
status: .waiting,
589+
actionTitle: nil,
590+
action: nil
591+
)
592+
}
593+
switch developerModeStatus {
594+
case .unknown, .checking:
595+
return ChecklistItem(
596+
title: "Developer Mode",
597+
subtitle: "Checking developer mode status…",
598+
status: .waiting,
599+
actionTitle: nil,
600+
action: nil
601+
)
602+
case .enabled:
603+
return ChecklistItem(
604+
title: "Developer Mode",
605+
subtitle: "Enabled.",
606+
status: .ready,
607+
actionTitle: nil,
608+
action: nil
609+
)
610+
case .disabled:
611+
return ChecklistItem(
612+
title: "Developer Mode",
613+
subtitle: "Turn on Developer Mode in Settings > Privacy & Security.",
614+
status: .actionRequired,
615+
actionTitle: nil,
616+
action: nil
617+
)
618+
case .error:
619+
return ChecklistItem(
620+
title: "Developer Mode",
621+
subtitle: "We couldn’t determine the status. Unlock your device and keep it connected.",
622+
status: .attention,
623+
actionTitle: nil,
624+
action: nil
625+
)
626+
}
627+
}()
628+
526629
return [
527630
pairingItem,
631+
developerModeItem,
528632
ChecklistItem(
529633
title: "Developer Disk Image",
530634
subtitle: ddiMounted ? "Mounted successfully." : "",
@@ -911,6 +1015,32 @@ struct HomeView: View {
9111015
private func primaryActionTapped() {
9121016
guard !isValidatingPairingFile else { return }
9131017
if pairingFileExists {
1018+
switch developerModeStatus {
1019+
case .unknown, .checking:
1020+
lastDeveloperModeCheck = nil
1021+
refreshBackground()
1022+
return
1023+
case .disabled:
1024+
lastDeveloperModeCheck = nil
1025+
refreshBackground()
1026+
showAlert(
1027+
title: "Developer Mode Required".localized,
1028+
message: "Turn on Developer Mode in Settings > Privacy & Security, then restart if prompted.".localized,
1029+
showOk: true
1030+
)
1031+
return
1032+
case .error:
1033+
lastDeveloperModeCheck = nil
1034+
refreshBackground()
1035+
showAlert(
1036+
title: "Unable to Verify Developer Mode".localized,
1037+
message: "Unlock your device and make sure it’s connected to the network, then try again.".localized,
1038+
showOk: true
1039+
)
1040+
return
1041+
case .enabled:
1042+
break
1043+
}
9141044
if !ddiMounted {
9151045
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 }
9161046
return
@@ -983,7 +1113,54 @@ struct HomeView: View {
9831113
let sizeValue = (attributes[.size] as? NSNumber)?.uint64Value ?? 0
9841114
return PairingFileSignature(modificationDate: modificationDate, fileSize: sizeValue)
9851115
}
986-
private func refreshBackground() { }
1116+
private func refreshBackground() {
1117+
guard pairingFileExists else {
1118+
developerModeStatus = .unknown
1119+
lastDeveloperModeCheck = nil
1120+
developerModeCheckToken = UUID()
1121+
isCheckingDeveloperMode = false
1122+
return
1123+
}
1124+
1125+
if isCheckingDeveloperMode {
1126+
return
1127+
}
1128+
1129+
let minimumInterval: TimeInterval = developerModeStatus == .enabled ? 15 : 5
1130+
if let last = lastDeveloperModeCheck, Date().timeIntervalSince(last) < minimumInterval {
1131+
return
1132+
}
1133+
1134+
isCheckingDeveloperMode = true
1135+
let checkToken = UUID()
1136+
developerModeCheckToken = checkToken
1137+
1138+
if developerModeStatus != .enabled {
1139+
developerModeStatus = .checking
1140+
}
1141+
1142+
DispatchQueue.global(qos: .utility).async {
1143+
let result = queryDeveloperModeStatus()
1144+
DispatchQueue.main.async {
1145+
guard developerModeCheckToken == checkToken else {
1146+
isCheckingDeveloperMode = false
1147+
return
1148+
}
1149+
1150+
switch result {
1151+
case .enabled:
1152+
developerModeStatus = .enabled
1153+
case .disabled:
1154+
developerModeStatus = .disabled
1155+
case .unavailable:
1156+
developerModeStatus = .error
1157+
}
1158+
1159+
lastDeveloperModeCheck = Date()
1160+
isCheckingDeveloperMode = false
1161+
}
1162+
}
1163+
}
9871164

9881165
private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback {
9891166
return { pid, debugProxyHandle, semaphore in
@@ -1404,6 +1581,14 @@ private struct PairingFileSignature: Equatable {
14041581
let fileSize: UInt64
14051582
}
14061583

1584+
private enum DeveloperModeStatus {
1585+
case unknown
1586+
case checking
1587+
case enabled
1588+
case disabled
1589+
case error
1590+
}
1591+
14071592
private enum ChecklistStatus {
14081593
case ready
14091594
case waiting

0 commit comments

Comments
 (0)