Hopefully fix some weird issues

This commit is contained in:
Stephen 2026-05-20 10:07:09 -04:00
parent f9c1f46fad
commit ec147679e1
49 changed files with 2458 additions and 2287 deletions

1
.gitignore vendored
View file

@ -15,6 +15,7 @@ xcuserdata/
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
.derivedData*/
*.moved-aside
*.pbxuser
!default.pbxuser

View file

@ -579,7 +579,7 @@
);
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.StikDebugTests;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
@ -611,7 +611,7 @@
);
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.StikDebugTests;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
@ -634,7 +634,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.StikDebugUITests;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug.uitests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
@ -656,7 +656,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.StikDebugUITests;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug.uitests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";

View file

@ -0,0 +1,47 @@
//
// AppBootstrapper.swift
// StikDebug
//
import Foundation
import ObjectiveC.runtime
import UIKit
enum AppBootstrapper {
static func configure() {
registerDefaultSettings()
startConfiguredKeepAliveServices()
applyDocumentPickerCopyWorkaround()
}
private static func registerDefaultSettings() {
let os = ProcessInfo.processInfo.operatingSystemVersion
let enableAdvancedOptions = os.majorVersion >= 19
UserDefaults.standard.register(defaults: [
"enableAdvancedOptions": enableAdvancedOptions,
UserDefaults.Keys.txmOverride: false,
"keepAliveAudio": true,
"keepAliveLocation": true
])
}
private static func startConfiguredKeepAliveServices() {
guard UserDefaults.standard.bool(forKey: "keepAliveAudio") else {
return
}
BackgroundAudioManager.shared.start()
}
private static func applyDocumentPickerCopyWorkaround() {
let fixedSelector = NSSelectorFromString("fix_initForOpeningContentTypes:asCopy:")
let originalSelector = #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:))
guard let fixedMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, fixedSelector),
let originalMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, originalSelector) else {
return
}
method_exchangeImplementations(originalMethod, fixedMethod)
}
}

View file

@ -298,7 +298,7 @@ struct StikDebugShortcuts: AppShortcutsProvider {
func ensureTunnel() async {
await MainActor.run {
pubTunnelConnected = false
markTunnelDisconnected()
startTunnelInBackground(showErrorUI: false)
}
try? await Task.sleep(nanoseconds: 1_000_000_000)

View file

@ -0,0 +1,58 @@
//
// StikDebugApp.swift
// StikDebug
//
// Created by Stephen on 3/26/25.
//
import SwiftUI
@main
struct StikDebugApp: App {
@Environment(\.scenePhase) private var scenePhase
@State private var shouldAttemptTunnelReconnect = false
init() {
AppBootstrapper.configure()
}
var body: some Scene {
WindowGroup {
MainTabView()
.task {
await downloadMissingDeveloperDiskImageFiles()
}
.onChange(of: scenePhase) { _, newPhase in
handleScenePhaseChange(newPhase)
}
}
}
private func handleScenePhaseChange(_ newPhase: ScenePhase) {
switch newPhase {
case .background:
shouldAttemptTunnelReconnect = true
case .active:
if shouldAttemptTunnelReconnect {
shouldAttemptTunnelReconnect = false
startTunnelInBackground(showErrorUI: false)
}
default:
break
}
}
private func downloadMissingDeveloperDiskImageFiles() async {
do {
try await DeveloperDiskImageService.shared.downloadMissingFiles()
} catch {
await MainActor.run {
showAlert(
title: "An Error has Occurred",
message: "[Download DDI Error]: \(error.localizedDescription)",
showOk: true
)
}
}
}
}

View file

@ -90,13 +90,4 @@ final class LogManager: ObservableObject {
self.errorCount = 0
}
}
func removeOldestLogs(count: Int) {
DispatchQueue.main.async {
let removed = self.logs.prefix(count)
self.logs.removeFirst(count)
let removedErrors = removed.filter { $0.type == .error }.count
self.errorCount = max(0, self.errorCount - removedErrors)
}
}
}

View file

@ -379,7 +379,9 @@ struct CSVDocument: FileDocument {
static var writableContentTypes: [UTType] { [UTType.commaSeparatedText] }
let url: URL?
init(url: URL?) { self.url = url }
init(configuration: ReadConfiguration) throws { fatalError("Not supported") }
init(configuration: ReadConfiguration) throws {
throw CocoaError(.fileReadUnsupportedScheme)
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
guard let url = url else { throw NSError(domain: "CSVDocument", code: -1) }
return try FileWrapper(url: url, options: .immediate)

View file

@ -0,0 +1,92 @@
//
// MountingProgress.swift
// StikDebug
//
import Foundation
import idevice
final class MountingProgress: ObservableObject {
static let shared = MountingProgress()
@Published private(set) var mountProgress: Double = 0.0
@Published private(set) var mountingThread: Thread?
@Published private(set) var coolisMounted: Bool = false
private init() {}
func checkforMounted() {
DispatchQueue.global(qos: .utility).async {
let mounted = isMounted()
DispatchQueue.main.async {
self.coolisMounted = mounted
}
}
}
func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
let percentage = Double(progress) / Double(total) * 100.0
DispatchQueue.main.async {
self.mountProgress = percentage
}
}
func pubMount() {
mount()
}
private func mount() {
let currentlyMounted = isMounted()
DispatchQueue.main.async {
self.coolisMounted = currentlyMounted
}
guard isPairing(), !currentlyMounted else {
return
}
if let mountingThread {
mountingThread.cancel()
self.mountingThread = nil
}
let thread = Thread { [weak self] in
guard let self else { return }
let mountError = mountPersonalDDI(
imagePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg").path,
trustcachePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path,
manifestPath: URL.documentsDirectory.appendingPathComponent("DDI/BuildManifest.plist").path
)
DispatchQueue.main.async {
if let mountError {
showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
if shouldTryAgain {
self.mount()
}
}
} else {
self.coolisMounted = true
self.checkforMounted()
}
self.mountingThread = nil
}
}
thread.qualityOfService = .background
thread.name = "mounting"
thread.start()
mountingThread = thread
}
}
func isPairing() -> Bool {
let pairingPath = PairingFileStore.prepareURL().path
var pairingFile: RpPairingFileHandle?
let error = rp_pairing_file_read(pairingPath, &pairingFile)
if error != nil {
return false
}
rp_pairing_file_free(pairingFile)
return true
}

View file

@ -14,6 +14,14 @@ final class SystemLogStream: ObservableObject {
let timestamp: Date
let message: String
let raw: String
let searchableRaw: String
init(timestamp: Date, message: String, raw: String) {
self.timestamp = timestamp
self.message = message
self.raw = raw
self.searchableRaw = raw.lowercased()
}
}
@Published private(set) var entries: [Entry] = []
@ -167,12 +175,6 @@ final class SystemLogStream: ObservableObject {
return line
}
func concatenatedLog(limit: Int = 500) -> String {
let slice = entries.suffix(limit)
return slice.map { "[\(DateFormatter.consoleFormatter.string(from: $0.timestamp))] \($0.raw)" }
.joined(separator: "\n")
}
private func scheduleFlushIfNeeded() {
guard !isPaused else { return }
guard !pendingEntries.isEmpty else { return }
@ -215,11 +217,3 @@ final class SystemLogStream: ObservableObject {
RunLoop.main.add(timer, forMode: .common)
}
}
private extension DateFormatter {
static let consoleFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter
}()
}

View file

@ -0,0 +1,86 @@
//
// HomeExternalAction.swift
// StikDebug
//
import Foundation
import SwiftUI
struct JITEnableConfiguration {
var bundleID: String?
var pid: Int?
var scriptData: Data?
var scriptName: String?
}
enum HomeExternalAction: Identifiable {
case enableJIT(JITEnableConfiguration)
case killProcess(Int)
case launchApp(String)
var id: String {
switch self {
case .enableJIT(let configuration):
return "enable-\(configuration.bundleID ?? "")-\(configuration.pid ?? 0)-\(configuration.scriptName ?? "")"
case .killProcess(let pid):
return "kill-\(pid)"
case .launchApp(let bundleID):
return "launch-\(bundleID)"
}
}
var title: String {
switch self {
case .enableJIT:
return "Enable JIT?"
case .killProcess:
return "Kill Process?"
case .launchApp:
return "Launch App?"
}
}
var message: String {
switch self {
case .enableJIT(let configuration):
let scriptText = configuration.scriptData == nil ? "" : " and run a script"
return "An external link wants to enable JIT\(scriptText) for \(targetDescription(for: configuration))."
case .killProcess(let pid):
return "An external link wants to kill process \(pid)."
case .launchApp(let bundleID):
return "An external link wants to launch \(bundleID)."
}
}
var confirmationTitle: String {
switch self {
case .enableJIT(let configuration):
return configuration.scriptData == nil ? "Enable JIT" : "Enable and Run Script"
case .killProcess:
return "Kill Process"
case .launchApp:
return "Launch App"
}
}
var role: ButtonRole? {
switch self {
case .enableJIT(let configuration):
return configuration.scriptData == nil ? nil : .destructive
case .killProcess:
return .destructive
case .launchApp:
return nil
}
}
private func targetDescription(for configuration: JITEnableConfiguration) -> String {
if let bundleID = configuration.bundleID {
return bundleID
}
if let pid = configuration.pid {
return "process \(pid)"
}
return "the requested app"
}
}

View file

