StikDebug/StikJIT/StikJITApp.swift

373 lines
14 KiB
Swift

//
// StikJITApp.swift
// StikJIT
//
// Created by Stephen on 3/26/25.
//
import SwiftUI
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: - 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()
.environment(LanguageManager.shared)
.id(LanguageManager.shared.language)
.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("\(error.localizedDescription) (Code: \(code))")
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: "\(error.localizedDescription)\n\nCheck that the embedded VPN is connected and that the pairing file is current.",
showOk: false,
showTryAgain: true
) { shouldTryAgain in
if shouldTryAgain {
DispatchQueue.main.async {
startTunnelInBackground()
}
}
}
}
}
}
}
}
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)
var errorDescription: String? {
switch self {
case .invalidURL(let string):
return "Invalid download URL: \(string)"
}
}
}
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
guard let url = URL(string: urlString) else {
throw DDIDownloadError.invalidURL(urlString)
}
let (tempLocalUrl, _) = try await URLSession.shared.download(from: url)
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.")
}