From 7249b6852b43bc0f0989d14cb3c30a4d587f4650 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sat, 6 Dec 2025 17:31:47 +0800 Subject: [PATCH 1/9] Heartbeat rework --- StikJIT/StikJITApp.swift | 96 ++++---- StikJIT/Utilities/mountDDI.swift | 193 +-------------- StikJIT/Views/ConsoleLogsView.swift | 4 - StikJIT/Views/DeviceInfoManager.swift | 8 + StikJIT/Views/HomeView.swift | 10 +- StikJIT/Views/ProfileView.swift | 2 +- StikJIT/idevice/JITEnableContext.h | 8 +- StikJIT/idevice/JITEnableContext.m | 328 ++++++++++++-------------- StikJIT/idevice/heartbeat.h | 5 +- StikJIT/idevice/heartbeat.m | 102 +++++--- StikJIT/idevice/ideviceinfo.c | 14 +- StikJIT/idevice/jit.c | 4 +- StikJIT/idevice/mount.h | 14 ++ StikJIT/idevice/mount.m | 99 ++++++++ 14 files changed, 417 insertions(+), 470 deletions(-) create mode 100644 StikJIT/idevice/mount.h create mode 100644 StikJIT/idevice/mount.m diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index a0fda6c7..8227ae19 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -647,12 +647,7 @@ struct HeartbeatApp: App { } } } - .onChange(of: scenePhase) { newPhase in - if newPhase == .active { - print("App became active – restarting heartbeat") - startHeartbeatInBackground() - } - } + } } @@ -713,8 +708,7 @@ class MountingProgress: ObservableObject { DispatchQueue.main.async { self.coolisMounted = currentlyMounted } - let pairingpath = URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path - + if isPairing(), !currentlyMounted { if let mountingThread = mountingThread { mountingThread.cancel() @@ -727,7 +721,6 @@ class MountingProgress: ObservableObject { 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, - pairingFilePath: pairingpath ) DispatchQueue.main.async { @@ -791,63 +784,62 @@ func startHeartbeatInBackground(requireVPNConnection: Bool = true) { heartbeatStartPending = false heartbeatStartInProgress = true - let heartBeatThread = Thread { + DispatchQueue.global(qos: .userInteractive).async { defer { DispatchQueue.main.async { heartbeatStartInProgress = false } } - let completionHandler: @convention(block) (Int32, String?) -> Void = { result, message in - if result == 0 { - print("Heartbeat started successfully: \(message ?? "")") - pubHeartBeat = true + do { + try JITEnableContext.shared.startHeartbeat() + print("Heartbeat started successfully") + pubHeartBeat = true + + if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path) { - if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path) { - MountingProgress.shared.pubMount() - } - } else { - print("Error: \(message ?? "") (Code: \(result))") - DispatchQueue.main.async { - if result == -9 { - do { - try FileManager.default.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist")) - print("Removed invalid pairing file") - } catch { - print("Error removing invalid pairing file: \(error)") - } - - 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: "Heartbeat Error", - message: "Failed to connect to Heartbeat (\(result)). Are you connected to WiFi or is Airplane Mode enabled? Cellular data isn’t supported. Please launch the app at least once with WiFi enabled. After that, you can switch to cellular data to turn on the VPN, and once the VPN is active you can use Airplane Mode.", - showOk: false, - showTryAgain: true - ) { shouldTryAgain in - if shouldTryAgain { - DispatchQueue.main.async { - startHeartbeatInBackground() - } +// MountingProgress.shared.pubMount() + } + } catch { + let err2 = error as NSError + let code = err2.code + print("Error: \(error.localizedDescription) (Code: \(code))") + DispatchQueue.main.async { + if code == -9 { + do { + try FileManager.default.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist")) + print("Removed invalid pairing file") + } catch { + print("Error removing invalid pairing file: \(error)") + } + + 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: "Heartbeat Error", + message: "Failed to connect to Heartbeat (\(code)). Are you connected to WiFi or is Airplane Mode enabled? Cellular data isn’t supported. Please launch the app at least once with WiFi enabled. After that, you can switch to cellular data to turn on the VPN, and once the VPN is active you can use Airplane Mode.", + showOk: false, + showTryAgain: true + ) { shouldTryAgain in + if shouldTryAgain { + DispatchQueue.main.async { + startHeartbeatInBackground() } } } } } } - JITEnableContext.shared.startHeartbeat(completionHandler: completionHandler, logger: nil) } - heartBeatThread.qualityOfService = .background - heartBeatThread.name = "Heartbeat" - heartBeatThread.start() + } func checkVPNConnection(callback: @escaping (Bool, String?) -> Void) { diff --git a/StikJIT/Utilities/mountDDI.swift b/StikJIT/Utilities/mountDDI.swift index 17a76d5d..0453e854 100644 --- a/StikJIT/Utilities/mountDDI.swift +++ b/StikJIT/Utilities/mountDDI.swift @@ -18,197 +18,28 @@ func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRaw MountingProgress.shared.progressCallback(progress: progress, total: total, context: context) } -func readFile(path: String) -> Data? { - guard let file = fopen(path, "rb") else { - perror("Failed to open file") - return nil - } - - fseek(file, 0, SEEK_END) - let fileSize = ftell(file) - fseek(file, 0, SEEK_SET) - - guard fileSize > 0 else { - fclose(file) - return nil - } - - var buffer = Data(count: fileSize) - buffer.withUnsafeMutableBytes { ptr in - fread(ptr.baseAddress, 1, fileSize, file) - } - - fclose(file) - return buffer -} - -func htons(_ value: UInt16) -> UInt16 { - return CFSwapInt16HostToBig(value) -} - func isMounted() -> Bool { guard TunnelManager.shared.tunnelStatus == .connected else { return false } - var addr = sockaddr_in() - memset(&addr, 0, MemoryLayout.size) - addr.sin_family = sa_family_t(AF_INET) - addr.sin_port = htons(UInt16(LOCKDOWN_PORT)) - let sockaddrPointer = UnsafeRawPointer(&addr).bindMemory(to: sockaddr.self, capacity: 1) - - let pairingFilePath = URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path - - guard inet_pton(AF_INET, "10.7.0.1", &addr.sin_addr) == 1 else { - print("Invalid IP address") - return false - } - - // Read pairing file - var pairingFile: IdevicePairingFile? - let err = idevice_pairing_file_read(pairingFilePath, &pairingFile) - if let err { - print("Failed to read pairing file: \(err)") - return false - } - - // Create TCP provider - var provider: TcpProviderHandle? - let providerError = idevice_tcp_provider_new(sockaddrPointer, pairingFile, "ImageMounterTest", &provider) - if let providerError { - print("Failed to create TCP provider: \(providerError)") - return false - } - - // Connect to image mounter - var client: ImageMounterHandle? - let connectError = image_mounter_connect(provider, &client) - if let connectError { - print("Failed to connect to image mounter: \(connectError)") - return false - } - idevice_provider_free(provider) - - var deviceList: UnsafeMutablePointer? - var devicesLen: size_t = 0 - let listError = image_mounter_copy_devices(client, &deviceList, &devicesLen) - if listError == nil { - var devices: [String] = [] - for i in 0..? - var xmlLength: Int32 = 0 - - // Use libplist function to convert to XML - plist_to_xml(device, &xmlData, &xmlLength) - if let xml = xmlData { - devices.append("\(xml)") - } - plist_mem_free(xmlData) - plist_free(device) - } - - image_mounter_free(client) - return devices.count != 0 - } else { - print("Failed to get device list: \(listError)") + do { + let result = try JITEnableContext.shared.getMountedDeviceCount() + return result > 0 + } catch { + print("Error while getMountedDeviceCount \(error)") return false } } -func mountPersonalDDI(deviceIP: String = "10.7.0.1", imagePath: String, trustcachePath: String, manifestPath: String, pairingFilePath: String) -> Int { - idevice_init_logger(Debug, Disabled, nil) - +func mountPersonalDDI(imagePath: String, trustcachePath: String, manifestPath: String) -> Int { print("Mounting \(imagePath) \(trustcachePath) \(manifestPath)") - guard let image = readFile(path: imagePath), - let trustcache = readFile(path: trustcachePath), - let buildManifest = readFile(path: manifestPath) else { - print("Failed to read one or more files") - return 1 // EC: 1 - } - - var addr = sockaddr_in() - memset(&addr, 0, MemoryLayout.size) - addr.sin_family = sa_family_t(AF_INET) - addr.sin_port = htons(UInt16(LOCKDOWN_PORT)) - let sockaddrPointer = UnsafeRawPointer(&addr).bindMemory(to: sockaddr.self, capacity: 1) - - guard inet_pton(AF_INET, deviceIP, &addr.sin_addr) == 1 else { - print("Invalid IP address") - return 2 // EC: 2 + do { + try JITEnableContext.shared.mountPersonalDDI(withImagePath: imagePath, trustcachePath: trustcachePath, manifestPath: manifestPath) + } catch { + print("Failed to mount ddi: \(error)") + return (error as NSError).code } - - var pairingFile: IdevicePairingFile? - let err = idevice_pairing_file_read(pairingFilePath.cString(using: .utf8), &pairingFile) - if let err { - print("Failed to read pairing file: \(err.pointee.code)") - return 3 // EC: 3 - } - - - var provider: TcpProviderHandle? - let providerError = idevice_tcp_provider_new(sockaddrPointer, pairingFile, "ImageMounterTest".cString(using: .utf8), &provider) - if let providerError { - print("Failed to create TCP provider: \(providerError)") - return 4 // EC: 4 - } - - - var pairingFile2: IdevicePairingFile? - let P2err = idevice_pairing_file_read(pairingFilePath.cString(using: .utf8), &pairingFile2) - if let P2err { - print("Failed to read pairing file: \(P2err.pointee.code)") - return 5 // EC: 5 - } - - var lockdownClient: LockdowndClientHandle? - if let err = lockdownd_connect(provider, &lockdownClient) { - print("Failed to connect to lockdownd") - return 6 // EC: 6 - } - - if let err = lockdownd_start_session(lockdownClient, pairingFile2) { - print("Failed to start session") - return 7 // EC: 7 - } - - var uniqueChipIDPlist: plist_t? - if let err = lockdownd_get_value(lockdownClient, "UniqueChipID".cString(using: .utf8), nil, &uniqueChipIDPlist) { - print("Failed to get UniqueChipID") - return 8 // EC: 8 - } - - var uniqueChipID: UInt64 = 0 - plist_get_uint_val(uniqueChipIDPlist, &uniqueChipID) - plist_free(uniqueChipIDPlist) - print(uniqueChipID) - - - var mounterClient: ImageMounterHandle? - if let err = image_mounter_connect(provider, &mounterClient) { - print("Failed to connect to image mounter") - return 9 // EC: 9 - } - - let result = image.withUnsafeBytes { imagePtr in - trustcache.withUnsafeBytes { trustcachePtr in - buildManifest.withUnsafeBytes { manifestPtr in - image_mounter_mount_personalized( - mounterClient, - provider, - imagePtr.baseAddress?.assumingMemoryBound(to: UInt8.self), - image.count, - trustcachePtr.baseAddress?.assumingMemoryBound(to: UInt8.self), - trustcache.count, - manifestPtr.baseAddress?.assumingMemoryBound(to: UInt8.self), - buildManifest.count, - nil, - uniqueChipID - ) - } - } - } - - return Int(result?.pointee.code ?? -1) + return 0 } diff --git a/StikJIT/Views/ConsoleLogsView.swift b/StikJIT/Views/ConsoleLogsView.swift index a45e3698..09f9ee44 100644 --- a/StikJIT/Views/ConsoleLogsView.swift +++ b/StikJIT/Views/ConsoleLogsView.swift @@ -158,9 +158,6 @@ struct ConsoleLogsView: View { } .navigationViewStyle(.stack) .preferredColorScheme(preferredScheme) - .onAppear { - startHeartbeatInBackground() - } .onDisappear { systemLogStream.stop() } @@ -716,7 +713,6 @@ struct ConsoleLogsView: View { private func toggleSyslogPlayback() { if !systemLogStream.isStreaming { - startHeartbeatInBackground() systemLogStream.start() } else { systemLogStream.togglePause() diff --git a/StikJIT/Views/DeviceInfoManager.swift b/StikJIT/Views/DeviceInfoManager.swift index cd0a335e..8271e663 100644 --- a/StikJIT/Views/DeviceInfoManager.swift +++ b/StikJIT/Views/DeviceInfoManager.swift @@ -31,6 +31,14 @@ final class DeviceInfoManager: ObservableObject { busy = true let path = docs.appendingPathComponent("pairingFile.plist").path Task.detached { + do { + try JITEnableContext.shared.ensureHeartbeat() + } catch { + await MainActor.run { + self.error = ("Initialization Failed", self.initErrorMessage(Int32((error as NSError).code))) + self.busy = false + } + } let code = path.withCString { c_deviceinfo_init($0) } await MainActor.run { if code != 0 { diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index e29ab5d7..3663faab 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -1229,10 +1229,14 @@ struct HomeView: View { } if pairingFileExists { if !ddiMounted { - showAlert(title: "Device Not Mounted".localized, message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.".localized, showOk: true) { _ in } - return + mounting.checkforMounted() + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + showAlert(title: "Device Not Mounted".localized, message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.".localized, showOk: true) { _ in } + return + } + } else { + isShowingInstalledApps = true } - isShowingInstalledApps = true } else { isShowingPairingFilePicker = true } diff --git a/StikJIT/Views/ProfileView.swift b/StikJIT/Views/ProfileView.swift index 699e08e6..87d4b1a7 100644 --- a/StikJIT/Views/ProfileView.swift +++ b/StikJIT/Views/ProfileView.swift @@ -181,7 +181,7 @@ struct ProfileView: View { if confirmRemove { CustomErrorView( title: "Confirm Removal", - message: "Remove profile for \(removeTargetName) (UUID: \(removeTargetUUID))?\n**Apps associated this profile may become unavailable.**", + message: "Remove profile for \(removeTargetName) (UUID: \(removeTargetUUID))?\n**Apps associated with this profile may become unavailable.**", onDismiss: { confirmRemove = false }, showButton: true, primaryButtonText: "Remove", diff --git a/StikJIT/idevice/JITEnableContext.h b/StikJIT/idevice/JITEnableContext.h index 56156e49..77eac4a7 100644 --- a/StikJIT/idevice/JITEnableContext.h +++ b/StikJIT/idevice/JITEnableContext.h @@ -8,6 +8,8 @@ @import UIKit; #include "idevice.h" #include "jit.h" +#include "heartbeat.h" +#include "mount.h" typedef void (^HeartbeatCompletionHandler)(int result, NSString *message); typedef void (^LogFuncC)(const char* message, ...); @@ -18,7 +20,8 @@ typedef void (^SyslogErrorHandler)(NSError *error); @interface JITEnableContext : NSObject @property (class, readonly)JITEnableContext* shared; - (IdevicePairingFile*)getPairingFileWithError:(NSError**)error; -- (void)startHeartbeatWithCompletionHandler:(HeartbeatCompletionHandler)completionHandler logger:(LogFunc)logger; +- (BOOL)ensureHeartbeatWithError:(NSError**)err; +- (BOOL)startHeartbeat:(NSError**)err; - (BOOL)debugAppWithBundleID:(NSString*)bundleID logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback; - (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback; - (NSDictionary*)getAppListWithError:(NSError**)error; @@ -34,4 +37,7 @@ typedef void (^SyslogErrorHandler)(NSError *error); - (BOOL)addProfile:(NSData*)profile error:(NSError **)error; - (NSArray*)fetchProcessListWithError:(NSError**)error; - (BOOL)killProcessWithPID:(int)pid error:(NSError **)error; + +- (NSUInteger)getMountedDeviceCount:(NSError**)error __attribute__((swift_error(zero_result))); +- (NSInteger)mountPersonalDDIWithImagePath:(NSString*)imagePath trustcachePath:(NSString*)trustcachePath manifestPath:(NSString*)manifestPath error:(NSError**)error __attribute__((swift_error(nonzero_result))); @end diff --git a/StikJIT/idevice/JITEnableContext.m b/StikJIT/idevice/JITEnableContext.m index 5cd9751b..18ab18d4 100644 --- a/StikJIT/idevice/JITEnableContext.m +++ b/StikJIT/idevice/JITEnableContext.m @@ -16,11 +16,12 @@ #include "JITEnableContext.h" #import "StikDebug-Swift.h" +#include +#import JITEnableContext* sharedJITContext = nil; @implementation JITEnableContext { - bool heartbeatRunning; IdeviceProviderHandle* provider; dispatch_queue_t syslogQueue; BOOL syslogStreaming; @@ -28,6 +29,13 @@ @implementation JITEnableContext { SyslogLineHandler syslogLineHandler; SyslogErrorHandler syslogErrorHandler; dispatch_queue_t processInspectorQueue; + + int heartbeatToken; + NSError* lastHeartbeatError; + os_unfair_lock heartbeatLock; + BOOL heartbeatRunning; + dispatch_semaphore_t heartbeatSemaphore; + } + (instancetype)shared { @@ -47,6 +55,13 @@ - (instancetype)init { syslogClient = NULL; dispatch_queue_attr_t qosAttr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); processInspectorQueue = dispatch_queue_create("com.stikdebug.processInspector", qosAttr); + + heartbeatToken = 0; + heartbeatLock = OS_UNFAIR_LOCK_INIT; + heartbeatRunning = NO; + heartbeatSemaphore = NULL; + lastHeartbeatError = nil; + return self; } @@ -101,85 +116,122 @@ - (IdevicePairingFile*)getPairingFileWithError:(NSError**)error { return pairingFile; } -- (void)startHeartbeatWithCompletionHandler:(HeartbeatCompletionHandler)completionHandler - logger:(LogFunc)logger -{ - NSError* err = nil; - IdevicePairingFile* pairingFile = [self getPairingFileWithError:&err]; - if (err) { - // silently swallow “pairing file not found” (-17) - if (err.code == -17) { +// only block until first heartbeat is completed or failed. +- (BOOL)startHeartbeat:(NSError**)err { + os_unfair_lock_lock(&heartbeatLock); + + // If heartbeat is already running, wait for it to complete + if (heartbeatRunning) { + dispatch_semaphore_t waitSemaphore = heartbeatSemaphore; + os_unfair_lock_unlock(&heartbeatLock); + + if (waitSemaphore) { + NSLog(@"waiting %p", pthread_self()); + dispatch_semaphore_wait(waitSemaphore, DISPATCH_TIME_FOREVER); + dispatch_semaphore_signal(waitSemaphore); + NSLog(@"waiting complete %p", pthread_self()); + } + *err = lastHeartbeatError; + return *err == nil; + } + + // Mark heartbeat as running + heartbeatRunning = YES; + heartbeatSemaphore = dispatch_semaphore_create(0); + dispatch_semaphore_t completionSemaphore = heartbeatSemaphore; + os_unfair_lock_unlock(&heartbeatLock); + + IdevicePairingFile* pairingFile = [self getPairingFileWithError:err]; + if (*err) { + os_unfair_lock_lock(&heartbeatLock); + heartbeatRunning = NO; + heartbeatSemaphore = NULL; + os_unfair_lock_unlock(&heartbeatLock); + dispatch_semaphore_signal(completionSemaphore); + return NO; + } + + globalHeartbeatToken++; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block bool completionCalled = false; + HeartbeatCompletionHandlerC Ccompletion = ^(int result, const char *message) { + if(completionCalled) { return; } - // for all other errors, log and forward - if (logger) { - logger(err.localizedDescription); + if (result != 0) { + *err = [self errorWithStr:[NSString stringWithCString:message + encoding:NSASCIIStringEncoding] code:result]; + self->lastHeartbeatError = *err; + } else { + self->lastHeartbeatError = nil; } - completionHandler(err.code, err.localizedDescription); - return; - } + completionCalled = true; - if(heartbeatRunning) { - return; - } - startHeartbeat( - pairingFile, - &provider, - &heartbeatRunning, - ^(int result, const char *message) { - completionHandler(result, - [NSString stringWithCString:message - encoding:NSASCIIStringEncoding]); - }, - [self createCLogger:logger] - ); + dispatch_semaphore_signal(semaphore); + }; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + NSLog(@"Start heartbeat NOW %p", pthread_self()); + startHeartbeat( + pairingFile, + &self->provider, + globalHeartbeatToken,Ccompletion + ); + }); + // allow 2 seconds for heartbeat, otherwise we declare timeout + intptr_t isTimeout = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(5 * NSEC_PER_SEC))); + if(isTimeout) { + Ccompletion(-1, "Heartbeat failed to complete in reasonable time."); + } else { + NSLog(@"Start heartbeat success %p", pthread_self()); + } + + os_unfair_lock_lock(&heartbeatLock); + heartbeatRunning = NO; + heartbeatSemaphore = NULL; + os_unfair_lock_unlock(&heartbeatLock); + dispatch_semaphore_signal(completionSemaphore); + + return *err == nil; } -- (void)ensureHeartbeat { - // wait a bit until heartbeat finish. wait at most 10s - int deadline = 50; - while((!lastHeartbeatDate || [[NSDate now] timeIntervalSinceDate:lastHeartbeatDate] > 15) && deadline) { - --deadline; - usleep(200); +- (BOOL)ensureHeartbeatWithError:(NSError**)err { + // if it's 15s after last heartbeat, we restart heartbeat. + if((!lastHeartbeatDate || [[NSDate now] timeIntervalSinceDate:lastHeartbeatDate] > 15)) { + return [self startHeartbeat:err]; } + return YES; } - (BOOL)debugAppWithBundleID:(NSString*)bundleID logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback { - if (!provider) { - if (logger) { - logger(@"Provider not initialized!"); - } - NSLog(@"Provider not initialized!"); + NSError* err = nil; + [self ensureHeartbeatWithError:&err]; + if(err) { + logger(err.localizedDescription); return NO; } - [self ensureHeartbeat]; - return debug_app(provider, [bundleID UTF8String], [self createCLogger:logger], jsCallback) == 0; } - (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback { - if (!provider) { - if (logger) { - logger(@"Provider not initialized!"); - } - NSLog(@"Provider not initialized!"); + NSError* err = nil; + [self ensureHeartbeatWithError:&err]; + if(err) { + logger(err.localizedDescription); return NO; } - [self ensureHeartbeat]; - return debug_app_pid(provider, pid, [self createCLogger:logger], jsCallback) == 0; } - (NSDictionary*)getAppListWithError:(NSError**)error { - if (!provider) { - NSLog(@"Provider not initialized!"); - *error = [self errorWithStr:@"Provider not initialized!" code:-1]; + [self ensureHeartbeatWithError:error]; + if(*error) { return nil; } @@ -193,9 +245,8 @@ - (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCall } - (NSDictionary*)getAllAppsWithError:(NSError**)error { - if (!provider) { - NSLog(@"Provider not initialized!"); - *error = [self errorWithStr:@"Provider not initialized!" code:-1]; + [self ensureHeartbeatWithError:error]; + if(*error) { return nil; } @@ -209,9 +260,8 @@ - (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCall } - (NSDictionary*)getHiddenSystemAppsWithError:(NSError**)error { - if (!provider) { - NSLog(@"Provider not initialized!"); - *error = [self errorWithStr:@"Provider not initialized!" code:-1]; + [self ensureHeartbeatWithError:error]; + if(*error) { return nil; } @@ -225,9 +275,8 @@ - (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCall } - (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error { - if (!provider) { - NSLog(@"Provider not initialized!"); - *error = [self errorWithStr:@"Provider not initialized!" code:-1]; + [self ensureHeartbeatWithError:error]; + if(*error) { return nil; } @@ -241,16 +290,13 @@ - (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error { } - (BOOL)launchAppWithoutDebug:(NSString*)bundleID logger:(LogFunc)logger { - if (!provider) { - if (logger) { - logger(@"Provider not initialized!"); - } - NSLog(@"Provider not initialized!"); + NSError* err = nil; + [self ensureHeartbeatWithError:&err]; + if(err) { + logger(err.localizedDescription); return NO; } - [self ensureHeartbeat]; - int result = launch_app_via_proxy(provider, [bundleID UTF8String], [self createCLogger:logger]); @@ -260,10 +306,10 @@ - (BOOL)launchAppWithoutDebug:(NSString*)bundleID logger:(LogFunc)logger { - (void)startSyslogRelayWithHandler:(SyslogLineHandler)lineHandler onError:(SyslogErrorHandler)errorHandler { - if (!provider) { - if (errorHandler) { - errorHandler([self errorWithStr:@"Provider not initialized!" code:-1]); - } + NSError* error = nil; + [self ensureHeartbeatWithError:&error]; + if(error) { + errorHandler(error); return; } if (!lineHandler || syslogStreaming) { @@ -279,8 +325,6 @@ - (void)startSyslogRelayWithHandler:(SyslogLineHandler)lineHandler __strong typeof(self) strongSelf = weakSelf; if (!strongSelf) { return; } - [strongSelf ensureHeartbeat]; - SyslogRelayClientHandle *client = NULL; IdeviceFfiError *err = syslog_relay_connect_tcp(strongSelf->provider, &client); if (err != NULL) { @@ -370,27 +414,26 @@ - (void)handleSyslogFailure:(NSError *)error { } - (NSArray*)fetchAllProfiles:(NSError **)error { - if (!provider) { - NSLog(@"Provider not initialized!"); - *error = [self errorWithStr:@"Provider not initialized!" code:-1]; + [self ensureHeartbeatWithError:error]; + if(*error) { return nil; } + return fetchAppProfiles(provider, error); } - (BOOL)removeProfileWithUUID:(NSString*)uuid error:(NSError **)error { - if (!provider) { - NSLog(@"Provider not initialized!"); - *error = [self errorWithStr:@"Provider not initialized!" code:-1]; + [self ensureHeartbeatWithError:error]; + if(*error) { return nil; } + return removeProfile(provider, uuid, error); } - (BOOL)addProfile:(NSData*)profile error:(NSError **)error { - if (!provider) { - NSLog(@"Provider not initialized!"); - *error = [self errorWithStr:@"Provider not initialized!" code:-1]; + [self ensureHeartbeatWithError:error]; + if(*error) { return nil; } return addProfile(provider, profile, error); @@ -404,10 +447,11 @@ - (void)dealloc { } - (NSArray*)fetchProcessesViaAppServiceWithError:(NSError **)error { - NSURL *documents = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject; - NSURL *pairingURL = [documents URLByAppendingPathComponent:@"pairingFile.plist"]; - IdevicePairingFile *pairingFile = NULL; - IdeviceProviderHandle *tempProvider = NULL; + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + IdeviceProviderHandle *providerToUse = provider; CoreDeviceProxyHandle *coreProxy = NULL; AdapterHandle *adapter = NULL; @@ -420,38 +464,6 @@ - (void)dealloc { IdeviceFfiError *ffiError = NULL; do { - if (!providerToUse) { - ffiError = idevice_pairing_file_read(pairingURL.path.UTF8String, &pairingFile); - if (ffiError) { - if (error) { - *error = [self errorWithStr:@"Unable to read pairing file" code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - struct sockaddr_in addr = {0}; - addr.sin_family = AF_INET; - addr.sin_port = htons(LOCKDOWN_PORT); - NSString *ip = [[NSUserDefaults standardUserDefaults] stringForKey:@"TunnelDeviceIP"]; - if (ip.length == 0) { - ip = @"10.7.0.2"; - } - inet_pton(AF_INET, ip.UTF8String, &addr.sin_addr); - - ffiError = idevice_tcp_provider_new((struct sockaddr *)&addr, pairingFile, "ProcessInspector", &tempProvider); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to open provider"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - providerToUse = tempProvider; - } ffiError = core_device_proxy_connect(providerToUse, &coreProxy); if (ffiError) { @@ -563,18 +575,13 @@ - (void)dealloc { if (coreProxy) { core_device_proxy_free(coreProxy); } - if (tempProvider) { - idevice_provider_free(tempProvider); - } - if (pairingFile) { - idevice_pairing_file_free(pairingFile); - } return result; } - (NSArray*)_fetchProcessListLocked:(NSError**)error { - if (provider) { - [self ensureHeartbeat]; + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; } return [self fetchProcessesViaAppServiceWithError:error]; } @@ -592,10 +599,11 @@ - (void)dealloc { } - (BOOL)killProcessWithPID:(int)pid error:(NSError **)error { - NSURL *documents = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject; - NSURL *pairingURL = [documents URLByAppendingPathComponent:@"pairingFile.plist"]; - IdevicePairingFile *pairingFile = NULL; - IdeviceProviderHandle *tempProvider = NULL; + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + IdeviceProviderHandle *providerToUse = provider; CoreDeviceProxyHandle *coreProxy = NULL; AdapterHandle *adapter = NULL; @@ -607,41 +615,6 @@ - (BOOL)killProcessWithPID:(int)pid error:(NSError **)error { BOOL success = NO; do { - if (!providerToUse) { - ffiError = idevice_pairing_file_read(pairingURL.path.UTF8String, &pairingFile); - if (ffiError) { - if (error) { - *error = [self errorWithStr:@"Unable to read pairing file" code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - struct sockaddr_in addr = {0}; - addr.sin_family = AF_INET; - addr.sin_port = htons(LOCKDOWN_PORT); - NSString *ip = [[NSUserDefaults standardUserDefaults] stringForKey:@"TunnelDeviceIP"]; - if (ip.length == 0) { - ip = @"10.7.0.2"; - } - inet_pton(AF_INET, ip.UTF8String, &addr.sin_addr); - - ffiError = idevice_tcp_provider_new((struct sockaddr *)&addr, pairingFile, "ProcessInspectorKill", &tempProvider); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to open provider"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - providerToUse = tempProvider; - } else { - [self ensureHeartbeat]; - } - ffiError = core_device_proxy_connect(providerToUse, &coreProxy); if (ffiError) { if (error) { @@ -742,13 +715,26 @@ - (BOOL)killProcessWithPID:(int)pid error:(NSError **)error { if (coreProxy) { core_device_proxy_free(coreProxy); } - if (tempProvider) { - idevice_provider_free(tempProvider); + return success; +} + +- (NSUInteger)getMountedDeviceCount:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return NO; + } + return getMountedDeviceCount(provider, error); +} +- (NSInteger)mountPersonalDDIWithImagePath:(NSString*)imagePath trustcachePath:(NSString*)trustcachePath manifestPath:(NSString*)manifestPath error:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return 0; } - if (pairingFile) { - idevice_pairing_file_free(pairingFile); + IdevicePairingFile* pairing = [self getPairingFileWithError:error]; + if(*error) { + return 0; } - return success; + return mountPersonalDDI(provider, pairing, imagePath, trustcachePath, manifestPath, error); } @end diff --git a/StikJIT/idevice/heartbeat.h b/StikJIT/idevice/heartbeat.h index 26233800..f39d37ea 100644 --- a/StikJIT/idevice/heartbeat.h +++ b/StikJIT/idevice/heartbeat.h @@ -14,9 +14,8 @@ typedef void (^HeartbeatCompletionHandlerC)(int result, const char *message); typedef void (^LogFuncC)(const char* message, ...); -extern bool isHeartbeat; +extern int globalHeartbeatToken; extern NSDate* lastHeartbeatDate; -void startHeartbeat(IdevicePairingFile* pairintFile, IdeviceProviderHandle** provider, bool* isHeartbeat, HeartbeatCompletionHandlerC completion, LogFuncC logger); - +void startHeartbeat(IdevicePairingFile* pairing_file, IdeviceProviderHandle** provider, int heartbeatToken, HeartbeatCompletionHandlerC completion); #endif /* HEARTBEAT_H */ diff --git a/StikJIT/idevice/heartbeat.m b/StikJIT/idevice/heartbeat.m index fafe403f..83b49f0e 100644 --- a/StikJIT/idevice/heartbeat.m +++ b/StikJIT/idevice/heartbeat.m @@ -10,34 +10,34 @@ #include #include #include "heartbeat.h" +#import - -bool isHeartbeat = false; +int globalHeartbeatToken = 0; NSDate* lastHeartbeatDate = nil; -void startHeartbeat(IdevicePairingFile* pairing_file, IdeviceProviderHandle** provider, bool* isHeartbeat, HeartbeatCompletionHandlerC completion, LogFuncC logger) { - - // Initialize logger - idevice_init_logger(Debug, Disabled, NULL); - - // Create the socket address (replace with your device's IP) - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons(LOCKDOWN_PORT); - inet_pton(AF_INET, "10.7.0.2", &addr.sin_addr); - - IdeviceProviderHandle* newProvider = 0; - IdeviceFfiError* err = idevice_tcp_provider_new((struct sockaddr *)&addr, pairing_file, - "ExampleProvider", &newProvider); - if (err != NULL) { - fprintf(stderr, "Failed to create TCP provider: [%d] %s", err->code, - err->message); - idevice_pairing_file_free(pairing_file); - idevice_error_free(err); - *isHeartbeat = false; - return; - } +void startHeartbeat(IdevicePairingFile* pairing_file, IdeviceProviderHandle** provider, int heartbeatToken, HeartbeatCompletionHandlerC completion) { + IdeviceProviderHandle* newProvider = *provider; + IdeviceFfiError* err = nil; + + // Create the socket address (replace with your device's IP) + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(LOCKDOWN_PORT); + inet_pton(AF_INET, "10.7.0.1", &addr.sin_addr); + + + err = idevice_tcp_provider_new((struct sockaddr *)&addr, pairing_file, + "ExampleProvider", &newProvider); + if (err != NULL) { + fprintf(stderr, "Failed to create TCP provider: [%d] %s", err->code, + err->message); + completion(err->code, err->message); + idevice_pairing_file_free(pairing_file); + idevice_error_free(err); + + return; + } // Connect to installation proxy HeartbeatClientHandle *client = NULL; @@ -45,21 +45,18 @@ void startHeartbeat(IdevicePairingFile* pairing_file, IdeviceProviderHandle** pr if (err != NULL) { fprintf(stderr, "Failed to connect to installation proxy: [%d] %s", err->code, err->message); + completion(err->code, err->message); idevice_provider_free(newProvider); idevice_error_free(err); - *isHeartbeat = false; - return; - } - if(*isHeartbeat) { - idevice_provider_free(newProvider); + return; } + + + *provider = newProvider; - // we mark heartbeat as success and set the default provider - *isHeartbeat = true; - *provider = newProvider; - - completion(0, "Heartbeat Completed"); + + bool completionCalled = false; u_int64_t current_interval = 15; while (1) { @@ -67,22 +64,49 @@ void startHeartbeat(IdevicePairingFile* pairing_file, IdeviceProviderHandle** pr u_int64_t new_interval = 0; err = heartbeat_get_marco(client, current_interval, &new_interval); if (err != NULL) { - fprintf(stderr, "Failed to get marco: [%d] %s", err->code, err->message); - heartbeat_client_free(client); + fprintf(stderr, "Failed to get marco: [%d] %s token = %d, pthread_self = %p\n", err->code, err->message, heartbeatToken, pthread_self()); + if(!completionCalled) { + completion(err->code, err->message); + } +// heartbeat_client_free(client); idevice_error_free(err); - *isHeartbeat = false; return; } + + // if a new heartbeat thread is running we quit current one + if (heartbeatToken != globalHeartbeatToken) { + heartbeat_client_free(client); + + NSLog(@"Quitting %d, now token = %d", heartbeatToken, globalHeartbeatToken); + return; + } + current_interval = new_interval + 5; // Reply err = heartbeat_send_polo(client); if (err != NULL) { fprintf(stderr, "Failed to get marco: [%d] %s", err->code, err->message); + if(!completionCalled) { + completion(err->code, err->message); + } heartbeat_client_free(client); idevice_error_free(err); - *isHeartbeat = false; + return; } + + if (lastHeartbeatDate && [[NSDate now] timeIntervalSinceDate:lastHeartbeatDate] > current_interval) { + lastHeartbeatDate = nil; +// NSLog(@"[SJ] Heartbeat marco receive timeout, probably disconnected, token = %d, pthread_self = %p", heartbeatToken, pthread_self()); + return; + } + lastHeartbeatDate = [NSDate now]; +// NSLog(@"[SJ] Heartbeat finished at %@, token = %d, pthread_self = %p", lastHeartbeatDate, heartbeatToken, pthread_self()); + + + if (!completionCalled) { + completion(0, "Heartbeat succeeded"); + } } } diff --git a/StikJIT/idevice/ideviceinfo.c b/StikJIT/idevice/ideviceinfo.c index ac8b3052..98ae253e 100644 --- a/StikJIT/idevice/ideviceinfo.c +++ b/StikJIT/idevice/ideviceinfo.c @@ -11,7 +11,6 @@ #include "idevice.h" static struct IdeviceProviderHandle * g_provider = NULL; -static struct HeartbeatClientHandle * g_hb = NULL; static struct LockdowndClientHandle * g_client = NULL; static struct IdevicePairingFile * g_sess_pf = NULL; @@ -29,7 +28,7 @@ int ideviceinfo_c_init(const char *pairing_file_path) { struct sockaddr_in sin = { .sin_family = AF_INET, .sin_port = htons(LOCKDOWN_PORT) }; - inet_pton(AF_INET, "10.7.0.2", &sin.sin_addr); + inet_pton(AF_INET, "10.7.0.1", &sin.sin_addr); err = idevice_tcp_provider_new((const struct sockaddr *)&sin, pf, @@ -41,13 +40,6 @@ int ideviceinfo_c_init(const char *pairing_file_path) { return 2; } - err = heartbeat_connect(g_provider, &g_hb); - if (!err) { - heartbeat_send_polo(g_hb); - } else { - idevice_error_free(err); - } - err = lockdownd_connect(g_provider, &g_client); if (err) { idevice_error_free(err); @@ -104,10 +96,6 @@ void ideviceinfo_c_cleanup(void) { idevice_pairing_file_free(g_sess_pf); g_sess_pf = NULL; } - if (g_hb) { - heartbeat_client_free(g_hb); - g_hb = NULL; - } if (g_provider) { idevice_provider_free(g_provider); g_provider = NULL; diff --git a/StikJIT/idevice/jit.c b/StikJIT/idevice/jit.c index d725d271..11c1ce10 100644 --- a/StikJIT/idevice/jit.c +++ b/StikJIT/idevice/jit.c @@ -85,7 +85,7 @@ void runDebugServerCommand(int pid, int debug_app(IdeviceProviderHandle* tcp_provider, const char *bundle_id, LogFuncC logger, DebugAppCallback callback) { // Initialize logger - idevice_init_logger(Info, Disabled, NULL); +// idevice_init_logger(Info, Disabled, NULL); IdeviceFfiError* err = 0; CoreDeviceProxyHandle *core_device = NULL; @@ -314,7 +314,7 @@ int debug_app_pid(IdeviceProviderHandle* tcp_provider, int pid, LogFuncC logger, } int launch_app_via_proxy(IdeviceProviderHandle* tcp_provider, const char *bundle_id, LogFuncC logger) { - idevice_init_logger(Info, Disabled, NULL); +// idevice_init_logger(Info, Disabled, NULL); IdeviceFfiError* err = NULL; CoreDeviceProxyHandle *core_device = NULL; diff --git a/StikJIT/idevice/mount.h b/StikJIT/idevice/mount.h new file mode 100644 index 00000000..2a09749a --- /dev/null +++ b/StikJIT/idevice/mount.h @@ -0,0 +1,14 @@ +// +// mount.h +// StikDebug +// +// Created by s s on 2025/12/6. +// + +#ifndef MOUNT_H +#define MOUNT_H +#include "idevice.h" +#include +size_t getMountedDeviceCount(IdeviceProviderHandle* provider, NSError** error); +int mountPersonalDDI(IdeviceProviderHandle* provider, IdevicePairingFile* pairingFile2, NSString* imagePath, NSString* trustcachePath, NSString* manifestPath, NSError** error); +#endif diff --git a/StikJIT/idevice/mount.m b/StikJIT/idevice/mount.m new file mode 100644 index 00000000..3439b33f --- /dev/null +++ b/StikJIT/idevice/mount.m @@ -0,0 +1,99 @@ +// +// mount1.m +// StikDebug +// +// Created by s s on 2025/12/6. +// +#include "mount.h" +@import Foundation; +NSError* makeError(int code, NSString* msg); +size_t getMountedDeviceCount(IdeviceProviderHandle* provider, NSError** error) { + ImageMounterHandle* client = 0; + IdeviceFfiError* err = image_mounter_connect(provider, &client); + if (err) { + *error = makeError(err->code, @(err->message)); + idevice_error_free(err); + return 0; + } + plist_t* devices; + size_t deviceLength = 0; + err = image_mounter_copy_devices(client, &devices, &deviceLength); + if (err) { + *error = makeError(err->code, @(err->message)); + idevice_error_free(err); + return 0; + } + // no need to read the device, we just check the length + for(int i = 0;i < deviceLength; ++i) { + plist_free(devices[i]); + } + idevice_data_free((uint8_t *)devices, deviceLength*sizeof(plist_t*)); + image_mounter_free(client); + return deviceLength; +} + + +int mountPersonalDDI(IdeviceProviderHandle* provider, IdevicePairingFile* pairingFile2, NSString* imagePath, NSString* trustcachePath, NSString* manifestPath, NSError** error) { + NSData* image = [NSData dataWithContentsOfFile:imagePath]; + NSData* trustcache = [NSData dataWithContentsOfFile:trustcachePath]; + NSData* buildManifest = [NSData dataWithContentsOfFile:manifestPath]; + if(!image || !trustcache || !buildManifest) { + *error = makeError(1, @"Failed to read one or more files"); + return 1; + } + + LockdowndClientHandle* lockdownClient = 0; + IdeviceFfiError* err = lockdownd_connect(provider, &lockdownClient); + if (err) { + *error = makeError(6, @(err->message)); + idevice_error_free(err); + return 6; + } + + err = lockdownd_start_session(lockdownClient, pairingFile2); + if (err) { + *error = makeError(7, @(err->message)); + idevice_error_free(err); + return 7; // EC: 7 + } + + plist_t uniqueChipIDPlist = 0; + err = lockdownd_get_value(lockdownClient, "UniqueChipID", 0, &uniqueChipIDPlist); + if (err) { + *error = makeError(8, @(err->message)); + idevice_error_free(err); + return 8; // EC: 8 + } + + uint64_t uniqueChipID = 0; + plist_get_uint_val(uniqueChipIDPlist, &uniqueChipID); + + ImageMounterHandle* mounterClient = 0; + err = image_mounter_connect(provider, &mounterClient); + if (err) { + *error = makeError(9, @(err->message)); + idevice_error_free(err); + return 9; // EC: 9 + } + + image_mounter_mount_personalized( + mounterClient, + provider, + [image bytes], + [image length], + [trustcache bytes], + [trustcache length], + [buildManifest bytes], + [buildManifest length], + nil, + uniqueChipID + ); + + if (err) { + *error = makeError(10, @(err->message)); + idevice_error_free(err); + return 10; // EC: 10 + } + + return 0; +} From 4d9aef8a3e8292bb3b7c646d3f01d85c51c88b71 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sat, 6 Dec 2025 19:44:13 +0800 Subject: [PATCH 2/9] remove some debug loggings --- StikJIT/idevice/JITEnableContext.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/StikJIT/idevice/JITEnableContext.m b/StikJIT/idevice/JITEnableContext.m index 18ab18d4..def78f10 100644 --- a/StikJIT/idevice/JITEnableContext.m +++ b/StikJIT/idevice/JITEnableContext.m @@ -126,10 +126,8 @@ - (BOOL)startHeartbeat:(NSError**)err { os_unfair_lock_unlock(&heartbeatLock); if (waitSemaphore) { - NSLog(@"waiting %p", pthread_self()); dispatch_semaphore_wait(waitSemaphore, DISPATCH_TIME_FOREVER); dispatch_semaphore_signal(waitSemaphore); - NSLog(@"waiting complete %p", pthread_self()); } *err = lastHeartbeatError; return *err == nil; @@ -171,7 +169,6 @@ - (BOOL)startHeartbeat:(NSError**)err { }; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - NSLog(@"Start heartbeat NOW %p", pthread_self()); startHeartbeat( pairingFile, &self->provider, From 9702ff3e8fbe3fd740d53c958d2d1f5c28c66cb2 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sat, 6 Dec 2025 20:00:15 +0800 Subject: [PATCH 3/9] stik mini tool poc --- StikJIT/Info.plist | 22 ++ StikJIT/JSSupport/ScriptListView.swift | 2 +- .../MiniToolSupport/MiniToolEditorView.swift | 97 +++++++ .../MiniToolSupport/MiniToolListView.swift | 255 ++++++++++++++++++ StikJIT/MiniToolSupport/MiniToolModels.swift | 89 ++++++ .../MiniToolSupport/MiniToolRunnerView.swift | 103 +++++++ StikJIT/MiniToolSupport/MiniToolRuntime.swift | 199 ++++++++++++++ StikJIT/Utilities/TabConfiguration.swift | 2 +- StikJIT/Views/MainTabView.swift | 1 + StikJIT/Views/SettingsView.swift | 1 + 10 files changed, 769 insertions(+), 2 deletions(-) create mode 100644 StikJIT/MiniToolSupport/MiniToolEditorView.swift create mode 100644 StikJIT/MiniToolSupport/MiniToolListView.swift create mode 100644 StikJIT/MiniToolSupport/MiniToolModels.swift create mode 100644 StikJIT/MiniToolSupport/MiniToolRunnerView.swift create mode 100644 StikJIT/MiniToolSupport/MiniToolRuntime.swift diff --git a/StikJIT/Info.plist b/StikJIT/Info.plist index 27dbccf4..41ae6230 100644 --- a/StikJIT/Info.plist +++ b/StikJIT/Info.plist @@ -28,5 +28,27 @@ UIFileSharingEnabled + UTExportedTypeDeclarations + + + UTTypeConformsTo + + com.apple.package + + UTTypeDescription + StikDebug Mini Tool + UTTypeIconFiles + + UTTypeIdentifier + com.stik.StikJIT.stiktool + UTTypeTagSpecification + + public.filename-extension + + stiktool + + + + diff --git a/StikJIT/JSSupport/ScriptListView.swift b/StikJIT/JSSupport/ScriptListView.swift index e29369a0..f9f7a083 100644 --- a/StikJIT/JSSupport/ScriptListView.swift +++ b/StikJIT/JSSupport/ScriptListView.swift @@ -440,7 +440,7 @@ struct ScriptListView: View { } // MARK: - Equal-width rounded-rectangle button (centered content) -private struct WideGlassyButton: View { +struct WideGlassyButton: View { let title: String let systemImage: String let action: () -> Void diff --git a/StikJIT/MiniToolSupport/MiniToolEditorView.swift b/StikJIT/MiniToolSupport/MiniToolEditorView.swift new file mode 100644 index 00000000..0f0391d3 --- /dev/null +++ b/StikJIT/MiniToolSupport/MiniToolEditorView.swift @@ -0,0 +1,97 @@ +import SwiftUI +import CodeEditorView +import LanguageSupport + +struct MiniToolEditorView: View { + let tool: MiniToolBundle + + private enum Target: String, CaseIterable, Identifiable { + case indexHTML = "index.html" + case backgroundJS = "background.js" + + var id: String { rawValue } + var displayName: String { + switch self { + case .indexHTML: return "index.html" + case .backgroundJS: return "background.js" + } + } + } + + @State private var htmlContent: String = "" + @State private var backgroundContent: String = "" + @State private var selectedTarget: Target = .indexHTML + @State private var position: CodeEditor.Position = .init() + @State private var messages: Set> = [] + + @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue + @Environment(\.colorScheme) private var colorScheme + @Environment(\.dismiss) private var dismiss + @Environment(\.themeExpansionManager) private var themeExpansion + + private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle } + private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) } + private var editorTheme: Theme { + colorScheme == .dark ? Theme.defaultDark : Theme.defaultLight + } + + var body: some View { + ZStack { + ThemedBackground(style: backgroundStyle) + .ignoresSafeArea() + + VStack(spacing: 12) { + Picker("File", selection: $selectedTarget) { + ForEach(Target.allCases) { target in + Text(target.displayName).tag(target) + } + } + .pickerStyle(.segmented) + + CodeEditor( + text: binding(for: selectedTarget), + position: $position, + messages: $messages, + language: .swift() + ) + .font(.system(.footnote, design: .monospaced)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .environment(\.codeEditorTheme, editorTheme) + } + .padding(.horizontal, 16) + .padding(.top, 12) + .padding(.bottom, 16) + } + .navigationTitle(tool.name) + .navigationBarTitleDisplayMode(.inline) + .onAppear(perform: load) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + save() + } + } + } + .preferredColorScheme(preferredScheme) + .tint(Color.white) + .toolbar(.hidden, for: .tabBar) + } + + private func binding(for target: Target) -> Binding { + switch target { + case .indexHTML: return $htmlContent + case .backgroundJS: return $backgroundContent + } + } + + private func load() { + htmlContent = (try? String(contentsOf: tool.indexURL)) ?? "" + backgroundContent = (try? String(contentsOf: tool.backgroundURL)) ?? "" + } + + private func save() { + try? htmlContent.write(to: tool.indexURL, atomically: true, encoding: .utf8) + try? backgroundContent.write(to: tool.backgroundURL, atomically: true, encoding: .utf8) + dismiss() + } +} diff --git a/StikJIT/MiniToolSupport/MiniToolListView.swift b/StikJIT/MiniToolSupport/MiniToolListView.swift new file mode 100644 index 00000000..ae550013 --- /dev/null +++ b/StikJIT/MiniToolSupport/MiniToolListView.swift @@ -0,0 +1,255 @@ +import SwiftUI +import UniformTypeIdentifiers +import UIKit + +struct MiniToolListView: View { + @StateObject private var store = MiniToolStore() + @State private var searchText = "" + @State private var showImporter = false + @State private var showDeleteConfirmation = false + @State private var pendingDelete: MiniToolBundle? + @State private var alertVisible = false + @State private var alertTitle = "" + @State private var alertMessage = "" + @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue + @Environment(\.themeExpansionManager) private var themeExpansion + @Environment(\.colorScheme) private var colorScheme + + private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle } + private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) } + + private var filteredTools: [MiniToolBundle] { + guard !searchText.isEmpty else { return store.tools } + return store.tools.filter { $0.name.localizedCaseInsensitiveContains(searchText) } + } + + var body: some View { + NavigationStack { + ZStack { + ThemedBackground(style: backgroundStyle) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 20) { + headerCard + + if filteredTools.isEmpty { + emptyCard + } else { + ForEach(filteredTools) { tool in + toolRow(tool) + } + } + } + .padding(.horizontal, 20) + .padding(.vertical, 30) + } + + if store.isBusy { + Color.black.opacity(0.35).ignoresSafeArea() + ProgressView("Working…") + .padding(16) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(.ultraThinMaterial) + .overlay( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .strokeBorder(Color.white.opacity(0.15), lineWidth: 1) + ) + ) + .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) + } + + if alertVisible { + CustomErrorView( + title: alertTitle, + message: alertMessage, + onDismiss: { alertVisible = false }, + messageType: .error + ) + } + } + .navigationTitle("Mini Tools") + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { showImporter = true } label: { + Label("Import", systemImage: "tray.and.arrow.down") + } + } + } + .onAppear { store.refresh() } + .onChange(of: store.lastError) { _, message in + guard let message else { return } + presentError(title: "Mini Tool", message: message) + store.lastError = nil + } + .alert("Delete Mini Tool?", isPresented: $showDeleteConfirmation, presenting: pendingDelete) { tool in + Button("Delete", role: .destructive) { delete(tool) } + Button("Cancel", role: .cancel) { pendingDelete = nil } + } message: { tool in + Text("Delete \(tool.name)? This removes its files permanently.") + } + .fileImporter( + isPresented: $showImporter, + allowedContentTypes: [UTType("com.stik.StikJIT.stiktool") ?? .data], + allowsMultipleSelection: false + ) { result in + switch result { + case .success(let urls): + guard let url = urls.first else { return } + store.importTool(from: url) + case .failure(let error): + presentError(title: "Import Failed", message: error.localizedDescription) + } + } + } + .preferredColorScheme(preferredScheme) + } + + // MARK: - Cards + + private var headerCard: some View { + VStack(spacing: 12) { + TextField("Search tools…", text: $searchText) + .padding(12) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .strokeBorder(Color.white.opacity(0.12), lineWidth: 1) + ) + + HStack(spacing: 12) { + WideGlassyButton(title: "Import", systemImage: "tray.and.arrow.down") { + showImporter = true + } + } + } + .padding(20) + .background(glassyBackground) + } + + private func toolRow(_ tool: MiniToolBundle) -> some View { + HStack(spacing: 12) { + NavigationLink { + MiniToolRunnerView(tool: tool) + } label: { + HStack(spacing: 12) { + Image(systemName: "shippingbox.fill") + .foregroundColor(.blue) + .imageScale(.large) + VStack(alignment: .leading, spacing: 4) { + Text(tool.name) + .font(.body.weight(.medium)) + .lineLimit(1) + Text(tool.url.lastPathComponent) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + } + .buttonStyle(.plain) + + Spacer() + + NavigationLink { + MiniToolEditorView(tool: tool) + } label: { + Image(systemName: "pencil") + .foregroundColor(.primary) + } + .buttonStyle(.borderless) + + Button(role: .destructive) { + pendingDelete = tool + showDeleteConfirmation = true + } label: { + Image(systemName: "trash") + .foregroundColor(.red) + } + .buttonStyle(.borderless) + } + .padding(20) + .background(glassyBackground) + .contextMenu { + Button { copy(tool.url.lastPathComponent) } label: { + Label("Copy Name", systemImage: "doc.on.doc") + } + Button { copy(tool.url.path) } label: { + Label("Copy Path", systemImage: "folder") + } + } + } + + private var emptyCard: some View { + VStack(spacing: 6) { + Label("No mini tools found", systemImage: "shippingbox") + .font(.subheadline.weight(.semibold)) + Text("Tap New or Import to add a tool.") + .font(.footnote) + .foregroundColor(.secondary) + } + .padding(40) + .frame(maxWidth: .infinity) + .background(glassyBackground) + } + + private var glassyBackground: some View { + ZStack { + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(.ultraThinMaterial) + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill( + LinearGradient( + gradient: Gradient(colors: overlayColors()), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .opacity(0.32) + RoundedRectangle(cornerRadius: 20, style: .continuous) + .strokeBorder(Color.white.opacity(0.15), lineWidth: 1) + } + .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) + } + + private func overlayColors() -> [Color] { + let colors: [Color] + switch backgroundStyle { + case .staticGradient(let palette): + colors = palette + case .animatedGradient(let palette, _): + colors = palette + case .blobs(_, let background): + colors = background + case .particles(let particle, let background): + colors = background.isEmpty ? [particle, particle.opacity(0.4)] : background + case .customGradient(let palette): + colors = palette + case .adaptiveGradient(let light, let dark): + colors = colorScheme == .dark ? dark : light + } + if colors.count >= 2 { return colors } + if let first = colors.first { return [first, first.opacity(0.6)] } + return [Color.blue, Color.purple] + } + + // MARK: - Actions + + private func delete(_ tool: MiniToolBundle) { + store.delete(tool) + if let error = store.lastError { + presentError(title: "Delete Failed", message: error) + } + } + + private func presentError(title: String, message: String) { + alertTitle = title + alertMessage = message + alertVisible = true + } + + private func copy(_ text: String) { + UIPasteboard.general.string = text + } +} diff --git a/StikJIT/MiniToolSupport/MiniToolModels.swift b/StikJIT/MiniToolSupport/MiniToolModels.swift new file mode 100644 index 00000000..51a77014 --- /dev/null +++ b/StikJIT/MiniToolSupport/MiniToolModels.swift @@ -0,0 +1,89 @@ +import Foundation + +struct MiniToolBundle: Identifiable, Hashable { + let url: URL + + var id: String { url.lastPathComponent } + var name: String { url.deletingPathExtension().lastPathComponent } + var indexURL: URL { url.appendingPathComponent("index.html") } + var backgroundURL: URL { url.appendingPathComponent("background.js") } + + var isValid: Bool { + let fm = FileManager.default + return fm.fileExists(atPath: indexURL.path) && fm.fileExists(atPath: backgroundURL.path) + } +} + +final class MiniToolStore: ObservableObject { + @Published private(set) var tools: [MiniToolBundle] = [] + @Published var isBusy: Bool = false + @Published var lastError: String? + + func refresh() { + let directory = toolsDirectory() + let contents = (try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil)) ?? [] + let bundles = contents + .filter { url in + let ext = url.pathExtension.lowercased() + return ext == "stiktool" || url.hasDirectoryPath + } + .map { MiniToolBundle(url: $0) } + .filter { $0.isValid } + .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + DispatchQueue.main.async { + self.tools = bundles + } + } + + func delete(_ tool: MiniToolBundle) { + do { + try FileManager.default.removeItem(at: tool.url) + refresh() + } catch { + lastError = error.localizedDescription + } + } + + func importTool(from externalURL: URL) { + isBusy = true + DispatchQueue.global(qos: .userInitiated).async { + defer { DispatchQueue.main.async { self.isBusy = false } } + do { + let accessing = externalURL.startAccessingSecurityScopedResource() + defer { if accessing { externalURL.stopAccessingSecurityScopedResource() } } + let destination = self.toolsDirectory().appendingPathComponent(externalURL.lastPathComponent) + let fm = FileManager.default + if fm.fileExists(atPath: destination.path) { + try fm.removeItem(at: destination) + } + try fm.copyItem(at: externalURL, to: destination) + DispatchQueue.main.async { self.refresh() } + } catch { + DispatchQueue.main.async { self.lastError = error.localizedDescription } + } + } + } + + func toolsDirectory() -> URL { + let dir = URL.documentsDirectory.appendingPathComponent("tools", isDirectory: true) + var isDir: ObjCBool = false + let fm = FileManager.default + if fm.fileExists(atPath: dir.path, isDirectory: &isDir) { + if !isDir.boolValue { + try? fm.removeItem(at: dir) + } + } + if !fm.fileExists(atPath: dir.path) { + try? fm.createDirectory(at: dir, withIntermediateDirectories: true) + } + return dir + } + + private func sanitizedToolName(_ raw: String) -> String { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_")) + let filtered = trimmed.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" } + let name = String(filtered).replacingOccurrences(of: "__+", with: "_", options: .regularExpression) + return name.isEmpty ? "Untitled" : name + } +} diff --git a/StikJIT/MiniToolSupport/MiniToolRunnerView.swift b/StikJIT/MiniToolSupport/MiniToolRunnerView.swift new file mode 100644 index 00000000..ce95c727 --- /dev/null +++ b/StikJIT/MiniToolSupport/MiniToolRunnerView.swift @@ -0,0 +1,103 @@ +import SwiftUI +import WebKit + +struct MiniToolRunnerView: View { + let tool: MiniToolBundle + @StateObject private var runtime: MiniToolRuntime + @State private var showLogs = false + + @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue + @Environment(\.themeExpansionManager) private var themeExpansion + + private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle } + private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) } + + init(tool: MiniToolBundle) { + self.tool = tool + _runtime = StateObject(wrappedValue: MiniToolRuntime(tool: tool)) + } + + var body: some View { + ZStack { + ThemedBackground(style: backgroundStyle) + .ignoresSafeArea() + + VStack(spacing: 12) { + MiniToolWebContainer(runtime: runtime) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .strokeBorder(Color.white.opacity(0.12), lineWidth: 1) + ) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + + if !runtime.logs.isEmpty && showLogs { + logList + } + } + .padding(16) + } + .navigationTitle(tool.name) + .navigationBarTitleDisplayMode(.inline) + .onAppear { runtime.start() } + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + showLogs.toggle() + } label: { + Label("Toggle Log", systemImage: "bubble.left") + } + Button { + runtime.reload() + } label: { + Label("Reload", systemImage: "arrow.clockwise") + } + } + } + .preferredColorScheme(preferredScheme) + } + + private var logList: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Label("Messages", systemImage: "bubble.left") + .font(.subheadline.weight(.semibold)) + Spacer() + } + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 6) { + ForEach(Array(runtime.logs.enumerated()), id: \.offset) { index, line in + Text(line) + .font(.caption.monospaced()) + .id(index) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + .onChange(of: runtime.logs.count) { _, newCount in + guard newCount > 0 else { return } + withAnimation { proxy.scrollTo(newCount - 1, anchor: .bottom) } + } + } + } + .padding(12) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .strokeBorder(Color.white.opacity(0.12), lineWidth: 1) + ) + } +} + +private struct MiniToolWebContainer: UIViewRepresentable { + @ObservedObject var runtime: MiniToolRuntime + + func makeUIView(context: Context) -> WKWebView { + runtime.webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + } +} diff --git a/StikJIT/MiniToolSupport/MiniToolRuntime.swift b/StikJIT/MiniToolSupport/MiniToolRuntime.swift new file mode 100644 index 00000000..c00be934 --- /dev/null +++ b/StikJIT/MiniToolSupport/MiniToolRuntime.swift @@ -0,0 +1,199 @@ +import Foundation +import SwiftUI +import WebKit +import JavaScriptCore + +final class MiniToolRuntime: NSObject, ObservableObject { + let tool: MiniToolBundle + @Published var logs: [String] = [] + @Published var isReady: Bool = false + + let webView: WKWebView + private var context: JSContext? + + private let messageHandlerName = "miniToolBridge" + + init(tool: MiniToolBundle) { + self.tool = tool + let configuration = WKWebViewConfiguration() + let controller = WKUserContentController() + controller.addUserScript(WKUserScript(source: MiniToolRuntime.frontendBridgeScript, + injectionTime: .atDocumentStart, + forMainFrameOnly: true)) + configuration.userContentController = controller + webView = WKWebView(frame: .zero, configuration: configuration) + + super.init() + + controller.add(self, name: messageHandlerName) + webView.navigationDelegate = self + } + + deinit { + webView.configuration.userContentController.removeScriptMessageHandler(forName: messageHandlerName) + } + + func start() { + DispatchQueue.main.async { + self.logs.removeAll() + } + loadBackground() + loadFrontend() + } + + func reload() { + start() + } + + // MARK: - Loading + + private func loadFrontend() { + guard FileManager.default.fileExists(atPath: tool.indexURL.path) else { + appendLog("index.html missing for \(tool.name)") + return + } + isReady = false + let url = tool.indexURL + webView.loadFileURL(url, allowingReadAccessTo: tool.url) + } + + private func loadBackground() { + context = JSContext() + context?.exceptionHandler = { [weak self] _, exception in + if let message = exception?.toString() { + self?.appendLog("Background exception: \(message)") + } + } + + let sendToFrontend: @convention(block) (Any?) -> Void = { [weak self] payload in + self?.deliverToFrontend(payload ?? NSNull()) + } + + let logFunction: @convention(block) (Any?) -> Void = { [weak self] msg in + self?.appendLog(msg as? String ?? "Unable to decode log message.") + + } + + context?.setObject(sendToFrontend, forKeyedSubscript: "__miniToolPostMessage" as NSString) + context?.setObject(logFunction, forKeyedSubscript: "__miniToolLog" as NSString) + + context?.evaluateScript(MiniToolRuntime.backgroundBridgeScript) + + do { + let script = try String(contentsOf: tool.backgroundURL) + context?.evaluateScript(script) + } catch { + appendLog("Failed to load background.js: \(error.localizedDescription)") + } + } + + private func deliverToBackground(_ payload: Any) { + guard let receiver = context?.objectForKeyedSubscript("__miniToolReceive"), + !receiver.isUndefined else { + appendLog("Background handler is not ready") + return + } + _ = receiver.call(withArguments: [payload]) + } + + private func deliverToFrontend(_ payload: Any) { + guard let json = MiniToolRuntime.encodePayload(payload) else { + appendLog("Unable to encode payload for frontend") + return + } + DispatchQueue.main.async { + let script = "window.miniTool && window.miniTool.__receive(\(json))" + self.webView.evaluateJavaScript(script) { _, error in + if let error { + self.appendLog("Frontend dispatch error: \(error.localizedDescription)") + } + } + } + } + + private func appendLog(_ text: String) { + DispatchQueue.main.async { + self.logs.append(text) + } + } +} + +// MARK: - WKScriptMessageHandler + +extension MiniToolRuntime: WKScriptMessageHandler { + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard message.name == messageHandlerName else { return } + if let dict = message.body as? [String: Any], let payload = dict["payload"] { + deliverToBackground(payload) + } else { + deliverToBackground(message.body) + } + } +} + +// MARK: - WKNavigationDelegate + +extension MiniToolRuntime: WKNavigationDelegate { + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + isReady = true + deliverToBackground(["type": "ui-ready", "tool": tool.name]) + deliverToFrontend(["type": "ready", "tool": tool.name]) + } +} + +// MARK: - Scripts & Encoding + +extension MiniToolRuntime { + static let frontendBridgeScript = """ + window.miniTool = window.miniTool || {}; + window.miniTool.__handler = null; + window.miniTool.onMessage = function(handler) { window.miniTool.__handler = handler; }; + window.miniTool.postMessage = function(payload) { + window.webkit.messageHandlers.miniToolBridge.postMessage({ payload: payload }); + }; + window.miniTool.__receive = function(payload) { + try { + if (typeof window.miniTool.__handler === 'function') { + window.miniTool.__handler(payload); + } + } catch (err) { + console.error(err); + } + }; + """ + + static let backgroundBridgeScript = """ + var miniTool = this.miniTool || {}; + miniTool.__handler = null; + miniTool.onMessage = function(handler) { miniTool.__handler = handler; }; + miniTool.postMessage = function(payload) { __miniToolPostMessage(payload); }; + miniTool.log = function(log) { __miniToolLog(log); } + function __miniToolReceive(payload) { + try { + if (typeof miniTool.__handler === 'function') { + miniTool.__handler(payload); + } + } catch (err) { + console.log(err); + } + } + this.miniTool = miniTool; + """ + + static func encodePayload(_ payload: Any) -> String? { + if JSONSerialization.isValidJSONObject(payload) { + if let data = try? JSONSerialization.data(withJSONObject: payload, options: []), + let string = String(data: data, encoding: .utf8) { + return string + } + } + if let string = payload as? String { + let escaped = string.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + return "\"\(escaped)\"" + } + if let number = payload as? NSNumber { + return number.stringValue + } + return nil + } +} diff --git a/StikJIT/Utilities/TabConfiguration.swift b/StikJIT/Utilities/TabConfiguration.swift index ca2449fb..a7c89900 100644 --- a/StikJIT/Utilities/TabConfiguration.swift +++ b/StikJIT/Utilities/TabConfiguration.swift @@ -3,7 +3,7 @@ import Foundation enum TabConfiguration { static let storageKey = "enabledTabIdentifiers" static let maxSelectableTabs = 4 - private static let baseAllowedIDs: [String] = ["home", "console", "scripts", "profiles", "processes", "deviceinfo"] + private static let baseAllowedIDs: [String] = ["home", "console", "scripts", "tools", "profiles", "processes", "deviceinfo"] static var allowedIDs: [String] { var ids = baseAllowedIDs if FeatureFlags.isLocationSpoofingEnabled { diff --git a/StikJIT/Views/MainTabView.swift b/StikJIT/Views/MainTabView.swift index 0951a0ba..f66ff441 100644 --- a/StikJIT/Views/MainTabView.swift +++ b/StikJIT/Views/MainTabView.swift @@ -53,6 +53,7 @@ struct MainTabView: View { TabDescriptor(id: "home", title: "Home", systemImage: "house") { AnyView(HomeView()) }, TabDescriptor(id: "console", title: "Console", systemImage: "terminal") { AnyView(ConsoleLogsView()) }, TabDescriptor(id: "scripts", title: "Scripts", systemImage: "scroll") { AnyView(ScriptListView()) }, + TabDescriptor(id: "tools", title: "Mini Tools", systemImage: "shippingbox.fill") { AnyView(MiniToolListView()) }, TabDescriptor(id: "profiles", title: "Profiles", systemImage: "magazine.fill") { AnyView(ProfileView()) }, TabDescriptor(id: "processes", title: "Processes", systemImage: "rectangle.stack.person.crop") { AnyView(ProcessInspectorView()) }, TabDescriptor(id: "deviceinfo", title: "Device Info", systemImage: "iphone.and.arrow.forward") { AnyView(DeviceInfoView()) }, diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 0bd69179..890dcde4 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -89,6 +89,7 @@ struct SettingsView: View { TabOption(id: "home", title: "Home", detail: "Dashboard overview", icon: "house", isBeta: false), TabOption(id: "console", title: "Console", detail: "Live device logs", icon: "terminal", isBeta: false), TabOption(id: "scripts", title: "Scripts", detail: "Manage automation scripts", icon: "scroll", isBeta: false), + TabOption(id: "tools", title: "Mini Tools", detail: "Import and run stiktool bundles", icon: "shippingbox.fill", isBeta: false), TabOption(id: "profiles", title: "Profiles", detail: "Install/remove profiles", icon: "magazine.fill", isBeta: false), TabOption(id: "processes", title: "Processes", detail: "Inspect running apps", icon: "rectangle.stack.person.crop", isBeta: true), TabOption(id: "deviceinfo", title: "Device Info", detail: "View detailed device metadata", icon: "iphone.and.arrow.forward", isBeta: false) From 1501fb9b1f728c852ecdcf9e33b231f7115bd6b3 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Fri, 12 Dec 2025 23:16:43 +0800 Subject: [PATCH 4/9] Saperate JITEnableContext into extensions and put each feature into their own files. Fix local pairing file is removed after switched to external device by directly loading the remote device's pairing file in place. --- StikJIT/Info.plist | 1 + StikJIT/StikJITApp.swift | 1 - StikJIT/Utilities/DeviceLibraryStore.swift | 8 +- StikJIT/Views/DeviceInfoManager.swift | 46 +- StikJIT/Views/MapSelectionView.swift | 18 +- StikJIT/idevice/JITEnableContext.h | 58 ++- StikJIT/idevice/JITEnableContext.m | 511 +-------------------- StikJIT/idevice/JITEnableContextInternal.h | 17 + StikJIT/idevice/applist.m | 66 +++ StikJIT/idevice/heartbeat.m | 39 +- StikJIT/idevice/ideviceinfo.c | 103 ----- StikJIT/idevice/ideviceinfo.h | 26 -- StikJIT/idevice/ideviceinfo.m | 83 ++++ StikJIT/idevice/{jit.c => jit.m} | 48 ++ StikJIT/idevice/mount.m | 24 + StikJIT/idevice/process.m | 287 ++++++++++++ StikJIT/idevice/profiles.m | 33 ++ StikJIT/idevice/syslog.m | 125 +++++ 18 files changed, 806 insertions(+), 688 deletions(-) create mode 100644 StikJIT/idevice/JITEnableContextInternal.h delete mode 100644 StikJIT/idevice/ideviceinfo.c create mode 100644 StikJIT/idevice/ideviceinfo.m rename StikJIT/idevice/{jit.c => jit.m} (91%) create mode 100644 StikJIT/idevice/process.m create mode 100644 StikJIT/idevice/syslog.m diff --git a/StikJIT/Info.plist b/StikJIT/Info.plist index 67a37e44..4992a7bc 100644 --- a/StikJIT/Info.plist +++ b/StikJIT/Info.plist @@ -44,6 +44,7 @@ + NSLocalNetworkUsageDescription StikDebug needs access to devices on your local network so it can connect to the targets you add to the Device Library. NSBonjourServices diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index 77f18f17..fa442d12 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -711,7 +711,6 @@ class MountingProgress: ObservableObject { mountingThread = Thread { [weak self] in guard let self = self else { return } let mountResult = mountPersonalDDI( - deviceIP: DeviceConnectionContext.targetIPAddress, 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, diff --git a/StikJIT/Utilities/DeviceLibraryStore.swift b/StikJIT/Utilities/DeviceLibraryStore.swift index ab87c740..b282c143 100644 --- a/StikJIT/Utilities/DeviceLibraryStore.swift +++ b/StikJIT/Utilities/DeviceLibraryStore.swift @@ -226,13 +226,7 @@ final class DeviceLibraryStore: ObservableObject { guard fileManager.fileExists(atPath: storedURL.path) else { throw DeviceLibraryError.pairingFileUnavailable } - let destinationURL = URL.documentsDirectory.appendingPathComponent("pairingFile.plist") - if fileManager.fileExists(atPath: destinationURL.path) { - try? fileManager.removeItem(at: destinationURL) - } - try fileManager.copyItem(at: storedURL, to: destinationURL) - try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: destinationURL.path) - + UserDefaults.standard.set(device.ipAddress, forKey: "TunnelDeviceIP") activeDeviceID = device.id UserDefaults.standard.set(device.id.uuidString, forKey: activeDeviceKey) diff --git a/StikJIT/Views/DeviceInfoManager.swift b/StikJIT/Views/DeviceInfoManager.swift index 8271e663..b06553f3 100644 --- a/StikJIT/Views/DeviceInfoManager.swift +++ b/StikJIT/Views/DeviceInfoManager.swift @@ -9,15 +9,12 @@ import SwiftUI import UIKit import UniformTypeIdentifiers -@_silgen_name("ideviceinfo_c_init") -private func c_deviceinfo_init(_ path: UnsafePointer) -> Int32 -@_silgen_name("ideviceinfo_c_get_xml") -private func c_deviceinfo_get_xml() -> UnsafePointer? -@_silgen_name("ideviceinfo_c_cleanup") -private func c_deviceinfo_cleanup() - // MARK: - Device Info Manager +struct LockdownClientSendable: @unchecked Sendable { + let raw: OpaquePointer +} + @MainActor final class DeviceInfoManager: ObservableObject { @Published var entries: [(key: String, value: String)] = [] @@ -25,11 +22,12 @@ final class DeviceInfoManager: ObservableObject { @Published var error: (title: String, message: String)? private var initialized = false private let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + + private var lockdownHandle: LockdownClientSendable? = nil func initAndLoad() { guard !initialized else { loadInfo(); return } busy = true - let path = docs.appendingPathComponent("pairingFile.plist").path Task.detached { do { try JITEnableContext.shared.ensureHeartbeat() @@ -39,29 +37,39 @@ final class DeviceInfoManager: ObservableObject { self.busy = false } } - let code = path.withCString { c_deviceinfo_init($0) } - await MainActor.run { - if code != 0 { - self.error = ("Initialization Failed", self.initErrorMessage(code)) - self.busy = false - } else { + do { + let lockdownHandle = LockdownClientSendable(raw: try JITEnableContext.shared.ideviceInfoInit()) + + await MainActor.run { + self.lockdownHandle = lockdownHandle self.initialized = true self.loadInfo() } + } catch { + await MainActor.run { + self.error = ("Initialization Failed", self.initErrorMessage(Int32((error as NSError).code))) + self.busy = false + } } + } } private func loadInfo() { busy = true Task.detached { - guard let cXml = c_deviceinfo_get_xml() else { + var cXml : UnsafeMutablePointer? = nil; + do { + cXml = try await JITEnableContext.shared.ideviceInfoGetXML(withLockdownClient: self.lockdownHandle?.raw) + } catch { await MainActor.run { - self.error = ("Fetch Error", "Failed to fetch device info") + self.error = ("Fetch Error", "Failed to fetch device info \(error)") self.busy = false } return } + guard let cXml else { return } + defer { free(UnsafeMutableRawPointer(mutating: cXml)) } guard let xml = String(validatingUTF8: cXml) else { await MainActor.run { @@ -91,7 +99,11 @@ final class DeviceInfoManager: ObservableObject { } func cleanup() { - c_deviceinfo_cleanup() + if let lockdownHandle { + lockdownd_client_free(lockdownHandle.raw) + self.lockdownHandle = nil + } + initialized = false } diff --git a/StikJIT/Views/MapSelectionView.swift b/StikJIT/Views/MapSelectionView.swift index 0af5d6dd..8b5d88e9 100644 --- a/StikJIT/Views/MapSelectionView.swift +++ b/StikJIT/Views/MapSelectionView.swift @@ -99,10 +99,22 @@ struct LocationSimulationView: View { } private var pairingFilePath: String { - FileManager.default + let docPathUrl = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask)[0] - .appendingPathComponent("pairingFile.plist") - .path + let currentDeviceUUIDStr = UserDefaults.standard.string(forKey: "DeviceLibraryActiveDeviceID") + + let pairingFileURL: URL + if let uuid = currentDeviceUUIDStr, + uuid != "00000000-0000-0000-0000-000000000001" { + + pairingFileURL = docPathUrl.appendingPathComponent( + "DeviceLibrary/Pairings/\(uuid).mobiledevicepairing" + ) + } else { + pairingFileURL = docPathUrl.appendingPathComponent("pairingFile.plist") + } + + return pairingFileURL.path() } private var pairingExists: Bool { diff --git a/StikJIT/idevice/JITEnableContext.h b/StikJIT/idevice/JITEnableContext.h index 77eac4a7..d56b7bc6 100644 --- a/StikJIT/idevice/JITEnableContext.h +++ b/StikJIT/idevice/JITEnableContext.h @@ -17,27 +17,65 @@ typedef void (^LogFunc)(NSString *message); typedef void (^SyslogLineHandler)(NSString *line); typedef void (^SyslogErrorHandler)(NSError *error); -@interface JITEnableContext : NSObject +@interface JITEnableContext : NSObject { + // process + @protected dispatch_queue_t processInspectorQueue; + @protected IdeviceProviderHandle* provider; + + // syslog + @protected dispatch_queue_t syslogQueue; + @protected BOOL syslogStreaming; + @protected SyslogRelayClientHandle *syslogClient; + @protected SyslogLineHandler syslogLineHandler; + @protected SyslogErrorHandler syslogErrorHandler; + + // ideviceInfo + @protected LockdowndClientHandle * g_client; +} @property (class, readonly)JITEnableContext* shared; - (IdevicePairingFile*)getPairingFileWithError:(NSError**)error; + - (BOOL)ensureHeartbeatWithError:(NSError**)err; - (BOOL)startHeartbeat:(NSError**)err; + +@end + +@interface JITEnableContext(JIT) - (BOOL)debugAppWithBundleID:(NSString*)bundleID logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback; - (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback; -- (NSDictionary*)getAppListWithError:(NSError**)error; -- (NSDictionary*)getAllAppsWithError:(NSError**)error; -- (NSDictionary*)getHiddenSystemAppsWithError:(NSError**)error; -- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error; - (BOOL)launchAppWithoutDebug:(NSString*)bundleID logger:(LogFunc)logger; -- (void)startSyslogRelayWithHandler:(SyslogLineHandler)lineHandler - onError:(SyslogErrorHandler)errorHandler NS_SWIFT_NAME(startSyslogRelay(handler:onError:)); -- (void)stopSyslogRelay; +@end + +@interface JITEnableContext(DDI) +- (NSUInteger)getMountedDeviceCount:(NSError**)error __attribute__((swift_error(zero_result))); +- (NSInteger)mountPersonalDDIWithImagePath:(NSString*)imagePath trustcachePath:(NSString*)trustcachePath manifestPath:(NSString*)manifestPath error:(NSError**)error __attribute__((swift_error(nonzero_result))); +@end + +@interface JITEnableContext(Profile) - (NSArray*)fetchAllProfiles:(NSError **)error; - (BOOL)removeProfileWithUUID:(NSString*)uuid error:(NSError **)error; - (BOOL)addProfile:(NSData*)profile error:(NSError **)error; +@end + +@interface JITEnableContext(Process) - (NSArray*)fetchProcessListWithError:(NSError**)error; - (BOOL)killProcessWithPID:(int)pid error:(NSError **)error; +@end -- (NSUInteger)getMountedDeviceCount:(NSError**)error __attribute__((swift_error(zero_result))); -- (NSInteger)mountPersonalDDIWithImagePath:(NSString*)imagePath trustcachePath:(NSString*)trustcachePath manifestPath:(NSString*)manifestPath error:(NSError**)error __attribute__((swift_error(nonzero_result))); +@interface JITEnableContext(App) +- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error; +- (NSDictionary*)getAppListWithError:(NSError**)error; +- (NSDictionary*)getAllAppsWithError:(NSError**)error; +- (NSDictionary*)getHiddenSystemAppsWithError:(NSError**)error; +@end + +@interface JITEnableContext(Syslog) +- (void)startSyslogRelayWithHandler:(SyslogLineHandler)lineHandler + onError:(SyslogErrorHandler)errorHandler NS_SWIFT_NAME(startSyslogRelay(handler:onError:)); +- (void)stopSyslogRelay; +@end + +@interface JITEnableContext(DeviceInfo) +- (LockdowndClientHandle*)ideviceInfoInit:(NSError**)error; +- (char*)ideviceInfoGetXMLWithLockdownClient:(LockdowndClientHandle*)lockdownClient error:(NSError**)error; @end diff --git a/StikJIT/idevice/JITEnableContext.m b/StikJIT/idevice/JITEnableContext.m index def78f10..927008f7 100644 --- a/StikJIT/idevice/JITEnableContext.m +++ b/StikJIT/idevice/JITEnableContext.m @@ -21,15 +21,7 @@ JITEnableContext* sharedJITContext = nil; -@implementation JITEnableContext { - IdeviceProviderHandle* provider; - dispatch_queue_t syslogQueue; - BOOL syslogStreaming; - SyslogRelayClientHandle *syslogClient; - SyslogLineHandler syslogLineHandler; - SyslogErrorHandler syslogErrorHandler; - dispatch_queue_t processInspectorQueue; - +@implementation JITEnableContext { int heartbeatToken; NSError* lastHeartbeatError; os_unfair_lock heartbeatLock; @@ -99,7 +91,13 @@ - (LogFuncC)createCLogger:(LogFunc)logger { - (IdevicePairingFile*)getPairingFileWithError:(NSError**)error { NSFileManager* fm = [NSFileManager defaultManager]; NSURL* docPathUrl = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject; - NSURL* pairingFileURL = [docPathUrl URLByAppendingPathComponent:@"pairingFile.plist"]; + NSString* currentDeviceUUIDStr = [NSUserDefaults.standardUserDefaults stringForKey:@"DeviceLibraryActiveDeviceID"]; + NSURL* pairingFileURL; + if(!currentDeviceUUIDStr || [currentDeviceUUIDStr isEqualToString:@"00000000-0000-0000-0000-000000000001"]) { + pairingFileURL = [docPathUrl URLByAppendingPathComponent:@"pairingFile.plist"]; + } else { + pairingFileURL = [docPathUrl URLByAppendingPathComponent:[NSString stringWithFormat:@"DeviceLibrary/Pairings/%@.mobiledevicepairing", currentDeviceUUIDStr]]; + } if (![fm fileExistsAtPath:pairingFileURL.path]) { NSLog(@"Pairing file not found!"); @@ -226,215 +224,9 @@ - (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCall [self createCLogger:logger], jsCallback) == 0; } -- (NSDictionary*)getAppListWithError:(NSError**)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - - NSString* errorStr = nil; - NSDictionary* apps = list_installed_apps(provider, &errorStr); - if (errorStr) { - *error = [self errorWithStr:errorStr code:-17]; - return nil; - } - return apps; -} - -- (NSDictionary*)getAllAppsWithError:(NSError**)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - - NSString* errorStr = nil; - NSDictionary* apps = list_all_apps(provider, &errorStr); - if (errorStr) { - *error = [self errorWithStr:errorStr code:-17]; - return nil; - } - return apps; -} - -- (NSDictionary*)getHiddenSystemAppsWithError:(NSError**)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - - NSString* errorStr = nil; - NSDictionary* apps = list_hidden_system_apps(provider, &errorStr); - if (errorStr) { - *error = [self errorWithStr:errorStr code:-17]; - return nil; - } - return apps; -} - -- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - - NSString* errorStr = nil; - UIImage* icon = getAppIcon(provider, bundleId, &errorStr); - if (errorStr) { - *error = [self errorWithStr:errorStr code:-17]; - return nil; - } - return icon; -} - -- (BOOL)launchAppWithoutDebug:(NSString*)bundleID logger:(LogFunc)logger { - NSError* err = nil; - [self ensureHeartbeatWithError:&err]; - if(err) { - logger(err.localizedDescription); - return NO; - } - - int result = launch_app_via_proxy(provider, - [bundleID UTF8String], - [self createCLogger:logger]); - return result == 0; -} - -- (void)startSyslogRelayWithHandler:(SyslogLineHandler)lineHandler - onError:(SyslogErrorHandler)errorHandler -{ - NSError* error = nil; - [self ensureHeartbeatWithError:&error]; - if(error) { - errorHandler(error); - return; - } - if (!lineHandler || syslogStreaming) { - return; - } - - syslogStreaming = YES; - syslogLineHandler = [lineHandler copy]; - syslogErrorHandler = [errorHandler copy]; - - __weak typeof(self) weakSelf = self; - dispatch_async(syslogQueue, ^{ - __strong typeof(self) strongSelf = weakSelf; - if (!strongSelf) { return; } - - SyslogRelayClientHandle *client = NULL; - IdeviceFfiError *err = syslog_relay_connect_tcp(strongSelf->provider, &client); - if (err != NULL) { - NSString *message = err->message ? [NSString stringWithCString:err->message encoding:NSASCIIStringEncoding] : @"Failed to connect to syslog relay"; - NSError *nsError = [strongSelf errorWithStr:message code:err->code]; - idevice_error_free(err); - [strongSelf handleSyslogFailure:nsError]; - return; - } - - strongSelf->syslogClient = client; - - while (strongSelf && strongSelf->syslogStreaming) { - char *message = NULL; - IdeviceFfiError *nextErr = syslog_relay_next(client, &message); - if (nextErr != NULL) { - NSString *errMsg = nextErr->message ? [NSString stringWithCString:nextErr->message encoding:NSASCIIStringEncoding] : @"Syslog relay read failed"; - NSError *nsError = [strongSelf errorWithStr:errMsg code:nextErr->code]; - idevice_error_free(nextErr); - if (message) { idevice_string_free(message); } - [strongSelf handleSyslogFailure:nsError]; - client = NULL; - break; - } - - if (!message) { - continue; - } - - NSString *line = [NSString stringWithCString:message encoding:NSUTF8StringEncoding]; - idevice_string_free(message); - if (!line || !strongSelf->syslogLineHandler) { - continue; - } - - SyslogLineHandler handlerCopy = strongSelf->syslogLineHandler; - if (handlerCopy) { - dispatch_async(dispatch_get_main_queue(), ^{ - handlerCopy(line); - }); - } - } - if (client) { - syslog_relay_client_free(client); - } - strongSelf->syslogClient = NULL; - strongSelf->syslogStreaming = NO; - strongSelf->syslogLineHandler = nil; - strongSelf->syslogErrorHandler = nil; - }); -} - -- (void)stopSyslogRelay { - if (!syslogStreaming) { - return; - } - syslogStreaming = NO; - syslogLineHandler = nil; - syslogErrorHandler = nil; - - dispatch_async(syslogQueue, ^{ - if (self->syslogClient) { - syslog_relay_client_free(self->syslogClient); - self->syslogClient = NULL; - } - }); -} - -- (void)handleSyslogFailure:(NSError *)error { - syslogStreaming = NO; - if (syslogClient) { - syslog_relay_client_free(syslogClient); - syslogClient = NULL; - } - SyslogErrorHandler errorCopy = syslogErrorHandler; - syslogLineHandler = nil; - syslogErrorHandler = nil; - - if (errorCopy) { - dispatch_async(dispatch_get_main_queue(), ^{ - errorCopy(error); - }); - } -} - -- (NSArray*)fetchAllProfiles:(NSError **)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - - return fetchAppProfiles(provider, error); -} - -- (BOOL)removeProfileWithUUID:(NSString*)uuid error:(NSError **)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - - return removeProfile(provider, uuid, error); -} - -- (BOOL)addProfile:(NSData*)profile error:(NSError **)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - return addProfile(provider, profile, error); -} - (void)dealloc { [self stopSyslogRelay]; @@ -443,295 +235,8 @@ - (void)dealloc { } } -- (NSArray*)fetchProcessesViaAppServiceWithError:(NSError **)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - - IdeviceProviderHandle *providerToUse = provider; - CoreDeviceProxyHandle *coreProxy = NULL; - AdapterHandle *adapter = NULL; - AdapterStreamHandle *stream = NULL; - RsdHandshakeHandle *handshake = NULL; - AppServiceHandle *appService = NULL; - ProcessTokenC *processes = NULL; - uintptr_t count = 0; - NSMutableArray *result = nil; - IdeviceFfiError *ffiError = NULL; - - do { - - ffiError = core_device_proxy_connect(providerToUse, &coreProxy); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to connect CoreDeviceProxy"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - uint16_t rsdPort = 0; - ffiError = core_device_proxy_get_server_rsd_port(coreProxy, &rsdPort); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Unable to resolve RSD port"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - ffiError = core_device_proxy_create_tcp_adapter(coreProxy, &adapter); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to create adapter"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - coreProxy = NULL; - ffiError = adapter_connect(adapter, rsdPort, (ReadWriteOpaque **)&stream); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Adapter connect failed"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - ffiError = rsd_handshake_new((ReadWriteOpaque *)stream, &handshake); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "RSD handshake failed"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - stream = NULL; - ffiError = app_service_connect_rsd(adapter, handshake, &appService); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Unable to open AppService"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - ffiError = app_service_list_processes(appService, &processes, &count); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to list processes"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - result = [NSMutableArray arrayWithCapacity:count]; - for (uintptr_t idx = 0; idx < count; idx++) { - ProcessTokenC proc = processes[idx]; - NSMutableDictionary *entry = [NSMutableDictionary dictionary]; - entry[@"pid"] = @(proc.pid); - if (proc.executable_url) { - entry[@"path"] = [NSString stringWithUTF8String:proc.executable_url]; - } - [result addObject:entry]; - } - } while (0); - - if (processes && count > 0) { - app_service_free_process_list(processes, count); - } - if (appService) { - app_service_free(appService); - } - if (handshake) { - rsd_handshake_free(handshake); - } - if (stream) { - adapter_stream_close(stream); - } - if (adapter) { - adapter_free(adapter); - } - if (coreProxy) { - core_device_proxy_free(coreProxy); - } - return result; -} - -- (NSArray*)_fetchProcessListLocked:(NSError**)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - return [self fetchProcessesViaAppServiceWithError:error]; -} - -- (NSArray*)fetchProcessListWithError:(NSError**)error { - __block NSArray *result = nil; - __block NSError *localError = nil; - dispatch_sync(processInspectorQueue, ^{ - result = [self _fetchProcessListLocked:&localError]; - }); - if (error && localError) { - *error = localError; - } - return result; -} - -- (BOOL)killProcessWithPID:(int)pid error:(NSError **)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return nil; - } - - IdeviceProviderHandle *providerToUse = provider; - CoreDeviceProxyHandle *coreProxy = NULL; - AdapterHandle *adapter = NULL; - AdapterStreamHandle *stream = NULL; - RsdHandshakeHandle *handshake = NULL; - AppServiceHandle *appService = NULL; - SignalResponseC *signalResponse = NULL; - IdeviceFfiError *ffiError = NULL; - BOOL success = NO; - - do { - ffiError = core_device_proxy_connect(providerToUse, &coreProxy); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to connect CoreDeviceProxy"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - uint16_t rsdPort = 0; - ffiError = core_device_proxy_get_server_rsd_port(coreProxy, &rsdPort); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Unable to resolve RSD port"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - ffiError = core_device_proxy_create_tcp_adapter(coreProxy, &adapter); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to create adapter"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - coreProxy = NULL; - ffiError = adapter_connect(adapter, rsdPort, (ReadWriteOpaque **)&stream); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Adapter connect failed"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - ffiError = rsd_handshake_new((ReadWriteOpaque *)stream, &handshake); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "RSD handshake failed"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - stream = NULL; - ffiError = app_service_connect_rsd(adapter, handshake, &appService); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Unable to open AppService"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - - ffiError = app_service_send_signal(appService, (uint32_t)pid, SIGKILL, &signalResponse); - if (ffiError) { - if (error) { - *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to kill process"] - code:ffiError->code]; - } - idevice_error_free(ffiError); - ffiError = NULL; - break; - } - success = YES; - } while (0); - - if (signalResponse) { - app_service_free_signal_response(signalResponse); - } - if (appService) { - app_service_free(appService); - } - if (handshake) { - rsd_handshake_free(handshake); - } - if (stream) { - adapter_stream_close(stream); - } - if (adapter) { - adapter_free(adapter); - } - if (coreProxy) { - core_device_proxy_free(coreProxy); - } - return success; -} - -- (NSUInteger)getMountedDeviceCount:(NSError**)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return NO; - } - return getMountedDeviceCount(provider, error); -} -- (NSInteger)mountPersonalDDIWithImagePath:(NSString*)imagePath trustcachePath:(NSString*)trustcachePath manifestPath:(NSString*)manifestPath error:(NSError**)error { - [self ensureHeartbeatWithError:error]; - if(*error) { - return 0; - } - IdevicePairingFile* pairing = [self getPairingFileWithError:error]; - if(*error) { - return 0; - } - return mountPersonalDDI(provider, pairing, imagePath, trustcachePath, manifestPath, error); -} @end diff --git a/StikJIT/idevice/JITEnableContextInternal.h b/StikJIT/idevice/JITEnableContextInternal.h new file mode 100644 index 00000000..1b6c8515 --- /dev/null +++ b/StikJIT/idevice/JITEnableContextInternal.h @@ -0,0 +1,17 @@ +// +// JITEnableContextInternal.h +// StikDebug +// +// Created by s s on 2025/12/12. +// +#include "idevice.h" +#import "JITEnableContext.h" +@import Foundation; + + +@interface JITEnableContext(Internal) + +- (LogFuncC)createCLogger:(LogFunc)logger; +- (NSError*)errorWithStr:(NSString*)str code:(int)code; + +@end diff --git a/StikJIT/idevice/applist.m b/StikJIT/idevice/applist.m index e17f9499..0c70045b 100644 --- a/StikJIT/idevice/applist.m +++ b/StikJIT/idevice/applist.m @@ -10,6 +10,8 @@ #include #include #import "applist.h" +#import "JITEnableContext.h" +#import "JITEnableContextInternal.h" static NSString *extractAppName(plist_t app) { @@ -206,3 +208,67 @@ static BOOL isHiddenSystemApp(plist_t app) springboard_services_free(client); return icon; } + +@implementation JITEnableContext(App) + +- (NSDictionary*)getAppListWithError:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + + NSString* errorStr = nil; + NSDictionary* apps = list_installed_apps(provider, &errorStr); + if (errorStr) { + *error = [self errorWithStr:errorStr code:-17]; + return nil; + } + return apps; +} + +- (NSDictionary*)getAllAppsWithError:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + + NSString* errorStr = nil; + NSDictionary* apps = list_all_apps(provider, &errorStr); + if (errorStr) { + *error = [self errorWithStr:errorStr code:-17]; + return nil; + } + return apps; +} + +- (NSDictionary*)getHiddenSystemAppsWithError:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + + NSString* errorStr = nil; + NSDictionary* apps = list_hidden_system_apps(provider, &errorStr); + if (errorStr) { + *error = [self errorWithStr:errorStr code:-17]; + return nil; + } + return apps; +} + +- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + + NSString* errorStr = nil; + UIImage* icon = getAppIcon(provider, bundleId, &errorStr); + if (errorStr) { + *error = [self errorWithStr:errorStr code:-17]; + return nil; + } + return icon; +} + +@end diff --git a/StikJIT/idevice/heartbeat.m b/StikJIT/idevice/heartbeat.m index abbe5e5c..ea2836f1 100644 --- a/StikJIT/idevice/heartbeat.m +++ b/StikJIT/idevice/heartbeat.m @@ -10,6 +10,7 @@ #include #include #include "heartbeat.h" +#include @import Foundation; int globalHeartbeatToken = 0; @@ -19,25 +20,27 @@ void startHeartbeat(IdevicePairingFile* pairing_file, IdeviceProviderHandle** pr IdeviceProviderHandle* newProvider = *provider; IdeviceFfiError* err = nil; - // Create the socket address (replace with your device's IP) - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons(LOCKDOWN_PORT); - inet_pton(AF_INET, "10.7.0.1", &addr.sin_addr); + // Create the socket address (replace with your device's IP) + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(LOCKDOWN_PORT); + + NSString* deviceIP = [[NSUserDefaults standardUserDefaults] stringForKey:@"TunnelDeviceIP"]; + inet_pton(AF_INET, deviceIP ? [deviceIP UTF8String] : "10.7.0.2", &addr.sin_addr); + + + err = idevice_tcp_provider_new((struct sockaddr *)&addr, pairing_file, + "ExampleProvider", &newProvider); + if (err != NULL) { + fprintf(stderr, "Failed to create TCP provider: [%d] %s", err->code, + err->message); + completion(err->code, err->message); + idevice_pairing_file_free(pairing_file); + idevice_error_free(err); - - err = idevice_tcp_provider_new((struct sockaddr *)&addr, pairing_file, - "ExampleProvider", &newProvider); - if (err != NULL) { - fprintf(stderr, "Failed to create TCP provider: [%d] %s", err->code, - err->message); - completion(err->code, err->message); - idevice_pairing_file_free(pairing_file); - idevice_error_free(err); - - return; - } + return; + } // Connect to installation proxy HeartbeatClientHandle *client = NULL; diff --git a/StikJIT/idevice/ideviceinfo.c b/StikJIT/idevice/ideviceinfo.c deleted file mode 100644 index 98ae253e..00000000 --- a/StikJIT/idevice/ideviceinfo.c +++ /dev/null @@ -1,103 +0,0 @@ -// -// ideviceinfo.c -// StikDebug -// -// Created by Stephen on 8/2/25. -// - -#include -#include -#include "ideviceinfo.h" -#include "idevice.h" - -static struct IdeviceProviderHandle * g_provider = NULL; -static struct LockdowndClientHandle * g_client = NULL; -static struct IdevicePairingFile * g_sess_pf = NULL; - -int ideviceinfo_c_init(const char *pairing_file_path) { - if (g_provider) { - return 0; - } - - struct IdevicePairingFile *pf = NULL; - struct IdeviceFfiError *err = idevice_pairing_file_read(pairing_file_path, &pf); - if (err) { - idevice_error_free(err); - return 1; - } - - struct sockaddr_in sin = { .sin_family = AF_INET, - .sin_port = htons(LOCKDOWN_PORT) }; - inet_pton(AF_INET, "10.7.0.1", &sin.sin_addr); - - err = idevice_tcp_provider_new((const struct sockaddr *)&sin, - pf, - "ideviceinfo-c", - &g_provider); - if (err) { - idevice_error_free(err); - idevice_pairing_file_free(pf); - return 2; - } - - err = lockdownd_connect(g_provider, &g_client); - if (err) { - idevice_error_free(err); - return 3; - } - - err = idevice_pairing_file_read(pairing_file_path, &g_sess_pf); - if (err) { - idevice_error_free(err); - lockdownd_client_free(g_client); - g_client = NULL; - return 4; - } - - err = lockdownd_start_session(g_client, g_sess_pf); - if (err) { - idevice_error_free(err); - lockdownd_client_free(g_client); - g_client = NULL; - return 4; - } - - return 0; -} - -char *ideviceinfo_c_get_xml(void) { - if (!g_client) { - return NULL; - } - - void *plist_obj = NULL; - struct IdeviceFfiError *err = lockdownd_get_value(g_client, NULL, NULL, &plist_obj); - if (err) { - idevice_error_free(err); - return NULL; - } - - char *xml = NULL; - uint32_t xml_len = 0; - if (plist_to_xml(plist_obj, &xml, &xml_len) != 0 || !xml) { - plist_free(plist_obj); - return NULL; - } - plist_free(plist_obj); - return xml; -} - -void ideviceinfo_c_cleanup(void) { - if (g_client) { - lockdownd_client_free(g_client); - g_client = NULL; - } - if (g_sess_pf) { - idevice_pairing_file_free(g_sess_pf); - g_sess_pf = NULL; - } - if (g_provider) { - idevice_provider_free(g_provider); - g_provider = NULL; - } -} diff --git a/StikJIT/idevice/ideviceinfo.h b/StikJIT/idevice/ideviceinfo.h index fd2ce947..78ef64b3 100644 --- a/StikJIT/idevice/ideviceinfo.h +++ b/StikJIT/idevice/ideviceinfo.h @@ -14,33 +14,7 @@ extern "C" { #endif -/** - * Initialize the device connection: - * - Reads the pairing file - * - Creates a TCP provider - * - Starts a persistent heartbeat - * - Connects & starts a lockdown session - * - * @param pairing_file_path path to pairingFile.plist - * @return 0 on success, non-zero on error - */ -int ideviceinfo_c_init(const char *pairing_file_path); -/** - * Fetches all device info via the already-open lockdown session, - * returning a malloc'd XML plist string (caller must free it). - * - * @return malloc'd XML on success, or NULL on error - */ -char *ideviceinfo_c_get_xml(void); - -/** - * Clean up everything: - * - Stops heartbeat - * - Frees lockdown client - * - Frees provider & pairing file - */ -void ideviceinfo_c_cleanup(void); #ifdef __cplusplus } diff --git a/StikJIT/idevice/ideviceinfo.m b/StikJIT/idevice/ideviceinfo.m new file mode 100644 index 00000000..7a4c94ef --- /dev/null +++ b/StikJIT/idevice/ideviceinfo.m @@ -0,0 +1,83 @@ +// +// ideviceinfo.c +// StikDebug +// +// Created by Stephen on 8/2/25. +// + +#include +#include +#include "ideviceinfo.h" +#include "idevice.h" +#import "JITEnableContext.h" +#import "JITEnableContextInternal.h" +@import Foundation; + +NSError* makeError(int code, NSString* msg); +LockdowndClientHandle* ideviceinfo_c_init(IdeviceProviderHandle* g_provider, IdevicePairingFile* g_sess_pf, NSError** error) { + struct LockdowndClientHandle * g_client = NULL; + struct IdeviceFfiError * err = lockdownd_connect(g_provider, &g_client); + if (err) { + *error = makeError(err->code, @(err->message)); + idevice_error_free(err); + return 0; + } + + err = lockdownd_start_session(g_client, g_sess_pf); + if (err) { + *error = makeError(err->code, @(err->message)); + idevice_error_free(err); + lockdownd_client_free(g_client); + g_client = NULL; + return 0; + } + + return g_client; +} + +char *ideviceinfo_c_get_xml(LockdowndClientHandle* g_client, NSError** error) { + if (!g_client) { + return NULL; + } + + void *plist_obj = NULL; + struct IdeviceFfiError *err = lockdownd_get_value(g_client, NULL, NULL, &plist_obj); + if (err) { + *error = makeError(err->code, @(err->message)); + idevice_error_free(err); + return NULL; + } + + char *xml = NULL; + uint32_t xml_len = 0; + if (plist_to_xml(plist_obj, &xml, &xml_len) != 0 || !xml) { + plist_free(plist_obj); + return NULL; + } + plist_free(plist_obj); + return xml; +} + +@implementation JITEnableContext(DeviceInfo) + +- (LockdowndClientHandle*)ideviceInfoInit:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return 0; + } + IdevicePairingFile* pf = [self getPairingFileWithError:error]; + if(*error) { + return 0; + } + + return ideviceinfo_c_init(provider, pf, error); +} + +- (char*)ideviceInfoGetXMLWithLockdownClient:(LockdowndClientHandle*)lockdownClient error:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return 0; + } + return ideviceinfo_c_get_xml(lockdownClient, error); +} +@end diff --git a/StikJIT/idevice/jit.c b/StikJIT/idevice/jit.m similarity index 91% rename from StikJIT/idevice/jit.c rename to StikJIT/idevice/jit.m index 11c1ce10..dff4c174 100644 --- a/StikJIT/idevice/jit.c +++ b/StikJIT/idevice/jit.m @@ -17,6 +17,8 @@ #include #include "jit.h" +#import "JITEnableContext.h" +#import "JITEnableContextInternal.h" void runDebugServerCommand(int pid, DebugProxyHandle* debug_proxy, @@ -424,3 +426,49 @@ int launch_app_via_proxy(IdeviceProviderHandle* tcp_provider, const char *bundle return result; } + + +@implementation JITEnableContext(JIT) + +- (BOOL)debugAppWithBundleID:(NSString*)bundleID logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback { + NSError* err = nil; + [self ensureHeartbeatWithError:&err]; + if(err) { + logger(err.localizedDescription); + return NO; + } + + return debug_app(provider, + [bundleID UTF8String], + [self createCLogger:logger], jsCallback) == 0; +} + +- (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback { + NSError* err = nil; + [self ensureHeartbeatWithError:&err]; + if(err) { + logger(err.localizedDescription); + return NO; + } + + return debug_app_pid(provider, + pid, + [self createCLogger:logger], jsCallback) == 0; +} + +- (BOOL)launchAppWithoutDebug:(NSString*)bundleID logger:(LogFunc)logger { + NSError* err = nil; + [self ensureHeartbeatWithError:&err]; + if(err) { + logger(err.localizedDescription); + return NO; + } + + int result = launch_app_via_proxy(provider, + [bundleID UTF8String], + [self createCLogger:logger]); + return result == 0; +} + + +@end diff --git a/StikJIT/idevice/mount.m b/StikJIT/idevice/mount.m index 3439b33f..e042d936 100644 --- a/StikJIT/idevice/mount.m +++ b/StikJIT/idevice/mount.m @@ -5,7 +5,10 @@ // Created by s s on 2025/12/6. // #include "mount.h" +#import "JITEnableContext.h" +#import "JITEnableContextInternal.h" @import Foundation; + NSError* makeError(int code, NSString* msg); size_t getMountedDeviceCount(IdeviceProviderHandle* provider, NSError** error) { ImageMounterHandle* client = 0; @@ -97,3 +100,24 @@ int mountPersonalDDI(IdeviceProviderHandle* provider, IdevicePairingFile* pairin return 0; } + +@implementation JITEnableContext(DDI) +- (NSUInteger)getMountedDeviceCount:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return NO; + } + return getMountedDeviceCount(provider, error); +} +- (NSInteger)mountPersonalDDIWithImagePath:(NSString*)imagePath trustcachePath:(NSString*)trustcachePath manifestPath:(NSString*)manifestPath error:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return 0; + } + IdevicePairingFile* pairing = [self getPairingFileWithError:error]; + if(*error) { + return 0; + } + return mountPersonalDDI(provider, pairing, imagePath, trustcachePath, manifestPath, error); +} +@end diff --git a/StikJIT/idevice/process.m b/StikJIT/idevice/process.m new file mode 100644 index 00000000..89ce6d7a --- /dev/null +++ b/StikJIT/idevice/process.m @@ -0,0 +1,287 @@ +// +// process.m +// StikDebug +// +// Created by s s on 2025/12/12. +// + +#import "JITEnableContext.h" +#import "JITEnableContextInternal.h" +@import Foundation; + +@implementation JITEnableContext(Process) + +- (NSArray*)fetchProcessesViaAppServiceWithError:(NSError **)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + + IdeviceProviderHandle *providerToUse = provider; + CoreDeviceProxyHandle *coreProxy = NULL; + AdapterHandle *adapter = NULL; + AdapterStreamHandle *stream = NULL; + RsdHandshakeHandle *handshake = NULL; + AppServiceHandle *appService = NULL; + ProcessTokenC *processes = NULL; + uintptr_t count = 0; + NSMutableArray *result = nil; + IdeviceFfiError *ffiError = NULL; + + do { + + ffiError = core_device_proxy_connect(providerToUse, &coreProxy); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to connect CoreDeviceProxy"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + uint16_t rsdPort = 0; + ffiError = core_device_proxy_get_server_rsd_port(coreProxy, &rsdPort); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Unable to resolve RSD port"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + ffiError = core_device_proxy_create_tcp_adapter(coreProxy, &adapter); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to create adapter"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + coreProxy = NULL; + ffiError = adapter_connect(adapter, rsdPort, (ReadWriteOpaque **)&stream); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Adapter connect failed"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + ffiError = rsd_handshake_new((ReadWriteOpaque *)stream, &handshake); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "RSD handshake failed"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + stream = NULL; + ffiError = app_service_connect_rsd(adapter, handshake, &appService); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Unable to open AppService"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + ffiError = app_service_list_processes(appService, &processes, &count); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to list processes"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + result = [NSMutableArray arrayWithCapacity:count]; + for (uintptr_t idx = 0; idx < count; idx++) { + ProcessTokenC proc = processes[idx]; + NSMutableDictionary *entry = [NSMutableDictionary dictionary]; + entry[@"pid"] = @(proc.pid); + if (proc.executable_url) { + entry[@"path"] = [NSString stringWithUTF8String:proc.executable_url]; + } + [result addObject:entry]; + } + } while (0); + + if (processes && count > 0) { + app_service_free_process_list(processes, count); + } + if (appService) { + app_service_free(appService); + } + if (handshake) { + rsd_handshake_free(handshake); + } + if (stream) { + adapter_stream_close(stream); + } + if (adapter) { + adapter_free(adapter); + } + if (coreProxy) { + core_device_proxy_free(coreProxy); + } + return result; +} + +- (NSArray*)_fetchProcessListLocked:(NSError**)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + return [self fetchProcessesViaAppServiceWithError:error]; +} + +- (NSArray*)fetchProcessListWithError:(NSError**)error { + __block NSArray *result = nil; + __block NSError *localError = nil; + dispatch_sync(processInspectorQueue, ^{ + result = [self _fetchProcessListLocked:&localError]; + }); + if (error && localError) { + *error = localError; + } + return result; +} + +- (BOOL)killProcessWithPID:(int)pid error:(NSError **)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + + IdeviceProviderHandle *providerToUse = provider; + CoreDeviceProxyHandle *coreProxy = NULL; + AdapterHandle *adapter = NULL; + AdapterStreamHandle *stream = NULL; + RsdHandshakeHandle *handshake = NULL; + AppServiceHandle *appService = NULL; + SignalResponseC *signalResponse = NULL; + IdeviceFfiError *ffiError = NULL; + BOOL success = NO; + + do { + ffiError = core_device_proxy_connect(providerToUse, &coreProxy); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to connect CoreDeviceProxy"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + uint16_t rsdPort = 0; + ffiError = core_device_proxy_get_server_rsd_port(coreProxy, &rsdPort); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Unable to resolve RSD port"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + ffiError = core_device_proxy_create_tcp_adapter(coreProxy, &adapter); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to create adapter"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + coreProxy = NULL; + ffiError = adapter_connect(adapter, rsdPort, (ReadWriteOpaque **)&stream); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Adapter connect failed"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + ffiError = rsd_handshake_new((ReadWriteOpaque *)stream, &handshake); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "RSD handshake failed"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + stream = NULL; + ffiError = app_service_connect_rsd(adapter, handshake, &appService); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Unable to open AppService"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + + ffiError = app_service_send_signal(appService, (uint32_t)pid, SIGKILL, &signalResponse); + if (ffiError) { + if (error) { + *error = [self errorWithStr:[NSString stringWithUTF8String:ffiError->message ?: "Failed to kill process"] + code:ffiError->code]; + } + idevice_error_free(ffiError); + ffiError = NULL; + break; + } + success = YES; + } while (0); + + if (signalResponse) { + app_service_free_signal_response(signalResponse); + } + if (appService) { + app_service_free(appService); + } + if (handshake) { + rsd_handshake_free(handshake); + } + if (stream) { + adapter_stream_close(stream); + } + if (adapter) { + adapter_free(adapter); + } + if (coreProxy) { + core_device_proxy_free(coreProxy); + } + return success; +} + + +@end diff --git a/StikJIT/idevice/profiles.m b/StikJIT/idevice/profiles.m index 0ba81fc4..81503aed 100644 --- a/StikJIT/idevice/profiles.m +++ b/StikJIT/idevice/profiles.m @@ -5,6 +5,8 @@ // Created by s s on 2025/11/29. // #include "profiles.h" +#import "JITEnableContext.h" +#import "JITEnableContextInternal.h" @import Foundation; NSError* makeError(int code, NSString* msg) { @@ -143,4 +145,35 @@ + (NSData*)decodeCMSData:(NSData *)cmsData return nil; } +@end + +@implementation JITEnableContext(Profile) + +- (NSArray*)fetchAllProfiles:(NSError **)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + + return fetchAppProfiles(provider, error); +} + +- (BOOL)removeProfileWithUUID:(NSString*)uuid error:(NSError **)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + + return removeProfile(provider, uuid, error); +} + +- (BOOL)addProfile:(NSData*)profile error:(NSError **)error { + [self ensureHeartbeatWithError:error]; + if(*error) { + return nil; + } + return addProfile(provider, profile, error); +} + + @end diff --git a/StikJIT/idevice/syslog.m b/StikJIT/idevice/syslog.m new file mode 100644 index 00000000..3e36b012 --- /dev/null +++ b/StikJIT/idevice/syslog.m @@ -0,0 +1,125 @@ +// +// syslog.m +// StikDebug +// +// Created by s s on 2025/12/12. +// + +#import "JITEnableContext.h" +#import "JITEnableContextInternal.h" + +@implementation JITEnableContext(Syslog) + +- (void)startSyslogRelayWithHandler:(SyslogLineHandler)lineHandler + onError:(SyslogErrorHandler)errorHandler +{ + NSError* error = nil; + [self ensureHeartbeatWithError:&error]; + if(error) { + errorHandler(error); + return; + } + if (!lineHandler || syslogStreaming) { + return; + } + + syslogStreaming = YES; + syslogLineHandler = [lineHandler copy]; + syslogErrorHandler = [errorHandler copy]; + + __weak typeof(self) weakSelf = self; + dispatch_async(syslogQueue, ^{ + __strong typeof(self) strongSelf = weakSelf; + if (!strongSelf) { return; } + + SyslogRelayClientHandle *client = NULL; + IdeviceFfiError *err = syslog_relay_connect_tcp(strongSelf->provider, &client); + if (err != NULL) { + NSString *message = err->message ? [NSString stringWithCString:err->message encoding:NSASCIIStringEncoding] : @"Failed to connect to syslog relay"; + NSError *nsError = [strongSelf errorWithStr:message code:err->code]; + idevice_error_free(err); + [strongSelf handleSyslogFailure:nsError]; + return; + } + + strongSelf->syslogClient = client; + + while (strongSelf && strongSelf->syslogStreaming) { + char *message = NULL; + IdeviceFfiError *nextErr = syslog_relay_next(client, &message); + if (nextErr != NULL) { + NSString *errMsg = nextErr->message ? [NSString stringWithCString:nextErr->message encoding:NSASCIIStringEncoding] : @"Syslog relay read failed"; + NSError *nsError = [strongSelf errorWithStr:errMsg code:nextErr->code]; + idevice_error_free(nextErr); + if (message) { idevice_string_free(message); } + [strongSelf handleSyslogFailure:nsError]; + client = NULL; + break; + } + + if (!message) { + continue; + } + + NSString *line = [NSString stringWithCString:message encoding:NSUTF8StringEncoding]; + idevice_string_free(message); + if (!line || !strongSelf->syslogLineHandler) { + continue; + } + + SyslogLineHandler handlerCopy = strongSelf->syslogLineHandler; + if (handlerCopy) { + dispatch_async(dispatch_get_main_queue(), ^{ + handlerCopy(line); + }); + } + } + + if (client) { + syslog_relay_client_free(client); + } + + strongSelf->syslogClient = NULL; + strongSelf->syslogStreaming = NO; + strongSelf->syslogLineHandler = nil; + strongSelf->syslogErrorHandler = nil; + }); +} + +- (void)stopSyslogRelay { + if (!syslogStreaming) { + return; + } + + syslogStreaming = NO; + syslogLineHandler = nil; + syslogErrorHandler = nil; + + dispatch_async(syslogQueue, ^{ + if (self->syslogClient) { + syslog_relay_client_free(self->syslogClient); + self->syslogClient = NULL; + } + }); +} + +- (void)handleSyslogFailure:(NSError *)error { + syslogStreaming = NO; + if (syslogClient) { + syslog_relay_client_free(syslogClient); + syslogClient = NULL; + } + SyslogErrorHandler errorCopy = syslogErrorHandler; + syslogLineHandler = nil; + syslogErrorHandler = nil; + + if (errorCopy) { + dispatch_async(dispatch_get_main_queue(), ^{ + errorCopy(error); + }); + } +} + + + +@end From 247882fe637a699af4fc48c3e30d9db9cab9cdf6 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sat, 13 Dec 2025 13:46:23 +0800 Subject: [PATCH 5/9] custom origin & xmlhttprequest --- StikJIT/MiniToolSupport/MiniToolRuntime.swift | 351 +++++++++++++++++- 1 file changed, 347 insertions(+), 4 deletions(-) diff --git a/StikJIT/MiniToolSupport/MiniToolRuntime.swift b/StikJIT/MiniToolSupport/MiniToolRuntime.swift index c00be934..4c99c103 100644 --- a/StikJIT/MiniToolSupport/MiniToolRuntime.swift +++ b/StikJIT/MiniToolSupport/MiniToolRuntime.swift @@ -1,4 +1,5 @@ import Foundation +import UniformTypeIdentifiers import SwiftUI import WebKit import JavaScriptCore @@ -8,25 +9,32 @@ final class MiniToolRuntime: NSObject, ObservableObject { @Published var logs: [String] = [] @Published var isReady: Bool = false - let webView: WKWebView + var webView: WKWebView private var context: JSContext? + private var appXHRTasks: [String: URLSessionDataTask] = [:] + private let messageHandlerName = "miniToolBridge" init(tool: MiniToolBundle) { self.tool = tool let configuration = WKWebViewConfiguration() +// configuration.setValue(true, forKey: "allowFileAccessFromFileURLs") // let the webview fetch other bundle assets let controller = WKUserContentController() controller.addUserScript(WKUserScript(source: MiniToolRuntime.frontendBridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: true)) configuration.userContentController = controller + webView = WKWebView(frame: .zero, configuration: configuration) + super.init() - + configuration.setURLSchemeHandler(self, forURLScheme: "app") + webView = WKWebView(frame: .zero, configuration: configuration) controller.add(self, name: messageHandlerName) webView.navigationDelegate = self + webView.isInspectable = true } deinit { @@ -53,8 +61,8 @@ final class MiniToolRuntime: NSObject, ObservableObject { return } isReady = false - let url = tool.indexURL - webView.loadFileURL(url, allowingReadAccessTo: tool.url) +// webView.loadFileURL(url, allowingReadAccessTo: tool.url) + webView.load(URLRequest(url: URL(string: "app://localhost")!)) } private func loadBackground() { @@ -118,11 +126,56 @@ final class MiniToolRuntime: NSObject, ObservableObject { } } +extension MiniToolRuntime : WKURLSchemeHandler { + func webView( + _ webView: WKWebView, + start urlSchemeTask: WKURLSchemeTask + ) { + guard let url = urlSchemeTask.request.url else { return } + + let path = url.path.isEmpty ? "index.html" : url.path + + let fileURL = tool.url + .appendingPathComponent(path) + + do { + let data = try Data(contentsOf: fileURL) + let mimeType : String + if let type = UTType(filenameExtension: fileURL.pathExtension), let mimetype = type.preferredMIMEType { + mimeType = mimetype + } else { + mimeType = UTType.data.preferredMIMEType! + } + + let response = URLResponse( + url: url, + mimeType: mimeType, + expectedContentLength: data.count, + textEncodingName: nil + ) + + urlSchemeTask.didReceive(response) + urlSchemeTask.didReceive(data) + urlSchemeTask.didFinish() + } catch { + urlSchemeTask.didFailWithError(error) + } + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { + + } +} + // MARK: - WKScriptMessageHandler extension MiniToolRuntime: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == messageHandlerName else { return } + if let dict = message.body as? [String: Any], dict["__appXHR"] as? Bool == true { + handleAppXHRMessage(dict) + return + } if let dict = message.body as? [String: Any], let payload = dict["payload"] { deliverToBackground(payload) } else { @@ -160,6 +213,182 @@ extension MiniToolRuntime { console.error(err); } }; + + (function() { + const handler = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.miniToolBridge; + function safePostMessage(body) { + if (!handler || !handler.postMessage) { + console.error('miniToolBridge unavailable for AppXMLHttpRequest'); + return; + } + handler.postMessage(body); + } + + function base64ToArrayBuffer(base64) { + const binaryString = atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; + } + + class AppXMLHttpRequest { + constructor() { + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.response = null; + this.responseType = ''; + this.onreadystatechange = null; + this.onload = null; + this.onerror = null; + this.onabort = null; + this._headers = {}; + this._method = null; + this._url = null; + this._async = true; + this._aborted = false; + this._id = AppXMLHttpRequest.__nextId(); + } + + open(method, url, async = true) { + this._method = method; + this._url = url; + this._async = async !== false; + this.readyState = 1; // OPENED + this._emitReadyState(); + } + + setRequestHeader(key, value) { + this._headers[key] = value; + } + + send(body = null) { + if (!this._method || !this._url) { + throw new Error('AppXMLHttpRequest: call open() before send().'); + } + + this.readyState = 2; // HEADERS_RECEIVED (simulated) + this._emitReadyState(); + + AppXMLHttpRequest.__pending[this._id] = this; + + let encodedBody = null; + let bodyIsBase64 = false; + if (typeof body === 'string') { + encodedBody = body; + } else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) { + const view = body instanceof ArrayBuffer ? new Uint8Array(body) : new Uint8Array(body.buffer, body.byteOffset || 0, body.byteLength || body.length || 0); + let binary = ''; + for (let i = 0; i < view.length; i++) { + binary += String.fromCharCode(view[i]); + } + encodedBody = btoa(binary); + bodyIsBase64 = true; + } else if (body != null) { + try { + encodedBody = JSON.stringify(body); + } catch (err) { + console.error('AppXMLHttpRequest: unable to serialize body', err); + } + } + + safePostMessage({ + __appXHR: true, + action: 'request', + id: this._id, + method: this._method, + url: this._url, + async: this._async, + headers: this._headers, + body: encodedBody, + bodyIsBase64: bodyIsBase64, + responseType: this.responseType + }); + } + + abort() { + this._aborted = true; + safePostMessage({ __appXHR: true, action: 'abort', id: this._id }); + } + + _complete(payload) { + if (this._aborted && !payload.aborted) { + return; + } + + this.status = payload.status || 0; + this.statusText = payload.statusText || ''; + this.responseText = payload.responseText || ''; + const base64 = payload.base64 || null; + + if (this.responseType === 'json') { + try { + this.response = this.responseText ? JSON.parse(this.responseText) : null; + } catch (err) { + console.error('AppXMLHttpRequest: failed to parse JSON response', err); + this.response = null; + } + } else if (this.responseType === 'arraybuffer' && base64) { + this.response = base64ToArrayBuffer(base64); + } else { + this.response = this.responseText; + } + + this.readyState = 4; // DONE + this._emitReadyState(); + + if (payload.aborted) { + if (typeof this.onabort === 'function') { + try { this.onabort(); } catch (err) { console.error(err); } + } + delete AppXMLHttpRequest.__pending[this._id]; + return; + } + + if (payload.error) { + if (typeof this.onerror === 'function') { + try { this.onerror(new Error(payload.error)); } catch (err) { console.error(err); } + } + } else if (typeof this.onload === 'function') { + try { this.onload(); } catch (err) { console.error(err); } + } + + delete AppXMLHttpRequest.__pending[this._id]; + } + + _emitReadyState() { + if (typeof this.onreadystatechange === 'function') { + try { + this.onreadystatechange(); + } catch (err) { + console.error(err); + } + } + } + + static __nextId() { + AppXMLHttpRequest.__counter += 1; + return `app-xhr-${AppXMLHttpRequest.__counter}`; + } + + static __receive(payload) { + const instance = AppXMLHttpRequest.__pending[payload.id]; + if (instance) { + instance._complete(payload); + } + } + } + + AppXMLHttpRequest.__counter = 0; + AppXMLHttpRequest.__pending = {}; + + window.__XMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = AppXMLHttpRequest; // alias with requested casing + })(); """ static let backgroundBridgeScript = """ @@ -197,3 +426,117 @@ extension MiniToolRuntime { return nil } } + +// MARK: - App-backed XHR + +private extension MiniToolRuntime { + func handleAppXHRMessage(_ payload: [String: Any]) { + guard let id = payload["id"] as? String else { + appendLog("AppXHR missing id") + return + } + + let action = payload["action"] as? String ?? "request" + + if action == "abort" { + if let task = appXHRTasks[id] { + task.cancel() + appXHRTasks[id] = nil + } + deliverAppXHRResponse(["id": id, "status": 0, "statusText": "aborted", "aborted": true]) + return + } + + guard let urlString = payload["url"] as? String, let url = URL(string: urlString) else { + appendLog("AppXHR invalid URL for id \(id)") + deliverAppXHRResponse(["id": id, "status": 0, "statusText": "invalid-url", "error": "Invalid URL"]) + return + } + + var request = URLRequest(url: url) + request.httpMethod = (payload["method"] as? String) ?? "GET" + + if let headers = payload["headers"] as? [String: Any] { + headers.forEach { key, value in + if let valueString = value as? String ?? (value as? NSNumber)?.stringValue { + request.setValue(valueString, forHTTPHeaderField: key) + } + } + } + + if let body = payload["body"] { + let isBase64 = payload["bodyIsBase64"] as? Bool ?? false + if let stringBody = body as? String { + if isBase64, let data = Data(base64Encoded: stringBody) { + request.httpBody = data + } else { + request.httpBody = stringBody.data(using: .utf8) + } + } else if JSONSerialization.isValidJSONObject(body), + let data = try? JSONSerialization.data(withJSONObject: body, options: []) { + request.httpBody = data + } + } + + let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in + guard let self else { return } + + var responsePayload: [String: Any] = [ + "id": id, + "status": 0, + "statusText": "" + ] + + if let http = response as? HTTPURLResponse { + responsePayload["status"] = http.statusCode + responsePayload["statusText"] = HTTPURLResponse.localizedString(forStatusCode: http.statusCode) + + let headers = http.allHeaderFields.reduce(into: [String: String]()) { partialResult, entry in + if let key = entry.key as? String { + partialResult[key] = String(describing: entry.value) + } + } + responsePayload["headers"] = headers + } + + if let error = error as NSError? { + if error.code == NSURLErrorCancelled { + responsePayload["aborted"] = true + responsePayload["statusText"] = "aborted" + } else { + responsePayload["error"] = error.localizedDescription + } + } + + if let data = data { + responsePayload["responseText"] = String(data: data, encoding: .utf8) ?? "" + responsePayload["base64"] = data.base64EncodedString() + } + + DispatchQueue.main.async { + self.appXHRTasks[id] = nil + self.deliverAppXHRResponse(responsePayload) + print("RESPONSE: \(responsePayload)") + } + } + + appXHRTasks[id] = task + task.resume() + } + + func deliverAppXHRResponse(_ payload: [String: Any]) { + guard let json = MiniToolRuntime.encodePayload(payload) else { + appendLog("AppXHR: Unable to encode response for id \(payload["id"] ?? "")") + return + } + + DispatchQueue.main.async { + let script = "window.XMLHttpRequest.__receive(\(json))" + self.webView.evaluateJavaScript(script) { _, error in + if let error { + self.appendLog("AppXHR deliver error: \(error.localizedDescription)") + } + } + } + } +} From edab5ae8d99cb7f23edda692f63a54ecdd908e4a Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sat, 13 Dec 2025 22:46:51 +0800 Subject: [PATCH 6/9] Fix some memory leak --- StikJIT/idevice/applist.m | 31 ++++++++++++++++++++----------- StikJIT/idevice/heartbeat.m | 2 +- StikJIT/idevice/ideviceinfo.m | 2 ++ StikJIT/idevice/mount.m | 5 ++++- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/StikJIT/idevice/applist.m b/StikJIT/idevice/applist.m index 0c70045b..8c52b9bf 100644 --- a/StikJIT/idevice/applist.m +++ b/StikJIT/idevice/applist.m @@ -13,6 +13,7 @@ #import "JITEnableContext.h" #import "JITEnableContextInternal.h" +NSError* makeError(int code, NSString* msg); static NSString *extractAppName(plist_t app) { plist_t displayNameNode = plist_dict_get_item(app, "CFBundleDisplayName"); @@ -21,10 +22,10 @@ plist_get_string_val(displayNameNode, &displayNameC); if (displayNameC && displayNameC[0] != '\0') { NSString *displayName = [NSString stringWithUTF8String:displayNameC]; - free(displayNameC); + plist_mem_free(displayNameC); return displayName; } - free(displayNameC); + plist_mem_free(displayNameC); } plist_t nameNode = plist_dict_get_item(app, "CFBundleName"); @@ -33,10 +34,10 @@ plist_get_string_val(nameNode, &nameC); if (nameC && nameC[0] != '\0') { NSString *name = [NSString stringWithUTF8String:nameC]; - free(nameC); + plist_mem_free(nameC); return name; } - free(nameC); + plist_mem_free(nameC); } return @"Unknown"; @@ -135,12 +136,12 @@ static BOOL isHiddenSystemApp(plist_t app) char *bidC = NULL; plist_get_string_val(bidNode, &bidC); if (!bidC || bidC[0] == '\0') { - free(bidC); + plist_mem_free(bidC); continue; } NSString *bundleID = [NSString stringWithUTF8String:bidC]; - free(bidC); + plist_mem_free(bidC); result[bundleID] = extractAppName(app); } @@ -154,21 +155,29 @@ static BOOL isHiddenSystemApp(plist_t app) BOOL (^filter)(plist_t app)) { InstallationProxyClientHandle *client = NULL; - if (installation_proxy_connect(provider, &client)) { - *error = @"Failed to connect to installation proxy"; + IdeviceFfiError* err = installation_proxy_connect(provider, &client); + if (err) { + *error = [NSString stringWithFormat:@"Failed to connect to installation proxy: %s", err->message]; + idevice_error_free(err); return nil; } - void *apps = NULL; + plist_t *apps = NULL; size_t count = 0; - if (installation_proxy_get_apps(client, NULL, NULL, 0, &apps, &count)) { + err = installation_proxy_get_apps(client, NULL, NULL, 0, (void*)&apps, &count); + if (err) { + *error = [NSString stringWithFormat:@"Failed to get apps: %s", err->message]; + idevice_error_free(err); installation_proxy_client_free(client); - *error = @"Failed to get apps"; return nil; } NSDictionary *result = buildAppDictionary(apps, count, requireGetTaskAllow, filter); installation_proxy_client_free(client); + for(int i = 0; i < count; ++i) { + plist_free(apps[i]); + } + idevice_data_free((uint8_t *)apps, sizeof(plist_t)*count); return result; } diff --git a/StikJIT/idevice/heartbeat.m b/StikJIT/idevice/heartbeat.m index ea2836f1..dd029e83 100644 --- a/StikJIT/idevice/heartbeat.m +++ b/StikJIT/idevice/heartbeat.m @@ -71,7 +71,7 @@ void startHeartbeat(IdevicePairingFile* pairing_file, IdeviceProviderHandle** pr if(!completionCalled) { completion(err->code, err->message); } -// heartbeat_client_free(client); + heartbeat_client_free(client); idevice_error_free(err); return; } diff --git a/StikJIT/idevice/ideviceinfo.m b/StikJIT/idevice/ideviceinfo.m index 7a4c94ef..abcd8026 100644 --- a/StikJIT/idevice/ideviceinfo.m +++ b/StikJIT/idevice/ideviceinfo.m @@ -19,11 +19,13 @@ struct IdeviceFfiError * err = lockdownd_connect(g_provider, &g_client); if (err) { *error = makeError(err->code, @(err->message)); + idevice_pairing_file_free(g_sess_pf); idevice_error_free(err); return 0; } err = lockdownd_start_session(g_client, g_sess_pf); + idevice_pairing_file_free(g_sess_pf); if (err) { *error = makeError(err->code, @(err->message)); idevice_error_free(err); diff --git a/StikJIT/idevice/mount.m b/StikJIT/idevice/mount.m index e042d936..fff1890a 100644 --- a/StikJIT/idevice/mount.m +++ b/StikJIT/idevice/mount.m @@ -30,7 +30,7 @@ size_t getMountedDeviceCount(IdeviceProviderHandle* provider, NSError** error) { for(int i = 0;i < deviceLength; ++i) { plist_free(devices[i]); } - idevice_data_free((uint8_t *)devices, deviceLength*sizeof(plist_t*)); + idevice_data_free((uint8_t *)devices, deviceLength*sizeof(plist_t)); image_mounter_free(client); return deviceLength; } @@ -41,6 +41,7 @@ int mountPersonalDDI(IdeviceProviderHandle* provider, IdevicePairingFile* pairin NSData* trustcache = [NSData dataWithContentsOfFile:trustcachePath]; NSData* buildManifest = [NSData dataWithContentsOfFile:manifestPath]; if(!image || !trustcache || !buildManifest) { + idevice_pairing_file_free(pairingFile2); *error = makeError(1, @"Failed to read one or more files"); return 1; } @@ -49,11 +50,13 @@ int mountPersonalDDI(IdeviceProviderHandle* provider, IdevicePairingFile* pairin IdeviceFfiError* err = lockdownd_connect(provider, &lockdownClient); if (err) { *error = makeError(6, @(err->message)); + idevice_pairing_file_free(pairingFile2); idevice_error_free(err); return 6; } err = lockdownd_start_session(lockdownClient, pairingFile2); + idevice_pairing_file_free(pairingFile2); if (err) { *error = makeError(7, @(err->message)); idevice_error_free(err); From 444eca7967f0dd087624648cc2f9addcc017eb41 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sun, 14 Dec 2025 01:26:06 +0800 Subject: [PATCH 7/9] allow background script access idevice (codes are from the old JSSupport branch) Tests are not carried out but most of the js api should work. Do we need to obfuscate some functions' name? --- .../JSSupport/IDeviceJSBridge.m | 192 ++++++++ .../JSSupport/IDeviceJSBridgeAFC.m | 363 ++++++++++++++ .../JSSupport/IDeviceJSBridgeAMFI.m | 79 +++ .../JSSupport/IDeviceJSBridgeAdapter.m | 37 ++ .../JSSupport/IDeviceJSBridgeCoreDevice.m | 66 +++ .../JSSupport/IDeviceJSBridgeDebugProxy.m | 90 ++++ .../IDeviceJSBridgeInstallationProxy.m | 292 +++++++++++ .../IDeviceJSBridgeLocationSimulation.m | 78 +++ .../JSSupport/IDeviceJSBridgeMisagent.m | 113 +++++ .../IDeviceJSBridgeNSData+LocalFile.m | 191 ++++++++ .../JSSupport/IDeviceJSBridgeProcessControl.m | 125 +++++ .../JSSupport/IDeviceJSBridgeRSD.m | 182 +++++++ .../JSSupport/IDeviceJSBridgeRemoteServer.m | 43 ++ .../JSSupport/IDeviceJSBridgeSBServices.m | 60 +++ StikJIT/MiniToolSupport/JSSupport/JSSupport.h | 39 ++ .../MiniToolSupport/JSSupport/NSInvocation.m | 44 ++ StikJIT/MiniToolSupport/JSSupport/idevice.js | 455 ++++++++++++++++++ .../MiniToolSupport/MiniToolListView.swift | 35 +- StikJIT/MiniToolSupport/MiniToolModels.swift | 22 + .../MiniToolSupport/MiniToolRunnerView.swift | 10 +- StikJIT/MiniToolSupport/MiniToolRuntime.swift | 90 +++- StikJIT/StikJIT-Bridging-Header.h | 1 + StikJIT/StikJITApp.swift | 1 + StikJIT/Views/SettingsView.swift | 4 +- StikJIT/idevice/JITEnableContext.h | 2 +- StikJIT/idevice/JITEnableContext.m | 4 + 26 files changed, 2579 insertions(+), 39 deletions(-) create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridge.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAFC.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAMFI.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAdapter.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeCoreDevice.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeDebugProxy.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeLocationSimulation.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeMisagent.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeNSData+LocalFile.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeProcessControl.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeRSD.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeRemoteServer.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeSBServices.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/JSSupport.h create mode 100644 StikJIT/MiniToolSupport/JSSupport/NSInvocation.m create mode 100644 StikJIT/MiniToolSupport/JSSupport/idevice.js diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridge.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridge.m new file mode 100644 index 00000000..1ece2502 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridge.m @@ -0,0 +1,192 @@ +// +// IDeviceJSBridge.m +// StikJIT +// +// Created by s s on 2025/4/24. +// +@import Foundation; +#import "JSSupport.h" + +@implementation IDeviceHandle + +@end + + +NSDictionary *dictionaryFromPlistData(NSData *plistData, NSError **error) { + if (!plistData) { + if (error) { + *error = [NSError errorWithDomain:@"PlistConversionErrorDomain" + code:1001 + userInfo:@{NSLocalizedDescriptionKey: @"Input plist data is nil."}]; + } + return nil; + } + + NSPropertyListFormat format; + NSDictionary *result = [NSPropertyListSerialization propertyListWithData:plistData + options:NSPropertyListImmutable + format:&format + error:error]; + if (![result isKindOfClass:[NSDictionary class]]) { + if (error && !*error) { + *error = [NSError errorWithDomain:@"PlistConversionErrorDomain" + code:1002 + userInfo:@{NSLocalizedDescriptionKey: @"Plist is not a dictionary."}]; + } + return nil; + } + + return result; +} + +NSData *plistDataFromDictionary(NSDictionary *dictionary, NSError **error) { + if (!dictionary || ![dictionary isKindOfClass:[NSDictionary class]]) { + if (error) { + *error = [NSError errorWithDomain:@"PlistSerializationErrorDomain" + code:2001 + userInfo:@{NSLocalizedDescriptionKey: @"Input is not a valid dictionary."}]; + } + return nil; + } + + NSData *data = [NSPropertyListSerialization dataWithPropertyList:dictionary + format:NSPropertyListXMLFormat_v1_0 + options:0 + error:error]; + return data; +} + +const char** cstrArrFromNSArray(NSArray* arr, int* validCount) { + const char** ans = 0; + *validCount = 0; + if(![arr isKindOfClass:NSArray.class]) { + return 0; + } else { + ans = malloc([arr count] * sizeof(void*)); + for (id str in arr) { + if([str isKindOfClass:NSString.class]) { + ans[*validCount] = [str UTF8String]; + (*validCount)++; + } + } + } + return ans; +} + +@implementation IDeviceJSBridge { + int maxHandleId; + int maxDataId; +} + +- (instancetype)init { + maxHandleId = 0; + maxDataId = 0; + handles = [[NSMutableDictionary alloc] init]; + dataPool = [[NSMutableDictionary alloc] init]; + + return self; +} + +- (NSString*)errFreeFromIdeviceFfiError:(IdeviceFfiError*)err { + NSString* ans = [NSString stringWithFormat:@"%s (%d)", err->message, err->code]; + idevice_error_free(err); + return ans; +} + +- (int)registerIdeviceHandle:(void*)handle freeFunc:(void*)freeFunc { + maxHandleId++; + int ans = maxHandleId; + IDeviceHandle* handleObj = [IDeviceHandle alloc]; + handleObj.handle = handle; + handleObj.freeFunc = freeFunc; + handles[@(maxHandleId)] = handleObj; + return ans; +} + +- (BOOL)freeIdeviceHandle:(int)handleId { + if(handles[@(handleId)]) { + IDeviceHandle* handleObj = handles[@(handleId)]; + void (*freeFunc)(void*) = handleObj.freeFunc; + freeFunc(handleObj.handle); + + + [handles removeObjectForKey:@(handleId)]; + return true; + } else { + return false; + } +} + + +- (bool)freeNSData:(int)handleId { + if(dataPool[@(handleId)]) { + [dataPool removeObjectForKey:@(handleId)]; + return true; + } else { + return false; + } +} + +- (int)registerNSData:(NSData*)data { + maxHandleId++; + int ans = maxHandleId; + dataPool[@(maxHandleId)] = data; + return ans; +} + +- (void)cleanUp { + NSArray *sortedKeys = [[handles allKeys] sortedArrayUsingSelector:@selector(compare:)]; + for(NSNumber* a in sortedKeys) { + [self freeIdeviceHandle:[a intValue]]; + } + for(NSNumber* a in dataPool) { + [self freeNSData:[a intValue]]; + } +} + +- (void)didReceiveScriptMessage:(nonnull NSDictionary *)message resolve:(JSValue*)resolveFunc reject:(JSValue*)rejectFunc { + if(![message isKindOfClass:NSDictionary.class]) { + [rejectFunc callWithArguments:@[@"Input is not a dictionary"]]; + return; + } + + NSString* handlerSelectorStr = [NSString stringWithFormat:@"%@WithBody:replyHandler:", message[@"command"]]; + if(![self respondsToSelector:NSSelectorFromString(handlerSelectorStr)]) { + [rejectFunc callWithArguments:@[@"Invalid idevice function!"]]; + return; + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + void (^replyHandler)(id _Nullable, NSString * _Nullable) = ^(id result, NSString* errMsg) { + if(errMsg) { + [rejectFunc callWithArguments:@[errMsg]]; + } else { + [resolveFunc callWithArguments:@[result]]; + } + }; + + NSInvocation* invocation = [NSInvocation invocationWithTarget:self selector:NSSelectorFromString(handlerSelectorStr) retainArguments:YES, message, replyHandler]; + [invocation invoke]; + }); + + +} + +- (void)idevice_freeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int handleId = [body[@"handle"] intValue]; + if(!handles[@(handleId)] || handles[@(handleId)].freeFunc != adapter_free) { + replyHandler(nil, @"Invalid handle"); + return; + } + + [self freeIdeviceHandle:handleId]; + + replyHandler(@(YES), nil); +} + +- (void)dealloc { + [self cleanUp]; +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAFC.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAFC.m new file mode 100644 index 00000000..07774910 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAFC.m @@ -0,0 +1,363 @@ +// +// IDeviceJSBridgeAFC.m +// StikJIT +// +// Created by s s on 2025/4/25. +// + +@import Foundation; +#import "JSSupport.h" + +@implementation IDeviceJSBridge (AFC) + +- (void)afc_client_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; + + AfcClientHandle* client = NULL; + IdeviceFfiError* err = afc_client_connect(provider, &client); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int clientHandleId = [self registerIdeviceHandle:client freeFunc:(void*)afc_client_free]; + replyHandler([NSNumber numberWithInt:clientHandleId], nil); +} + +- (void)afc_list_directoryWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + NSString* path = body[@"path"]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) { + replyHandler(nil, @"Invalid afc client handle"); + return; + } + if (![path isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid path"); + return; + } + + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AfcClientHandle* client = clientHandleObj.handle; + + char** entries = NULL; + size_t count = 0; + IdeviceFfiError* err = afc_list_directory(client, [path UTF8String], &entries, &count); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + NSMutableArray* result = [NSMutableArray arrayWithCapacity:count]; + for (size_t i = 0; i < count; i++) { + [result addObject:[NSString stringWithUTF8String:entries[i]]]; + free(entries[i]); + } + free(entries); + + replyHandler(result, nil); +} + +- (void)afc_make_directoryWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + NSString* path = body[@"path"]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) { + replyHandler(nil, @"Invalid afc client handle"); + return; + } + if (![path isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid path"); + return; + } + + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AfcClientHandle* client = clientHandleObj.handle; + + IdeviceFfiError* err = afc_make_directory(client, [path UTF8String]); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +- (void)afc_remove_pathWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + NSString* path = body[@"path"]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) { + replyHandler(nil, @"Invalid afc client handle"); + return; + } + if (![path isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid path"); + return; + } + + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AfcClientHandle* client = clientHandleObj.handle; + + IdeviceFfiError* err = afc_remove_path(client, [path UTF8String]); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +- (void)afc_remove_path_and_contentsWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + NSString* path = body[@"path"]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) { + replyHandler(nil, @"Invalid afc client handle"); + return; + } + if (![path isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid path"); + return; + } + + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AfcClientHandle* client = clientHandleObj.handle; + + IdeviceFfiError* err = afc_remove_path_and_contents(client, [path UTF8String]); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +- (void)afc_rename_pathWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + NSString* source = body[@"source"]; + NSString* target = body[@"target"]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) { + replyHandler(nil, @"Invalid afc client handle"); + return; + } + if (![source isKindOfClass:NSString.class] || ![target isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid source or target path"); + return; + } + + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AfcClientHandle* client = clientHandleObj.handle; + + IdeviceFfiError* err = afc_rename_path(client, [source UTF8String], [target UTF8String]); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +- (void)afc_get_file_infoWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + NSString* path = body[@"path"]; + + if (!path || ![path isKindOfClass:[NSString class]]) { + replyHandler(nil, @"Missing or invalid path"); + return; + } + + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) { + replyHandler(nil, @"Invalid afc client handle"); + return; + } + + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AfcClientHandle* client = clientHandleObj.handle; + + struct AfcFileInfo info; + struct IdeviceFfiError* err = afc_get_file_info(client, [path UTF8String], &info); + + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + NSDictionary* ans = @{ + @"size": @(info.size), + @"blocks": @(info.blocks), + @"creation": @(info.creation), + @"modified": @(info.modified), + @"st_nlink": @(info.st_nlink), + @"st_ifmt": @(info.st_ifmt), + @"st_link_target": @(info.st_link_target) + }; + afc_file_info_free(&info); + + replyHandler(ans, nil); +} + +- (void)afc_get_device_infoWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) { + replyHandler(nil, @"Invalid afc client handle"); + return; + } + + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AfcClientHandle* client = clientHandleObj.handle; + + struct AfcDeviceInfo info; + struct IdeviceFfiError* err = afc_get_device_info(client, &info); + + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + NSDictionary* ans = @{ + @"model": @(info.model), + @"total_bytes": @(info.total_bytes), + @"free_bytes": @(info.free_bytes), + @"block_size": @(info.block_size), + }; + afc_device_info_free(&info); + + replyHandler(ans, nil); +} + +- (void)afc_file_openWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int clientId = [body[@"handle"] intValue]; + NSString *path = body[@"path"]; + NSNumber *modeNumber = body[@"mode"]; + + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) { + replyHandler(nil, @"Invalid afc client handle"); + return; + } + + if (![path isKindOfClass:NSString.class] || ![modeNumber isKindOfClass:NSNumber.class]) { + replyHandler(nil, @"Invalid path or mode"); + return; + } + + IDeviceHandle *clientHandleObj = handles[@(clientId)]; + AfcClientHandle *client = clientHandleObj.handle; + + AfcFileHandle *fileHandle = NULL; + IdeviceFfiError* err = afc_file_open(client, [path UTF8String], (AfcFopenMode)[modeNumber intValue], &fileHandle); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int handleId = [self registerIdeviceHandle:fileHandle freeFunc:(void *)afc_file_close]; + replyHandler(@(handleId), nil); +} + +- (void)afc_file_closeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != afc_file_close) { + replyHandler(nil, @"Invalid afc file handle"); + return; + } + + IDeviceHandle *fileHandleObj = handles[@(handleId)]; + AfcFileHandle *fileHandle = fileHandleObj.handle; + + IdeviceFfiError* err = afc_file_close(fileHandle); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + [handles removeObjectForKey:@(handleId)]; + replyHandler(@YES, nil); +} + +- (void)afc_file_readWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != afc_file_close) { + replyHandler(nil, @"Invalid afc file handle"); + return; + } + + IDeviceHandle *fileHandleObj = handles[@(handleId)]; + AfcFileHandle *fileHandle = fileHandleObj.handle; + + unsigned char* file_data = 0; + size_t len = 0; + IdeviceFfiError* err = afc_file_read(fileHandle, &file_data, &len); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + NSData* data = [NSData dataWithBytes:file_data length:len]; + free(file_data); + int nsdataHandleId = [self registerNSData:data]; + replyHandler(@(nsdataHandleId), nil); +} + +- (void)afc_file_writeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != afc_file_close) { + replyHandler(nil, @"Invalid afc file handle"); + return; + } + + IDeviceHandle *fileHandleObj = handles[@(handleId)]; + AfcFileHandle *fileHandle = fileHandleObj.handle; + + int nsdataHandleId = [body[@"data_handle"] intValue]; + if (!dataPool[@(nsdataHandleId)]) { + replyHandler(nil, @"Invalid NSData handle"); + return; + } + NSData* data = dataPool[@(nsdataHandleId)]; + + IdeviceFfiError* err = afc_file_write(fileHandle, [data bytes], [data length]); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@(YES), nil); +} + +- (void)afc_make_linkWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int clientId = [body[@"handle"] intValue]; + NSString *target = body[@"target"]; + NSString *source = body[@"source"]; + NSNumber *linkTypeNum = body[@"link_type"]; + + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) { + replyHandler(nil, @"Invalid afc client handle"); + return; + } + + if (![target isKindOfClass:NSString.class] || ![source isKindOfClass:NSString.class] || ![linkTypeNum isKindOfClass:NSNumber.class]) { + replyHandler(nil, @"Invalid target/source/link_type"); + return; + } + + IDeviceHandle *clientHandleObj = handles[@(clientId)]; + AfcClientHandle *client = clientHandleObj.handle; + + IdeviceFfiError* err = afc_make_link(client, + [target UTF8String], + [source UTF8String], + (AfcLinkType)[linkTypeNum intValue]); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAMFI.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAMFI.m new file mode 100644 index 00000000..551b51be --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAMFI.m @@ -0,0 +1,79 @@ +// +// IDeviceJSBridgeAMFI.m +// StikJIT +// +// Created by s s on 2025/4/26. +// +@import Foundation; +#import "JSSupport.h" +#import "../../idevice/JITEnableContext.h" +#import "../../idevice/idevice.h" + +@implementation IDeviceJSBridge (AMFI) + +- (void)amfi_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + IdeviceProviderHandle *provider = [JITEnableContext.shared getTcpProviderHandle]; + + AmfiClientHandle *client = NULL; + IdeviceFfiError* err = amfi_connect(provider, &client); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int handleId = [self registerIdeviceHandle:client freeFunc:(void *)amfi_client_free]; + replyHandler(@(handleId), nil); +} + +- (void)amfi_reveal_developer_mode_option_in_uiWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int clientId = [body[@"handle"] intValue]; + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != amfi_client_free) { + replyHandler(nil, @"Invalid AmfiClient handle"); + return; + } + + AmfiClientHandle *client = handles[@(clientId)].handle; + IdeviceFfiError* err = amfi_reveal_developer_mode_option_in_ui(client); + if (err ) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +- (void)amfi_enable_developer_modeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int clientId = [body[@"handle"] intValue]; + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != amfi_client_free) { + replyHandler(nil, @"Invalid AmfiClient handle"); + return; + } + + AmfiClientHandle *client = handles[@(clientId)].handle; + IdeviceFfiError* err = amfi_enable_developer_mode(client); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +- (void)amfi_accept_developer_modeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int clientId = [body[@"handle"] intValue]; + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != amfi_client_free) { + replyHandler(nil, @"Invalid AmfiClient handle"); + return; + } + + AmfiClientHandle *client = handles[@(clientId)].handle; + IdeviceFfiError* err = amfi_accept_developer_mode(client); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAdapter.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAdapter.m new file mode 100644 index 00000000..48ec3a4a --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAdapter.m @@ -0,0 +1,37 @@ +// +// IDeviceJSBridgeAdapter.m +// StikJIT +// +// Created by s s on 2025/4/25. +// +@import Foundation; +#import "JSSupport.h" + +@implementation IDeviceJSBridge (Adapter) + +- (void)adapter_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"adapter"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != adapter_free) { + replyHandler(nil, @"Invalid adapter handle"); + return; + } + + int port = [body[@"port"] intValue]; + if(port < 0 || port > 65536 ) { + replyHandler(nil, @"Invalid port"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AdapterHandle* handle = clientHandleObj.handle; + AdapterStreamHandle *stream = NULL; + IdeviceFfiError* err = adapter_connect(handle, port, (ReadWriteOpaque **)&stream); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + int handleId = [self registerIdeviceHandle:stream freeFunc:(void *)adapter_stream_close]; + replyHandler(@(handleId), nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeCoreDevice.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeCoreDevice.m new file mode 100644 index 00000000..10998c94 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeCoreDevice.m @@ -0,0 +1,66 @@ +// +// IDeviceJSBridgeCoreDevice.m +// StikJIT +// +// Created by s s on 2025/4/25. +// +@import Foundation; +#import "JSSupport.h" +@implementation IDeviceJSBridge (CoreDevice) + +- (void)core_device_proxy_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; + + CoreDeviceProxyHandle *core_device = NULL; + IdeviceFfiError* err = core_device_proxy_connect(provider, &core_device); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + int handleId = [self registerIdeviceHandle:core_device freeFunc:(void*)core_device_proxy_free]; + replyHandler(@(handleId), nil); +} + +- (void)core_device_proxy_get_server_rsd_portWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != core_device_proxy_free) { + replyHandler(nil, @"Invalid core device proxy handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + CoreDeviceProxyHandle* core_device = clientHandleObj.handle; + + // Get server RSD port + uint16_t rsd_port; + IdeviceFfiError* err = core_device_proxy_get_server_rsd_port(core_device, &rsd_port); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + replyHandler(@(rsd_port), nil); +} + +- (void)core_device_proxy_create_tcp_adapterWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != core_device_proxy_free) { + replyHandler(nil, @"Invalid core device proxy handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + CoreDeviceProxyHandle* core_device = clientHandleObj.handle; + + AdapterHandle *adapter = NULL; + IdeviceFfiError* err = core_device_proxy_create_tcp_adapter(core_device, &adapter); + [handles removeObjectForKey:@(clientId)]; + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int handleId = [self registerIdeviceHandle:adapter freeFunc:(void*)adapter_free]; + replyHandler(@(handleId), nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeDebugProxy.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeDebugProxy.m new file mode 100644 index 00000000..40f23cf9 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeDebugProxy.m @@ -0,0 +1,90 @@ +// +// IDeviceJSBridgeDebugProxy.m +// StikJIT +// +// Created by s s on 2025/4/25. +// +@import Foundation; +#import "JSSupport.h" + +@implementation IDeviceJSBridge (DebugProxy) + +- (void)debug_proxy_connect_rsdWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"adapter"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != adapter_free) { + replyHandler(nil, @"Invalid adapter handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AdapterHandle* adapter = clientHandleObj.handle; + + int handshakeId = [body[@"handshake"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != rsd_handshake_free) { + replyHandler(nil, @"Invalid handshake handle"); + return; + } + IDeviceHandle* handshakeHandleObj = handles[@(handshakeId)]; + RsdHandshakeHandle* handshake = handshakeHandleObj.handle; + + DebugProxyHandle *debug_proxy = NULL; + IdeviceFfiError* err = debug_proxy_connect_rsd(adapter, handshake, &debug_proxy); + [handles removeObjectForKey:@(clientId)]; + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int handleId = [self registerIdeviceHandle:debug_proxy freeFunc:(void*)debug_proxy_free]; + replyHandler(@(handleId), nil); +} + +- (void)debug_proxy_send_commandWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != debug_proxy_free) { + replyHandler(nil, @"Invalid debug proxy handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + DebugProxyHandle* debug_proxy = clientHandleObj.handle; + + NSObject* debugCommandObj = body[@"debug_command"]; + DebugserverCommandHandle* command = 0; + if([debugCommandObj isKindOfClass:NSString.class]) { + command = debugserver_command_new([(NSString*)debugCommandObj UTF8String], NULL, 0); + } else if ([debugCommandObj isKindOfClass:NSDictionary.class]) { + NSDictionary* commandBody = (NSDictionary*)debugCommandObj; + + NSString* name = commandBody[@"name"]; + if(![name isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid command name"); + return; + } + + NSArray* args = commandBody[@"args"]; + int argsCount = 0; + const char** argsCharArr = cstrArrFromNSArray(args, &argsCount); + + command = debugserver_command_new([name UTF8String], argsCharArr, argsCount); + } else { + replyHandler(nil, @"Invalid command"); + return; + } + + char* attach_response = 0; + IdeviceFfiError* err = debug_proxy_send_command(debug_proxy, command, &attach_response); + debugserver_command_free(command); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + NSString* commandResponse = nil; + if(attach_response) { + commandResponse = @(attach_response); + } + idevice_string_free(attach_response); + replyHandler(commandResponse, nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m new file mode 100644 index 00000000..0ffcb356 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m @@ -0,0 +1,292 @@ +// +// IDeviceJSBridgeInstallationProxy.m +// StikJIT +// +// Created by s s on 2025/4/25. +// +@import Foundation; +@import JavaScriptCore; +#import "JSSupport.h" + +struct InstallationProxyCallbackContext { + JSValue* callback; +}; + +void installationProxyCallback(uint64_t progress, struct InstallationProxyCallbackContext* context) { + [context->callback callWithArguments:@[@(progress)]]; +} + +@implementation IDeviceJSBridge (InstallationProxy) + +- (void)installation_proxy_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; + + InstallationProxyClientHandle *client = NULL; + IdeviceFfiError* err = installation_proxy_connect(provider, &client); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + int handleId = [self registerIdeviceHandle:client freeFunc:(void*)installation_proxy_client_free]; + replyHandler(@(handleId), nil); +} + +- (void)installation_proxy_get_appsWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"client"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) { + replyHandler(nil, @"Invalid client handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + InstallationProxyClientHandle* client = clientHandleObj.handle; + NSString* applicationType = body[@"application_type"]; + if(![applicationType isKindOfClass:NSString.class]) { + applicationType = nil; + } + + NSArray* bundleIdentifiers = body[@"bundle_identifiers"]; + + int bundleIdentifiersCount; + const char** bundleIdentifiersCharArr = cstrArrFromNSArray(bundleIdentifiers, &bundleIdentifiersCount); + void *apps = NULL; + size_t apps_len = 0; + IdeviceFfiError* err = installation_proxy_get_apps(client, [applicationType UTF8String], bundleIdentifiersCharArr, bundleIdentifiersCount, &apps, &apps_len); + if(bundleIdentifiersCharArr){ + free(bundleIdentifiersCharArr); + } + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + plist_t *app_list = (plist_t *)apps; + NSMutableArray* ans = [[NSMutableArray alloc] init]; + for(int i = 0; i < apps_len; ++i) { + char* buf = 0; + uint32_t plistlen = 0; + plist_to_bin(app_list[i], &buf, &plistlen); + NSError* err2 = 0; + NSDictionary* appDict = dictionaryFromPlistData([NSData dataWithBytes:buf length:plistlen], &err2); + plist_mem_free(buf); + if(err2) { + replyHandler(nil, @"failed to parse plist data"); + return; + } + [ans addObject:appDict]; + } + replyHandler(ans, nil); + + +} + +- (void)installation_proxy_installWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + + int clientId = [body[@"client"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) { + replyHandler(nil, @"Invalid client handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + InstallationProxyClientHandle* client = clientHandleObj.handle; + + NSString* packagePath = body[@"package_path"]; + if(![packagePath isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid package path"); + return; + } + + NSDictionary* optionsDict = body[@"options"]; + plist_t optionsPlist = 0; + if([optionsDict isKindOfClass:NSDictionary.class]) { + NSError* error = 0; + NSData* optionsNSData = plistDataFromDictionary(optionsDict, &error); + if(error) { + replyHandler(nil, [NSString stringWithFormat:@"failed to parse options %@", error.localizedDescription]); + return; + } + plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0); + } + + JSValue* callback = body[@"callback"]; + IdeviceFfiError* err = 0; + if(!callback) { + err = installation_proxy_install(client, [packagePath UTF8String], optionsPlist); + } else { + struct InstallationProxyCallbackContext context; + context.callback = callback; + + err = installation_proxy_install_with_callback(client, [packagePath UTF8String], optionsPlist, (void (*)(uint64_t, void *))installationProxyCallback, &context); + + } + + if(optionsPlist) { + plist_free(optionsPlist); + } + + if(err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + replyHandler(@YES, nil); +} + +- (void)installation_proxy_upgradeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + + int clientId = [body[@"client"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) { + replyHandler(nil, @"Invalid client handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + InstallationProxyClientHandle* client = clientHandleObj.handle; + + NSString* packagePath = body[@"package_path"]; + if(![packagePath isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid package path"); + return; + } + + NSDictionary* optionsDict = body[@"options"]; + plist_t optionsPlist = 0; + if([optionsDict isKindOfClass:NSDictionary.class]) { + NSError* error = 0; + NSData* optionsNSData = plistDataFromDictionary(optionsDict, &error); + if(error) { + replyHandler(nil, [NSString stringWithFormat:@"failed to parse options %@", error.localizedDescription]); + return; + } + plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0); + } + + JSValue* callback = body[@"callback"]; + IdeviceFfiError* err = 0; + if(!callback) { + err = installation_proxy_upgrade(client, [packagePath UTF8String], optionsPlist); + } else { + struct InstallationProxyCallbackContext context; + context.callback = callback; + + err = installation_proxy_upgrade_with_callback(client, [packagePath UTF8String], optionsPlist, (void (*)(uint64_t, void *))installationProxyCallback, &context); + + } + + if(optionsPlist) { + plist_free(optionsPlist); + } + + if(err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + replyHandler(@YES, nil); +} + +- (void)installation_proxy_uninstallWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + + int clientId = [body[@"client"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) { + replyHandler(nil, @"Invalid client handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + InstallationProxyClientHandle* client = clientHandleObj.handle; + + NSString* bundleId = body[@"bundle_id"]; + if(![bundleId isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid bundle id"); + return; + } + + NSDictionary* optionsDict = body[@"options"]; + plist_t optionsPlist = 0; + if([optionsDict isKindOfClass:NSDictionary.class]) { + NSError* error = 0; + NSData* optionsNSData = plistDataFromDictionary(optionsDict, &error); + if(error) { + replyHandler(nil, [NSString stringWithFormat:@"failed to parse options %@", error.localizedDescription]); + return; + } + plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0); + } + + JSValue* callback = body[@"callback"]; + IdeviceFfiError* err = 0; + if(!callback) { + err = installation_proxy_uninstall(client, [bundleId UTF8String], optionsPlist); + } else { + struct InstallationProxyCallbackContext context; + context.callback = callback; + + err = installation_proxy_uninstall_with_callback(client, [bundleId UTF8String], optionsPlist, (void (*)(uint64_t, void *))installationProxyCallback, &context); + + } + + if(optionsPlist) { + plist_free(optionsPlist); + } + + if(err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + replyHandler(@YES, nil); +} + +- (void)installation_proxy_browseWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"client"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) { + replyHandler(nil, @"Invalid client handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + InstallationProxyClientHandle* client = clientHandleObj.handle; + + NSDictionary* optionsDict = body[@"options"]; + plist_t optionsPlist = 0; + if([optionsDict isKindOfClass:NSDictionary.class]) { + NSError* error = 0; + NSData* optionsNSData = plistDataFromDictionary(optionsDict, &error); + if(error) { + replyHandler(nil, [NSString stringWithFormat:@"failed to parse options %@", error.localizedDescription]); + return; + } + plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0); + } + + plist_t *apps = NULL; + size_t apps_len = 0; + IdeviceFfiError* err = installation_proxy_browse(client, optionsPlist, &apps, &apps_len); + + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + NSMutableArray* ans = [[NSMutableArray alloc] init]; + for(int i = 0; i < apps_len; ++i) { + char* buf = 0; + uint32_t plistlen = 0; + plist_to_bin(apps[i], &buf, &plistlen); + NSError* err2 = 0; + NSDictionary* appDict = dictionaryFromPlistData([NSData dataWithBytes:buf length:plistlen], &err2); + plist_mem_free(buf); + if(err2) { + replyHandler(nil, @"failed to parse plist data"); + return; + } + [ans addObject:appDict]; + if([ans count] >= 100) { + break; + } + } + replyHandler(ans, nil); + + +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeLocationSimulation.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeLocationSimulation.m new file mode 100644 index 00000000..382fc018 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeLocationSimulation.m @@ -0,0 +1,78 @@ +// +// IDeviceJSBridgeLocationSimulation.m +// StikJIT +// +// Created by s s on 2025/4/26. +// +@import Foundation; +#import "JSSupport.h" + +@implementation IDeviceJSBridge (LocationSimulation) + +- (void)location_simulation_newWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int serverId = [body[@"server"] intValue]; + if (!handles[@(serverId)] || handles[@(serverId)].freeFunc != remote_server_free) { + replyHandler(nil, @"Invalid remote server handle"); + return; + } + + IDeviceHandle *serverHandleObj = handles[@(serverId)]; + RemoteServerHandle *server = serverHandleObj.handle; + + LocationSimulationHandle *simulation = NULL; + IdeviceFfiError* err = location_simulation_new(server, &simulation); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int handleId = [self registerIdeviceHandle:simulation freeFunc:(void *)location_simulation_free]; + replyHandler(@(handleId), nil); +} + +- (void)location_simulation_clearWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != location_simulation_free) { + replyHandler(nil, @"Invalid LocationSimulationAdapterHandle"); + return; + } + + IDeviceHandle *handleObj = handles[@(handleId)]; + LocationSimulationHandle *simulation = handleObj.handle; + + IdeviceFfiError* err = location_simulation_clear(simulation); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +- (void)location_simulation_setWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != location_simulation_free) { + replyHandler(nil, @"Invalid LocationSimulationAdapterHandle"); + return; + } + + NSNumber *lat = body[@"latitude"]; + NSNumber *lon = body[@"longitude"]; + if (![lat isKindOfClass:NSNumber.class] || ![lon isKindOfClass:NSNumber.class]) { + replyHandler(nil, @"latitude or longitude is invalid"); + return; + } + + IDeviceHandle *handleObj = handles[@(handleId)]; + LocationSimulationHandle *simulation = handleObj.handle; + + IdeviceFfiError* err = location_simulation_set(simulation, lat.doubleValue, lon.doubleValue); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeMisagent.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeMisagent.m new file mode 100644 index 00000000..f682dbd6 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeMisagent.m @@ -0,0 +1,113 @@ +// +// IDeviceJSBridgeMisagent.m +// StikJIT +// +// Created by s s on 2025/4/26. +// +@import Foundation; +#import "JSSupport.h" + +@implementation IDeviceJSBridge (Misagent) + +- (void)misagent_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; + + MisagentClientHandle* client = NULL; + IdeviceFfiError* err = misagent_connect(provider, &client); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int clientHandleId = [self registerIdeviceHandle:client freeFunc:(void*)misagent_client_free]; + replyHandler(@(clientHandleId), nil); +} + +- (void)misagent_installWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int clientId = [body[@"handle"] intValue]; + int dataHandleId = [body[@"data_handle"] intValue]; + + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != misagent_client_free) { + replyHandler(nil, @"Invalid misagent client handle"); + return; + } + + if (!dataPool[@(dataHandleId)]) { + replyHandler(nil, @"Invalid NSData handle"); + return; + } + + IDeviceHandle *clientHandleObj = handles[@(clientId)]; + MisagentClientHandle *client = clientHandleObj.handle; + NSData *data = dataPool[@(dataHandleId)]; + + IdeviceFfiError* err = misagent_install(client, data.bytes, data.length); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +- (void)misagent_removeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int clientId = [body[@"handle"] intValue]; + NSString *profileId = body[@"profile_id"]; + + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != misagent_client_free) { + replyHandler(nil, @"Invalid misagent client handle"); + return; + } + + if (![profileId isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid profile_id"); + return; + } + + IDeviceHandle *clientHandleObj = handles[@(clientId)]; + MisagentClientHandle *client = clientHandleObj.handle; + + IdeviceFfiError* err = misagent_remove(client, [profileId UTF8String]); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@YES, nil); +} + +- (void)misagent_copy_allWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int clientId = [body[@"handle"] intValue]; + + if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != misagent_client_free) { + replyHandler(nil, @"Invalid misagent client handle"); + return; + } + + IDeviceHandle *clientHandleObj = handles[@(clientId)]; + MisagentClientHandle *client = clientHandleObj.handle; + + uint8_t **profiles = NULL; + size_t *profile_lens = NULL; + size_t count = 0; + + IdeviceFfiError* err = misagent_copy_all(client, &profiles, &profile_lens, &count); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + NSMutableArray *profileHandleIds = [NSMutableArray array]; + for (size_t i = 0; i < count; i++) { + NSData *profileData = [NSData dataWithBytes:profiles[i] length:profile_lens[i]]; + free(profiles[i]); + int nsdataHandleId = [self registerNSData:profileData]; + [profileHandleIds addObject:@(nsdataHandleId)]; + } + + misagent_free_profiles(profiles, profile_lens, count); + + replyHandler(profileHandleIds, nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeNSData+LocalFile.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeNSData+LocalFile.m new file mode 100644 index 00000000..e5742be1 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeNSData+LocalFile.m @@ -0,0 +1,191 @@ +// +// IDeviceJSBridgeNSData.m +// StikJIT +// +// Created by s s on 2025/4/25. +// +@import Foundation; +#import "JSSupport.h" + +@implementation IDeviceJSBridge (NSData) + +- (void)nsdata_freeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!dataPool[@(handleId)]) { + replyHandler(nil, @"Invalid NSData handle"); + return; + } + bool ans = [self freeNSData:handleId]; + replyHandler(@(ans), nil); +} + +- (void)nsdata_readWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!dataPool[@(handleId)]) { + replyHandler(nil, @"Invalid NSData handle"); + return; + } + NSData* data = dataPool[@(handleId)]; + NSString* ans = [data base64EncodedStringWithOptions:0]; + replyHandler(ans, nil); +} + +- (void)nsdata_read_rangeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!dataPool[@(handleId)]) { + replyHandler(nil, @"Invalid NSData handle"); + return; + } + NSData* data = dataPool[@(handleId)]; + NSUInteger begin = [body[@"begin"] intValue]; + NSUInteger end = [body[@"end"] intValue]; + if(begin < 0 || end >= [data length] || begin > end) { + replyHandler(nil, @"Invalid range"); + return; + } + + [data subdataWithRange:NSMakeRange(begin, end)]; + NSString* ans = [data base64EncodedStringWithOptions:0]; + replyHandler(ans, nil); +} + +- (void)nsdata_get_sizeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!dataPool[@(handleId)]) { + replyHandler(nil, @"Invalid NSData handle"); + return; + } + NSData* data = dataPool[@(handleId)]; + NSUInteger ans = [data length]; + replyHandler(@(ans), nil); +} + +- (void)nsdata_createWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + NSString* dataStr = body[@"base64Data"]; + if (![dataStr isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid base64Data"); + return; + } + + NSData* data = [[NSData alloc] initWithBase64EncodedString:dataStr options:0]; + if (!data) { + replyHandler(nil, @"Failed to decode base64Data"); + return; + } + int ans = [self registerNSData:data]; + replyHandler(@(ans), nil); +} + +@end + + +@implementation IDeviceJSBridge (LocalFile) + +- (void)local_file_openWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler { + NSString *relativePath = body[@"path"]; + NSURL *miniToolDataURL = [[NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] + .lastObject URLByAppendingPathComponent:@"MiniToolData"] ; + BOOL isDir = false; + if(![NSFileManager.defaultManager fileExistsAtPath:miniToolDataURL.path isDirectory:&isDir] || !isDir) { + [NSFileManager.defaultManager removeItemAtURL:miniToolDataURL error:nil]; + [NSFileManager.defaultManager createDirectoryAtURL:miniToolDataURL withIntermediateDirectories:YES attributes:@{} error:nil]; + } + NSString* path = [miniToolDataURL.path stringByAppendingPathComponent:relativePath]; + NSString *mode = body[@"mode"]; + + // Prevent path traversal attacks by ensuring the resolved path stays within MiniToolData directory + NSString *resolvedPath = [path stringByStandardizingPath]; + NSString *baseDir = [miniToolDataURL.path stringByStandardizingPath]; + if (![resolvedPath hasPrefix:baseDir]) { + replyHandler(nil, @"Path traversal is not allowed."); + return; + } + + if (![path isKindOfClass:NSString.class] || ![mode isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid file path or mode"); + return; + } + + FILE *file = fopen([path UTF8String], [mode UTF8String]); + if (!file) { + replyHandler(nil, @"Failed to open file"); + return; + } + + int handleId = [self registerIdeviceHandle:(void*)file freeFunc:(void *)fclose]; + replyHandler(@(handleId), nil); +} + +- (void)local_file_closeWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int fileId = [body[@"file"] intValue]; + + if (!handles[@(fileId)] || handles[@(fileId)].freeFunc != fclose) { + replyHandler(nil, @"Invalid file handle"); + return; + } + + FILE *file = handles[@(fileId)].handle; + fclose(file); + [handles removeObjectForKey:@(fileId)]; + + replyHandler(@(YES), nil); +} + +- (void)local_file_get_sizeWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int fileId = [body[@"file"] intValue]; + + if (!handles[@(fileId)] || handles[@(fileId)].freeFunc != fclose) { + replyHandler(nil, @"Invalid file handle"); + return; + } + + FILE *file = handles[@(fileId)].handle; + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); // Reset position + replyHandler(@(size), nil); +} + +- (void)local_file_read_chunkWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int fileId = [body[@"file"] intValue]; + long offset = [body[@"offset"] longValue]; + long length = [body[@"length"] longValue]; + + if (!handles[@(fileId)] || handles[@(fileId)].freeFunc != fclose) { + replyHandler(nil, @"Invalid file handle"); + return; + } + + FILE *file = handles[@(fileId)].handle; + fseek(file, offset, SEEK_SET); + void *buffer = malloc(length); + size_t read = fread(buffer, 1, length, file); + + NSData *data = [NSData dataWithBytesNoCopy:buffer length:read freeWhenDone:YES]; + int dataHandleId = [self registerNSData:data]; + replyHandler(@(dataHandleId), nil); +} + +- (void)local_file_write_chunkWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int fileId = [body[@"file"] intValue]; + int dataId = [body[@"data"] intValue]; + long offset = [body[@"offset"] longValue]; + + if (!handles[@(fileId)] || handles[@(fileId)].freeFunc != fclose) { + replyHandler(nil, @"Invalid file handle"); + return; + } + if (!dataPool[@(dataId)]) { + replyHandler(nil, @"Invalid data handle"); + return; + } + + FILE *file = handles[@(fileId)].handle; + NSData *data = dataPool[@(dataId)]; + fseek(file, offset, SEEK_SET); + size_t written = fwrite(data.bytes, 1, data.length, file); + + replyHandler(@(written), nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeProcessControl.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeProcessControl.m new file mode 100644 index 00000000..2b1a35cf --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeProcessControl.m @@ -0,0 +1,125 @@ +// +// IDeviceJSBridgeProcessControl.m +// StikJIT +// +// Created by s s on 2025/4/25. +// +@import Foundation; +#import "JSSupport.h" +@implementation IDeviceJSBridge (ProcessControl) + +- (void)process_control_newWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"server"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != remote_server_free) { + replyHandler(nil, @"Invalid remote server handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + RemoteServerHandle* remote_server = clientHandleObj.handle; + + ProcessControlHandle *process_control = NULL; + IdeviceFfiError* err = process_control_new(remote_server, &process_control); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int handleId = [self registerIdeviceHandle:process_control freeFunc:(void*)process_control_free]; + replyHandler(@(handleId), nil); +} + +- (void)process_control_launch_appWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != process_control_free) { + replyHandler(nil, @"Invalid process control handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + ProcessControlHandle* process_control = clientHandleObj.handle; + + NSString* bundleId = body[@"bundle_id"]; + if(![bundleId isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid bundle id"); + return; + } + + NSArray* envVars = body[@"env_vars"]; + int envVarsCount = 0; + const char** envVarsCharArr = cstrArrFromNSArray(envVars, &envVarsCount); + + NSArray* arguments = body[@"arguments"]; + int argumentsCount = 0; + const char** argumentsCharArr = cstrArrFromNSArray(arguments, &argumentsCount); + + bool startSuspended = [body[@"start_suspended"] boolValue]; + bool killExisting = [body[@"kill_existing"] boolValue]; + + uint64_t pid = 0; + IdeviceFfiError* err = process_control_launch_app(process_control, [bundleId UTF8String], envVarsCharArr, envVarsCount, argumentsCharArr, argumentsCount, + startSuspended, killExisting, &pid); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + if(envVarsCharArr) { + free(envVarsCharArr); + } + + if(argumentsCharArr) { + free(argumentsCharArr); + } + + replyHandler(@(pid), nil); +} + +- (void)process_control_disable_memory_limitWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != process_control_free) { + replyHandler(nil, @"Invalid process control handle"); + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + ProcessControlHandle* process_control = clientHandleObj.handle; + + uint64_t pid = [body[@"pid"] unsignedLongLongValue]; + if(pid == 0) { + replyHandler(nil, @"Invalid pid"); + return; + } + + IdeviceFfiError* err = process_control_disable_memory_limit(process_control, pid); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@(YES), nil); +} + +- (void)process_control_kill_appWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"handle"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != process_control_free) { + replyHandler(nil, @"Invalid process control handle"); + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + ProcessControlHandle* process_control = clientHandleObj.handle; + + uint64_t pid = [body[@"pid"] unsignedLongLongValue]; + if(pid == 0) { + replyHandler(nil, @"Invalid pid"); + return; + } + + IdeviceFfiError* err = process_control_kill_app(process_control, pid); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@(YES), nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeRSD.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeRSD.m new file mode 100644 index 00000000..1da76688 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeRSD.m @@ -0,0 +1,182 @@ +// +// IDeviceJSBridgeRSD.m +// StikDebug +// +// Created by s s on 2025/12/13. +// + +@import Foundation; +#import "JSSupport.h" + +static NSDictionary *RsdServiceToDictionary(const CRsdService *service) { + if (!service) { + return @{}; + } + + NSMutableArray *features = [[NSMutableArray alloc] initWithCapacity:service->features_count]; + for (size_t i = 0; i < service->features_count; ++i) { + if (service->features[i]) { + [features addObject:@(service->features[i])]; + } + } + + NSString *name = service->name ? @(service->name) : @""; + NSString *entitlement = service->entitlement ? @(service->entitlement) : @""; + + return @{ + @"name": name, + @"entitlement": entitlement, + @"port": @(service->port), + @"uses_remote_xpc": @(service->uses_remote_xpc), + @"features_count": @(service->features_count), + @"features": features, + @"service_version": @(service->service_version) + }; +} + +@implementation IDeviceJSBridge (RSD) + +- (void)rsd_handshake_newWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int socketId = [body[@"socket"] intValue]; + if (!handles[@(socketId)] || handles[@(socketId)].freeFunc != adapter_stream_close) { + replyHandler(nil, @"Invalid socket handle"); + return; + } + IDeviceHandle *socketHandleObj = handles[@(socketId)]; + ReadWriteOpaque *socket = socketHandleObj.handle; + + RsdHandshakeHandle *handshake = NULL; + IdeviceFfiError *err = rsd_handshake_new(socket, &handshake); + [handles removeObjectForKey:@(socketId)]; + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int handleId = [self registerIdeviceHandle:handshake freeFunc:(void *)rsd_handshake_free]; + replyHandler(@(handleId), nil); +} + +- (void)rsd_get_protocol_versionWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) { + replyHandler(nil, @"Invalid RSD handshake handle"); + return; + } + RsdHandshakeHandle *handshake = handles[@(handleId)].handle; + + size_t version = 0; + IdeviceFfiError *err = rsd_get_protocol_version(handshake, &version); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@(version), nil); +} + +- (void)rsd_get_uuidWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) { + replyHandler(nil, @"Invalid RSD handshake handle"); + return; + } + RsdHandshakeHandle *handshake = handles[@(handleId)].handle; + + char *uuid = NULL; + IdeviceFfiError *err = rsd_get_uuid(handshake, &uuid); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + NSString *uuidStr = uuid ? @(uuid) : nil; + if (uuid) { + rsd_free_string(uuid); + } + + replyHandler(uuidStr, nil); +} + +- (void)rsd_get_servicesWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) { + replyHandler(nil, @"Invalid RSD handshake handle"); + return; + } + RsdHandshakeHandle *handshake = handles[@(handleId)].handle; + + CRsdServiceArray *services = NULL; + IdeviceFfiError *err = rsd_get_services(handshake, &services); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + NSMutableArray *ans = [[NSMutableArray alloc] init]; + if (services && services->services) { + for (size_t i = 0; i < services->count; ++i) { + [ans addObject:RsdServiceToDictionary(&services->services[i])]; + } + } + + if (services) { + rsd_free_services(services); + } + replyHandler(ans, nil); +} + +- (void)rsd_service_availableWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) { + replyHandler(nil, @"Invalid RSD handshake handle"); + return; + } + RsdHandshakeHandle *handshake = handles[@(handleId)].handle; + + NSString *serviceName = body[@"service_name"]; + if (![serviceName isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid service name"); + return; + } + + bool available = false; + IdeviceFfiError *err = rsd_service_available(handshake, [serviceName UTF8String], &available); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + replyHandler(@(available), nil); +} + +- (void)rsd_get_service_infoWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + int handleId = [body[@"handle"] intValue]; + if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) { + replyHandler(nil, @"Invalid RSD handshake handle"); + return; + } + RsdHandshakeHandle *handshake = handles[@(handleId)].handle; + + NSString *serviceName = body[@"service_name"]; + if (![serviceName isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid service name"); + return; + } + + CRsdService *serviceInfo = NULL; + IdeviceFfiError *err = rsd_get_service_info(handshake, [serviceName UTF8String], &serviceInfo); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + NSDictionary *ans = RsdServiceToDictionary(serviceInfo); + if (serviceInfo) { + rsd_free_service(serviceInfo); + } + + replyHandler(ans, nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeRemoteServer.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeRemoteServer.m new file mode 100644 index 00000000..eeba3bc3 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeRemoteServer.m @@ -0,0 +1,43 @@ +// +// IDeviceJSBridgeRemoteServer.m +// StikJIT +// +// Created by s s on 2025/4/25. +// +@import Foundation; +#import "JSSupport.h" + +@implementation IDeviceJSBridge (RemoteServer) + +- (void)remote_server_connect_rsdWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"adapter"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != adapter_free) { + replyHandler(nil, @"Invalid adapter handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + AdapterHandle* adapter = clientHandleObj.handle; + + int handshakeId = [body[@"handshake"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != rsd_handshake_free) { + replyHandler(nil, @"Invalid handshake handle"); + return; + } + IDeviceHandle* handshakeHandleObj = handles[@(handshakeId)]; + RsdHandshakeHandle* handshake = handshakeHandleObj.handle; + + RemoteServerHandle *remote_server = NULL; + IdeviceFfiError* err = remote_server_connect_rsd(adapter, handshake, &remote_server); + [handles removeObjectForKey:@(clientId)]; + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + int handleId = [self registerIdeviceHandle:remote_server freeFunc:(void*)remote_server_free]; + replyHandler(@(handleId), nil); +} + + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeSBServices.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeSBServices.m new file mode 100644 index 00000000..d876d971 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeSBServices.m @@ -0,0 +1,60 @@ +// +// IDeviceJSBridgeSBServices.m +// StikJIT +// +// Created by s s on 2025/4/25. +// + +@import Foundation; +#import "JSSupport.h" + +@implementation IDeviceJSBridge (SBServices) + +- (void)springboard_services_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; + + SpringBoardServicesClientHandle *sb_services = NULL; + IdeviceFfiError* err = springboard_services_connect(provider, &sb_services); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + int handleId = [self registerIdeviceHandle:sb_services freeFunc:(void*)springboard_services_free]; + replyHandler(@(handleId), nil); +} + + +- (void)springboard_services_get_iconWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + + int clientId = [body[@"client"] intValue]; + if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != springboard_services_free) { + replyHandler(nil, @"Invalid springboard services client handle"); + return; + } + IDeviceHandle* clientHandleObj = handles[@(clientId)]; + SpringBoardServicesClientHandle* client = clientHandleObj.handle; + NSString* bundleID = body[@"bundle_id"]; + if(![bundleID isKindOfClass:NSString.class]) { + replyHandler(nil, @"Invalid bundle id"); + return; + } + + void *pngData = NULL; + size_t data_len = 0; + IdeviceFfiError* err = springboard_services_get_icon(client, [bundleID UTF8String], &pngData, &data_len); + if (err) { + replyHandler(nil, [self errFreeFromIdeviceFfiError:err]); + return; + } + + if(data_len == 0) { + replyHandler(@(-1), nil); + } + + NSData* data = [NSData dataWithBytes:pngData length:data_len]; + free(pngData); + int handleId = [self registerNSData:data]; + replyHandler(@(handleId), nil); +} + +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/JSSupport.h b/StikJIT/MiniToolSupport/JSSupport/JSSupport.h new file mode 100644 index 00000000..d7a84de9 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/JSSupport.h @@ -0,0 +1,39 @@ +// +// JSSupport.h +// StikJIT +// +// Created by s s on 2025/4/24. +// +#import "../../idevice/JITEnableContext.h" +#import "../../idevice/idevice.h" +@import JavaScriptCore; + +@interface NSInvocation(MCUtilities) +-(void)invokeOnMainThreadWaitUntilDone:(BOOL)wait; ++(NSInvocation*)invocationWithTarget:(id)target + selector:(SEL)aSelector + retainArguments:(BOOL)retainArguments, ...; +@end + +@interface IDeviceHandle : NSObject +@property void* handle; +@property void* freeFunc; +@end + +@interface IDeviceJSBridge : NSObject { + NSMutableDictionary* handles; + NSMutableDictionary* dataPool; +} +- (void)didReceiveScriptMessage:(NSDictionary *)message resolve:(JSValue*)resolveFunc reject:(JSValue*)rejectFunc; +- (void)cleanUp; +- (NSString*)errFreeFromIdeviceFfiError:(IdeviceFfiError*)err; +- (int)registerIdeviceHandle:(void*)handle freeFunc:(void*)freeFunc; +- (BOOL)freeIdeviceHandle:(int)handleId; +- (int)registerNSData:(NSData*)data; +- (bool)freeNSData:(int)handleId; +@end + + +NSDictionary *dictionaryFromPlistData(NSData *plistData, NSError **error); +NSData *plistDataFromDictionary(NSDictionary *dictionary, NSError **error); +const char** cstrArrFromNSArray(NSArray* arr, int* validCount); diff --git a/StikJIT/MiniToolSupport/JSSupport/NSInvocation.m b/StikJIT/MiniToolSupport/JSSupport/NSInvocation.m new file mode 100644 index 00000000..981b9fa2 --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/NSInvocation.m @@ -0,0 +1,44 @@ +// +// NSInvocation.m +// StikJIT +// Source: http://blog.jayway.com/2010/03/30/performing-any-selector-on-the-main-thread/ +// Created by s s on 2025/4/24. +// +@import Foundation; + +@implementation NSInvocation(MCUtilities) +-(void)invokeOnMainThreadWaitUntilDone:(BOOL)wait +{ + [self performSelectorOnMainThread:@selector(invoke) + withObject:nil + waitUntilDone:wait]; +} ++(NSInvocation*)invocationWithTarget:(id)target + selector:(SEL)aSelector + retainArguments:(BOOL)retainArguments, ... +{ + va_list ap; + va_start(ap, retainArguments); + char* args = (char*)ap; + NSMethodSignature* signature = [target methodSignatureForSelector:aSelector]; + NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:signature]; + if (retainArguments) { + [invocation retainArguments]; + } + [invocation setTarget:target]; + [invocation setSelector:aSelector]; + for (int index = 2; index < [signature numberOfArguments]; index++) { + const char *type = [signature getArgumentTypeAtIndex:index]; + NSUInteger size, align; + NSGetSizeAndAlignment(type, &size, &align); + NSUInteger mod = (NSUInteger)args % align; + if (mod != 0) { + args += (align - mod); + } + [invocation setArgument:args atIndex:index]; + args += size; + } + va_end(ap); + return invocation; +} +@end diff --git a/StikJIT/MiniToolSupport/JSSupport/idevice.js b/StikJIT/MiniToolSupport/JSSupport/idevice.js new file mode 100644 index 00000000..2a19563b --- /dev/null +++ b/StikJIT/MiniToolSupport/JSSupport/idevice.js @@ -0,0 +1,455 @@ +// +// idevice.js +// StikJIT +// +// Created by s s on 2025/4/24. +// + +async function core_device_proxy_connect() { + return await __postIdeviceMessage({ + "command": "core_device_proxy_connect" + }); +} + +async function core_device_proxy_get_server_rsd_port(core_device_handle) { + return await __postIdeviceMessage({ + "command": "core_device_proxy_get_server_rsd_port", + "handle": core_device_handle + }); +} + +async function core_device_proxy_create_tcp_adapter(core_device_handle) { + return await __postIdeviceMessage({ + "command": "core_device_proxy_create_tcp_adapter", + "handle": core_device_handle + }); +} + +async function adapter_connect(adapter, port) { + return await __postIdeviceMessage({ + "command": "adapter_connect", + "adapter": adapter, + "port": port + }); +} + +async function rsd_handshake_new(socket) { + return await __postIdeviceMessage({ + "command": "rsd_handshake_new", + "socket": socket + }); +} + +async function rsd_get_protocol_version(handle) { + return await __postIdeviceMessage({ + "command": "rsd_get_protocol_version", + "handle": handle + }); +} + +async function rsd_get_uuid(handle) { + return await __postIdeviceMessage({ + "command": "rsd_get_uuid", + "handle": handle + }); +} + +async function rsd_get_services(handle) { + return await __postIdeviceMessage({ + "command": "rsd_get_services", + "handle": handle + }); +} + +async function rsd_service_available(handle, service_name) { + return await __postIdeviceMessage({ + "command": "rsd_service_available", + "handle": handle, + "service_name": service_name + }); +} + +async function rsd_get_service_info(handle, service_name) { + return await __postIdeviceMessage({ + "command": "rsd_get_service_info", + "handle": handle, + "service_name": service_name + }); +} + +async function remote_server_connect_rsd(adapter, handshake) { + return await __postIdeviceMessage({ + "command": "remote_server_connect_rsd", + "adapter": adapter, + "handshake": handshake + }); +} + +async function process_control_new(server) { + return await __postIdeviceMessage({ + "command": "process_control_new", + "server": server + }); +} + +async function process_control_launch_app(handle, bundle_id, env_vars, arguments, start_suspended, kill_existing) { + return await __postIdeviceMessage({ + "command": "process_control_launch_app", + "handle": handle, + "bundle_id": bundle_id, + "env_vars": env_vars, + "arguments": arguments, + "start_suspended": start_suspended, + "kill_existing": kill_existing + }); +} + +async function process_control_disable_memory_limit(handle, pid) { + return await __postIdeviceMessage({ + "command": "process_control_disable_memory_limit", + "handle": handle, + "pid": pid + }); +} + +async function process_control_kill_app(handle, pid) { + return await __postIdeviceMessage({ + "command": "process_control_kill_app", + "handle": handle, + "pid": pid + }); +} + +async function debug_proxy_connect_rsd(adapter, handshake) { + return await __postIdeviceMessage({ + "command": "debug_proxy_connect_rsd", + "adapter": adapter, + "handshake": handshake + }); +} + +async function debug_proxy_send_command(handle, command) { + return await __postIdeviceMessage({ + "command": "debug_proxy_send_command", + "handle": handle, + "debug_command": command + }); +} + +async function springboard_services_connect() { + return await __postIdeviceMessage({ + "command": "springboard_services_connect" + }); +} + +async function springboard_services_get_icon(client, bundle_id) { + return await __postIdeviceMessage({ + "command": "springboard_services_get_icon", + "client": client, + "bundle_id": bundle_id + }); +} + +async function nsdata_read(handle) { + return await __postIdeviceMessage({ + "command": "nsdata_read", + "handle": handle, + }); +} + +async function nsdata_read_range(handle, begin, end) { + return await __postIdeviceMessage({ + "command": "nsdata_read_range", + "handle": handle, + "begin": begin, + "end": end + }); +} + +async function afc_client_connect() { + return await __postIdeviceMessage({ + "command": "afc_client_connect" + }); +} + +async function afc_list_directory(handle, path) { + return await __postIdeviceMessage({ + "command": "afc_list_directory", + "handle": handle, + "path": path + }); +} + +async function afc_make_directory(handle, path) { + return await __postIdeviceMessage({ + "command": "afc_make_directory", + "handle": handle, + "path": path + }); +} + +async function afc_remove_path(handle, path) { + return await __postIdeviceMessage({ + "command": "afc_remove_path", + "handle": handle, + "path": path + }); +} + +async function afc_remove_path_and_contents(handle, path) { + return await __postIdeviceMessage({ + "command": "afc_remove_path_and_contents", + "handle": handle, + "path": path + }); +} + +async function afc_rename_path(handle, source, target) { + return await __postIdeviceMessage({ + "command": "afc_rename_path", + "handle": handle, + "source": source, + "target": target + }); +} + +async function afc_get_file_info(handle, path) { + return await __postIdeviceMessage({ + "command": "afc_get_file_info", + "handle": handle, + "path": path + }); +} + +async function afc_get_device_info(handle) { + return await __postIdeviceMessage({ + "command": "afc_get_device_info", + "handle": handle, + }); +} + +async function afc_file_open(handle, path, mode) { + return await __postIdeviceMessage({ + "command": "afc_file_open", + "handle": handle, + "path": path, + "mode": mode + }); +} + +async function afc_file_close(handle) { + return await __postIdeviceMessage({ + "command": "afc_file_close", + "handle": handle + }); +} + +async function afc_file_read(handle) { + return await __postIdeviceMessage({ + "command": "afc_file_read", + "handle": handle + }); +} + +async function afc_file_write(handle, data_handle) { + return await __postIdeviceMessage({ + "command": "afc_file_write", + "handle": handle, + "data_handle": data_handle + }); +} + +async function installation_proxy_connect() { + return await __postIdeviceMessage({ + "command": "installation_proxy_connect" + }); +} + +async function installation_proxy_get_apps(client, applicationType, bundleIdentifiers) { + return await __postIdeviceMessage({ + "command": "installation_proxy_get_apps", + "client": client, + "application_type": applicationType, + "bundle_identifiers": bundleIdentifiers + }); +} + +async function installation_proxy_browse(client, options) { + return await __postIdeviceMessage({ + "command": "installation_proxy_browse", + "client": client, + "options": options + }); +} + +async function installation_proxy_install(client, package_path, options, callback) { + return await __postIdeviceMessage({ + "command": "installation_proxy_install", + "client": client, + "package_path": package_path, + "options": options, + "callback_id": -1 + }); +} + +async function installation_proxy_upgrade(client, package_path, options, callback) { + return await __postIdeviceMessage({ + "command": "installation_proxy_install", + "client": client, + "package_path": package_path, + "options": options, + "callback_id": -1 + }); +} + +async function installation_proxy_uninstall(client, bundle_id, options, callback) { + return await __postIdeviceMessage({ + "command": "installation_proxy_install", + "client": client, + "bundle_id": bundle_id, + "options": options, + "callback": callback + }); +} + +async function amfi_connect() { + return await __postIdeviceMessage({ + "command": "amfi_connect" + }); +} + +async function amfi_reveal_developer_mode_option_in_ui(handle) { + return await __postIdeviceMessage({ + "command": "amfi_reveal_developer_mode_option_in_ui", + "handle": handle + }); +} + +async function amfi_enable_developer_mode(handle) { + return await __postIdeviceMessage({ + "command": "amfi_enable_developer_mode", + "handle": handle + }); +} + +async function amfi_accept_developer_mode(handle) { + return await __postIdeviceMessage({ + "command": "amfi_accept_developer_mode", + "handle": handle + }); +} + +async function misagent_connect() { + return await __postIdeviceMessage({ + "command": "misagent_connect" + }); +} + +async function misagent_install(handle, data_handle) { + return await __postIdeviceMessage({ + "command": "misagent_install", + "handle": handle, + "data_handle": data_handle + }); +} + +async function misagent_remove(handle, profile_id) { + return await __postIdeviceMessage({ + "command": "misagent_remove", + "handle": handle, + "profile_id": profile_id + }); +} + +async function misagent_copy_all(handle) { + return await __postIdeviceMessage({ + "command": "misagent_copy_all", + "handle": handle, + }); +} + +async function location_simulation_new(server) { + return await __postIdeviceMessage({ + "command": "location_simulation_new", + "server": server + }); +} + +async function location_simulation_clear(handle) { + return await __postIdeviceMessage({ + "command": "location_simulation_clear", + "handle": handle + }); +} + +async function location_simulation_set(handle, latitude, longitude) { + return await __postIdeviceMessage({ + "command": "location_simulation_set", + "handle": handle, + "latitude": latitude, + "longitude": longitude, + }); +} + +// data related, not idevice + +async function nsdata_get_size(handle) { + return await __postIdeviceMessage({ + "command": "nsdata_get_size", + "handle": handle, + }); +} + +async function nsdata_free(handle) { + return await __postIdeviceMessage({ + "command": "nsdata_free", + "handle": handle, + }); +} + +async function nsdata_create(base64Data) { + return await __postIdeviceMessage({ + "command": "nsdata_create", + "base64Data": base64Data, + }); +} + +async function local_file_open(path, mode) { + return await __postIdeviceMessage({ + "command": "local_file_open", + "path": path, + "mode": mode + }); +} + +async function local_file_close(file) { + return await __postIdeviceMessage({ + "command": "local_file_close", + "file": file, + }); +} + +async function local_file_get_size(file) { + return await __postIdeviceMessage({ + "command": "local_file_get_size", + "file": file, + }); +} + +async function local_file_read_chunk(file, offset, length) { + return await __postIdeviceMessage({ + "command": "local_file_read_chunk", + "file": file, + "offset": offset, + "length": length + }); +} + +async function local_file_write_chunk(file, data, offset) { + return await __postIdeviceMessage({ + "command": "local_file_write_chunk", + "file": file, + "offset": offset, + "data": data + }); +} diff --git a/StikJIT/MiniToolSupport/MiniToolListView.swift b/StikJIT/MiniToolSupport/MiniToolListView.swift index ae550013..2235cf60 100644 --- a/StikJIT/MiniToolSupport/MiniToolListView.swift +++ b/StikJIT/MiniToolSupport/MiniToolListView.swift @@ -147,27 +147,28 @@ struct MiniToolListView: View { .lineLimit(1) } } - } - .buttonStyle(.plain) + + Spacer() - Spacer() + NavigationLink { + MiniToolEditorView(tool: tool) + } label: { + Image(systemName: "pencil") + .foregroundColor(.primary) + } + .buttonStyle(.borderless) - NavigationLink { - MiniToolEditorView(tool: tool) - } label: { - Image(systemName: "pencil") - .foregroundColor(.primary) + Button(role: .destructive) { + pendingDelete = tool + showDeleteConfirmation = true + } label: { + Image(systemName: "trash") + .foregroundColor(.red) + } + .buttonStyle(.borderless) } - .buttonStyle(.borderless) + .buttonStyle(.plain) - Button(role: .destructive) { - pendingDelete = tool - showDeleteConfirmation = true - } label: { - Image(systemName: "trash") - .foregroundColor(.red) - } - .buttonStyle(.borderless) } .padding(20) .background(glassyBackground) diff --git a/StikJIT/MiniToolSupport/MiniToolModels.swift b/StikJIT/MiniToolSupport/MiniToolModels.swift index 51a77014..f025e080 100644 --- a/StikJIT/MiniToolSupport/MiniToolModels.swift +++ b/StikJIT/MiniToolSupport/MiniToolModels.swift @@ -12,6 +12,13 @@ struct MiniToolBundle: Identifiable, Hashable { let fm = FileManager.default return fm.fileExists(atPath: indexURL.path) && fm.fileExists(atPath: backgroundURL.path) } + + func getHostName() -> String { + let d = name.data(using: .utf8) + let b = d!.base64EncodedString().replacingOccurrences(of:"+", with:"").replacingOccurrences(of:"/", with:"").replacingOccurrences(of:"=", with:"") + + return "\(b).stiktool" + } } final class MiniToolStore: ObservableObject { @@ -78,6 +85,21 @@ final class MiniToolStore: ObservableObject { } return dir } + + static func toolsDataDirectory() -> URL { + let dir = URL.documentsDirectory.appendingPathComponent("MiniToolData", isDirectory: true) + var isDir: ObjCBool = false + let fm = FileManager.default + if fm.fileExists(atPath: dir.path, isDirectory: &isDir) { + if !isDir.boolValue { + try? fm.removeItem(at: dir) + } + } + if !fm.fileExists(atPath: dir.path) { + try? fm.createDirectory(at: dir, withIntermediateDirectories: true) + } + return dir + } private func sanitizedToolName(_ raw: String) -> String { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/StikJIT/MiniToolSupport/MiniToolRunnerView.swift b/StikJIT/MiniToolSupport/MiniToolRunnerView.swift index ce95c727..b0353682 100644 --- a/StikJIT/MiniToolSupport/MiniToolRunnerView.swift +++ b/StikJIT/MiniToolSupport/MiniToolRunnerView.swift @@ -5,6 +5,7 @@ struct MiniToolRunnerView: View { let tool: MiniToolBundle @StateObject private var runtime: MiniToolRuntime @State private var showLogs = false + @State private var initiated = false @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue @Environment(\.themeExpansionManager) private var themeExpansion @@ -40,7 +41,12 @@ struct MiniToolRunnerView: View { } .navigationTitle(tool.name) .navigationBarTitleDisplayMode(.inline) - .onAppear { runtime.start() } + .onAppear { + if !initiated { + runtime.start() + initiated = true + } + } .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { @@ -95,7 +101,7 @@ private struct MiniToolWebContainer: UIViewRepresentable { @ObservedObject var runtime: MiniToolRuntime func makeUIView(context: Context) -> WKWebView { - runtime.webView + runtime.webView! } func updateUIView(_ uiView: WKWebView, context: Context) { diff --git a/StikJIT/MiniToolSupport/MiniToolRuntime.swift b/StikJIT/MiniToolSupport/MiniToolRuntime.swift index 4c99c103..67296765 100644 --- a/StikJIT/MiniToolSupport/MiniToolRuntime.swift +++ b/StikJIT/MiniToolSupport/MiniToolRuntime.swift @@ -9,36 +9,34 @@ final class MiniToolRuntime: NSObject, ObservableObject { @Published var logs: [String] = [] @Published var isReady: Bool = false - var webView: WKWebView + var webView: WKWebView? private var context: JSContext? private var appXHRTasks: [String: URLSessionDataTask] = [:] private let messageHandlerName = "miniToolBridge" + + private var ideviceJSBridge : IDeviceJSBridge? = IDeviceJSBridge() init(tool: MiniToolBundle) { self.tool = tool + super.init() let configuration = WKWebViewConfiguration() -// configuration.setValue(true, forKey: "allowFileAccessFromFileURLs") // let the webview fetch other bundle assets let controller = WKUserContentController() controller.addUserScript(WKUserScript(source: MiniToolRuntime.frontendBridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: true)) configuration.userContentController = controller - - webView = WKWebView(frame: .zero, configuration: configuration) - - - super.init() - configuration.setURLSchemeHandler(self, forURLScheme: "app") + let leakAvoider = LeakAvoider(delegate: self) + configuration.setURLSchemeHandler(leakAvoider, forURLScheme: "app") webView = WKWebView(frame: .zero, configuration: configuration) - controller.add(self, name: messageHandlerName) - webView.navigationDelegate = self - webView.isInspectable = true + controller.add(leakAvoider, name: messageHandlerName) + webView!.navigationDelegate = self + webView!.isInspectable = true } deinit { - webView.configuration.userContentController.removeScriptMessageHandler(forName: messageHandlerName) + webView!.configuration.userContentController.removeScriptMessageHandler(forName: messageHandlerName) } func start() { @@ -61,8 +59,7 @@ final class MiniToolRuntime: NSObject, ObservableObject { return } isReady = false -// webView.loadFileURL(url, allowingReadAccessTo: tool.url) - webView.load(URLRequest(url: URL(string: "app://localhost")!)) + webView!.load(URLRequest(url: URL(string: "app://\(tool.getHostName())")!)) } private func loadBackground() { @@ -82,10 +79,26 @@ final class MiniToolRuntime: NSObject, ObservableObject { } + let ideviceFunction: @convention(block) (Any?) -> JSValue = { [weak self] msg in + return JSValue.init(newPromiseIn: self?.context!) { resolve, reject in + if let msg = msg as? [String: Any] { + if let bridge = self?.ideviceJSBridge { + bridge.didReceiveScriptMessage(msg, resolve: resolve, reject: reject); + } else { + resolve?.call(withArguments: ["Current Runtime is Terminated."]) + } + } + } + } context?.setObject(sendToFrontend, forKeyedSubscript: "__miniToolPostMessage" as NSString) context?.setObject(logFunction, forKeyedSubscript: "__miniToolLog" as NSString) + context?.setObject(ideviceFunction, forKeyedSubscript: "__postIdeviceMessage" as NSString) context?.evaluateScript(MiniToolRuntime.backgroundBridgeScript) + if let ideviceJSURL = Bundle.main.url(forResource: "idevice", withExtension: "js"), + let ideviceJSText = try? String(contentsOf: ideviceJSURL, encoding: .utf8) { + context?.evaluateScript(ideviceJSText) + } do { let script = try String(contentsOf: tool.backgroundURL) @@ -111,7 +124,7 @@ final class MiniToolRuntime: NSObject, ObservableObject { } DispatchQueue.main.async { let script = "window.miniTool && window.miniTool.__receive(\(json))" - self.webView.evaluateJavaScript(script) { _, error in + self.webView!.evaluateJavaScript(script) { _, error in if let error { self.appendLog("Frontend dispatch error: \(error.localizedDescription)") } @@ -132,6 +145,11 @@ extension MiniToolRuntime : WKURLSchemeHandler { start urlSchemeTask: WKURLSchemeTask ) { guard let url = urlSchemeTask.request.url else { return } + + guard let host = url.host(), host == tool.getHostName() else { + urlSchemeTask.didFailWithError(NSError(domain: "Invalid Host Name", code: 404)) + return + } let path = url.path.isEmpty ? "index.html" : url.path @@ -192,6 +210,18 @@ extension MiniToolRuntime: WKNavigationDelegate { deliverToBackground(["type": "ui-ready", "tool": tool.name]) deliverToFrontend(["type": "ready", "tool": tool.name]) } + + func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) { + download.delegate = self + } + +} + +extension MiniToolRuntime: WKDownloadDelegate { + + func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? { + return MiniToolStore.toolsDataDirectory().appending(path: suggestedFilename, directoryHint: .notDirectory) + } } // MARK: - Scripts & Encoding @@ -516,7 +546,6 @@ private extension MiniToolRuntime { DispatchQueue.main.async { self.appXHRTasks[id] = nil self.deliverAppXHRResponse(responsePayload) - print("RESPONSE: \(responsePayload)") } } @@ -532,7 +561,7 @@ private extension MiniToolRuntime { DispatchQueue.main.async { let script = "window.XMLHttpRequest.__receive(\(json))" - self.webView.evaluateJavaScript(script) { _, error in + self.webView!.evaluateJavaScript(script) { _, error in if let error { self.appendLog("AppXHR deliver error: \(error.localizedDescription)") } @@ -540,3 +569,30 @@ private extension MiniToolRuntime { } } } + +class LeakAvoider : NSObject, WKURLSchemeHandler, WKScriptMessageHandler, WKDownloadDelegate { + func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? { + return await self.delegate?.download(download, decideDestinationUsing: response, suggestedFilename: suggestedFilename) + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + self.delegate?.userContentController(userContentController, didReceive: message) + } + + func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + self.delegate?.webView(webView, start: urlSchemeTask) + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + self.delegate?.webView(webView, stop: urlSchemeTask) + } + + + + weak var delegate : (WKURLSchemeHandler & WKScriptMessageHandler & WKDownloadDelegate)? + init (delegate:WKURLSchemeHandler & WKScriptMessageHandler & WKDownloadDelegate) { + self.delegate = delegate + super.init() + } + +} diff --git a/StikJIT/StikJIT-Bridging-Header.h b/StikJIT/StikJIT-Bridging-Header.h index c4b0c21b..c720e009 100644 --- a/StikJIT/StikJIT-Bridging-Header.h +++ b/StikJIT/StikJIT-Bridging-Header.h @@ -11,3 +11,4 @@ #include "idevice/ls.h" #include "idevice/profiles.h" #include "idevice/something.h" +#include "MiniToolSupport/JSSupport/JSSupport.h" diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index fa442d12..0936cdde 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -749,6 +749,7 @@ func isPairing() -> Bool { } return false } + idevice_pairing_file_free(pairingFile) return true } diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 06e059dc..d895fef9 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -89,8 +89,8 @@ struct SettingsView: View { TabOption(id: "home", title: "Home", detail: "Dashboard overview", icon: "house", isBeta: false), TabOption(id: "console", title: "Console", detail: "Live device logs", icon: "terminal", isBeta: false), TabOption(id: "scripts", title: "Scripts", detail: "Manage automation scripts", icon: "scroll", isBeta: false), - TabOption(id: "tools", title: "Mini Tools", detail: "Import and run stiktool bundles", icon: "shippingbox.fill", isBeta: false), - TabOption(id: "profiles", title: "Profiles", detail: "Install/remove profiles", icon: "magazine.fill", isBeta: false), + TabOption(id: "tools", title: "Mini Tools", detail: "Import and run stiktool bundles", icon: "shippingbox", isBeta: false), + TabOption(id: "profiles", title: "Profiles", detail: "Install/remove profiles", icon: "magazine", isBeta: false), TabOption(id: "deviceinfo", title: "Device Info", detail: "View detailed device metadata", icon: "iphone.and.arrow.forward", isBeta: false) ] if FeatureFlags.showBetaTabs { diff --git a/StikJIT/idevice/JITEnableContext.h b/StikJIT/idevice/JITEnableContext.h index d56b7bc6..949cb301 100644 --- a/StikJIT/idevice/JITEnableContext.h +++ b/StikJIT/idevice/JITEnableContext.h @@ -34,7 +34,7 @@ typedef void (^SyslogErrorHandler)(NSError *error); } @property (class, readonly)JITEnableContext* shared; - (IdevicePairingFile*)getPairingFileWithError:(NSError**)error; - +- (IdeviceProviderHandle*)getTcpProviderHandle; - (BOOL)ensureHeartbeatWithError:(NSError**)err; - (BOOL)startHeartbeat:(NSError**)err; diff --git a/StikJIT/idevice/JITEnableContext.m b/StikJIT/idevice/JITEnableContext.m index 927008f7..292fb6c2 100644 --- a/StikJIT/idevice/JITEnableContext.m +++ b/StikJIT/idevice/JITEnableContext.m @@ -114,6 +114,10 @@ - (IdevicePairingFile*)getPairingFileWithError:(NSError**)error { return pairingFile; } +- (IdeviceProviderHandle*)getTcpProviderHandle { + return provider; +} + // only block until first heartbeat is completed or failed. - (BOOL)startHeartbeat:(NSError**)err { os_unfair_lock_lock(&heartbeatLock); From 9d8c92b9b35baad16e89be5f49da47c8c66d1119 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sun, 14 Dec 2025 15:05:05 +0800 Subject: [PATCH 8/9] Fix JITEnableContext singleton initializer race condition --- .../JSSupport/IDeviceJSBridgeAFC.m | 7 +++ .../JSSupport/IDeviceJSBridgeAMFI.m | 7 +++ .../JSSupport/IDeviceJSBridgeCoreDevice.m | 7 +++ .../IDeviceJSBridgeInstallationProxy.m | 11 +++- .../JSSupport/IDeviceJSBridgeMisagent.m | 7 +++ .../JSSupport/IDeviceJSBridgeSBServices.m | 7 +++ StikJIT/MiniToolSupport/JSSupport/idevice.js | 4 +- StikJIT/Views/HomeView.swift | 53 +++++++++++-------- StikJIT/idevice/JITEnableContext.m | 9 ++-- 9 files changed, 83 insertions(+), 29 deletions(-) diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAFC.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAFC.m index 07774910..3b331182 100644 --- a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAFC.m +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAFC.m @@ -11,6 +11,13 @@ @implementation IDeviceJSBridge (AFC) - (void)afc_client_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + NSError* heartbeatErr = nil; + [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr]; + if(heartbeatErr) { + replyHandler(nil, heartbeatErr.localizedDescription); + return; + } + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; AfcClientHandle* client = NULL; diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAMFI.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAMFI.m index 551b51be..3d3c1073 100644 --- a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAMFI.m +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAMFI.m @@ -12,6 +12,13 @@ @implementation IDeviceJSBridge (AMFI) - (void)amfi_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + NSError* heartbeatErr = nil; + [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr]; + if(heartbeatErr) { + replyHandler(nil, heartbeatErr.localizedDescription); + return; + } + IdeviceProviderHandle *provider = [JITEnableContext.shared getTcpProviderHandle]; AmfiClientHandle *client = NULL; diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeCoreDevice.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeCoreDevice.m index 10998c94..97c2709e 100644 --- a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeCoreDevice.m +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeCoreDevice.m @@ -9,6 +9,13 @@ @implementation IDeviceJSBridge (CoreDevice) - (void)core_device_proxy_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + NSError* heartbeatErr = nil; + [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr]; + if(heartbeatErr) { + replyHandler(nil, heartbeatErr.localizedDescription); + return; + } + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; CoreDeviceProxyHandle *core_device = NULL; diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m index 0ffcb356..aff0f8b2 100644 --- a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m @@ -19,6 +19,13 @@ void installationProxyCallback(uint64_t progress, struct InstallationProxyCallba @implementation IDeviceJSBridge (InstallationProxy) - (void)installation_proxy_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + NSError* heartbeatErr = nil; + [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr]; + if(heartbeatErr) { + replyHandler(nil, heartbeatErr.localizedDescription); + return; + } + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; InstallationProxyClientHandle *client = NULL; @@ -109,7 +116,9 @@ - (void)installation_proxy_installWithBody:(NSDictionary *)body replyHandler:(no plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0); } - JSValue* callback = body[@"callback"]; +// JSValue* callback = body[@"callback"]; + // TODO implement real callback + JSValue* callback = nil; IdeviceFfiError* err = 0; if(!callback) { err = installation_proxy_install(client, [packagePath UTF8String], optionsPlist); diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeMisagent.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeMisagent.m index f682dbd6..bc94b951 100644 --- a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeMisagent.m +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeMisagent.m @@ -10,6 +10,13 @@ @implementation IDeviceJSBridge (Misagent) - (void)misagent_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + NSError* heartbeatErr = nil; + [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr]; + if(heartbeatErr) { + replyHandler(nil, heartbeatErr.localizedDescription); + return; + } + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; MisagentClientHandle* client = NULL; diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeSBServices.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeSBServices.m index d876d971..4896b39e 100644 --- a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeSBServices.m +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeSBServices.m @@ -11,6 +11,13 @@ @implementation IDeviceJSBridge (SBServices) - (void)springboard_services_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler { + NSError* heartbeatErr = nil; + [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr]; + if(heartbeatErr) { + replyHandler(nil, heartbeatErr.localizedDescription); + return; + } + IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle]; SpringBoardServicesClientHandle *sb_services = NULL; diff --git a/StikJIT/MiniToolSupport/JSSupport/idevice.js b/StikJIT/MiniToolSupport/JSSupport/idevice.js index 2a19563b..60a07cde 100644 --- a/StikJIT/MiniToolSupport/JSSupport/idevice.js +++ b/StikJIT/MiniToolSupport/JSSupport/idevice.js @@ -288,7 +288,7 @@ async function installation_proxy_install(client, package_path, options, callbac "client": client, "package_path": package_path, "options": options, - "callback_id": -1 + "callback": callback }); } @@ -298,7 +298,7 @@ async function installation_proxy_upgrade(client, package_path, options, callbac "client": client, "package_path": package_path, "options": options, - "callback_id": -1 + "callback": callback }); } diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 6b494d87..316f2a29 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -466,14 +466,22 @@ struct HomeView: View { @ViewBuilder private var connectionStatusBadge: some View { - if isConnectionCheckRunning { - statusBadge(icon: "clock.arrow.circlepath", text: "Checking…", color: .orange) - } else if allStatusIndicatorsGreen { - statusBadge(icon: "checkmark.circle.fill", text: "Ready", color: .green) - } else if connectionHasError { - statusBadge(icon: "exclamationmark.triangle.fill", text: "Needs attention", color: .yellow) - } else { - statusBadge(icon: "circle.lefthalf.filled", text: "Not ready", color: .yellow) + HStack { + Button { + refreshStatusTapped() + } label: { + iconOnlyStatusBadge(icon: "arrow.clockwise", text: "", color: .blue) + }.disabled(isConnectionCheckRunning) + + if isConnectionCheckRunning { + statusBadge(icon: "clock.arrow.circlepath", text: "Checking…", color: .orange) + } else if allStatusIndicatorsGreen { + statusBadge(icon: "checkmark.circle.fill", text: "Ready", color: .green) + } else if connectionHasError { + statusBadge(icon: "exclamationmark.triangle.fill", text: "Needs attention", color: .yellow) + } else { + statusBadge(icon: "circle.lefthalf.filled", text: "Not ready", color: .yellow) + } } } @@ -489,6 +497,19 @@ struct HomeView: View { .foregroundStyle(color) } + private func iconOnlyStatusBadge(icon: String, text: String, color: Color) -> some View { + Label(text, systemImage: icon) + .font(.footnote.weight(.semibold)) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background( + Circle() + .fill(color.opacity(0.15)) + ) + .foregroundStyle(color) + .labelStyle(.iconOnly) + } + private var connectionHasError: Bool { if case .failure = connectionCheckState { return true } if case .timeout = connectionCheckState { return true } @@ -502,7 +523,7 @@ struct HomeView: View { } private var statusLightsRow: some View { - HStack(spacing: 12) { + HStack(alignment: .center) { ForEach(statusLights) { light in if let action = light.action { Button(action: action) { @@ -514,7 +535,7 @@ struct HomeView: View { StatusLightView(light: light) } } - } + }.frame(maxWidth: .infinity) } private var statusLights: [StatusLightData] { @@ -539,18 +560,6 @@ struct HomeView: View { icon: "waveform.path.ecg", status: heartbeatIndicatorStatus, detail: heartbeatDetailText - ), - StatusLightData( - type: .refresh, - title: "Refresh", - icon: "arrow.clockwise", - status: refreshIndicatorStatus, - detail: "", - action: refreshStatusTapped, - isEnabled: !isConnectionCheckRunning, - indicatorIconName: "arrow.clockwise", - indicatorColor: .blue, - tintOverride: .blue ) ] } diff --git a/StikJIT/idevice/JITEnableContext.m b/StikJIT/idevice/JITEnableContext.m index 292fb6c2..4f71bfb8 100644 --- a/StikJIT/idevice/JITEnableContext.m +++ b/StikJIT/idevice/JITEnableContext.m @@ -19,7 +19,7 @@ #include #import -JITEnableContext* sharedJITContext = nil; +static JITEnableContext* sharedJITContext = nil; @implementation JITEnableContext { int heartbeatToken; @@ -31,9 +31,10 @@ @implementation JITEnableContext { } + (instancetype)shared { - if (!sharedJITContext) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ sharedJITContext = [[JITEnableContext alloc] init]; - } + }); return sharedJITContext; } @@ -177,7 +178,7 @@ - (BOOL)startHeartbeat:(NSError**)err { globalHeartbeatToken,Ccompletion ); }); - // allow 2 seconds for heartbeat, otherwise we declare timeout + // allow 5 seconds for heartbeat, otherwise we declare timeout intptr_t isTimeout = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(5 * NSEC_PER_SEC))); if(isTimeout) { Ccompletion(-1, "Heartbeat failed to complete in reasonable time."); From 4b27d431dbf39b8d54a4c78dffe8ec50800cf831 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sun, 14 Dec 2025 16:34:22 +0800 Subject: [PATCH 9/9] Fix instprox callback --- .../JSSupport/IDeviceJSBridge.m | 4 +- .../IDeviceJSBridgeInstallationProxy.m | 31 +++--- StikJIT/MiniToolSupport/JSSupport/JSSupport.h | 2 + StikJIT/MiniToolSupport/JSSupport/idevice.js | 102 ++++++++++++++---- StikJIT/MiniToolSupport/MiniToolRuntime.swift | 3 +- 5 files changed, 102 insertions(+), 40 deletions(-) diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridge.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridge.m index 1ece2502..14b527bc 100644 --- a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridge.m +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridge.m @@ -78,12 +78,12 @@ @implementation IDeviceJSBridge { int maxDataId; } -- (instancetype)init { +- (instancetype)initWithContext:(JSContext*)context { maxHandleId = 0; maxDataId = 0; handles = [[NSMutableDictionary alloc] init]; dataPool = [[NSMutableDictionary alloc] init]; - + self->context = context; return self; } diff --git a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m index aff0f8b2..59706cad 100644 --- a/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m +++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m @@ -9,11 +9,12 @@ #import "JSSupport.h" struct InstallationProxyCallbackContext { - JSValue* callback; + int callbackId; + JSContext* context; }; void installationProxyCallback(uint64_t progress, struct InstallationProxyCallbackContext* context) { - [context->callback callWithArguments:@[@(progress)]]; + [context->context evaluateScript:[NSString stringWithFormat:@"handle_installation_proxy_js_callback(%d, %llu)", context->callbackId, progress]]; } @implementation IDeviceJSBridge (InstallationProxy) @@ -116,16 +117,14 @@ - (void)installation_proxy_installWithBody:(NSDictionary *)body replyHandler:(no plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0); } -// JSValue* callback = body[@"callback"]; - // TODO implement real callback - JSValue* callback = nil; + int callbackId = [body[@"callback_id"] intValue]; IdeviceFfiError* err = 0; - if(!callback) { + if(callbackId == -1) { err = installation_proxy_install(client, [packagePath UTF8String], optionsPlist); } else { struct InstallationProxyCallbackContext context; - context.callback = callback; - + context.callbackId = callbackId; + context.context = self->context; err = installation_proxy_install_with_callback(client, [packagePath UTF8String], optionsPlist, (void (*)(uint64_t, void *))installationProxyCallback, &context); } @@ -170,14 +169,14 @@ - (void)installation_proxy_upgradeWithBody:(NSDictionary *)body replyHandler:(no plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0); } - JSValue* callback = body[@"callback"]; + int callbackId = [body[@"callback_id"] intValue]; IdeviceFfiError* err = 0; - if(!callback) { + if(callbackId == -1) { err = installation_proxy_upgrade(client, [packagePath UTF8String], optionsPlist); } else { struct InstallationProxyCallbackContext context; - context.callback = callback; - + context.callbackId = callbackId; + context.context = self->context; err = installation_proxy_upgrade_with_callback(client, [packagePath UTF8String], optionsPlist, (void (*)(uint64_t, void *))installationProxyCallback, &context); } @@ -222,14 +221,14 @@ - (void)installation_proxy_uninstallWithBody:(NSDictionary *)body replyHandler:( plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0); } - JSValue* callback = body[@"callback"]; + int callbackId = [body[@"callback_id"] intValue]; IdeviceFfiError* err = 0; - if(!callback) { + if(callbackId == -1) { err = installation_proxy_uninstall(client, [bundleId UTF8String], optionsPlist); } else { struct InstallationProxyCallbackContext context; - context.callback = callback; - + context.callbackId = callbackId; + context.context = self->context; err = installation_proxy_uninstall_with_callback(client, [bundleId UTF8String], optionsPlist, (void (*)(uint64_t, void *))installationProxyCallback, &context); } diff --git a/StikJIT/MiniToolSupport/JSSupport/JSSupport.h b/StikJIT/MiniToolSupport/JSSupport/JSSupport.h index d7a84de9..56b02233 100644 --- a/StikJIT/MiniToolSupport/JSSupport/JSSupport.h +++ b/StikJIT/MiniToolSupport/JSSupport/JSSupport.h @@ -23,7 +23,9 @@ @interface IDeviceJSBridge : NSObject { NSMutableDictionary* handles; NSMutableDictionary* dataPool; + JSContext* context; } +- (instancetype)initWithContext:(JSContext*)context; - (void)didReceiveScriptMessage:(NSDictionary *)message resolve:(JSValue*)resolveFunc reject:(JSValue*)rejectFunc; - (void)cleanUp; - (NSString*)errFreeFromIdeviceFfiError:(IdeviceFfiError*)err; diff --git a/StikJIT/MiniToolSupport/JSSupport/idevice.js b/StikJIT/MiniToolSupport/JSSupport/idevice.js index 60a07cde..e627b265 100644 --- a/StikJIT/MiniToolSupport/JSSupport/idevice.js +++ b/StikJIT/MiniToolSupport/JSSupport/idevice.js @@ -259,6 +259,27 @@ async function afc_file_write(handle, data_handle) { }); } +// for installation_proxy_callback + +var handle_installation_proxy_callback_dict = {"max": 0} +function handle_installation_proxy_js_callback(id, progress) { + let func = handle_installation_proxy_callback_dict[id]; + if(func) { + func(progress) + } +} + +function installation_proxy_js_callback_register(callback) { + let cur = handle_installation_proxy_callback_dict['max'] + handle_installation_proxy_callback_dict['max']++ + handle_installation_proxy_callback_dict[cur] = callback + return cur; +} + +function installation_proxy_js_callback_unregister(id) { + delete handle_installation_proxy_callback_dict[id] +} + async function installation_proxy_connect() { return await __postIdeviceMessage({ "command": "installation_proxy_connect" @@ -283,33 +304,72 @@ async function installation_proxy_browse(client, options) { } async function installation_proxy_install(client, package_path, options, callback) { - return await __postIdeviceMessage({ - "command": "installation_proxy_install", - "client": client, - "package_path": package_path, - "options": options, - "callback": callback - }); + if(callback) { + let id = installation_proxy_js_callback_register(callback) + let ans = await __postIdeviceMessage({ + "command": "installation_proxy_install", + "client": client, + "package_path": package_path, + "options": options, + "callback_id": id + }); + installation_proxy_js_callback_unregister(id) + return ans; + } else { + return await __postIdeviceMessage({ + "command": "installation_proxy_install", + "client": client, + "package_path": package_path, + "options": options, + "callback_id": -1 + }); + } } async function installation_proxy_upgrade(client, package_path, options, callback) { - return await __postIdeviceMessage({ - "command": "installation_proxy_install", - "client": client, - "package_path": package_path, - "options": options, - "callback": callback - }); + if(callback) { + let id = installation_proxy_js_callback_register(callback) + let ans = await __postIdeviceMessage({ + "command": "installation_proxy_upgrade", + "client": client, + "package_path": package_path, + "options": options, + "callback_id": id + }); + installation_proxy_js_callback_unregister(id) + return ans; + } else { + return await __postIdeviceMessage({ + "command": "installation_proxy_install", + "client": client, + "package_path": package_path, + "options": options, + "callback_id": -1 + }); + } } async function installation_proxy_uninstall(client, bundle_id, options, callback) { - return await __postIdeviceMessage({ - "command": "installation_proxy_install", - "client": client, - "bundle_id": bundle_id, - "options": options, - "callback": callback - }); + if(callback) { + let id = installation_proxy_js_callback_register(callback) + let ans = await __postIdeviceMessage({ + "command": "installation_proxy_uninstall", + "client": client, + "bundle_id": bundle_id, + "options": options, + "callback_id": id + }); + installation_proxy_js_callback_unregister(id) + return ans; + } else { + return await __postIdeviceMessage({ + "command": "installation_proxy_install", + "client": client, + "bundle_id": bundle_id, + "options": options, + "callback_id": -1 + }); + } } async function amfi_connect() { diff --git a/StikJIT/MiniToolSupport/MiniToolRuntime.swift b/StikJIT/MiniToolSupport/MiniToolRuntime.swift index 67296765..50987959 100644 --- a/StikJIT/MiniToolSupport/MiniToolRuntime.swift +++ b/StikJIT/MiniToolSupport/MiniToolRuntime.swift @@ -16,7 +16,7 @@ final class MiniToolRuntime: NSObject, ObservableObject { private let messageHandlerName = "miniToolBridge" - private var ideviceJSBridge : IDeviceJSBridge? = IDeviceJSBridge() + private var ideviceJSBridge : IDeviceJSBridge? = nil init(tool: MiniToolBundle) { self.tool = tool @@ -64,6 +64,7 @@ final class MiniToolRuntime: NSObject, ObservableObject { private func loadBackground() { context = JSContext() + ideviceJSBridge = IDeviceJSBridge(context: context) context?.exceptionHandler = { [weak self] _, exception in if let message = exception?.toString() { self?.appendLog("Background exception: \(message)")