@ -0,0 +1,252 @@
//
// AppIconRepository.swift
// StikDebug
//
import SwiftUI
import UIKit
enum AppIconRepository {
private static let memory: NSCache<NSString, UIImage> = {
let cache = NSCache<NSString, UIImage>()
cache.countLimit = 2_000
cache.totalCostLimit = 64 * 1024 * 1024
return cache
}()
private static let diskQueue = DispatchQueue(label: "com.stik.iconcache.disk", qos: .utility)
private static let fetchSemaphore = AsyncSemaphore(permits: 4)
private static let registry = IconFetchRegistry()
private static let appGroupIdentifier = "group.com.stik.sj"
static func cachedImage(for bundleID: String) -> UIImage? {
memory.object(forKey: bundleID as NSString)
}
static func image(for bundleID: String) async -> UIImage? {
if let memoryImage = cachedImage(for: bundleID) {
return memoryImage
}
if let diskImage = await loadFromDisk(bundleID: bundleID) {
storeInMemory(diskImage, for: bundleID)
return diskImage
}
return await fetchAndStore(bundleID: bundleID)
}
static func prefetch(bundleIDs: [String]) {
for bundleID in Set(bundleIDs) {
Task.detached(priority: .utility) {
_ = await image(for: bundleID)
}
}
}
private static func fetchAndStore(bundleID: String) async -> UIImage? {
let task = await registry.task(for: bundleID) {
Task.detached(priority: .utility) {
await fetchSemaphore.acquire()
let result: UIImage?
if let fetched = await fetchFromSource(bundleID: bundleID) {
let prepared = prepareForDisplay(fetched)
store(prepared, for: bundleID)
result = prepared
} else {
result = nil
}
await fetchSemaphore.release()
await registry.clear(bundleID: bundleID)
return result
}
}
return await task.value
}
private static func fetchFromSource(bundleID: String) async -> UIImage? {
await withCheckedContinuation { continuation in
AppStoreIconFetcher.getIcon(for: bundleID) { image in
continuation.resume(returning: image)
}
}
}
private static func loadFromDisk(bundleID: String) async -> UIImage? {
let imageScale = await MainActor.run { UIScreen.main.scale }
return await withCheckedContinuation { (continuation: CheckedContinuation<UIImage?, Never>) in
diskQueue.async {
guard let url = iconURL(for: bundleID),
FileManager.default.fileExists(atPath: url.path) else {
continuation.resume(returning: nil)
return
}
guard let data = try? Data(contentsOf: url) else {
try? FileManager.default.removeItem(at: url)
continuation.resume(returning: nil)
return
}
guard let image = UIImage(data: data, scale: imageScale) else {
try? FileManager.default.removeItem(at: url)
continuation.resume(returning: nil)
return
}
continuation.resume(returning: prepareForDisplay(image))
}
}
}
private static func store(_ image: UIImage, for bundleID: String) {
storeInMemory(image, for: bundleID)
storeOnDisk(image, bundleID: bundleID)
}
private static func storeInMemory(_ image: UIImage, for bundleID: String) {
memory.setObject(image, forKey: bundleID as NSString, cost: memoryCost(for: image))
}
private static func storeOnDisk(_ image: UIImage, bundleID: String) {
diskQueue.async {
guard let url = iconURL(for: bundleID),
let data = image.pngData() else {
return
}
try? data.write(to: url, options: .atomic)
}
}
private static func iconURL(for bundleID: String) -> URL? {
guard let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier),
let fileName = cacheFileName(for: bundleID) else {
return nil
}
let directory = container.appendingPathComponent("icons", isDirectory: true)
do {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
} catch {
return nil
}
return directory.appendingPathComponent(fileName)
}
private static func cacheFileName(for bundleID: String) -> String? {
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: ".-_"))
let sanitized = String(bundleID.unicodeScalars.map { scalar in
allowed.contains(scalar) ? Character(scalar) : "_"
})
guard !sanitized.isEmpty else {
return nil
}
return "\(sanitized).png"
}
private static func memoryCost(for image: UIImage) -> Int {
let width = Int(image.size.width * image.scale)
let height = Int(image.size.height * image.scale)
return max(width * height * 4, 1)
}
private static func prepareForDisplay(_ image: UIImage) -> UIImage {
image.preparingForDisplay() ?? image
}
}
@MainActor
final class IconLoader: ObservableObject {
@Published private(set) var image: UIImage?
private let bundleID: String
private var didStart = false
init(bundleID: String) {
self.bundleID = bundleID
if let cached = AppIconRepository.cachedImage(for: bundleID) {
image = cached
didStart = true
}
}
func beginLoading() {
if image != nil {
didStart = true
return
}
guard !didStart else {
return
}
didStart = true
let targetID = bundleID
Task { [weak self] in
if let resolved = await AppIconRepository.image(for: targetID) {
guard let self else { return }
withAnimation(.linear(duration: 0.12)) {
self.image = resolved
}
} else {
self?.didStart = false
}
}
}
}
private actor IconFetchRegistry {
private var tasks: [String: Task<UIImage?, Never>] = [:]
func task(for bundleID: String, create: () -> Task<UIImage?, Never>) -> Task<UIImage?, Never> {
if let existing = tasks[bundleID] {
return existing
}
let task = create()
tasks[bundleID] = task
return task
}
func clear(bundleID: String) {
tasks[bundleID] = nil
}
}
private actor AsyncSemaphore {
private var permits: Int
private var waiters: [CheckedContinuation<Void, Never>] = []
init(permits: Int) {
self.permits = permits
}
func acquire() async {
if permits > 0 {
permits -= 1
return
}
await withCheckedContinuation { continuation in
waiters.append(continuation)
}
}
func release() {
if let next = waiters.first {
waiters.removeFirst()
next.resume()
} else {
permits += 1
}
}
}

View file

@ -0,0 +1,46 @@
//
// InstalledAppListItem.swift
// StikDebug
//
import Foundation
struct InstalledAppListItem: Identifiable, Equatable {
let bundleID: String
let name: String
private let normalizedBundleID: String
private let normalizedName: String
var id: String {
bundleID
}
init(bundleID: String, name: String) {
self.bundleID = bundleID
self.name = name
self.normalizedBundleID = Self.normalized(bundleID)
self.normalizedName = Self.normalized(name)
}
func matches(_ query: String) -> Bool {
query.isEmpty || normalizedBundleID.contains(query) || normalizedName.contains(query)
}
static func sorted(from apps: [String: String]) -> [InstalledAppListItem] {
apps.map { InstalledAppListItem(bundleID: $0.key, name: $0.value) }
.sorted { lhs, rhs in
let comparison = lhs.name.localizedCaseInsensitiveCompare(rhs.name)
if comparison == .orderedSame {
return lhs.bundleID < rhs.bundleID
}
return comparison == .orderedAscending
}
}
static func normalized(_ text: String) -> String {
text.trimmingCharacters(in: .whitespacesAndNewlines)
.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)
.lowercased()
}
}

View file

@ -0,0 +1,361 @@
//
// InstalledAppRows.swift
// StikDebug
//
import SwiftUI
import UIKit
import WidgetKit
struct AppButton: View {
let bundleID: String
let appName: String
@Binding var recentApps: [String]
@Binding var favoriteApps: [String]
@AppStorage("loadAppIconsOnJIT") private var loadAppIconsOnJIT = true
@AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false
var onSelectApp: (String, String) -> Void
let sharedDefaults: UserDefaults
@State private var showScriptPicker = false
@State private var assignedScriptName: String?
@StateObject private var iconLoader: IconLoader
init(
bundleID: String,
appName: String,
recentApps: Binding<[String]>,
favoriteApps: Binding<[String]>,
onSelectApp: @escaping (String, String) -> Void,
sharedDefaults: UserDefaults
) {
self.bundleID = bundleID
self.appName = appName
self._recentApps = recentApps
self._favoriteApps = favoriteApps
self.onSelectApp = onSelectApp
self.sharedDefaults = sharedDefaults
_iconLoader = StateObject(wrappedValue: IconLoader(bundleID: bundleID))
_assignedScriptName = State(initialValue: AppButton.currentAssignment(for: bundleID))
}
var body: some View {
Button(action: selectApp) {
HStack(spacing: loadAppIconsOnJIT ? 16 : 12) {
AppIconView(image: loadAppIconsOnJIT ? iconLoader.image : nil)
VStack(alignment: .leading, spacing: 3) {
Text(appName)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.primary)
.lineLimit(1)
Text(bundleID)
.font(.system(size: 14))
.foregroundStyle(.secondary)
.lineLimit(1)
.textSelection(.enabled)
}
Spacer()
if favoriteApps.contains(bundleID) {
Image(systemName: "star.fill")
.imageScale(.medium)
.foregroundStyle(.yellow)
.accessibilityHidden(true)
}
}
.padding(.vertical, loadAppIconsOnJIT ? 4 : 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.contextMenu {
Button(action: toggleFavorite) {
Label(
favoriteApps.contains(bundleID) ? "Remove Favorite" : "Add to Favorites",
systemImage: favoriteApps.contains(bundleID) ? "star.slash" : "star"
)
.disabled(!favoriteApps.contains(bundleID) && favoriteApps.count >= 4)
}
Button {
copyBundleID()
} label: {
Label("Copy Bundle ID", systemImage: "doc.on.doc")
}
if enableAdvancedOptions {
Button {
showScriptPicker = true
} label: {
Label("Assign Script", systemImage: "chevron.left.slash.chevron.right")
}
if assignedScriptName != nil {
Button(action: resetScriptAssignment) {
Label("Reset Script", systemImage: "arrow.uturn.left")
}
}
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
toggleFavorite()
} label: {
Label(favoriteApps.contains(bundleID) ? "Unfavorite" : "Favorite", systemImage: "star")
}
.tint(.yellow)
Button {
copyBundleID()
} label: {
Label("Copy ID", systemImage: "doc.on.doc")
}
}
.sheet(isPresented: $showScriptPicker) {
ScriptListView { url in
assignScript(url)
showScriptPicker = false
}
}
.onAppear(perform: beginIconLoadingIfNeeded)
.onChange(of: loadAppIconsOnJIT) { _, newValue in
if newValue {
iconLoader.beginLoading()
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(format: "Enable JIT for %@".localized, appName))
.accessibilityValue(accessibilityValue)
.accessibilityHint("Double-tap to open the app and enable JIT. Use the actions rotor for favorites or bundle ID.".localized)
.accessibilityAddTraits(.isButton)
.accessibilityRemoveTraits(.isStaticText)
.accessibilityAction(named: Text(favoriteAccessibilityActionLabel)) {
toggleFavorite()
}
.accessibilityAction(named: Text("Copy Bundle ID".localized)) {
copyBundleID()
}
}
private var accessibilityValue: String {
var parts = [String(format: "Bundle ID %@".localized, bundleID)]
if favoriteApps.contains(bundleID) {
parts.append("Favorite".localized)
}
if let assignedScriptName {
parts.append(String(format: "Assigned script %@".localized, assignedScriptName))
}
return parts.joined(separator: ", ")
}
private var favoriteAccessibilityActionLabel: String {
favoriteApps.contains(bundleID)
? "Remove from Favorites".localized
: "Add to Favorites".localized
}
private func beginIconLoadingIfNeeded() {
guard loadAppIconsOnJIT else {
return
}
iconLoader.beginLoading()
}
private func selectApp() {
Haptics.selection()
recentApps.removeAll { $0 == bundleID }
recentApps.insert(bundleID, at: 0)
if recentApps.count > 3 {
recentApps = Array(recentApps.prefix(3))
}
persistIfChanged()
onSelectApp(bundleID, appName)
}
private func toggleFavorite() {
Haptics.light()
let wasFavorite = favoriteApps.contains(bundleID)
if wasFavorite {
favoriteApps.removeAll { $0 == bundleID }
} else if favoriteApps.count < 4 {
favoriteApps.insert(bundleID, at: 0)
recentApps.removeAll { $0 == bundleID }
} else {
AccessibilityAnnouncer.announce("Favorites are full".localized)
return
}
persistIfChanged()
AccessibilityAnnouncer.announce(wasFavorite ? "Removed from Favorites".localized : "Added to Favorites".localized)
}
private func copyBundleID() {
UIPasteboard.general.string = bundleID
Haptics.light()
AccessibilityAnnouncer.announce("Bundle ID copied".localized)
}
private func assignScript(_ url: URL?) {
if let url {
let filename = url.lastPathComponent
ScriptStore.updateAssignedScriptName(filename, for: bundleID)
assignedScriptName = filename
} else {
ScriptStore.updateAssignedScriptName(nil, for: bundleID)
assignedScriptName = nil
}
Haptics.light()
}
private func resetScriptAssignment() {
assignScript(nil)
}
private static func currentAssignment(for bundleID: String) -> String? {
ScriptStore.assignedScriptName(for: bundleID)
}
private func persistIfChanged() {
var touched = false
let previousRecents = (sharedDefaults.array(forKey: "recentApps") as? [String]) ?? []
let previousFavorites = (sharedDefaults.array(forKey: "favoriteApps") as? [String]) ?? []
if previousRecents != recentApps {
sharedDefaults.set(recentApps, forKey: "recentApps")
touched = true
}
if previousFavorites != favoriteApps {
sharedDefaults.set(favoriteApps, forKey: "favoriteApps")
touched = true
}
if touched {
WidgetCenter.shared.reloadAllTimelines()
}
}
}
struct LaunchAppRow: View {
let bundleID: String
let appName: String
let isLaunching: Bool
var launchAction: () -> Void
@AppStorage("loadAppIconsOnJIT") private var loadAppIconsOnJIT = true
@StateObject private var iconLoader: IconLoader
init(
bundleID: String,
appName: String,
isLaunching: Bool,
launchAction: @escaping () -> Void
) {
self.bundleID = bundleID
self.appName = appName
self.isLaunching = isLaunching
self.launchAction = launchAction
_iconLoader = StateObject(wrappedValue: IconLoader(bundleID: bundleID))
}
var body: some View {
Button {
guard !isLaunching else { return }
launchAction()
} label: {
HStack(spacing: loadAppIconsOnJIT ? 16 : 12) {
AppIconView(image: loadAppIconsOnJIT ? iconLoader.image : nil)
VStack(alignment: .leading, spacing: 3) {
Text(appName)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.primary)
.lineLimit(1)
Text(bundleID)
.font(.system(size: 14))
.foregroundStyle(.secondary)
.lineLimit(1)
.textSelection(.enabled)
}
Spacer()
if isLaunching {
ProgressView().controlSize(.small)
} else {
Text("Launch".localized)
.font(.footnote.weight(.semibold))
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Capsule().fill(Color.accentColor.opacity(0.18)))
.foregroundStyle(Color.accentColor)
}
}
.padding(.vertical, loadAppIconsOnJIT ? 4 : 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.disabled(isLaunching)
.onAppear(perform: beginIconLoadingIfNeeded)
.onChange(of: loadAppIconsOnJIT) { _, newValue in
if newValue {
iconLoader.beginLoading()
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(format: "Launch %@".localized, appName))
.accessibilityValue(accessibilityValue)
.accessibilityHint(isLaunching
? "Launch request in progress.".localized
: "Double-tap to launch this app without enabling JIT.".localized)
.accessibilityAddTraits(.isButton)
.accessibilityRemoveTraits(.isStaticText)
.accessibilityAction(named: Text("Launch App".localized)) {
guard !isLaunching else { return }
launchAction()
}
}
private var accessibilityValue: String {
let state = isLaunching ? "Launching".localized : "Ready".localized
return "\(state), \(String(format: "Bundle ID %@".localized, bundleID))"
}
private func beginIconLoadingIfNeeded() {
guard loadAppIconsOnJIT else {
return
}
iconLoader.beginLoading()
}
}
private struct AppIconView: View {
let image: UIImage?
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 54, height: 54)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.shadow(color: .black.opacity(0.18), radius: 3, x: 0, y: 1.5)
.transition(.opacity.combined(with: .scale))
} else {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(UIColor.systemGray5))
.frame(width: 54, height: 54)
.overlay {
Image(systemName: "app")
.font(.system(size: 24, weight: .regular))
.foregroundStyle(.gray)
}
}
}
.accessibilityHidden(true)
}
}

