diff --git a/StikJIT/Info.plist b/StikJIT/Info.plist
index 9b96e3b4..ca1acb2d 100644
--- a/StikJIT/Info.plist
+++ b/StikJIT/Info.plist
@@ -15,12 +15,43 @@
+
+ ITSAppUsesNonExemptEncryption
+
+
+ UIFileSharingEnabled
+
+
+ UTExportedTypeDeclarations
+
+
+ UTTypeConformsTo
+
+ com.apple.package
+
+ UTTypeDescription
+ StikDebug Mini Tool
+ UTTypeIconFiles
+
+ UTTypeIdentifier
+ com.stik.StikJIT.stiktool
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ stiktool
+
+
+
+
+
+ NSLocalNetworkUsageDescription
+ StikDebug needs access to devices on your local network so it can connect to the targets you add to the Device Library.
+
NSBonjourServices
_stikdebug._tcp
_stikdebug._udp
- UIFileSharingEnabled
-
-
+
\ No newline at end of file
diff --git a/StikJIT/JSSupport/ScriptListView.swift b/StikJIT/JSSupport/ScriptListView.swift
index e7c8824e..67121956 100644
--- a/StikJIT/JSSupport/ScriptListView.swift
+++ b/StikJIT/JSSupport/ScriptListView.swift
@@ -441,7 +441,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/JSSupport/IDeviceJSBridge.m b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridge.m
new file mode 100644
index 00000000..14b527bc
--- /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)initWithContext:(JSContext*)context {
+ maxHandleId = 0;
+ maxDataId = 0;
+ handles = [[NSMutableDictionary alloc] init];
+ dataPool = [[NSMutableDictionary alloc] init];
+ self->context = context;
+ 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..3b331182
--- /dev/null
+++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAFC.m
@@ -0,0 +1,370 @@
+//
+// 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 {
+ NSError* heartbeatErr = nil;
+ [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
+ if(heartbeatErr) {
+ replyHandler(nil, heartbeatErr.localizedDescription);
+ return;
+ }
+
+ 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..3d3c1073
--- /dev/null
+++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeAMFI.m
@@ -0,0 +1,86 @@
+//
+// 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 {
+ NSError* heartbeatErr = nil;
+ [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
+ if(heartbeatErr) {
+ replyHandler(nil, heartbeatErr.localizedDescription);
+ return;
+ }
+
+ 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..97c2709e
--- /dev/null
+++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeCoreDevice.m
@@ -0,0 +1,73 @@
+//
+// 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 {
+ NSError* heartbeatErr = nil;
+ [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
+ if(heartbeatErr) {
+ replyHandler(nil, heartbeatErr.localizedDescription);
+ return;
+ }
+
+ 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..59706cad
--- /dev/null
+++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeInstallationProxy.m
@@ -0,0 +1,300 @@
+//
+// IDeviceJSBridgeInstallationProxy.m
+// StikJIT
+//
+// Created by s s on 2025/4/25.
+//
+@import Foundation;
+@import JavaScriptCore;
+#import "JSSupport.h"
+
+struct InstallationProxyCallbackContext {
+ int callbackId;
+ JSContext* context;
+};
+
+void installationProxyCallback(uint64_t progress, struct InstallationProxyCallbackContext* context) {
+ [context->context evaluateScript:[NSString stringWithFormat:@"handle_installation_proxy_js_callback(%d, %llu)", context->callbackId, progress]];
+}
+
+@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;
+ 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);
+ }
+
+ int callbackId = [body[@"callback_id"] intValue];
+ IdeviceFfiError* err = 0;
+ if(callbackId == -1) {
+ err = installation_proxy_install(client, [packagePath UTF8String], optionsPlist);
+ } else {
+ struct InstallationProxyCallbackContext context;
+ context.callbackId = callbackId;
+ context.context = self->context;
+ 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);
+ }
+
+ int callbackId = [body[@"callback_id"] intValue];
+ IdeviceFfiError* err = 0;
+ if(callbackId == -1) {
+ err = installation_proxy_upgrade(client, [packagePath UTF8String], optionsPlist);
+ } else {
+ struct InstallationProxyCallbackContext context;
+ context.callbackId = callbackId;
+ context.context = self->context;
+ 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);
+ }
+
+ int callbackId = [body[@"callback_id"] intValue];
+ IdeviceFfiError* err = 0;
+ if(callbackId == -1) {
+ err = installation_proxy_uninstall(client, [bundleId UTF8String], optionsPlist);
+ } else {
+ struct InstallationProxyCallbackContext context;
+ context.callbackId = callbackId;
+ context.context = self->context;
+ 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..bc94b951
--- /dev/null
+++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeMisagent.m
@@ -0,0 +1,120 @@
+//
+// 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 {
+ NSError* heartbeatErr = nil;
+ [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
+ if(heartbeatErr) {
+ replyHandler(nil, heartbeatErr.localizedDescription);
+ return;
+ }
+
+ 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..4896b39e
--- /dev/null
+++ b/StikJIT/MiniToolSupport/JSSupport/IDeviceJSBridgeSBServices.m
@@ -0,0 +1,67 @@
+//
+// 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 {
+ NSError* heartbeatErr = nil;
+ [JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
+ if(heartbeatErr) {
+ replyHandler(nil, heartbeatErr.localizedDescription);
+ return;
+ }
+
+ 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..56b02233
--- /dev/null
+++ b/StikJIT/MiniToolSupport/JSSupport/JSSupport.h
@@ -0,0 +1,41 @@
+//
+// 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;
+ JSContext* context;
+}
+- (instancetype)initWithContext:(JSContext*)context;
+- (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..e627b265
--- /dev/null
+++ b/StikJIT/MiniToolSupport/JSSupport/idevice.js
@@ -0,0 +1,515 @@
+//
+// 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
+ });
+}
+
+// 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"
+ });
+}
+
+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) {
+ 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) {
+ 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) {
+ 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() {
+ 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/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..2235cf60
--- /dev/null
+++ b/StikJIT/MiniToolSupport/MiniToolListView.swift
@@ -0,0 +1,256 @@
+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)
+ }
+ }
+
+ 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)
+ }
+ .buttonStyle(.plain)
+
+ }
+ .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..f025e080
--- /dev/null
+++ b/StikJIT/MiniToolSupport/MiniToolModels.swift
@@ -0,0 +1,111 @@
+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)
+ }
+
+ 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 {
+ @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
+ }
+
+ 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)
+ 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..b0353682
--- /dev/null
+++ b/StikJIT/MiniToolSupport/MiniToolRunnerView.swift
@@ -0,0 +1,109 @@
+import SwiftUI
+import WebKit
+
+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
+
+ 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 {
+ if !initiated {
+ runtime.start()
+ initiated = true
+ }
+ }
+ .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..50987959
--- /dev/null
+++ b/StikJIT/MiniToolSupport/MiniToolRuntime.swift
@@ -0,0 +1,599 @@
+import Foundation
+import UniformTypeIdentifiers
+import SwiftUI
+import WebKit
+import JavaScriptCore
+
+final class MiniToolRuntime: NSObject, ObservableObject {
+ let tool: MiniToolBundle
+ @Published var logs: [String] = []
+ @Published var isReady: Bool = false
+
+ var webView: WKWebView?
+ private var context: JSContext?
+
+ private var appXHRTasks: [String: URLSessionDataTask] = [:]
+
+ private let messageHandlerName = "miniToolBridge"
+
+ private var ideviceJSBridge : IDeviceJSBridge? = nil
+
+ init(tool: MiniToolBundle) {
+ self.tool = tool
+ super.init()
+ let configuration = WKWebViewConfiguration()
+ let controller = WKUserContentController()
+ controller.addUserScript(WKUserScript(source: MiniToolRuntime.frontendBridgeScript,
+ injectionTime: .atDocumentStart,
+ forMainFrameOnly: true))
+ configuration.userContentController = controller
+ let leakAvoider = LeakAvoider(delegate: self)
+ configuration.setURLSchemeHandler(leakAvoider, forURLScheme: "app")
+ webView = WKWebView(frame: .zero, configuration: configuration)
+ controller.add(leakAvoider, name: messageHandlerName)
+ webView!.navigationDelegate = self
+ webView!.isInspectable = true
+ }
+
+ 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
+ webView!.load(URLRequest(url: URL(string: "app://\(tool.getHostName())")!))
+ }
+
+ 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)")
+ }
+ }
+
+ 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.")
+
+ }
+
+ 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)
+ 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)
+ }
+ }
+}
+
+extension MiniToolRuntime : WKURLSchemeHandler {
+ func webView(
+ _ webView: WKWebView,
+ 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
+
+ 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 {
+ 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])
+ }
+
+ 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
+
+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);
+ }
+ };
+
+ (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 = """
+ 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
+ }
+}
+
+// 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)
+ }
+ }
+
+ 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)")
+ }
+ }
+ }
+ }
+}
+
+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 4329230b..0936cdde 100644
--- a/StikJIT/StikJITApp.swift
+++ b/StikJIT/StikJITApp.swift
@@ -638,12 +638,7 @@ struct HeartbeatApp: App {
}
}
}
- .onChange(of: scenePhase) { newPhase in
- if newPhase == .active {
- print("App became active – restarting heartbeat")
- startHeartbeatInBackground()
- }
- }
+
}
}
@@ -706,8 +701,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()
@@ -717,11 +711,9 @@ 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,
- pairingFilePath: pairingpath
)
DispatchQueue.main.async {
@@ -757,6 +749,7 @@ func isPairing() -> Bool {
}
return false
}
+ idevice_pairing_file_free(pairingFile)
return true
}
@@ -786,63 +779,62 @@ func startHeartbeatInBackground(requireVPNConnection: Bool? = nil) {
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/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/Utilities/TabConfiguration.swift b/StikJIT/Utilities/TabConfiguration.swift
index 38653773..f820ec4f 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 coreIDs: [String] = ["home", "console", "scripts", "profiles", "deviceinfo"]
+ private static let coreIDs: [String] = ["home", "console", "scripts", "profiles", "deviceinfo", "tools"]
static var allowedIDs: [String] {
var ids = coreIDs
if FeatureFlags.showBetaTabs {
diff --git a/StikJIT/Utilities/mountDDI.swift b/StikJIT/Utilities/mountDDI.swift
index f488347b..e9413067 100644
--- a/StikJIT/Utilities/mountDDI.swift
+++ b/StikJIT/Utilities/mountDDI.swift
@@ -18,34 +18,6 @@ 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 {
if DeviceConnectionContext.requiresLoopbackVPN {
guard TunnelManager.shared.tunnelStatus == .connected else {
@@ -53,164 +25,23 @@ func isMounted() -> Bool {
}
}
- 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, DeviceConnectionContext.targetIPAddress, &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
- }
-
- 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
+ do {
+ try JITEnableContext.shared.mountPersonalDDI(withImagePath: imagePath, trustcachePath: trustcachePath, manifestPath: manifestPath)
+ } catch {
+ print("Failed to mount ddi: \(error)")
+ return (error as NSError).code
}
-
- 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..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,35 +22,54 @@ 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 {
- let code = path.withCString { c_deviceinfo_init($0) }
- await MainActor.run {
- if code != 0 {
- self.error = ("Initialization Failed", self.initErrorMessage(code))
+ do {
+ try JITEnableContext.shared.ensureHeartbeat()
+ } catch {
+ await MainActor.run {
+ self.error = ("Initialization Failed", self.initErrorMessage(Int32((error as NSError).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 {
@@ -83,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/HomeView.swift b/StikJIT/Views/HomeView.swift
index 020d0af0..be987e41 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/Views/MainTabView.swift b/StikJIT/Views/MainTabView.swift
index dbb7a9f2..cbf18528 100644
--- a/StikJIT/Views/MainTabView.swift
+++ b/StikJIT/Views/MainTabView.swift
@@ -55,7 +55,8 @@ struct MainTabView: View {
TabDescriptor(id: "console", title: "Console", systemImage: "terminal") { AnyView(ConsoleLogsView()) },
TabDescriptor(id: "scripts", title: "Scripts", systemImage: "scroll") { AnyView(ScriptListView()) },
TabDescriptor(id: "profiles", title: "Profiles", systemImage: "magazine.fill") { AnyView(ProfileView()) },
- TabDescriptor(id: "deviceinfo", title: "Device Info", systemImage: "iphone.and.arrow.forward") { AnyView(DeviceInfoView()) }
+ TabDescriptor(id: "deviceinfo", title: "Device Info", systemImage: "iphone.and.arrow.forward") { AnyView(DeviceInfoView()) },
+ TabDescriptor(id: "tools", title: "Mini Tools", systemImage: "shippingbox.fill") { AnyView(MiniToolListView()) },
]
if FeatureFlags.showBetaTabs {
tabs.append(TabDescriptor(id: "processes", title: "Processes", systemImage: "rectangle.stack.person.crop") { AnyView(ProcessInspectorView()) })
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/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/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift
index ad344aa9..d895fef9 100644
--- a/StikJIT/Views/SettingsView.swift
+++ b/StikJIT/Views/SettingsView.swift
@@ -89,7 +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: "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 56156e49..949cb301 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, ...);
@@ -15,23 +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;
-- (void)startHeartbeatWithCompletionHandler:(HeartbeatCompletionHandler)completionHandler logger:(LogFunc)logger;
+- (IdeviceProviderHandle*)getTcpProviderHandle;
+- (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
+
+@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 5cd9751b..4f71bfb8 100644
--- a/StikJIT/idevice/JITEnableContext.m
+++ b/StikJIT/idevice/JITEnableContext.m
@@ -16,24 +16,25 @@
#include "JITEnableContext.h"
#import "StikDebug-Swift.h"
+#include
+#import
+
+static JITEnableContext* sharedJITContext = nil;
+
+@implementation JITEnableContext {
+ int heartbeatToken;
+ NSError* lastHeartbeatError;
+ os_unfair_lock heartbeatLock;
+ BOOL heartbeatRunning;
+ dispatch_semaphore_t heartbeatSemaphore;
-JITEnableContext* sharedJITContext = nil;
-
-@implementation JITEnableContext {
- bool heartbeatRunning;
- IdeviceProviderHandle* provider;
- dispatch_queue_t syslogQueue;
- BOOL syslogStreaming;
- SyslogRelayClientHandle *syslogClient;
- SyslogLineHandler syslogLineHandler;
- SyslogErrorHandler syslogErrorHandler;
- dispatch_queue_t processInspectorQueue;
}
+ (instancetype)shared {
- if (!sharedJITContext) {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
sharedJITContext = [[JITEnableContext alloc] init];
- }
+ });
return sharedJITContext;
}
@@ -47,6 +48,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;
}
@@ -84,7 +92,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!");
@@ -101,300 +115,123 @@ - (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) {
+- (IdeviceProviderHandle*)getTcpProviderHandle {
+ return provider;
+}
+
+// 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) {
+ dispatch_semaphore_wait(waitSemaphore, DISPATCH_TIME_FOREVER);
+ dispatch_semaphore_signal(waitSemaphore);
+ }
+ *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), ^{
+ startHeartbeat(
+ pairingFile,
+ &self->provider,
+ globalHeartbeatToken,Ccompletion
+ );
+ });
+ // 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.");
+ } 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];
- 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 {
- if (!provider) {
- NSLog(@"Provider not initialized!");
- *error = [self errorWithStr:@"Provider not initialized!" code:-1];
- 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 {
- if (!provider) {
- NSLog(@"Provider not initialized!");
- *error = [self errorWithStr:@"Provider not initialized!" code:-1];
- 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 {
- if (!provider) {
- NSLog(@"Provider not initialized!");
- *error = [self errorWithStr:@"Provider not initialized!" code:-1];
- 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 {
- if (!provider) {
- if (logger) {
- logger(@"Provider not initialized!");
- }
- NSLog(@"Provider not initialized!");
- return NO;
- }
-
- [self ensureHeartbeat];
-
- int result = launch_app_via_proxy(provider,
- [bundleID UTF8String],
- [self createCLogger:logger]);
- return result == 0;
-}
-
-- (void)startSyslogRelayWithHandler:(SyslogLineHandler)lineHandler
- onError:(SyslogErrorHandler)errorHandler
-{
- if (!provider) {
- if (errorHandler) {
- errorHandler([self errorWithStr:@"Provider not initialized!" code:-1]);
- }
- 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; }
- [strongSelf ensureHeartbeat];
-
- 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 {
- if (!provider) {
- NSLog(@"Provider not initialized!");
- *error = [self errorWithStr:@"Provider not initialized!" code:-1];
- 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];
- 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];
- return nil;
- }
- return addProfile(provider, profile, error);
-}
- (void)dealloc {
[self stopSyslogRelay];
@@ -403,352 +240,8 @@ - (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;
- 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 {
- 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) {
- 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);
- }
- if (tempProvider) {
- idevice_provider_free(tempProvider);
- }
- if (pairingFile) {
- idevice_pairing_file_free(pairingFile);
- }
- return result;
-}
-
-- (NSArray*)_fetchProcessListLocked:(NSError**)error {
- if (provider) {
- [self ensureHeartbeat];
- }
- 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 {
- NSURL *documents = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
- NSURL *pairingURL = [documents URLByAppendingPathComponent:@"pairingFile.plist"];
- IdevicePairingFile *pairingFile = NULL;
- IdeviceProviderHandle *tempProvider = NULL;
- 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 {
- 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) {
- *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);
- }
- if (tempProvider) {
- idevice_provider_free(tempProvider);
- }
- if (pairingFile) {
- idevice_pairing_file_free(pairingFile);
- }
- return success;
-}
@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..8c52b9bf 100644
--- a/StikJIT/idevice/applist.m
+++ b/StikJIT/idevice/applist.m
@@ -10,7 +10,10 @@
#include
#include
#import "applist.h"
+#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");
@@ -19,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");
@@ -31,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";
@@ -133,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);
}
@@ -152,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;
}
@@ -206,3 +217,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.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 6e22cd2b..dd029e83 100644
--- a/StikJIT/idevice/heartbeat.m
+++ b/StikJIT/idevice/heartbeat.m
@@ -10,37 +10,35 @@
#include
#include
#include "heartbeat.h"
+#include
@import Foundation;
-
-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);
-
+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);
- NSString *ipOverride = [[NSUserDefaults standardUserDefaults] stringForKey:@"TunnelDeviceIP"];
- if (ipOverride.length == 0) {
- ipOverride = @"10.7.0.2";
- }
- inet_pton(AF_INET, ipOverride.UTF8String, &addr.sin_addr);
- IdeviceProviderHandle* newProvider = 0;
- IdeviceFfiError* err = idevice_tcp_provider_new((struct sockaddr *)&addr, pairing_file,
- "ExampleProvider", &newProvider);
+ 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);
- *isHeartbeat = false;
+
return;
}
@@ -50,21 +48,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) {
@@ -72,22 +67,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);
+ 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
deleted file mode 100644
index ac8b3052..00000000
--- a/StikJIT/idevice/ideviceinfo.c
+++ /dev/null
@@ -1,115 +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 HeartbeatClientHandle * g_hb = 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.2", &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 = 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);
- 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_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/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..abcd8026
--- /dev/null
+++ b/StikJIT/idevice/ideviceinfo.m
@@ -0,0 +1,85 @@
+//
+// 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_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);
+ 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 90%
rename from StikJIT/idevice/jit.c
rename to StikJIT/idevice/jit.m
index d725d271..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,
@@ -85,7 +87,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 +316,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;
@@ -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.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..fff1890a
--- /dev/null
+++ b/StikJIT/idevice/mount.m
@@ -0,0 +1,126 @@
+//
+// mount1.m
+// StikDebug
+//
+// 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;
+ 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) {
+ idevice_pairing_file_free(pairingFile2);
+ *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_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);
+ 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;
+}
+
+@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