Heartbeat Rework + Mini Tool (#299)

* Heartbeat rework

* remove some debug loggings

* stik mini tool poc

* Saperate JITEnableContext into extensions and put each feature into their own files.
Fix local pairing file is removed after switched to external device by directly loading the remote device's pairing file in place.

* custom origin & xmlhttprequest

* Fix some memory leak

* allow background script access idevice (codes are from the old JSSupport branch)

Tests are not carried out but most of the js api should work.

Do we need to obfuscate some functions' name?

* Fix JITEnableContext singleton initializer race condition

* Fix instprox callback

---------

Co-authored-by: Stephen B <158498287+StephenDev0@users.noreply.github.com>
This commit is contained in:
Huge_Black 2025-12-15 00:35:32 +08:00 • committed by GitHub
parent f5f3d3e09b
commit dca36d5c00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 4955 additions and 1114 deletions

View file

@ -15,12 +15,43 @@
</array>
</dict>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIFileSharingEnabled</key>
<true/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.package</string>
</array>
<key>UTTypeDescription</key>
<string>StikDebug Mini Tool</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.stik.StikJIT.stiktool</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>stiktool</string>
</array>
</dict>
</dict>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>StikDebug needs access to devices on your local network so it can connect to the targets you add to the Device Library.</string>
<key>NSBonjourServices</key>
<array>
<string>_stikdebug._tcp</string>
<string>_stikdebug._udp</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
</dict>
</plist>
</plist>

View file

@ -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

View file

@ -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<NSNumber *> *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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<NSString *> *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

View file

@ -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

View file

@ -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

View file

@ -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<NSNumber*, IDeviceHandle*>* handles;
NSMutableDictionary<NSNumber*, NSData*>* 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);

View file

@ -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

View file

@ -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
});
}

View file

@ -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<TextLocated<Message>> = []
@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<String> {
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()
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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) {
}
}

View file

@ -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"] ?? "<unknown>")")
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()
}
}

View file

@ -11,3 +11,4 @@
#include "idevice/ls.h"
#include "idevice/profiles.h"
#include "idevice/something.h"
#include "MiniToolSupport/JSSupport/JSSupport.h"

View file

@ -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) {

View file

@ -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)

View file

@ -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 {

View file

@ -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<sockaddr_in>.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<plist_t?>?
var devicesLen: size_t = 0
let listError = image_mounter_copy_devices(client, &deviceList, &devicesLen)
if listError == nil {
var devices: [String] = []
for i in 0..<devicesLen {
let device = deviceList?[i]
var xmlData: UnsafeMutablePointer<CChar>?
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
do {
try JITEnableContext.shared.mountPersonalDDI(withImagePath: imagePath, trustcachePath: trustcachePath, manifestPath: manifestPath)
} catch {
print("Failed to mount ddi: \(error)")
return (error as NSError).code
}
var addr = sockaddr_in()
memset(&addr, 0, MemoryLayout<sockaddr_in>.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
}
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
}

View file

@ -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()

View file

@ -9,15 +9,12 @@ import SwiftUI
import UIKit
import UniformTypeIdentifiers
@_silgen_name("ideviceinfo_c_init")
private func c_deviceinfo_init(_ path: UnsafePointer<CChar>) -> Int32
@_silgen_name("ideviceinfo_c_get_xml")
private func c_deviceinfo_get_xml() -> UnsafePointer<CChar>?
@_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<CChar>? = 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
}

View file

@ -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
)
]
}

View file

@ -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()) })

View file

@ -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 {

View file

@ -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",

View file

@ -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 {

View file

@ -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<NSString*, NSString*>*)getAppListWithError:(NSError**)error;
- (NSDictionary<NSString*, NSString*>*)getAllAppsWithError:(NSError**)error;
- (NSDictionary<NSString*, NSString*>*)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<NSData*>*)fetchAllProfiles:(NSError **)error;
- (BOOL)removeProfileWithUUID:(NSString*)uuid error:(NSError **)error;
- (BOOL)addProfile:(NSData*)profile error:(NSError **)error;
@end
@interface JITEnableContext(Process)
- (NSArray<NSDictionary*>*)fetchProcessListWithError:(NSError**)error;
- (BOOL)killProcessWithPID:(int)pid error:(NSError **)error;
@end
@interface JITEnableContext(App)
- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error;
- (NSDictionary<NSString*, NSString*>*)getAppListWithError:(NSError**)error;
- (NSDictionary<NSString*, NSString*>*)getAllAppsWithError:(NSError**)error;
- (NSDictionary<NSString*, NSString*>*)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

View file