View file

@ -0,0 +1,147 @@
//
// InstalledAppsViewModel.swift
// StikDebug
//
import Combine
import Foundation
final class InstalledAppsViewModel: ObservableObject {
@Published private(set) var debuggableApps: [String: String] = [:]
@Published private(set) var nonDebuggableApps: [String: String] = [:]
@Published private(set) var systemApps: [String: String] = [:]
@Published private(set) var debuggableItems: [InstalledAppListItem] = []
@Published private(set) var launchItems: [InstalledAppListItem] = []
@Published private(set) var isLoading = false
@Published private(set) var lastError: String?
private let workQueue = DispatchQueue(label: "com.stik.installedApps", qos: .userInitiated)
private let cache = UserDefaults(suiteName: ScriptStore.favoriteAppNamesSuiteName) ?? .standard
private let cacheKeyDebuggable = "cachedDebuggableApps"
private let cacheKeyNonDebuggable = "cachedNonDebuggableApps"
private let cacheKeySystem = "cachedSystemApps"
init() {
loadCachedApps()
refreshAppLists()
}
func refreshAppLists() {
isLoading = true
lastError = nil
workQueue.async { [weak self] in
guard let self else { return }
do {
let debuggable = try JITEnableContext.shared.getAppList()
let allApps = try JITEnableContext.shared.getAllApps()
let hiddenSystem = (try? JITEnableContext.shared.getHiddenSystemApps()) ?? [:]
let classifiedApps = Self.classifyApps(
allApps: allApps,
debuggable: debuggable,
hiddenSystem: hiddenSystem
)
DispatchQueue.main.async {
self.apply(
debuggable: debuggable,
nonDebuggable: classifiedApps.nonDebuggable,
system: classifiedApps.system
)
self.isLoading = false
self.cacheApps(
debuggable: debuggable,
nonDebuggable: classifiedApps.nonDebuggable,
system: classifiedApps.system
)
}
} catch {
DispatchQueue.main.async {
self.isLoading = false
self.lastError = error.localizedDescription
}
}
}
}
func displayName(for bundleID: String) -> String? {
debuggableApps[bundleID] ?? systemApps[bundleID] ?? nonDebuggableApps[bundleID]
}
func launchWithoutDebug(bundleID: String, completion: @escaping (Bool) -> Void) {
workQueue.async {
let success = JITEnableContext.shared.launchAppWithoutDebug(bundleID, logger: nil)
DispatchQueue.main.async {
completion(success)
}
}
}
private func loadCachedApps() {
let cachedDebuggable = decode(cacheKeyDebuggable)
let cachedNonDebuggable = decode(cacheKeyNonDebuggable)
let cachedSystem = decode(cacheKeySystem)
if !cachedDebuggable.isEmpty || !cachedNonDebuggable.isEmpty || !cachedSystem.isEmpty {
apply(debuggable: cachedDebuggable, nonDebuggable: cachedNonDebuggable, system: cachedSystem)
}
}
private func apply(debuggable: [String: String], nonDebuggable: [String: String], system: [String: String]) {
debuggableApps = debuggable
nonDebuggableApps = nonDebuggable
systemApps = system
debuggableItems = InstalledAppListItem.sorted(from: debuggable)
launchItems = InstalledAppListItem.sorted(from: Self.launchApps(nonDebuggable: nonDebuggable, system: system))
}
private func cacheApps(debuggable: [String: String], nonDebuggable: [String: String], system: [String: String]) {
cache.set(encode(debuggable), forKey: cacheKeyDebuggable)
cache.set(encode(nonDebuggable), forKey: cacheKeyNonDebuggable)
cache.set(encode(system), forKey: cacheKeySystem)
}
private func decode(_ key: String) -> [String: String] {
guard let data = cache.data(forKey: key),
let decoded = try? JSONDecoder().decode([String: String].self, from: data) else {
return [:]
}
return decoded
}
private func encode(_ value: [String: String]) -> Data? {
try? JSONEncoder().encode(value)
}
private static func classifyApps(
allApps: [String: String],
debuggable: [String: String],
hiddenSystem: [String: String]
) -> (nonDebuggable: [String: String], system: [String: String]) {
var nonDebuggable: [String: String] = [:]
var system: [String: String] = [:]
for (bundleID, name) in allApps where debuggable[bundleID] == nil {
if let hiddenName = hiddenSystem[bundleID] {
system[bundleID] = hiddenName
} else {
nonDebuggable[bundleID] = name
}
}
for (bundleID, name) in hiddenSystem where system[bundleID] == nil && debuggable[bundleID] == nil {
system[bundleID] = name
}
return (nonDebuggable, system)
}
private static func launchApps(nonDebuggable: [String: String], system: [String: String]) -> [String: String] {
var combined = nonDebuggable
for (bundleID, name) in system {
combined[bundleID] = name
}
return combined
}
}

View file

@ -8,10 +8,11 @@
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.stik.StikDebug.enableJIT</string>
<string>com.stik.stikdebug.enableJIT</string>
<key>CFBundleURLSchemes</key>
<array>
<string>stikdebug</string>
<string>stikjit</string>
</array>
</dict>
</array>
@ -40,7 +41,7 @@
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.stik.StikDebug.stiktool</string>
<string>com.stik.stikdebug.stiktool</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>

View file

@ -10,7 +10,7 @@ import UIKit
class AppStoreIconFetcher {
private static var cache = [String: UIImage]()
private static let queue = DispatchQueue(label: "com.stik.StikDebug.iconFetchQueue", attributes: .concurrent)
private static let queue = DispatchQueue(label: "com.stik.stikdebug.iconFetchQueue", attributes: .concurrent)
static func getIcon(for bundleID: String, completion: @escaping (UIImage?) -> Void) {
if let icon = cache[bundleID] {

View file

@ -0,0 +1,64 @@
//
// DebugKeepAliveLease.swift
// StikDebug
//
import Foundation
import UIKit
final class DebugKeepAliveLease {
private let stateLock = NSLock()
private var isActive = false
private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
init() {
activate()
}
func invalidate() {
stateLock.lock()
guard isActive else {
stateLock.unlock()
return
}
isActive = false
stateLock.unlock()
runOnMain {
BackgroundAudioManager.shared.requestStop()
BackgroundLocationManager.shared.requestStop()
if self.backgroundTaskID != .invalid {
UIApplication.shared.endBackgroundTask(self.backgroundTaskID)
self.backgroundTaskID = .invalid
}
}
}
private func activate() {
stateLock.lock()
guard !isActive else {
stateLock.unlock()
return
}
isActive = true
stateLock.unlock()
runOnMain {
BackgroundAudioManager.shared.requestStart()
BackgroundLocationManager.shared.requestStart()
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "StikDebugDebugSession") { [weak self] in
LogManager.shared.addWarningLog("Debug session background task expired")
self?.invalidate()
}
}
}
private func runOnMain(_ work: @escaping () -> Void) {
if Thread.isMainThread {
work()
} else {
DispatchQueue.main.sync(execute: work)
}
}
}

View file

@ -0,0 +1,124 @@
//
// DeveloperDiskImageService.swift
// StikDebug
//
import Foundation
final class DeveloperDiskImageService {
static let shared = DeveloperDiskImageService()
private let fileManager: FileManager
private let session: URLSession
init(fileManager: FileManager = .default, session: URLSession = .shared) {
self.fileManager = fileManager
self.session = session
}
func downloadMissingFiles() async throws {
for item in Self.downloadItems {
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
guard !fileManager.fileExists(atPath: destinationURL.path) else {
continue
}
try await downloadFile(from: item.urlString, to: destinationURL)
}
}
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
guard let url = URL(string: urlString),
url.scheme?.lowercased() == "https" else {
throw DDIDownloadError.invalidURL(urlString)
}
let (temporaryURL, response) = try await session.download(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw DDIDownloadError.invalidResponse
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw DDIDownloadError.badStatus(httpResponse.statusCode)
}
try fileManager.createDirectory(
at: destinationURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.moveItem(at: temporaryURL, to: destinationURL)
}
func redownload(progressHandler: ((Double, String) -> Void)? = nil) async throws {
let totalStages = Double(Self.downloadItems.count + 1)
var completedStages = 0.0
progressHandler?(0.0, "Removing existing DDI files...")
for item in Self.downloadItems {
let fileURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
}
}
completedStages += 1.0
progressHandler?(completedStages / totalStages, "Starting downloads...")
for item in Self.downloadItems {
progressHandler?(completedStages / totalStages, "Downloading \(item.name)...")
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
try await downloadFile(from: item.urlString, to: destinationURL)
completedStages += 1.0
progressHandler?(completedStages / totalStages, "\(item.name) ready")
}
progressHandler?(1.0, "DDI download complete.")
}
private static let downloadItems: [DDIDownloadItem] = [
.init(
name: "Build Manifest",
relativePath: "DDI/BuildManifest.plist",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist"
),
.init(
name: "Image",
relativePath: "DDI/Image.dmg",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg"
),
.init(
name: "TrustCache",
relativePath: "DDI/Image.dmg.trustcache",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache"
)
]
}
private struct DDIDownloadItem {
let name: String
let relativePath: String
let urlString: String
}
enum DDIDownloadError: LocalizedError {
case invalidURL(String)
case invalidResponse
case badStatus(Int)
var errorDescription: String? {
switch self {
case .invalidURL(let string):
return "Invalid download URL: \(string)"
case .invalidResponse:
return "The DDI server returned an invalid response."
case .badStatus(let statusCode):
return "The DDI server returned HTTP \(statusCode)."
}
}
}
func redownloadDDI(progressHandler: ((Double, String) -> Void)? = nil) async throws {
try await DeveloperDiskImageService.shared.redownload(progressHandler: progressHandler)
}

View file

@ -0,0 +1,215 @@
//
// TunnelManager.swift
// StikDebug
//
import Foundation
final class TunnelManager: ObservableObject {
static let shared = TunnelManager()
@Published private(set) var isConnected = false
private var isStarting = false
private init() {}
func markDisconnected() {
runOnMain {
self.isConnected = false
}
}
func start(showErrorUI: Bool = true) {
guard Thread.isMainThread else {
DispatchQueue.main.async {
self.start(showErrorUI: showErrorUI)
}
return
}
let pairingFileURL = PairingFileStore.prepareURL()
guard FileManager.default.fileExists(atPath: pairingFileURL.path) else {
isConnected = false
return
}
guard !isStarting else {
return
}
isStarting = true
DispatchQueue.global(qos: .userInteractive).async { [showErrorUI] in
let result: Result<Void, NSError>
do {
try JITEnableContext.shared.startTunnel()
result = .success(())
} catch {
result = .failure(error as NSError)
}
DispatchQueue.main.async {
self.finishStart(result, showErrorUI: showErrorUI)
}
}
}
private func finishStart(_ result: Result<Void, NSError>, showErrorUI: Bool) {
isStarting = false
switch result {
case .success:
isConnected = true
LogManager.shared.addInfoLog("Tunnel connected successfully")
mountDeveloperDiskImageIfNeeded()
case .failure(let error):
isConnected = false
handleStartFailure(error, showErrorUI: showErrorUI)
}
}
private func mountDeveloperDiskImageIfNeeded() {
let trustcachePath = URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path
guard FileManager.default.fileExists(atPath: trustcachePath),
!MountingProgress.shared.coolisMounted,
MountingProgress.shared.mountingThread == nil else {
return
}
MountingProgress.shared.pubMount()
}
private func handleStartFailure(_ error: NSError, showErrorUI: Bool) {
LogManager.shared.addErrorLog(tunnelConnectionLogMessage(for: error))
guard showErrorUI else {
return
}
if error.code == -9 {
handleInvalidPairingFile()
return
}
showAlert(
title: "Connection Error",
message: tunnelConnectionAlertMessage(for: error),
showOk: false,
showTryAgain: true
) { shouldTryAgain in
if shouldTryAgain {
startTunnelInBackground()
}
}
}
private func handleInvalidPairingFile() {
do {
try PairingFileStore.remove()
LogManager.shared.addInfoLog("Removed invalid pairing file")
} catch {
LogManager.shared.addErrorLog("Failed to remove invalid pairing file: \(error.localizedDescription)")
}
showAlert(
title: "Invalid Pairing File",
message: "The pairing file is invalid or expired. Please select a new pairing file.",
showOk: true,
showTryAgain: false,
primaryButtonText: "Select New File"
) { _ in
NotificationCenter.default.post(name: NSNotification.Name("ShowPairingFilePicker"), object: nil)
}
}
private func runOnMain(_ work: @escaping () -> Void) {
if Thread.isMainThread {
work()
} else {
DispatchQueue.main.async(execute: work)
}
}
}
func startTunnelInBackground(showErrorUI: Bool = true) {
TunnelManager.shared.start(showErrorUI: showErrorUI)
}
func markTunnelDisconnected() {
TunnelManager.shared.markDisconnected()
}
private func tunnelConnectionLogMessage(for error: NSError) -> String {
let target = "\(DeviceConnectionContext.targetIPAddress):49152"
return "Tunnel connection failed for \(target): \(error.localizedDescription) (Domain: \(error.domain), Code: \(error.code), Raw: \(String(describing: error)))"
}
private func tunnelConnectionAlertMessage(for error: NSError) -> String {
let targetIP = DeviceConnectionContext.targetIPAddress
let rawMessage = error.localizedDescription
let lowercasedMessage = rawMessage.lowercased()
let likelyCause: String
let recoverySteps: [String]
if error.code == 48 || lowercasedMessage.contains("address already in use") || lowercasedMessage.contains("port already in use") {
likelyCause = "A port needed for the tunnel is already in use."
recoverySteps = [
"Close other JIT, debugging, proxy, or VPN apps that may be using the tunnel.",
"Disconnect and reconnect LocalDevVPN.",
"Restart StikDebug, then try again.",
"If it keeps happening, reboot the device to clear the stuck port."
]
} else if error.code == 54 || lowercasedMessage.contains("connection reset") {
likelyCause = "The device or VPN closed the tunnel connection before setup finished."
recoverySteps = [
"Open LocalDevVPN and confirm the VPN is connected.",
"Make sure LocalDevVPN is using the default \(DeviceConnectionContext.defaultTargetIPAddress) address.",
"Reconnect Wi-Fi and LocalDevVPN, then try again.",
"If this keeps happening, select a fresh pairing file."
]
} else if error.code == -18 || lowercasedMessage.contains("parse target ip") {
likelyCause = "The configured target IP address is not valid."
recoverySteps = [
"Open Settings and check the target IP address.",
"Use the default \(DeviceConnectionContext.defaultTargetIPAddress)."
]
} else if lowercasedMessage.contains("timed out") || lowercasedMessage.contains("timeout") {
likelyCause = "The app could not reach the device before the connection timed out."
recoverySteps = [
"Confirm Wi-Fi and LocalDevVPN are both connected.",
"Wake and unlock the target device.",
"Confirm LocalDevVPN is exposing the device at \(targetIP)."
]
} else if lowercasedMessage.contains("network is unreachable") || lowercasedMessage.contains("no route") {
likelyCause = "The VPN route to the device is not available."
recoverySteps = [
"Disconnect and reconnect LocalDevVPN.",
"Confirm iOS shows the VPN indicator.",
"Try switching Wi-Fi off and on."
]
} else {
likelyCause = "The tunnel could not be created."
recoverySteps = [
"Confirm Wi-Fi and LocalDevVPN are connected.",
"Wake and unlock the target device.",
"Reconnect LocalDevVPN, then try again."
]
}
let steps = recoverySteps.enumerated()
.map { "\($0.offset + 1). \($0.element)" }
.joined(separator: "\n")
return """
\(likelyCause)
Target: \(targetIP):49152
Expected LocalDevVPN IP: \(DeviceConnectionContext.defaultTargetIPAddress)
Try this:
\(steps)
Technical details:
Code \(error.code): \(rawMessage)
"""
}

View file

@ -1,601 +0,0 @@
//
// StikDebugApp.swift
// StikDebug
//
// Created by Stephen on 3/26/25.
//
import SwiftUI
import Network
import idevice
// Register default settings before the app starts
private func registerAdvancedOptionsDefault() {
let os = ProcessInfo.processInfo.operatingSystemVersion
// Enable advanced options by default on iOS 19/26 and above
let enabled = os.majorVersion >= 19
UserDefaults.standard.register(defaults: ["enableAdvancedOptions": enabled])
UserDefaults.standard.register(defaults: [UserDefaults.Keys.txmOverride: false])
UserDefaults.standard.register(defaults: ["keepAliveAudio": true])
UserDefaults.standard.register(defaults: ["keepAliveLocation": true])
}
// MARK: - DNS Checker
class DNSChecker: ObservableObject {
@Published var appleIP: String?
@Published var controlIP: String?
@Published var dnsError: String?
func checkDNS() {
checkIfConnectedToWifi { [weak self] wifiConnected in
guard let self = self else { return }
if wifiConnected {
let group = DispatchGroup()
group.enter()
self.lookupIPAddress(for: "gs.apple.com") { ip in
DispatchQueue.main.async {
self.appleIP = ip
}
group.leave()
}
group.enter()
self.lookupIPAddress(for: "google.com") { ip in
DispatchQueue.main.async {
self.controlIP = ip
}
group.leave()
}
group.notify(queue: .main) {
if self.controlIP == nil {
self.dnsError = "No internet connection."
} else if self.appleIP == nil {
self.dnsError = "Apple DNS blocked. Your network might be filtering Apple traffic."
} else {
self.dnsError = nil
}
}
} else {
DispatchQueue.main.async {
self.dnsError = nil
}
}
}
}
private func checkIfConnectedToWifi(completion: @escaping (Bool) -> Void) {
let monitor = NWPathMonitor(requiredInterfaceType: .wifi)
monitor.pathUpdateHandler = { path in
completion(path.status == .satisfied)
monitor.cancel()
}
let queue = DispatchQueue.global(qos: .background)
monitor.start(queue: queue)
}
private func lookupIPAddress(for host: String, completion: @escaping (String?) -> Void) {
DispatchQueue.global(qos: .background).async {
var hints = addrinfo(
ai_flags: 0,
ai_family: AF_UNSPEC,
ai_socktype: SOCK_STREAM,
ai_protocol: 0,
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil
)
var res: UnsafeMutablePointer<addrinfo>?
let err = getaddrinfo(host, nil, &hints, &res)
if err != 0 {
DispatchQueue.main.async { completion(nil) }
return
}
var ipAddress: String?
var ptr = res
while ptr != nil {
if let addr = ptr?.pointee.ai_addr {
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
if getnameinfo(addr, ptr!.pointee.ai_addrlen,
&hostBuffer, socklen_t(hostBuffer.count),
nil, 0, NI_NUMERICHOST) == 0 {
ipAddress = String(cString: hostBuffer)
break
}
}
ptr = ptr?.pointee.ai_next
}
freeaddrinfo(res)
DispatchQueue.main.async { completion(ipAddress) }
}
}
}
// MARK: - Main App
// Global state variable for the tunnel connection.
var pubTunnelConnected = false
private var tunnelStartPending = false
private var tunnelStartInProgress = false
private var tunnelPendingShowUI = true
@main
struct HeartbeatApp: App {
@StateObject private var mount = MountingProgress.shared
@Environment(\.scenePhase) private var scenePhase // Observe scene lifecycle
@State private var shouldAttemptTunnelReconnect = false
init() {
registerAdvancedOptionsDefault()
if UserDefaults.standard.bool(forKey: "keepAliveAudio") {
BackgroundAudioManager.shared.start()
}
let fixSelector = NSSelectorFromString("fix_initForOpeningContentTypes:asCopy:")
if let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, fixSelector),
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:))) {
method_exchangeImplementations(origMethod, fixMethod)
}
}
private func handleScenePhaseChange(_ newPhase: ScenePhase) {
switch newPhase {
case .background:
shouldAttemptTunnelReconnect = true
case .active:
if shouldAttemptTunnelReconnect {
shouldAttemptTunnelReconnect = false
startTunnelInBackground(showErrorUI: false)
}
default:
break
}
}
var body: some Scene {
WindowGroup {
MainTabView()
.onAppear {
Task {
let fileManager = FileManager.default
for item in ddiDownloadItems {
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
if fileManager.fileExists(atPath: destinationURL.path) { continue }
do {
try await downloadFile(from: item.urlString, to: destinationURL)
} catch {
await MainActor.run {
showAlert(title: "An Error has Occurred",
message: "[Download DDI Error]: \(error.localizedDescription)",
showOk: true)
}
break
}
}
}
}
.onChange(of: scenePhase) { _, newPhase in
handleScenePhaseChange(newPhase)
}
}
}
}
// MARK: - Additional Helpers
actor FunctionGuard<T> {
private var runningTask: Task<T, Never>?
func execute(_ work: @escaping @Sendable () -> T) async -> T {
if let task = runningTask {
return await task.value
}
let task = Task.detached { work() }
runningTask = task
let result = await task.value
runningTask = nil
return result
}
}
class MountingProgress: ObservableObject {
static var shared = MountingProgress()
@Published var mountProgress: Double = 0.0
@Published var mountingThread: Thread?
@Published var coolisMounted: Bool = false
func checkforMounted() {
DispatchQueue.global(qos: .utility).async {
let mounted = isMounted()
DispatchQueue.main.async {
self.coolisMounted = mounted
}
}
}
func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
let percentage = Double(progress) / Double(total) * 100.0
DispatchQueue.main.async {
self.mountProgress = percentage
}
}
func pubMount() {
mount()
}
private func mount() {
let currentlyMounted = isMounted()
DispatchQueue.main.async {
self.coolisMounted = currentlyMounted
}
if isPairing(), !currentlyMounted {
if let mountingThread = mountingThread {
mountingThread.cancel()
self.mountingThread = nil
}
let thread = Thread { [weak self] in
guard let self = self else { return }
let mountError = mountPersonalDDI(
imagePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg").path,
trustcachePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path,
manifestPath: URL.documentsDirectory.appendingPathComponent("DDI/BuildManifest.plist").path,
)
DispatchQueue.main.async {
if let mountError {
showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
if shouldTryAgain { self.mount() }
}
} else {
self.coolisMounted = true
self.checkforMounted()
}
self.mountingThread = nil
}
}
thread.qualityOfService = .background
thread.name = "mounting"
thread.start()
mountingThread = thread
}
}
}
func isPairing() -> Bool {
let pairingpath = PairingFileStore.prepareURL().path
var pairingFile: RpPairingFileHandle?
let err = rp_pairing_file_read(pairingpath, &pairingFile)
if err != nil { return false }
rp_pairing_file_free(pairingFile)
return true
}
func startTunnelInBackground(showErrorUI: Bool = true) {
assert(Thread.isMainThread, "startTunnelInBackground must be called on the main thread")
let pairingFileURL = PairingFileStore.prepareURL()
guard FileManager.default.fileExists(atPath: pairingFileURL.path) else {
tunnelStartPending = false
tunnelPendingShowUI = true
return
}
guard !tunnelStartInProgress else {
return
}
tunnelStartPending = false
tunnelPendingShowUI = true
tunnelStartInProgress = true
DispatchQueue.global(qos: .userInteractive).async {
defer {
DispatchQueue.main.async {
tunnelStartInProgress = false
}
}
do {
try JITEnableContext.shared.startTunnel()
LogManager.shared.addInfoLog("Tunnel connected successfully")
pubTunnelConnected = true
DispatchQueue.main.async {
let trustcachePath = URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path
guard FileManager.default.fileExists(atPath: trustcachePath),
!MountingProgress.shared.coolisMounted,
MountingProgress.shared.mountingThread == nil else { return }
MountingProgress.shared.pubMount()
}
} catch {
let err2 = error as NSError
let code = err2.code
LogManager.shared.addErrorLog(tunnelConnectionLogMessage(for: err2))
guard showErrorUI else { return }
DispatchQueue.main.async {
if code == -9 {
do {
try PairingFileStore.remove()
LogManager.shared.addInfoLog("Removed invalid pairing file")
} catch {
LogManager.shared.addErrorLog("Failed to remove invalid pairing file: \(error.localizedDescription)")
}
showAlert(
title: "Invalid Pairing File",
message: "The pairing file is invalid or expired. Please select a new pairing file.",
showOk: true,
showTryAgain: false,
primaryButtonText: "Select New File"
) { _ in
NotificationCenter.default.post(name: NSNotification.Name("ShowPairingFilePicker"), object: nil)
}
} else {
showAlert(
title: "Connection Error",
message: tunnelConnectionAlertMessage(for: err2),
showOk: false,
showTryAgain: true
) { shouldTryAgain in
if shouldTryAgain {
DispatchQueue.main.async {
startTunnelInBackground()
}
}
}
}
}
}
}
}
private func tunnelConnectionLogMessage(for error: NSError) -> String {
let target = "\(DeviceConnectionContext.targetIPAddress):49152"
return "Tunnel connection failed for \(target): \(error.localizedDescription) (Domain: \(error.domain), Code: \(error.code), Raw: \(String(describing: error)))"
}
private func tunnelConnectionAlertMessage(for error: NSError) -> String {
let targetIP = DeviceConnectionContext.targetIPAddress
let rawMessage = error.localizedDescription
let lowercasedMessage = rawMessage.lowercased()
let likelyCause: String
let recoverySteps: [String]
if error.code == 48 || lowercasedMessage.contains("address already in use") || lowercasedMessage.contains("port already in use") {
likelyCause = "A port needed for the tunnel is already in use."
recoverySteps = [
"Close other JIT, debugging, proxy, or VPN apps that may be using the tunnel.",
"Disconnect and reconnect LocalDevVPN.",
"Restart StikDebug, then try again.",
"If it keeps happening, reboot the device to clear the stuck port."
]
} else if error.code == 54 || lowercasedMessage.contains("connection reset") {
likelyCause = "The device or VPN closed the tunnel connection before setup finished."
recoverySteps = [
"Open LocalDevVPN and confirm the VPN is connected.",
"Make sure LocalDevVPN is using the default \(DeviceConnectionContext.defaultTargetIPAddress) address.",
"Reconnect Wi-Fi and LocalDevVPN, then try again.",
"If this keeps happening, select a fresh pairing file."
]
} else if error.code == -18 || lowercasedMessage.contains("parse target ip") {
likelyCause = "The configured target IP address is not valid."
recoverySteps = [
"Open Settings and check the target IP address.",
"Use the default \(DeviceConnectionContext.defaultTargetIPAddress)."
]
} else if lowercasedMessage.contains("timed out") || lowercasedMessage.contains("timeout") {
likelyCause = "The app could not reach the device before the connection timed out."
recoverySteps = [
"Confirm Wi-Fi and LocalDevVPN are both connected.",
"Wake and unlock the target device.",
"Confirm LocalDevVPN is exposing the device at \(targetIP)."
]
} else if lowercasedMessage.contains("network is unreachable") || lowercasedMessage.contains("no route") {
likelyCause = "The VPN route to the device is not available."
recoverySteps = [
"Disconnect and reconnect LocalDevVPN.",
"Confirm iOS shows the VPN indicator.",
"Try switching Wi-Fi off and on."
]
} else {
likelyCause = "The tunnel could not be created."
recoverySteps = [
"Confirm Wi-Fi and LocalDevVPN are connected.",
"Wake and unlock the target device.",
"Reconnect LocalDevVPN, then try again."
]
}
let steps = recoverySteps.enumerated()
.map { "\($0.offset + 1). \($0.element)" }
.joined(separator: "\n")
return """
\(likelyCause)
Target: \(targetIP):49152
Expected LocalDevVPN IP: \(DeviceConnectionContext.defaultTargetIPAddress)
Try this:
\(steps)
Technical details:
Code \(error.code): \(rawMessage)
"""
}
func checkDeviceConnection(callback: @escaping (Bool, String?) -> Void) {
let targetIP = DeviceConnectionContext.targetIPAddress
let host = NWEndpoint.Host(targetIP)
let port = NWEndpoint.Port(rawValue: 62078)!
let connection = NWConnection(host: host, port: port, using: .tcp)
var timeoutWorkItem: DispatchWorkItem?
timeoutWorkItem = DispatchWorkItem { [weak connection] in
if connection?.state != .ready {
connection?.cancel()
DispatchQueue.main.async {
if timeoutWorkItem?.isCancelled == false {
let message = "[TIMEOUT] Could not reach the device at \(targetIP). Make sure it’s online and on the same network."
callback(false, message)
}
}
}
}
connection.stateUpdateHandler = { [weak connection] state in
switch state {
case .ready:
timeoutWorkItem?.cancel()
connection?.cancel()
DispatchQueue.main.async {
callback(true, nil)
}
case .failed(let error):
timeoutWorkItem?.cancel()
connection?.cancel()
DispatchQueue.main.async {
let message = "Could not reach the device at \(targetIP): \(error.localizedDescription)"
callback(false, message)
}
default:
break
}
}
connection.start(queue: .global())
if let workItem = timeoutWorkItem {
DispatchQueue.global().asyncAfter(deadline: .now() + 20, execute: workItem)
}
}
public func showAlert(title: String, message: String, showOk: Bool, showTryAgain: Bool = false, primaryButtonText: String? = nil, completion: ((Bool) -> Void)? = nil) {
DispatchQueue.main.async {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootViewController = scene.windows.first?.rootViewController else {
return
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
if showTryAgain {
alert.addAction(UIAlertAction(title: primaryButtonText ?? "Try Again", style: .default) { _ in
completion?(true)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
completion?(false)
})
} else if showOk {
alert.addAction(UIAlertAction(title: primaryButtonText ?? "OK", style: .default) { _ in
completion?(true)
})
} else {
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completion?(true)
})
}
var topController = rootViewController
while let presented = topController.presentedViewController {
topController = presented
}
topController.present(alert, animated: true)
}
}
private struct DDIDownloadItem {
let name: String
let relativePath: String
let urlString: String
}
private let ddiDownloadItems: [DDIDownloadItem] = [
.init(
name: "Build Manifest",
relativePath: "DDI/BuildManifest.plist",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist"
),
.init(
name: "Image",
relativePath: "DDI/Image.dmg",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg"
),
.init(
name: "TrustCache",
relativePath: "DDI/Image.dmg.trustcache",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache"
)
]
enum DDIDownloadError: LocalizedError {
case invalidURL(String)
case invalidResponse
case badStatus(Int)
var errorDescription: String? {
switch self {
case .invalidURL(let string):
return "Invalid download URL: \(string)"
case .invalidResponse:
return "The DDI server returned an invalid response."
case .badStatus(let statusCode):
return "The DDI server returned HTTP \(statusCode)."
}
}
}
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
guard let url = URL(string: urlString),
url.scheme?.lowercased() == "https" else {
throw DDIDownloadError.invalidURL(urlString)
}
let (tempLocalUrl, response) = try await URLSession.shared.download(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw DDIDownloadError.invalidResponse
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw DDIDownloadError.badStatus(httpResponse.statusCode)
}
let fileManager = FileManager.default
try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(),
withIntermediateDirectories: true)
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.moveItem(at: tempLocalUrl, to: destinationURL)
}
func redownloadDDI(progressHandler: ((Double, String) -> Void)? = nil) async throws {
let fileManager = FileManager.default
let totalStages = Double(ddiDownloadItems.count + 1)
var completedStages = 0.0
progressHandler?(0.0, "Removing existing DDI files…")
for item in ddiDownloadItems {
let fileURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
}
}
completedStages += 1.0
progressHandler?(completedStages / totalStages, "Starting downloads…")
for item in ddiDownloadItems {
progressHandler?(completedStages / totalStages, "Downloading \(item.name)…")
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
try await downloadFile(from: item.urlString, to: destinationURL)
completedStages += 1.0
progressHandler?(completedStages / totalStages, "\(item.name) ready")
}
progressHandler?(1.0, "DDI download complete.")
}