@ -16,24 +16,25 @@
#include "JITEnableContext.h"
#import "StikDebug-Swift.h"
#include <os/lock.h>
#import <pthread.h>
JITEnableContext* sharedJITContext = nil;
static JITEnableContext* sharedJITContext = nil;
@implementation JITEnableContext {
int heartbeatToken;
NSError* lastHeartbeatError;
os_unfair_lock heartbeatLock;
BOOL heartbeatRunning;
dispatch_semaphore_t heartbeatSemaphore;
@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 @@ JITEnableContext* sharedJITContext = nil;
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 @@ JITEnableContext* sharedJITContext = nil;
- (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 @@ JITEnableContext* sharedJITContext = nil;
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) {
return;
}
// for all other errors, log and forward
if (logger) {
logger(err.localizedDescription);
}
completionHandler(err.code, err.localizedDescription);
return;
}
if(heartbeatRunning) {
return;
}
startHeartbeat(
pairingFile,
&provider,
&heartbeatRunning,
^(int result, const char *message) {
completionHandler(result,
[NSString stringWithCString:message
encoding:NSASCIIStringEncoding]);
},
[self createCLogger:logger]
);
- (IdeviceProviderHandle*)getTcpProviderHandle {
return provider;
}
- (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);
// 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;
}
if (result != 0) {
*err = [self errorWithStr:[NSString stringWithCString:message
encoding:NSASCIIStringEncoding] code:result];
self->lastHeartbeatError = *err;
} else {
self->lastHeartbeatError = nil;
}
completionCalled = true;
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;
}
- (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<NSString*, NSString*>*)getAppListWithError:(NSError**)error {
if (!provider) {
NSLog(@"Provider not initialized!");
*error = [self errorWithStr:@"Provider not initialized!" code:-1];
return nil;
}
NSString* errorStr = nil;
NSDictionary<NSString*, NSString*>* apps = list_installed_apps(provider, &errorStr);
if (errorStr) {
*error = [self errorWithStr:errorStr code:-17];
return nil;
}
return apps;
}
- (NSDictionary<NSString*, NSString*>*)getAllAppsWithError:(NSError**)error {
if (!provider) {
NSLog(@"Provider not initialized!");
*error = [self errorWithStr:@"Provider not initialized!" code:-1];
return nil;
}
NSString* errorStr = nil;
NSDictionary<NSString*, NSString*>* apps = list_all_apps(provider, &errorStr);
if (errorStr) {
*error = [self errorWithStr:errorStr code:-17];
return nil;
}
return apps;
}
- (NSDictionary<NSString*, NSString*>*)getHiddenSystemAppsWithError:(NSError**)error {
if (!provider) {
NSLog(@"Provider not initialized!");
*error = [self errorWithStr:@"Provider not initialized!" code:-1];
return nil;
}
NSString* errorStr = nil;
NSDictionary<NSString*, NSString*>* 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<NSData*>*)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 @@ JITEnableContext* sharedJITContext = nil;
}
}
- (NSArray<NSDictionary*>*)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<NSDictionary*>*)_fetchProcessListLocked:(NSError**)error {
if (provider) {
[self ensureHeartbeat];
}
return [self fetchProcessesViaAppServiceWithError:error];
}
- (NSArray<NSDictionary*>*)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

View file

@ -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

View file

@ -10,7 +10,10 @@
#include <stdlib.h>
#include <string.h>
#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 @@ static NSString *extractAppName(plist_t app)
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 @@ static NSString *extractAppName(plist_t app)
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 NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
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 NSDictionary<NSString*, NSString*> *performAppQuery(IdeviceProviderHandle
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<NSString*, NSString*> *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 @@ UIImage* getAppIcon(IdeviceProviderHandle* provider, NSString* bundleID, NSStrin
springboard_services_free(client);
return icon;
}
@implementation JITEnableContext(App)
- (NSDictionary<NSString*, NSString*>*)getAppListWithError:(NSError**)error {
[self ensureHeartbeatWithError:error];
if(*error) {
return nil;
}
NSString* errorStr = nil;
NSDictionary<NSString*, NSString*>* apps = list_installed_apps(provider, &errorStr);
if (errorStr) {
*error = [self errorWithStr:errorStr code:-17];
return nil;
}
return apps;
}
- (NSDictionary<NSString*, NSString*>*)getAllAppsWithError:(NSError**)error {
[self ensureHeartbeatWithError:error];
if(*error) {
return nil;
}
NSString* errorStr = nil;
NSDictionary<NSString*, NSString*>* apps = list_all_apps(provider, &errorStr);
if (errorStr) {
*error = [self errorWithStr:errorStr code:-17];
return nil;
}
return apps;
}
- (NSDictionary<NSString*, NSString*>*)getHiddenSystemAppsWithError:(NSError**)error {
[self ensureHeartbeatWithError:error];
if(*error) {
return nil;
}
NSString* errorStr = nil;
NSDictionary<NSString*, NSString*>* 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

View file

@ -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 */

View file

@ -10,37 +10,35 @@
#include <CoreFoundation/CoreFoundation.h>
#include <limits.h>
#include "heartbeat.h"
#include <pthread.h>
@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");
}
}
}

View file

@ -1,115 +0,0 @@
//
// ideviceinfo.c
// StikDebug
//
// Created by Stephen on 8/2/25.
//
#include <stdlib.h>
#include <arpa/inet.h>
#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;
}
}

View file

@ -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
}

View file

@ -0,0 +1,85 @@
//
// ideviceinfo.c
// StikDebug
//
// Created by Stephen on 8/2/25.
//
#include <stdlib.h>
#include <arpa/inet.h>
#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

View file

@ -17,6 +17,8 @@
#include <limits.h>
#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 @@ cleanup:
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

14
StikJIT/idevice/mount.h Normal file
View file

@ -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 <Foundation/Foundation.h>
size_t getMountedDeviceCount(IdeviceProviderHandle* provider, NSError** error);
int mountPersonalDDI(IdeviceProviderHandle* provider, IdevicePairingFile* pairingFile2, NSString* imagePath, NSString* trustcachePath, NSString* manifestPath, NSError** error);
#endif

126
StikJIT/idevice/mount.m Normal file
View file

@ -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

287
StikJIT/idevice/process.m Normal file
View file

@ -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<NSDictionary*>*)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<NSDictionary*>*)_fetchProcessListLocked:(NSError**)error {
[self ensureHeartbeatWithError:error];
if(*error) {
return nil;
}
return [self fetchProcessesViaAppServiceWithError:error];
}
- (NSArray<NSDictionary*>*)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

View file

@ -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 @@ bool addProfile(IdeviceProviderHandle* provider, NSData* profile, NSError** erro
return nil;
}
@end
@implementation JITEnableContext(Profile)
- (NSArray<NSData*>*)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

125
StikJIT/idevice/syslog.m Normal file
View file

@ -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