View file

@ -0,0 +1,14 @@
//
// AccessibilityAnnouncer.swift
// StikDebug
//
import UIKit
enum AccessibilityAnnouncer {
static func announce(_ message: String) {
DispatchQueue.main.async {
UIAccessibility.post(notification: .announcement, argument: message)
}
}
}

View file

@ -0,0 +1,91 @@
//
// AlertPresenter.swift
// StikDebug
//
import UIKit
enum AlertPresenter {
static func show(
title: String,
message: String,
showOk: Bool,
showTryAgain: Bool = false,
primaryButtonText: String? = nil,
completion: ((Bool) -> Void)? = nil
) {
DispatchQueue.main.async {
guard let rootViewController = UIApplication.shared.activeRootViewController else {
return
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
if showTryAgain {
alert.addAction(UIAlertAction(title: primaryButtonText ?? "Try Again", style: .default) { _ in
completion?(true)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
completion?(false)
})
} else if showOk {
alert.addAction(UIAlertAction(title: primaryButtonText ?? "OK", style: .default) { _ in
completion?(true)
})
} else {
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completion?(true)
})
}
UIApplication.shared.topViewController(from: rootViewController)?.present(alert, animated: true)
}
}
static func dismissPresentedAlert() {
DispatchQueue.main.async {
guard let rootViewController = UIApplication.shared.activeRootViewController,
let topController = UIApplication.shared.topViewController(from: rootViewController),
topController is UIAlertController else {
return
}
topController.dismiss(animated: true)
}
}
}
public func showAlert(
title: String,
message: String,
showOk: Bool,
showTryAgain: Bool = false,
primaryButtonText: String? = nil,
completion: ((Bool) -> Void)? = nil
) {
AlertPresenter.show(
title: title,
message: message,
showOk: showOk,
showTryAgain: showTryAgain,
primaryButtonText: primaryButtonText,
completion: completion
)
}
private extension UIApplication {
var activeRootViewController: UIViewController? {
connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap(\.windows)
.first(where: \.isKeyWindow)?
.rootViewController
}
func topViewController(from rootViewController: UIViewController) -> UIViewController? {
var topController: UIViewController? = rootViewController
while let presented = topController?.presentedViewController {
topController = presented
}
return topController
}
}

View file

@ -0,0 +1,42 @@
//
// AppStorageCodable.swift
// StikDebug
//
import Foundation
extension Array: @retroactive RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data) else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8) else {
return "[]"
}
return result
}
}
extension Dictionary: @retroactive RawRepresentable where Key: Codable, Value: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Key: Value].self, from: data) else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8) else {
return "{}"
}
return result
}
}

View file

@ -0,0 +1,16 @@
//
// FileManager+Lookup.swift
// StikDebug
//
import Foundation
extension FileManager {
func filePath(atPath path: String, withLength length: Int) -> String? {
guard let file = try? contentsOfDirectory(atPath: path).first(where: { $0.count == length }) else {
return nil
}
return "\(path)/\(file)"
}
}

View file

@ -0,0 +1,20 @@
//
// Haptics.swift
// StikDebug
//
import UIKit
enum Haptics {
static func light() {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
static func medium() {
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
}
static func selection() {
UISelectionFeedbackGenerator().selectionChanged()
}
}

View file

@ -0,0 +1,11 @@
//
// NotificationName+StikDebug.swift
// StikDebug
//
import Foundation
extension Notification.Name {
static let pairingFileImported = Notification.Name("PairingFileImported")
static let intentJSScriptReady = Notification.Name("intentJSScriptReady")
}

View file

@ -0,0 +1,109 @@
//
// PairingFileStore.swift
// StikDebug
//
import Foundation
import UniformTypeIdentifiers
enum PairingFileStore {
static let fileName = "rp_pairing_file.plist"
static let supportedContentTypes: [UTType] = [
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!,
.propertyList
]
private static let legacyFileName = "pairingFile.plist"
static var url: URL {
directoryURL.appendingPathComponent(fileName)
}
@discardableResult
static func prepareURL(fileManager: FileManager = .default) -> URL {
let destination = url
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
guard !fileManager.fileExists(atPath: destination.path) else {
removeLegacyCopies(fileManager: fileManager)
return destination
}
migrateLegacyCopy(to: destination, fileManager: fileManager)
return destination
}
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
let destination = prepareURL(fileManager: fileManager)
if fileManager.fileExists(atPath: destination.path) {
try fileManager.removeItem(at: destination)
}
removeLegacyCopies(fileManager: fileManager)
try fileManager.copyItem(at: sourceURL, to: destination)
protectPairingFile(at: destination, fileManager: fileManager)
}
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
let accessing = sourceURL.startAccessingSecurityScopedResource()
defer {
if accessing {
sourceURL.stopAccessingSecurityScopedResource()
}
}
guard fileManager.fileExists(atPath: sourceURL.path) else {
throw CocoaError(.fileNoSuchFile)
}
try replace(with: sourceURL, fileManager: fileManager)
}
static func remove(fileManager: FileManager = .default) throws {
let destination = prepareURL(fileManager: fileManager)
if fileManager.fileExists(atPath: destination.path) {
try fileManager.removeItem(at: destination)
}
removeLegacyCopies(fileManager: fileManager)
}
private static var directoryURL: URL {
FileManager.default
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("Pairing", isDirectory: true)
}
private static var legacyURLs: [URL] {
[
URL.documentsDirectory.appendingPathComponent(fileName),
URL.documentsDirectory.appendingPathComponent(legacyFileName)
]
}
private static func migrateLegacyCopy(to destination: URL, fileManager: FileManager) {
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
do {
try fileManager.moveItem(at: legacyURL, to: destination)
} catch {
if let data = try? Data(contentsOf: legacyURL) {
try? data.write(to: destination, options: .atomic)
try? fileManager.removeItem(at: legacyURL)
}
}
protectPairingFile(at: destination, fileManager: fileManager)
break
}
}
private static func removeLegacyCopies(fileManager: FileManager) {
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
try? fileManager.removeItem(at: legacyURL)
}
}
private static func protectPairingFile(at url: URL, fileManager: FileManager) {
try? fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
}
}

View file

@ -0,0 +1,44 @@
//
// ProcessInfo+TXM.swift
// StikDebug
//
import Foundation
public extension ProcessInfo {
var hasTXM: Bool {
if isTXMOverridden {
return true
}
return ProcessInfo.hasTXMSupport(
operatingSystemVersion: operatingSystemVersion,
localTXMDetector: ProcessInfo.detectLocalTXM
)
}
var isTXMOverridden: Bool {
UserDefaults.standard.bool(forKey: UserDefaults.Keys.txmOverride)
}
static func hasTXMSupport(
operatingSystemVersion: OperatingSystemVersion,
localTXMDetector: () -> Bool
) -> Bool {
guard operatingSystemVersion.majorVersion >= 26 else {
return false
}
return localTXMDetector()
}
private static func detectLocalTXM() -> Bool {
if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36),
let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) {
return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0
}
return FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map {
access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0
} ?? false
}
}

View file

@ -1,112 +1,9 @@
//
// Extensions.swift
// ScriptStore.swift
// StikDebug
//
// Created by s s on 2025/7/9.
//
import Foundation
import UniformTypeIdentifiers
import UIKit
enum AccessibilityAnnouncer {
static func announce(_ message: String) {
DispatchQueue.main.async {
UIAccessibility.post(notification: .announcement, argument: message)
}
}
}
enum PairingFileStore {
static let fileName = "rp_pairing_file.plist"
private static let legacyFileName = "pairingFile.plist"
static let supportedContentTypes: [UTType] = [
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!,
.propertyList
]
static var url: URL {
directoryURL.appendingPathComponent(fileName)
}
private static var directoryURL: URL {
FileManager.default
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("Pairing", isDirectory: true)
}
private static var legacyURLs: [URL] {
[
URL.documentsDirectory.appendingPathComponent(fileName),
URL.documentsDirectory.appendingPathComponent(legacyFileName)
]
}
@discardableResult
static func prepareURL(fileManager: FileManager = .default) -> URL {
let destination = url
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
guard !fileManager.fileExists(atPath: destination.path) else {
removeLegacyCopies(fileManager: fileManager)
return destination
}
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
do {
try fileManager.moveItem(at: legacyURL, to: destination)
} catch {
if let data = try? Data(contentsOf: legacyURL) {
try? data.write(to: destination, options: .atomic)
try? fileManager.removeItem(at: legacyURL)
}
}
try? fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: destination.path)
break
}
return destination
}
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
let destination = prepareURL(fileManager: fileManager)
if fileManager.fileExists(atPath: destination.path) {
try fileManager.removeItem(at: destination)
}
removeLegacyCopies(fileManager: fileManager)
try fileManager.copyItem(at: sourceURL, to: destination)
try? fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: destination.path)
}
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
let accessing = sourceURL.startAccessingSecurityScopedResource()
defer {
if accessing {
sourceURL.stopAccessingSecurityScopedResource()
}
}
guard fileManager.fileExists(atPath: sourceURL.path) else {
throw CocoaError(.fileNoSuchFile)
}
try replace(with: sourceURL, fileManager: fileManager)
}
static func remove(fileManager: FileManager = .default) throws {
let destination = prepareURL(fileManager: fileManager)
if fileManager.fileExists(atPath: destination.path) {
try fileManager.removeItem(at: destination)
}
removeLegacyCopies(fileManager: fileManager)
}
private static func removeLegacyCopies(fileManager: FileManager) {
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
try? fileManager.removeItem(at: legacyURL)
}
}
}
struct ScriptResource {
let resourceName: String
@ -137,12 +34,15 @@ enum ScriptStore {
let directory = directoryURL
var isDirectory: ObjCBool = false
let exists = fileManager.fileExists(atPath: directory.path, isDirectory: &isDirectory)
if exists && !isDirectory.boolValue {
try fileManager.removeItem(at: directory)
}
if !exists || !isDirectory.boolValue {
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}
try ensureBundledScripts(in: directory, fileManager: fileManager)
return directory
}
@ -151,6 +51,7 @@ enum ScriptStore {
guard let scriptName = normalizedScriptFileName(scriptName) else {
throw CocoaError(.fileReadInvalidFileName)
}
let directory = try prepareDirectory(fileManager: fileManager)
return directory.appendingPathComponent(scriptName)
}
@ -173,16 +74,40 @@ enum ScriptStore {
assignedScript(for: bundleID, fileManager: fileManager) ?? autoScript(for: bundleID, fileManager: fileManager)
}
static func favoriteAppName(for bundleID: String, defaults: UserDefaults? = UserDefaults(suiteName: favoriteAppNamesSuiteName)) -> String? {
static func favoriteAppName(
for bundleID: String,
defaults: UserDefaults? = UserDefaults(suiteName: favoriteAppNamesSuiteName)
) -> String? {
let names = defaults?.dictionary(forKey: favoriteAppNamesKey) as? [String: String]
return names?[bundleID]
}
static func normalizedScriptFileName(_ candidate: String) -> String? {
let trimmed = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty,
!trimmed.contains("/"),
!trimmed.contains("\\"),
!trimmed.contains("\0") else {
return nil
}
let fileName = URL(fileURLWithPath: trimmed).lastPathComponent
guard fileName == trimmed,
fileName != ".",
fileName != "..",
URL(fileURLWithPath: fileName).pathExtension.lowercased() == "js" else {
return nil
}
return fileName
}
private static func ensureBundledScripts(in directory: URL, fileManager: FileManager) throws {
for resource in bundledResources {
guard let bundleURL = Bundle.main.url(forResource: resource.resourceName, withExtension: "js") else {
continue
}
let destination = directory.appendingPathComponent(resource.fileName)
if !fileManager.fileExists(atPath: destination.path) {
try fileManager.copyItem(at: bundleURL, to: destination)
@ -191,13 +116,12 @@ enum ScriptStore {
}
private static func assignedScript(for bundleID: String, fileManager: FileManager) -> (data: Data, name: String)? {
guard let scriptName = assignedScriptName(for: bundleID) else {
return nil
}
guard let scriptURL = try? scriptURL(named: scriptName, fileManager: fileManager),
guard let scriptName = assignedScriptName(for: bundleID),
let scriptURL = try? scriptURL(named: scriptName, fileManager: fileManager),
let data = try? Data(contentsOf: scriptURL) else {
return nil
}
return (data, scriptName)
}
@ -224,6 +148,7 @@ enum ScriptStore {
let data = try? Data(contentsOf: bundleURL) else {
return nil
}
return (data, resource.fileName)
}
@ -231,52 +156,4 @@ enum ScriptStore {
let rawMap = defaults.dictionary(forKey: assignmentKey) as? [String: String] ?? [:]
return rawMap.compactMapValues(normalizedScriptFileName)
}
static func normalizedScriptFileName(_ candidate: String) -> String? {
let trimmed = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty,
!trimmed.contains("/"),
!trimmed.contains("\\"),
!trimmed.contains("\0") else {
return nil
}
let fileName = URL(fileURLWithPath: trimmed).lastPathComponent
guard fileName == trimmed,
fileName != ".",
fileName != "..",
URL(fileURLWithPath: fileName).pathExtension.lowercased() == "js" else {
return nil
}
return fileName
}
}
extension FileManager {
func filePath(atPath path: String, withLength length: Int) -> String? {
guard let file = try? contentsOfDirectory(atPath: path).first(where: { $0.count == length }) else { return nil }
return "\(path)/\(file)"
}
}
extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
}
}
extension Notification.Name {
static let pairingFileImported = Notification.Name("PairingFileImported")
static let intentJSScriptReady = Notification.Name("intentJSScriptReady")
}
extension UserDefaults {
enum Keys {
/// Forces the app to treat the current device as TXM-capable so scripts always run.
static let txmOverride = "overrideTXMForScripts"
static let bundleScriptMap = "BundleScriptMap"
static let defaultScriptName = "DefaultScriptName"
static let defaultScriptNameValue = "attachDetach.js"
}
}

View file

@ -0,0 +1,16 @@
//
// UIDocumentPickerViewController+CopyWorkaround.swift
// StikDebug
//
import UIKit
import UniformTypeIdentifiers
extension UIDocumentPickerViewController {
@objc func fix_init(
forOpeningContentTypes contentTypes: [UTType],
asCopy: Bool
) -> UIDocumentPickerViewController {
fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
}
}

View file

@ -0,0 +1,16 @@
//
// UserDefaults+Keys.swift
// StikDebug
//
import Foundation
extension UserDefaults {
enum Keys {
/// Forces the app to treat the current device as TXM-capable so scripts always run.
static let txmOverride = "overrideTXMForScripts"
static let bundleScriptMap = "BundleScriptMap"
static let defaultScriptName = "DefaultScriptName"
static let defaultScriptNameValue = "attachDetach.js"
}
}

View file

@ -1,15 +0,0 @@
//
// HapticFeedbackHelper.swift
// StikDebug
//
// Created by Stephen on 3/27/25.
//
import SwiftUI
struct HapticFeedbackHelper {
static func trigger() {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
}
}

View file

@ -1,32 +0,0 @@
//
// LogManagerBridge.swift
// StikDebug
//
// Created by neoarz on 3/29/25.
//
import Foundation
//objc bridge for logs
@objc class LogManagerBridge: NSObject {
@objc static let shared = LogManagerBridge()
private override init() {
super.init()
}
@objc func addInfoLog(_ message: String) {
LogManager.shared.addInfoLog(message)
}
@objc func addErrorLog(_ message: String) {
LogManager.shared.addErrorLog(message)
}
@objc func addDebugLog(_ message: String) {
LogManager.shared.addDebugLog(message)
}
@objc func addWarningLog(_ message: String) {
LogManager.shared.addWarningLog(message)
}
}

View file

@ -1,28 +0,0 @@
//
// Security.swift
// StikDebug
// from MeloNX
// Created by s s on 2025/4/6.
//
import Foundation
import Security
typealias SecTaskRef = OpaquePointer
@_silgen_name("SecTaskCopyValueForEntitlement")
func SecTaskCopyValueForEntitlement(
_ task: SecTaskRef,
_ entitlement: CFString,
_ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
) -> CFTypeRef?
@_silgen_name("SecTaskCreateFromSelf")
func SecTaskCreateFromSelf(
_ allocator: CFAllocator?
) -> SecTaskRef?
func checkAppEntitlement(_ ent: String) -> Bool {
guard let task = SecTaskCreateFromSelf(nil) else { return false }
guard let value = SecTaskCopyValueForEntitlement(task, ent as CFString, nil) else { return false }
return value.boolValue != nil && value.boolValue
}

View file

@ -36,7 +36,7 @@ struct ConsoleLogsView: View {
return systemLogStream.entries
}
let query = syslogSearchText.lowercased()
return systemLogStream.entries.filter { $0.raw.lowercased().contains(query) }
return systemLogStream.entries.filter { $0.searchableRaw.contains(query) }
}
var body: some View {
@ -165,7 +165,7 @@ struct ConsoleLogsView: View {
}
.coordinateSpace(name: "jitScroll")
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
jitIsAtBottom = offset > -20
updateJITBottomState(offset > -20)
}
.onChange(of: logManager.logs.count) { _, _ in
guard jitIsAtBottom, let lastLog = logManager.logs.last else { return }
@ -229,7 +229,7 @@ struct ConsoleLogsView: View {
}
.coordinateSpace(name: "syslogScroll")
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
syslogIsAtBottom = offset > -20
updateSyslogBottomState(offset > -20)
}
.onChange(of: systemLogStream.entries.count) { _, _ in
guard syslogIsAtBottom, syslogSearchText.isEmpty,
@ -460,6 +460,16 @@ struct ConsoleLogsView: View {
showingCustomAlert = true
}
private func updateJITBottomState(_ isAtBottom: Bool) {
guard jitIsAtBottom != isAtBottom else { return }
jitIsAtBottom = isAtBottom
}
private func updateSyslogBottomState(_ isAtBottom: Bool) {
guard syslogIsAtBottom != isAtBottom else { return }
syslogIsAtBottom = isAtBottom
}
nonisolated private static func parseAppLogEntries<S: Sequence>(from lines: S, skipPrefixes: [String] = []) -> [LogManager.LogEntry] where S.Element == String {
var parsed: [LogManager.LogEntry] = []
parsed.reserveCapacity(lines.underestimatedCount)

View file

@ -1,167 +1,28 @@
//
// ContentView.swift
// HomeView.swift
// StikDebug
//
// Created by Stephen on 3/26/25.
//
import SwiftUI
import UIKit
struct JITEnableConfiguration {
var bundleID: String? = nil
var pid : Int? = nil
var scriptData: Data? = nil
var scriptName : String? = nil
}
private enum ExternalURLAction: Identifiable {
case enableJIT(JITEnableConfiguration)
case killProcess(Int)
case launchApp(String)
var id: String {
switch self {
case .enableJIT(let config):
return "enable-\(config.bundleID ?? "")-\(config.pid ?? 0)-\(config.scriptName ?? "")"
case .killProcess(let pid):
return "kill-\(pid)"
case .launchApp(let bundleID):
return "launch-\(bundleID)"
}
}
var title: String {
switch self {
case .enableJIT:
return "Enable JIT?"
case .killProcess:
return "Kill Process?"
case .launchApp:
return "Launch App?"
}
}
var message: String {
switch self {
case .enableJIT(let config):
let scriptText = config.scriptData == nil ? "" : " and run a script"
return "An external link wants to enable JIT\(scriptText) for \(targetDescription(for: config))."
case .killProcess(let pid):
return "An external link wants to kill process \(pid)."
case .launchApp(let bundleID):
return "An external link wants to launch \(bundleID)."
}
}
var confirmationTitle: String {
switch self {
case .enableJIT(let config):
return config.scriptData == nil ? "Enable JIT" : "Enable and Run Script"
case .killProcess:
return "Kill Process"
case .launchApp:
return "Launch App"
}
}
var role: ButtonRole? {
switch self {
case .enableJIT(let config):
return config.scriptData == nil ? nil : .destructive
case .killProcess:
return .destructive
case .launchApp:
return nil
}
}
private func targetDescription(for config: JITEnableConfiguration) -> String {
if let bundleID = config.bundleID {
return bundleID
}
if let pid = config.pid {
return "process \(pid)"
}
return "the requested app"
}
}
private final class DebugKeepAliveLease {
private let stateLock = NSLock()
private var isActive = false
private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
init() {
activate()
}
func invalidate() {
stateLock.lock()
guard isActive else {
stateLock.unlock()
return
}
isActive = false
stateLock.unlock()
runOnMain {
BackgroundAudioManager.shared.requestStop()
BackgroundLocationManager.shared.requestStop()
if self.backgroundTaskID != .invalid {
UIApplication.shared.endBackgroundTask(self.backgroundTaskID)
self.backgroundTaskID = .invalid
}
}
}
private func activate() {
stateLock.lock()
guard !isActive else {
stateLock.unlock()
return
}
isActive = true
stateLock.unlock()
runOnMain {
BackgroundAudioManager.shared.requestStart()
BackgroundLocationManager.shared.requestStart()
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "StikDebugDebugSession") { [weak self] in
LogManager.shared.addWarningLog("Debug session background task expired")
self?.invalidate()
}
}
}
private func runOnMain(_ work: @escaping () -> Void) {
if Thread.isMainThread {
work()
} else {
DispatchQueue.main.sync(execute: work)
}
}
}
struct HomeView: View {
@AppStorage("autoQuitAfterEnablingJIT") private var doAutoQuitAfterEnablingJIT = false
@AppStorage("bundleID") private var bundleID: String = ""
@State private var isProcessing = false
@State private var viewDidAppeared = false
@State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil
@State private var isShowingPairingFilePicker = false
@State private var debugFeedback: DebugFeedback?
@State private var pendingExternalURLAction: ExternalURLAction?
@AppStorage(UserDefaults.Keys.defaultScriptName) private var selectedScript = UserDefaults.Keys.defaultScriptNameValue
@State var scriptViewShow = false
@State private var isShowingConsole = false
@AppStorage(UserDefaults.Keys.defaultScriptName) var selectedScript = UserDefaults.Keys.defaultScriptNameValue
@State var jsModel: RunJSViewModel?
@ObservedObject private var mounting = MountingProgress.shared
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State private var hasAppeared = false
@State private var pendingJITEnableConfiguration: JITEnableConfiguration?
@State private var isShowingPairingFilePicker = false
@State private var debugFeedback: DebugFeedback?
@State private var pendingExternalURLAction: HomeExternalAction?
@State private var isShowingScriptRunner = false
@State private var scriptRunModel: RunJSViewModel?
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
private struct DebugFeedback: Identifiable {
let id = UUID()
@ -173,7 +34,7 @@ struct HomeView: View {
var body: some View {
InstalledAppsListView(onSelectApp: { selectedBundle, selectedName in
bundleID = selectedBundle
HapticFeedbackHelper.trigger()
Haptics.medium()
startJITInBackground(bundleID: selectedBundle, displayName: selectedName)
}, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true })
.overlay(alignment: .bottom) {
@ -183,27 +44,10 @@ struct HomeView: View {
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.onAppear {
startTunnelInBackground()
MountingProgress.shared.checkforMounted()
viewDidAppeared = true
if let config = pendingJITEnableConfiguration {
startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true)
pendingJITEnableConfiguration = nil
}
}
.onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady)) { notification in
guard let model = notification.userInfo?["model"] as? RunJSViewModel else { return }
jsModel = model
if let name = notification.userInfo?["scriptName"] as? String {
selectedScript = name
}
scriptViewShow = true
}
.onAppear(perform: handleAppear)
.onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady), perform: handleScriptReadyNotification)
.onReceive(timer) { _ in
if mounting.mountingThread == nil && !mounting.coolisMounted {
MountingProgress.shared.checkforMounted()
}
refreshMountStatusIfNeeded()
}
.onOpenURL { url in
handleExternalURL(url)
@ -231,48 +75,18 @@ struct HomeView: View {
} message: { action in
Text(action.message)
}
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: PairingFileStore.supportedContentTypes) { result in
switch result {
case .success(let url):
let fileManager = FileManager.default
do {
try PairingFileStore.importFromPicker(url, fileManager: fileManager)
pubTunnelConnected = false
startTunnelInBackground()
NotificationCenter.default.post(name: .pairingFileImported, object: nil)
// Dismiss any existing connection error alert
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let root = scene.windows.first?.rootViewController {
var top = root
while let presented = top.presentedViewController { top = presented }
if top is UIAlertController { top.dismiss(animated: true) }
}
} catch {
print("Error copying pairing file: \(error)")
}
case .failure(let error):
print("Failed to import pairing file: \(error)")
}
}
.sheet(isPresented: $isShowingConsole) {
.fileImporter(
isPresented: $isShowingPairingFilePicker,
allowedContentTypes: PairingFileStore.supportedContentTypes,
onCompletion: importPairingFile
)
.sheet(isPresented: $isShowingScriptRunner) {
NavigationStack {
ConsoleLogsView()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close") {
isShowingConsole = false
}
}
}
}
}
.sheet(isPresented: $scriptViewShow) {
NavigationStack {
if let jsModel {
RunJSView(model: jsModel)
if let scriptRunModel {
RunJSView(model: scriptRunModel)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") { scriptViewShow = false }
Button("Done") { isShowingScriptRunner = false }
}
}
.navigationTitle(selectedScript)
@ -282,6 +96,59 @@ struct HomeView: View {
}
}
private func handleAppear() {
startTunnelInBackground()
MountingProgress.shared.checkforMounted()
hasAppeared = true
if let config = pendingJITEnableConfiguration {
startJITInBackground(
bundleID: config.bundleID,
pid: config.pid,
scriptData: config.scriptData,
scriptName: config.scriptName,
triggeredByURLScheme: true
)
pendingJITEnableConfiguration = nil
}
}
private func handleScriptReadyNotification(_ notification: Notification) {
guard let model = notification.userInfo?["model"] as? RunJSViewModel else {
return
}
scriptRunModel = model
if let name = notification.userInfo?["scriptName"] as? String {
selectedScript = name
}
isShowingScriptRunner = true
}
private func refreshMountStatusIfNeeded() {
guard mounting.mountingThread == nil, !mounting.coolisMounted else {
return
}
MountingProgress.shared.checkforMounted()
}
private func importPairingFile(_ result: Result<URL, Error>) {
switch result {
case .success(let url):
do {
try PairingFileStore.importFromPicker(url)
markTunnelDisconnected()
startTunnelInBackground()
NotificationCenter.default.post(name: .pairingFileImported, object: nil)
AlertPresenter.dismissPresentedAlert()
} catch {
LogManager.shared.addErrorLog("Failed to import pairing file: \(error.localizedDescription)")
}
case .failure(let error):
LogManager.shared.addErrorLog("Pairing file picker failed: \(error.localizedDescription)")
}
}
private func queryValue(_ names: [String], in components: URLComponents?) -> String? {
guard let queryItems = components?.queryItems else { return nil }
for name in names {
@ -336,10 +203,10 @@ struct HomeView: View {
}
}
private func performExternalURLAction(_ action: ExternalURLAction) {
private func performExternalURLAction(_ action: HomeExternalAction) {
switch action {
case .enableJIT(let config):
if viewDidAppeared {
if hasAppeared {
startJITInBackground(
bundleID: config.bundleID,
pid: config.pid,
@ -351,7 +218,7 @@ struct HomeView: View {
pendingJITEnableConfiguration = config
}
case .killProcess(let pid):
pubTunnelConnected = false
markTunnelDisconnected()
startTunnelInBackground(showErrorUI: false)
DispatchQueue.global(qos: .userInitiated).async {
sleep(1)
@ -367,7 +234,7 @@ struct HomeView: View {
}
}
case .launchApp(let bundleID):
HapticFeedbackHelper.trigger()
Haptics.medium()
DispatchQueue.global(qos: .userInitiated).async {
let _ = JITEnableContext.shared.launchAppWithoutDebug(bundleID, logger: nil)
}
@ -403,8 +270,8 @@ struct HomeView: View {
semaphore: semaphore)
DispatchQueue.main.async {
jsModel = model
scriptViewShow = true
scriptRunModel = model
isShowingScriptRunner = true
}
do {
@ -419,7 +286,6 @@ struct HomeView: View {
}
private func startJITInBackground(bundleID: String? = nil, pid: Int? = nil, scriptData: Data? = nil, scriptName: String? = nil, triggeredByURLScheme: Bool = false, displayName: String? = nil) {
isProcessing = true
let targetName = displayName ?? bundleID ?? pid.map { String(format: "process %d".localized, $0) } ?? "app".localized
let startingMessage = String(format: "Starting JIT for %@".localized, targetName)
LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))")
@ -429,7 +295,7 @@ struct HomeView: View {
AccessibilityAnnouncer.announce(startingMessage)
if triggeredByURLScheme {
pubTunnelConnected = false
markTunnelDisconnected()
startTunnelInBackground(showErrorUI: false)
}
@ -443,7 +309,6 @@ struct HomeView: View {
let finishProcessing: (Bool, String?) -> Void = { success, detail in
DispatchQueue.main.async {
isProcessing = false
let message = success
? String(format: "JIT request completed for %@".localized, targetName)
: String(format: "JIT failed for %@".localized, targetName)
@ -523,40 +388,3 @@ struct HomeView: View {
#Preview {
HomeView()
}
public extension ProcessInfo {
var hasTXM: Bool {
if isTXMOverridden {
return true
}
return ProcessInfo.hasTXMSupport(
operatingSystemVersion: operatingSystemVersion,
localTXMDetector: ProcessInfo.detectLocalTXM
)
}
var isTXMOverridden: Bool {
UserDefaults.standard.bool(forKey: UserDefaults.Keys.txmOverride)
}
static func hasTXMSupport(
operatingSystemVersion: OperatingSystemVersion,
localTXMDetector: () -> Bool
) -> Bool {
guard operatingSystemVersion.majorVersion >= 26 else {
return false
}
return localTXMDetector()
}
private static func detectLocalTXM() -> Bool {
if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36),
let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) {
return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0
} else {
return (FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map {
access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0
}) ?? false
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -8,10 +8,6 @@
import SwiftUI
import Foundation
extension Notification.Name {
static let switchToTab = Notification.Name("MainTabSwitchNotification")
}
private enum ExternalLocationAction: Identifiable {
case simulate(URL, Double, Double)
case clear
@ -55,7 +51,6 @@ private enum ExternalLocationAction: Identifiable {
struct MainTabView: View {
@AppStorage("primaryTabSelection") private var selection: String = AppFeature.home.id
@State private var switchObserver: Any?
@State private var detachedFeature: AppFeature?
@State private var didSetInitialHome = false
@State private var pendingLocationAction: ExternalLocationAction?
@ -77,17 +72,6 @@ struct MainTabView: View {
selection = AppFeature.home.id
didSetInitialHome = true
}
guard switchObserver == nil else { return }
switchObserver = NotificationCenter.default.addObserver(forName: .switchToTab, object: nil, queue: .main) { note in
guard let id = note.object as? String else { return }
openFeature(id: id)
}
}
.onDisappear {
if let observer = switchObserver {
NotificationCenter.default.removeObserver(observer)
switchObserver = nil
}
}
.onOpenURL { url in
handleURL(url)

View file

@ -1651,7 +1651,7 @@ private struct RouteSearchSheet: View {
} else if !activeResults.isEmpty {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(Array(activeResults.enumerated()), id: \.offset) { index, result in
ForEach(Array(activeResults.enumerated()), id: \.element) { index, result in
Button {
resolve(result)
} label: {

View file

@ -265,9 +265,9 @@ struct ProfileView: View {
) { result in
switch result {
case .success(let url):
print("Saved at \(url)")
LogManager.shared.addInfoLog("Provisioning profile exported to \(url.lastPathComponent)")
case .failure(let error):
print("Error: \(error.localizedDescription)")
LogManager.shared.addErrorLog("Provisioning profile export failed: \(error.localizedDescription)")
}
}
}